mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
add clearnet functionality to frontend (#2814)
* add clearnet functionality to frontend * add pattern and add sync db on rpcs * add domain pattern * show acme name instead of url if known * dont blow up if domain not present after delete * use common name for letsencrypt * normalize urls * refactor start-os ui net service * backend migration and rpcs for serverInfo.host * fix cors * implement clearnet for main startos ui * ability to add and remove tor addresses, including vanity * add guard to prevent duplicate addresses * misc bugfixes * better heuristics for launching UIs * fix ipv6 mocks * fix ipv6 display bug * rewrite url selection for launch ui --------- Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
@@ -113,7 +113,6 @@ export class MainLoop {
|
||||
}))
|
||||
.find((conf) => conf.internal == internalPort)
|
||||
await effects.bind({
|
||||
kind: "multi",
|
||||
id: interfaceId,
|
||||
internalPort,
|
||||
preferredExternalPort: torConf?.external || internalPort,
|
||||
|
||||
@@ -401,6 +401,7 @@ export class SystemForEmbassy implements System {
|
||||
return [
|
||||
port,
|
||||
{
|
||||
protocol: null,
|
||||
secure: null,
|
||||
preferredExternalPort: Number.parseInt(
|
||||
torPort || lanPort || String(port),
|
||||
|
||||
327
core/Cargo.lock
generated
327
core/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ futures = "0.3.28"
|
||||
lazy_async_pool = "0.3.3"
|
||||
models = { path = "../models" }
|
||||
pin-project = "1.1.3"
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/no-dyn-ctx" }
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -24,7 +24,7 @@ patch-db = { version = "*", path = "../../patch-db/patch-db", features = [
|
||||
rand = "0.8.5"
|
||||
regex = "1.10.2"
|
||||
reqwest = "0.12"
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/no-dyn-ctx" }
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
|
||||
rustls = "0.23"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -168,6 +168,6 @@ fn doesnt_reallocate() {
|
||||
mime: InternedString::intern("png"),
|
||||
data: Cow::Borrowed(&random[..i]),
|
||||
};
|
||||
assert_eq!(dbg!(icon.to_string()).capacity(), icon.data_url_len());
|
||||
assert_eq!(icon.to_string().capacity(), icon.data_url_len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub enum ErrorKind {
|
||||
ConfigGen = 27,
|
||||
ParseNumber = 28,
|
||||
Database = 29,
|
||||
InvalidPackageId = 30,
|
||||
InvalidId = 30,
|
||||
InvalidSignature = 31,
|
||||
Backup = 32,
|
||||
Restore = 33,
|
||||
@@ -125,7 +125,7 @@ impl ErrorKind {
|
||||
ConfigGen => "Config Generation Error",
|
||||
ParseNumber => "Number Parsing Error",
|
||||
Database => "Database Error",
|
||||
InvalidPackageId => "Invalid Package ID",
|
||||
InvalidId => "Invalid ID",
|
||||
InvalidSignature => "Invalid Signature",
|
||||
Backup => "Backup Error",
|
||||
Restore => "Restore Error",
|
||||
@@ -226,7 +226,7 @@ impl From<std::convert::Infallible> for Error {
|
||||
}
|
||||
impl From<InvalidId> for Error {
|
||||
fn from(err: InvalidId) -> Self {
|
||||
Error::new(err, ErrorKind::InvalidPackageId)
|
||||
Error::new(err, ErrorKind::InvalidId)
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for Error {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use yasi::InternedString;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Invalid ID")]
|
||||
pub struct InvalidId;
|
||||
#[error("Invalid ID: {0}")]
|
||||
pub struct InvalidId(pub(super) InternedString);
|
||||
|
||||
@@ -43,7 +43,7 @@ impl TryFrom<InternedString> for Id {
|
||||
if ID_REGEX.is_match(&value) {
|
||||
Ok(Id(value))
|
||||
} else {
|
||||
Err(InvalidId)
|
||||
Err(InvalidId(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ impl TryFrom<String> for Id {
|
||||
if ID_REGEX.is_match(&value) {
|
||||
Ok(Id(InternedString::intern(value)))
|
||||
} else {
|
||||
Err(InvalidId)
|
||||
Err(InvalidId(InternedString::intern(value)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ impl TryFrom<&str> for Id {
|
||||
if ID_REGEX.is_match(value) {
|
||||
Ok(Id(InternedString::intern(value)))
|
||||
} else {
|
||||
Err(InvalidId)
|
||||
Err(InvalidId(InternedString::intern(value)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ keywords = [
|
||||
name = "start-os"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/Start9Labs/start-os"
|
||||
version = "0.3.6-alpha.11"
|
||||
version = "0.3.6-alpha.12"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
@@ -117,7 +117,7 @@ id-pool = { version = "0.2.2", default-features = false, features = [
|
||||
"u16",
|
||||
] }
|
||||
imbl = "2.0.3"
|
||||
imbl-value = { git = "https://github.com/Start9Labs/imbl-value.git" }
|
||||
imbl-value = "0.1.2"
|
||||
include_dir = { version = "0.7.3", features = ["metadata"] }
|
||||
indexmap = { version = "2.0.2", features = ["serde"] }
|
||||
indicatif = { version = "0.17.7", features = ["tokio"] }
|
||||
@@ -172,7 +172,7 @@ regex = "1.10.2"
|
||||
reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] }
|
||||
reqwest_cookie_store = "0.8.0"
|
||||
rpassword = "7.2.0"
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/no-dyn-ctx" }
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
|
||||
rust-argon2 = "2.0.0"
|
||||
rustyline-async = "0.4.1"
|
||||
semver = { version = "1.0.20", features = ["serde"] }
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct AccountInfo {
|
||||
pub server_id: String,
|
||||
pub hostname: Hostname,
|
||||
pub password: String,
|
||||
pub tor_key: TorSecretKeyV3,
|
||||
pub tor_keys: Vec<TorSecretKeyV3>,
|
||||
pub root_ca_key: PKey<Private>,
|
||||
pub root_ca_cert: X509,
|
||||
pub ssh_key: ssh_key::PrivateKey,
|
||||
@@ -34,7 +34,7 @@ impl AccountInfo {
|
||||
pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> {
|
||||
let server_id = generate_id();
|
||||
let hostname = generate_hostname();
|
||||
let tor_key = TorSecretKeyV3::generate();
|
||||
let tor_key = vec![TorSecretKeyV3::generate()];
|
||||
let root_ca_key = generate_key()?;
|
||||
let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?;
|
||||
let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random(
|
||||
@@ -45,7 +45,7 @@ impl AccountInfo {
|
||||
server_id,
|
||||
hostname,
|
||||
password: hash_password(password)?,
|
||||
tor_key,
|
||||
tor_keys: tor_key,
|
||||
root_ca_key,
|
||||
root_ca_cert,
|
||||
ssh_key,
|
||||
@@ -58,8 +58,11 @@ impl AccountInfo {
|
||||
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
|
||||
let password = db.as_private().as_password().de()?;
|
||||
let key_store = db.as_private().as_key_store();
|
||||
let tor_addr = db.as_public().as_server_info().as_onion_address().de()?;
|
||||
let tor_key = key_store.as_onion().get_key(&tor_addr)?;
|
||||
let tor_addrs = db.as_public().as_server_info().as_host().as_onions().de()?;
|
||||
let tor_keys = tor_addrs
|
||||
.into_iter()
|
||||
.map(|tor_addr| key_store.as_onion().get_key(&tor_addr))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let cert_store = key_store.as_local_certs();
|
||||
let root_ca_key = cert_store.as_root_key().de()?.0;
|
||||
let root_ca_cert = cert_store.as_root_cert().de()?.0;
|
||||
@@ -70,7 +73,7 @@ impl AccountInfo {
|
||||
server_id,
|
||||
hostname,
|
||||
password,
|
||||
tor_key,
|
||||
tor_keys,
|
||||
root_ca_key,
|
||||
root_ca_cert,
|
||||
ssh_key,
|
||||
@@ -82,17 +85,16 @@ impl AccountInfo {
|
||||
let server_info = db.as_public_mut().as_server_info_mut();
|
||||
server_info.as_id_mut().ser(&self.server_id)?;
|
||||
server_info.as_hostname_mut().ser(&self.hostname.0)?;
|
||||
server_info
|
||||
.as_lan_address_mut()
|
||||
.ser(&self.hostname.lan_address().parse()?)?;
|
||||
server_info
|
||||
.as_pubkey_mut()
|
||||
.ser(&self.ssh_key.public_key().to_openssh()?)?;
|
||||
let onion_address = self.tor_key.public().get_onion_address();
|
||||
server_info.as_onion_address_mut().ser(&onion_address)?;
|
||||
server_info
|
||||
.as_tor_address_mut()
|
||||
.ser(&format!("https://{onion_address}").parse()?)?;
|
||||
server_info.as_host_mut().as_onions_mut().ser(
|
||||
&self
|
||||
.tor_keys
|
||||
.iter()
|
||||
.map(|tor_key| tor_key.public().get_onion_address())
|
||||
.collect(),
|
||||
)?;
|
||||
db.as_private_mut().as_password_mut().ser(&self.password)?;
|
||||
db.as_private_mut()
|
||||
.as_ssh_privkey_mut()
|
||||
@@ -101,7 +103,9 @@ impl AccountInfo {
|
||||
.as_compat_s9pk_key_mut()
|
||||
.ser(Pem::new_ref(&self.compat_s9pk_key))?;
|
||||
let key_store = db.as_private_mut().as_key_store_mut();
|
||||
key_store.as_onion_mut().insert_key(&self.tor_key)?;
|
||||
for tor_key in &self.tor_keys {
|
||||
key_store.as_onion_mut().insert_key(tor_key)?;
|
||||
}
|
||||
let cert_store = key_store.as_local_certs_mut();
|
||||
cert_store
|
||||
.as_root_key_mut()
|
||||
|
||||
@@ -85,7 +85,7 @@ impl OsBackupV0 {
|
||||
&mut rand::thread_rng(),
|
||||
ssh_key::Algorithm::Ed25519,
|
||||
)?,
|
||||
tor_key: TorSecretKeyV3::from(self.tor_key.0),
|
||||
tor_keys: vec![TorSecretKeyV3::from(self.tor_key.0)],
|
||||
compat_s9pk_key: ed25519_dalek::SigningKey::generate(&mut rand::thread_rng()),
|
||||
},
|
||||
ui: self.ui,
|
||||
@@ -114,7 +114,7 @@ impl OsBackupV1 {
|
||||
root_ca_key: self.root_ca_key.0,
|
||||
root_ca_cert: self.root_ca_cert.0,
|
||||
ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)),
|
||||
tor_key: TorSecretKeyV3::from(ed25519_expand_key(&self.net_key.0)),
|
||||
tor_keys: vec![TorSecretKeyV3::from(ed25519_expand_key(&self.net_key.0))],
|
||||
compat_s9pk_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key),
|
||||
},
|
||||
ui: self.ui,
|
||||
@@ -132,7 +132,7 @@ struct OsBackupV2 {
|
||||
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
||||
ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key
|
||||
tor_key: TorSecretKeyV3, // Base64 Encoded Ed25519 Expanded Secret Key
|
||||
tor_keys: Vec<TorSecretKeyV3>, // Base64 Encoded Ed25519 Expanded Secret Key
|
||||
compat_s9pk_key: Pem<ed25519_dalek::SigningKey>, // PEM Encoded ED25519 Key
|
||||
ui: Value, // JSON Value
|
||||
}
|
||||
@@ -146,7 +146,7 @@ impl OsBackupV2 {
|
||||
root_ca_key: self.root_ca_key.0,
|
||||
root_ca_cert: self.root_ca_cert.0,
|
||||
ssh_key: self.ssh_key.0,
|
||||
tor_key: self.tor_key,
|
||||
tor_keys: self.tor_keys,
|
||||
compat_s9pk_key: self.compat_s9pk_key.0,
|
||||
},
|
||||
ui: self.ui,
|
||||
@@ -159,7 +159,7 @@ impl OsBackupV2 {
|
||||
root_ca_key: Pem(backup.account.root_ca_key.clone()),
|
||||
root_ca_cert: Pem(backup.account.root_ca_cert.clone()),
|
||||
ssh_key: Pem(backup.account.ssh_key.clone()),
|
||||
tor_key: backup.account.tor_key.clone(),
|
||||
tor_keys: backup.account.tor_keys.clone(),
|
||||
compat_s9pk_key: Pem(backup.account.compat_s9pk_key.clone()),
|
||||
ui: backup.ui.clone(),
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::db::model::Database;
|
||||
use crate::disk::mount::backup::BackupMountGuard;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
|
||||
use crate::init::{init, InitResult};
|
||||
use crate::init::init;
|
||||
use crate::prelude::*;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::service::service_map::DownloadInstallFuture;
|
||||
@@ -109,13 +109,13 @@ pub async fn recover_full_embassy(
|
||||
db.put(&ROOT, &Database::init(&os_backup.account)?).await?;
|
||||
drop(db);
|
||||
|
||||
let InitResult { net_ctrl } = init(&ctx.webserver, &ctx.config, init_phases).await?;
|
||||
let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?;
|
||||
|
||||
let rpc_ctx = RpcContext::init(
|
||||
&ctx.webserver,
|
||||
&ctx.config,
|
||||
disk_guid.clone(),
|
||||
Some(net_ctrl),
|
||||
Some(init_result),
|
||||
rpc_ctx_phases,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::disk::fsck::RepairStrategy;
|
||||
use crate::disk::main::DEFAULT_PASSWORD;
|
||||
use crate::disk::REPAIR_DISK_PATH;
|
||||
use crate::firmware::{check_for_firmware_update, update_firmware};
|
||||
use crate::init::{InitPhases, InitResult, STANDBY_MODE_PATH};
|
||||
use crate::init::{InitPhases, STANDBY_MODE_PATH};
|
||||
use crate::net::web_server::{UpgradableListener, WebServer};
|
||||
use crate::prelude::*;
|
||||
use crate::progress::FullProgressTracker;
|
||||
@@ -188,14 +188,14 @@ async fn setup_or_init(
|
||||
}));
|
||||
}
|
||||
|
||||
let InitResult { net_ctrl } =
|
||||
let init_result =
|
||||
crate::init::init(&server.acceptor_setter(), config, init_phases).await?;
|
||||
|
||||
let rpc_ctx = RpcContext::init(
|
||||
&server.acceptor_setter(),
|
||||
config,
|
||||
disk_guid,
|
||||
Some(net_ctrl),
|
||||
Some(init_result),
|
||||
rpc_ctx_phases,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -108,8 +108,6 @@ pub struct ServerConfig {
|
||||
#[arg(long)]
|
||||
pub tor_socks: Option<SocketAddr>,
|
||||
#[arg(long)]
|
||||
pub dns_bind: Option<Vec<SocketAddr>>,
|
||||
#[arg(long)]
|
||||
pub revision_cache_size: Option<usize>,
|
||||
#[arg(long)]
|
||||
pub disable_encryption: Option<bool>,
|
||||
@@ -125,7 +123,6 @@ impl ContextConfig for ServerConfig {
|
||||
self.os_partitions = self.os_partitions.take().or(other.os_partitions);
|
||||
self.tor_control = self.tor_control.take().or(other.tor_control);
|
||||
self.tor_socks = self.tor_socks.take().or(other.tor_socks);
|
||||
self.dns_bind = self.dns_bind.take().or(other.dns_bind);
|
||||
self.revision_cache_size = self
|
||||
.revision_cache_size
|
||||
.take()
|
||||
|
||||
@@ -26,9 +26,9 @@ use crate::auth::Sessions;
|
||||
use crate::context::config::ServerConfig;
|
||||
use crate::db::model::Database;
|
||||
use crate::disk::OsPartitionInfo;
|
||||
use crate::init::check_time_is_synchronized;
|
||||
use crate::init::{check_time_is_synchronized, InitResult};
|
||||
use crate::lxc::{ContainerId, LxcContainer, LxcManager};
|
||||
use crate::net::net_controller::{NetController, PreInitNetController};
|
||||
use crate::net::net_controller::{NetController, NetService};
|
||||
use crate::net::utils::{find_eth_iface, find_wifi_iface};
|
||||
use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter};
|
||||
use crate::net::wifi::WpaCli;
|
||||
@@ -53,6 +53,7 @@ pub struct RpcContextSeed {
|
||||
pub sync_db: watch::Sender<u64>,
|
||||
pub account: RwLock<AccountInfo>,
|
||||
pub net_controller: Arc<NetController>,
|
||||
pub os_net_service: NetService,
|
||||
pub s9pk_arch: Option<&'static str>,
|
||||
pub services: ServiceMap,
|
||||
pub metrics_cache: RwLock<Option<crate::system::Metrics>>,
|
||||
@@ -119,7 +120,7 @@ impl RpcContext {
|
||||
webserver: &WebServerAcceptorSetter<UpgradableListener>,
|
||||
config: &ServerConfig,
|
||||
disk_guid: Arc<String>,
|
||||
net_ctrl: Option<PreInitNetController>,
|
||||
init_result: Option<InitResult>,
|
||||
InitRpcContextPhases {
|
||||
mut load_db,
|
||||
mut init_net_ctrl,
|
||||
@@ -133,7 +134,7 @@ impl RpcContext {
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
load_db.start();
|
||||
let db = if let Some(net_ctrl) = &net_ctrl {
|
||||
let db = if let Some(InitResult { net_ctrl, .. }) = &init_result {
|
||||
net_ctrl.db.clone()
|
||||
} else {
|
||||
TypedPatchDb::<Database>::load(config.db().await?).await?
|
||||
@@ -144,31 +145,28 @@ impl RpcContext {
|
||||
tracing::info!("Opened PatchDB");
|
||||
|
||||
init_net_ctrl.start();
|
||||
let net_controller = Arc::new(
|
||||
NetController::init(
|
||||
if let Some(net_ctrl) = net_ctrl {
|
||||
net_ctrl
|
||||
} else {
|
||||
let net_ctrl = PreInitNetController::init(
|
||||
db.clone(),
|
||||
config
|
||||
.tor_control
|
||||
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))),
|
||||
tor_proxy,
|
||||
&account.hostname,
|
||||
account.tor_key.clone(),
|
||||
)
|
||||
.await?;
|
||||
webserver.try_upgrade(|a| net_ctrl.net_iface.upgrade_listener(a))?;
|
||||
net_ctrl
|
||||
},
|
||||
config
|
||||
.dns_bind
|
||||
.as_deref()
|
||||
.unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
let (net_controller, os_net_service) = if let Some(InitResult {
|
||||
net_ctrl,
|
||||
os_net_service,
|
||||
}) = init_result
|
||||
{
|
||||
(net_ctrl, os_net_service)
|
||||
} else {
|
||||
let net_ctrl = Arc::new(
|
||||
NetController::init(
|
||||
db.clone(),
|
||||
config
|
||||
.tor_control
|
||||
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))),
|
||||
tor_proxy,
|
||||
&account.hostname,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
webserver.try_upgrade(|a| net_ctrl.net_iface.upgrade_listener(a))?;
|
||||
let os_net_service = net_ctrl.os_bindings().await?;
|
||||
(net_ctrl, os_net_service)
|
||||
};
|
||||
init_net_ctrl.complete();
|
||||
tracing::info!("Initialized Net Controller");
|
||||
|
||||
@@ -230,6 +228,7 @@ impl RpcContext {
|
||||
db,
|
||||
account: RwLock::new(account),
|
||||
net_controller,
|
||||
os_net_service,
|
||||
s9pk_arch: if config.multi_arch_s9pks.unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -40,7 +40,7 @@ lazy_static::lazy_static! {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetupResult {
|
||||
pub tor_address: String,
|
||||
pub tor_addresses: Vec<String>,
|
||||
#[ts(type = "string")]
|
||||
pub hostname: Hostname,
|
||||
#[ts(type = "string")]
|
||||
@@ -51,7 +51,11 @@ impl TryFrom<&AccountInfo> for SetupResult {
|
||||
type Error = Error;
|
||||
fn try_from(value: &AccountInfo) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
tor_address: format!("https://{}", value.tor_key.public().get_onion_address()),
|
||||
tor_addresses: value
|
||||
.tor_keys
|
||||
.iter()
|
||||
.map(|tor_key| format!("https://{}", tor_key.public().get_onion_address()))
|
||||
.collect(),
|
||||
hostname: value.hostname.clone(),
|
||||
lan_address: value.hostname.lan_address(),
|
||||
root_ca: String::from_utf8(value.root_ca_cert.to_pem()?)?,
|
||||
|
||||
@@ -10,19 +10,22 @@ use itertools::Itertools;
|
||||
use models::PackageId;
|
||||
use openssl::hash::MessageDigest;
|
||||
use patch_db::{HasModel, Value};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use torut::onion::OnionAddressV3;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::db::model::package::AllPackageData;
|
||||
use crate::net::acme::AcmeProvider;
|
||||
use crate::net::host::address::DomainConfig;
|
||||
use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo};
|
||||
use crate::net::host::Host;
|
||||
use crate::net::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::FullProgress;
|
||||
use crate::system::SmtpValue;
|
||||
use crate::util::cpupower::Governor;
|
||||
use crate::util::lshw::LshwDevice;
|
||||
use crate::util::serde::MaybeUtf8String;
|
||||
use crate::version::{Current, VersionT};
|
||||
use crate::{ARCH, PLATFORM};
|
||||
|
||||
@@ -38,7 +41,6 @@ pub struct Public {
|
||||
}
|
||||
impl Public {
|
||||
pub fn init(account: &AccountInfo) -> Result<Self, Error> {
|
||||
let lan_address = account.hostname.lan_address().parse().unwrap();
|
||||
Ok(Self {
|
||||
server_info: ServerInfo {
|
||||
arch: get_arch(),
|
||||
@@ -46,14 +48,42 @@ impl Public {
|
||||
id: account.server_id.clone(),
|
||||
version: Current::default().semver(),
|
||||
hostname: account.hostname.no_dot_host_name(),
|
||||
host: Host {
|
||||
bindings: [(
|
||||
80,
|
||||
BindInfo {
|
||||
enabled: false,
|
||||
options: BindOptions {
|
||||
preferred_external_port: 80,
|
||||
add_ssl: Some(AddSslOptions {
|
||||
preferred_external_port: 443,
|
||||
alpn: Some(AlpnInfo::Specified(vec![
|
||||
MaybeUtf8String("http/1.1".into()),
|
||||
MaybeUtf8String("h2".into()),
|
||||
])),
|
||||
}),
|
||||
secure: None,
|
||||
},
|
||||
net: NetInfo {
|
||||
assigned_port: None,
|
||||
assigned_ssl_port: Some(443),
|
||||
public: false,
|
||||
},
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
onions: account
|
||||
.tor_keys
|
||||
.iter()
|
||||
.map(|k| k.public().get_onion_address())
|
||||
.collect(),
|
||||
domains: BTreeMap::new(),
|
||||
hostname_info: BTreeMap::new(),
|
||||
},
|
||||
last_backup: None,
|
||||
package_version_compat: Current::default().compat().clone(),
|
||||
post_init_migration_todos: BTreeSet::new(),
|
||||
lan_address,
|
||||
onion_address: account.tor_key.public().get_onion_address(),
|
||||
tor_address: format!("https://{}", account.tor_key.public().get_onion_address())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
network_interfaces: BTreeMap::new(),
|
||||
acme: BTreeMap::new(),
|
||||
status_info: ServerStatus {
|
||||
@@ -115,6 +145,7 @@ pub struct ServerInfo {
|
||||
pub id: String,
|
||||
#[ts(type = "string")]
|
||||
pub hostname: InternedString,
|
||||
pub host: Host,
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
@@ -123,13 +154,6 @@ pub struct ServerInfo {
|
||||
pub post_init_migration_todos: BTreeSet<Version>,
|
||||
#[ts(type = "string | null")]
|
||||
pub last_backup: Option<DateTime<Utc>>,
|
||||
#[ts(type = "string")]
|
||||
pub lan_address: Url,
|
||||
#[ts(type = "string")]
|
||||
pub onion_address: OnionAddressV3,
|
||||
/// for backwards compatibility
|
||||
#[ts(type = "string")]
|
||||
pub tor_address: Url,
|
||||
#[ts(as = "BTreeMap::<String, NetworkInterfaceInfo>")]
|
||||
#[serde(default)]
|
||||
pub network_interfaces: BTreeMap<InternedString, NetworkInterfaceInfo>,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::io::Cursor;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use axum::extract::ws::{self};
|
||||
@@ -25,7 +26,8 @@ use crate::db::model::public::ServerStatus;
|
||||
use crate::db::model::Database;
|
||||
use crate::disk::mount::util::unmount;
|
||||
use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH;
|
||||
use crate::net::net_controller::PreInitNetController;
|
||||
use crate::net::net_controller::{NetController, NetService};
|
||||
use crate::net::utils::find_wifi_iface;
|
||||
use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter};
|
||||
use crate::prelude::*;
|
||||
use crate::progress::{
|
||||
@@ -197,7 +199,8 @@ pub async fn init_postgres(datadir: impl AsRef<Path>) -> Result<(), Error> {
|
||||
}
|
||||
|
||||
pub struct InitResult {
|
||||
pub net_ctrl: PreInitNetController,
|
||||
pub net_ctrl: Arc<NetController>,
|
||||
pub os_net_service: NetService,
|
||||
}
|
||||
|
||||
pub struct InitPhases {
|
||||
@@ -347,19 +350,21 @@ pub async fn init(
|
||||
let account = AccountInfo::load(&peek)?;
|
||||
|
||||
start_net.start();
|
||||
let net_ctrl = PreInitNetController::init(
|
||||
db.clone(),
|
||||
cfg.tor_control
|
||||
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))),
|
||||
cfg.tor_socks.unwrap_or(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
9050,
|
||||
))),
|
||||
&account.hostname,
|
||||
account.tor_key,
|
||||
)
|
||||
.await?;
|
||||
let net_ctrl = Arc::new(
|
||||
NetController::init(
|
||||
db.clone(),
|
||||
cfg.tor_control
|
||||
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))),
|
||||
cfg.tor_socks.unwrap_or(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
9050,
|
||||
))),
|
||||
&account.hostname,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
webserver.try_upgrade(|a| net_ctrl.net_iface.upgrade_listener(a))?;
|
||||
let os_net_service = net_ctrl.os_bindings().await?;
|
||||
start_net.complete();
|
||||
|
||||
mount_logs.start();
|
||||
@@ -394,8 +399,6 @@ pub async fn init(
|
||||
mount_logs.complete();
|
||||
tracing::info!("Mounted Logs");
|
||||
|
||||
let mut server_info = peek.as_public().as_server_info().de()?;
|
||||
|
||||
load_ca_cert.start();
|
||||
// write to ca cert store
|
||||
tokio::fs::write(
|
||||
@@ -423,7 +426,15 @@ pub async fn init(
|
||||
load_ca_cert.complete();
|
||||
|
||||
load_wifi.start();
|
||||
crate::net::wifi::synchronize_network_manager(MAIN_DATA, &mut server_info.wifi).await?;
|
||||
let wifi_interface = find_wifi_iface().await?;
|
||||
let wifi = db
|
||||
.mutate(|db| {
|
||||
let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut();
|
||||
wifi.as_interface_mut().ser(&wifi_interface)?;
|
||||
wifi.de()
|
||||
})
|
||||
.await?;
|
||||
crate::net::wifi::synchronize_network_manager(MAIN_DATA, &wifi).await?;
|
||||
load_wifi.complete();
|
||||
tracing::info!("Synchronized WiFi");
|
||||
|
||||
@@ -448,8 +459,10 @@ pub async fn init(
|
||||
crate::disk::mount::util::bind(&tmp_docker, CONTAINER_DATADIR, false).await?;
|
||||
init_tmp.complete();
|
||||
|
||||
let server_info = db.peek().await.into_public().into_server_info();
|
||||
set_governor.start();
|
||||
let governor = if let Some(governor) = &server_info.governor {
|
||||
let selected_governor = server_info.as_governor().de()?;
|
||||
let governor = if let Some(governor) = &selected_governor {
|
||||
if cpupower::get_available_governors()
|
||||
.await?
|
||||
.contains(governor)
|
||||
@@ -470,11 +483,11 @@ pub async fn init(
|
||||
set_governor.complete();
|
||||
|
||||
sync_clock.start();
|
||||
server_info.ntp_synced = false;
|
||||
let mut ntp_synced = false;
|
||||
let mut not_made_progress = 0u32;
|
||||
for _ in 0..1800 {
|
||||
if check_time_is_synchronized().await? {
|
||||
server_info.ntp_synced = true;
|
||||
ntp_synced = true;
|
||||
break;
|
||||
}
|
||||
let t = SystemTime::now();
|
||||
@@ -491,7 +504,7 @@ pub async fn init(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !server_info.ntp_synced {
|
||||
if !ntp_synced {
|
||||
tracing::warn!("Timed out waiting for system time to synchronize");
|
||||
} else {
|
||||
tracing::info!("Syncronized system clock");
|
||||
@@ -499,15 +512,16 @@ pub async fn init(
|
||||
sync_clock.complete();
|
||||
|
||||
enable_zram.start();
|
||||
if server_info.zram {
|
||||
crate::system::enable_zram().await?
|
||||
if server_info.as_zram().de()? {
|
||||
crate::system::enable_zram().await?;
|
||||
tracing::info!("Enabled ZRAM");
|
||||
}
|
||||
enable_zram.complete();
|
||||
|
||||
update_server_info.start();
|
||||
server_info.ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
||||
server_info.devices = lshw().await?;
|
||||
server_info.status_info = ServerStatus {
|
||||
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
||||
let devices = lshw().await?;
|
||||
let status_info = ServerStatus {
|
||||
updated: false,
|
||||
update_progress: None,
|
||||
backup_progress: None,
|
||||
@@ -515,10 +529,15 @@ pub async fn init(
|
||||
restarting: false,
|
||||
};
|
||||
db.mutate(|v| {
|
||||
v.as_public_mut().as_server_info_mut().ser(&server_info)?;
|
||||
let server_info = v.as_public_mut().as_server_info_mut();
|
||||
server_info.as_ntp_synced_mut().ser(&ntp_synced)?;
|
||||
server_info.as_ram_mut().ser(&ram)?;
|
||||
server_info.as_devices_mut().ser(&devices)?;
|
||||
server_info.as_status_info_mut().ser(&status_info)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
tracing::info!("Updated server info");
|
||||
update_server_info.complete();
|
||||
|
||||
launch_service_network.start();
|
||||
@@ -527,6 +546,7 @@ pub async fn init(
|
||||
.arg("lxc-net.service")
|
||||
.invoke(ErrorKind::Lxc)
|
||||
.await?;
|
||||
tracing::info!("Launched service intranet");
|
||||
launch_service_network.complete();
|
||||
|
||||
validate_db.start();
|
||||
@@ -535,6 +555,7 @@ pub async fn init(
|
||||
d.ser(&model)
|
||||
})
|
||||
.await?;
|
||||
tracing::info!("Validated database");
|
||||
validate_db.complete();
|
||||
|
||||
if let Some(progress) = postinit {
|
||||
@@ -543,7 +564,10 @@ pub async fn init(
|
||||
|
||||
tracing::info!("System initialized.");
|
||||
|
||||
Ok(InitResult { net_ctrl })
|
||||
Ok(InitResult {
|
||||
net_ctrl,
|
||||
os_net_service,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init_api<C: Context>() -> ParentHandler<C> {
|
||||
|
||||
@@ -87,6 +87,7 @@ use crate::context::{
|
||||
CliContext, DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext,
|
||||
};
|
||||
use crate::disk::fsck::RequiresReboot;
|
||||
use crate::net::net;
|
||||
use crate::registry::context::{RegistryContext, RegistryUrlParams};
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
|
||||
@@ -313,7 +314,7 @@ pub fn server<C: Context>() -> ParentHandler<C> {
|
||||
.no_display()
|
||||
.with_about("Remove system smtp server and credentials")
|
||||
.with_call_remote::<CliContext>()
|
||||
)
|
||||
).subcommand("host", net::host::server_host_api::<C>().with_about("Commands for modifying the host for the system ui"))
|
||||
}
|
||||
|
||||
pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
@@ -427,7 +428,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
.subcommand("attach", from_fn_async(service::cli_attach).no_display())
|
||||
.subcommand(
|
||||
"host",
|
||||
net::host::host::<C>().with_about("Manage network hosts for a package"),
|
||||
net::host::host_api::<C>().with_about("Manage network hosts for a package"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use axum::body::Body;
|
||||
use axum::extract::Request;
|
||||
use axum::response::Response;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use http::{HeaderMap, HeaderValue, Method};
|
||||
use rpc_toolkit::{Empty, Middleware};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -52,6 +53,13 @@ impl<Context: Send + Sync + 'static> Middleware<Context> for Cors {
|
||||
request: &mut Request,
|
||||
) -> Result<(), Response> {
|
||||
self.get_cors_headers(request);
|
||||
if request.method() == Method::OPTIONS {
|
||||
let mut response = Response::new(Body::empty());
|
||||
response
|
||||
.headers_mut()
|
||||
.extend(std::mem::take(&mut self.headers));
|
||||
return Err(response);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn process_http_response(&mut self, _: &Context, response: &mut Response) {
|
||||
|
||||
@@ -173,16 +173,26 @@ impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> {
|
||||
}
|
||||
|
||||
pub fn acme<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new().subcommand(
|
||||
"init",
|
||||
from_fn_async(init)
|
||||
.no_display()
|
||||
.with_about("Setup ACME certificate acquisition")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"init",
|
||||
from_fn_async(init)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Setup ACME certificate acquisition")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Setup ACME certificate acquisition")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub struct AcmeProvider(pub Url);
|
||||
impl FromStr for AcmeProvider {
|
||||
@@ -193,9 +203,31 @@ impl FromStr for AcmeProvider {
|
||||
"letsencrypt-staging" => async_acme::acme::LETS_ENCRYPT_STAGING_DIRECTORY.parse(),
|
||||
s => s.parse(),
|
||||
}
|
||||
.map(|mut u: Url| {
|
||||
let path = u
|
||||
.path_segments()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(|p| p.to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
if let Ok(mut path_mut) = u.path_segments_mut() {
|
||||
path_mut.clear();
|
||||
path_mut.extend(path);
|
||||
}
|
||||
u
|
||||
})
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for AcmeProvider {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
crate::util::serde::deserialize_from_str(deserializer)
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for AcmeProvider {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
@@ -230,3 +262,24 @@ pub async fn init(
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
pub struct RemoveAcmeParams {
|
||||
#[arg(long)]
|
||||
pub provider: AcmeProvider,
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
ctx: RpcContext,
|
||||
RemoveAcmeParams { provider }: RemoveAcmeParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_acme_mut()
|
||||
.remove(&provider)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -19,6 +19,7 @@ use trust_dns_server::server::{Request, RequestHandler, ResponseHandler, Respons
|
||||
use trust_dns_server::ServerFuture;
|
||||
|
||||
use crate::net::forward::START9_BRIDGE_IFACE;
|
||||
use crate::util::sync::Watch;
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
@@ -140,38 +141,46 @@ impl RequestHandler for Resolver {
|
||||
|
||||
impl DnsController {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init(bind: &[SocketAddr]) -> Result<Self, Error> {
|
||||
pub async fn init(mut lxcbr_status: Watch<bool>) -> Result<Self, Error> {
|
||||
let services = Arc::new(RwLock::new(BTreeMap::new()));
|
||||
|
||||
let mut server = ServerFuture::new(Resolver {
|
||||
services: services.clone(),
|
||||
});
|
||||
server.register_listener(
|
||||
TcpListener::bind(bind)
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?,
|
||||
Duration::from_secs(30),
|
||||
);
|
||||
server.register_socket(UdpSocket::bind(bind).await.with_kind(ErrorKind::Network)?);
|
||||
|
||||
Command::new("resolvectl")
|
||||
.arg("dns")
|
||||
.arg(START9_BRIDGE_IFACE)
|
||||
.arg("127.0.0.1")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
Command::new("resolvectl")
|
||||
.arg("domain")
|
||||
.arg(START9_BRIDGE_IFACE)
|
||||
.arg("embassy")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
let dns_server = tokio::spawn(async move {
|
||||
server.register_listener(
|
||||
TcpListener::bind((Ipv4Addr::LOCALHOST, 53))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?,
|
||||
Duration::from_secs(30),
|
||||
);
|
||||
server.register_socket(
|
||||
UdpSocket::bind((Ipv4Addr::LOCALHOST, 53))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?,
|
||||
);
|
||||
|
||||
lxcbr_status.wait_for(|a| *a).await;
|
||||
|
||||
Command::new("resolvectl")
|
||||
.arg("dns")
|
||||
.arg(START9_BRIDGE_IFACE)
|
||||
.arg("127.0.0.1")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
Command::new("resolvectl")
|
||||
.arg("domain")
|
||||
.arg(START9_BRIDGE_IFACE)
|
||||
.arg("embassy")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
|
||||
let dns_server = tokio::spawn(
|
||||
server
|
||||
.block_until_done()
|
||||
.map_err(|e| Error::new(e, ErrorKind::Network)),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::new(e, ErrorKind::Network))
|
||||
})
|
||||
.into();
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use models::{HostId, PackageId};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use torut::onion::OnionAddressV3;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::net::acme::AcmeProvider;
|
||||
use crate::net::host::{all_hosts, HostApiKind};
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde};
|
||||
|
||||
@@ -35,19 +38,51 @@ pub struct DomainConfig {
|
||||
pub acme: Option<AcmeProvider>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
pub struct AddressApiParams {
|
||||
host: HostId,
|
||||
fn check_duplicates(db: &DatabaseModel) -> Result<(), Error> {
|
||||
let mut onions = BTreeSet::<OnionAddressV3>::new();
|
||||
let mut domains = BTreeSet::<InternedString>::new();
|
||||
let mut check_onion = |onion: OnionAddressV3| {
|
||||
if onions.contains(&onion) {
|
||||
return Err(Error::new(
|
||||
eyre!("onion address {onion} is already in use"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
onions.insert(onion);
|
||||
Ok(())
|
||||
};
|
||||
let mut check_domain = |domain: InternedString| {
|
||||
if domains.contains(&domain) {
|
||||
return Err(Error::new(
|
||||
eyre!("domain {domain} is already in use"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
domains.insert(domain);
|
||||
Ok(())
|
||||
};
|
||||
for host in all_hosts(db) {
|
||||
let host = host?;
|
||||
for onion in host.as_onions().de()? {
|
||||
check_onion(onion)?;
|
||||
}
|
||||
for domain in host.as_domains().keys()? {
|
||||
check_domain(domain)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn address<C: Context>() -> ParentHandler<C, AddressApiParams, PackageId> {
|
||||
ParentHandler::<C, AddressApiParams, PackageId>::new()
|
||||
pub fn address_api<C: Context, Kind: HostApiKind>(
|
||||
) -> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
|
||||
ParentHandler::<C, Kind::Params, Kind::InheritedParams>::new()
|
||||
.subcommand(
|
||||
"domain",
|
||||
ParentHandler::<C, Empty, (PackageId, HostId)>::new()
|
||||
ParentHandler::<C, Empty, Kind::Inheritance>::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_domain)
|
||||
from_fn_async(add_domain::<Kind>)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(|_, a| a)
|
||||
.no_display()
|
||||
.with_about("Add an address to this host")
|
||||
@@ -55,20 +90,22 @@ pub fn address<C: Context>() -> ParentHandler<C, AddressApiParams, PackageId> {
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_domain)
|
||||
from_fn_async(remove_domain::<Kind>)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(|_, a| a)
|
||||
.no_display()
|
||||
.with_about("Remove an address from this host")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.with_inherited(|AddressApiParams { host }, package| (package, host)),
|
||||
.with_inherited(Kind::inheritance),
|
||||
)
|
||||
.subcommand(
|
||||
"onion",
|
||||
ParentHandler::<C, Empty, (PackageId, HostId)>::new()
|
||||
ParentHandler::<C, Empty, Kind::Inheritance>::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_onion)
|
||||
from_fn_async(add_onion::<Kind>)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(|_, a| a)
|
||||
.no_display()
|
||||
.with_about("Add an address to this host")
|
||||
@@ -76,18 +113,19 @@ pub fn address<C: Context>() -> ParentHandler<C, AddressApiParams, PackageId> {
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_onion)
|
||||
from_fn_async(remove_onion::<Kind>)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(|_, a| a)
|
||||
.no_display()
|
||||
.with_about("Remove an address from this host")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.with_inherited(|AddressApiParams { host }, package| (package, host)),
|
||||
.with_inherited(Kind::inheritance),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_addresses)
|
||||
.with_inherited(|AddressApiParams { host }, package| (package, host))
|
||||
from_fn_async(list_addresses::<Kind>)
|
||||
.with_inherited(Kind::inheritance)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
|
||||
use prettytable::*;
|
||||
@@ -136,14 +174,14 @@ pub struct AddDomainParams {
|
||||
pub acme: Option<AcmeProvider>,
|
||||
}
|
||||
|
||||
pub async fn add_domain(
|
||||
pub async fn add_domain<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
AddDomainParams {
|
||||
domain,
|
||||
private,
|
||||
acme,
|
||||
}: AddDomainParams,
|
||||
(package, host): (PackageId, HostId),
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
@@ -153,13 +191,7 @@ pub async fn add_domain(
|
||||
}
|
||||
}
|
||||
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package)
|
||||
.or_not_found(&package)?
|
||||
.as_hosts_mut()
|
||||
.as_idx_mut(&host)
|
||||
.or_not_found(&host)?
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_domains_mut()
|
||||
.insert(
|
||||
&domain,
|
||||
@@ -167,12 +199,11 @@ pub async fn add_domain(
|
||||
public: !private,
|
||||
acme,
|
||||
},
|
||||
)
|
||||
)?;
|
||||
check_duplicates(db)
|
||||
})
|
||||
.await?;
|
||||
let service = ctx.services.get(&package).await;
|
||||
let service_ref = service.as_ref().or_not_found(&package)?;
|
||||
service_ref.update_host(host).await?;
|
||||
Kind::sync_host(&ctx, inheritance).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -182,27 +213,19 @@ pub struct RemoveDomainParams {
|
||||
pub domain: InternedString,
|
||||
}
|
||||
|
||||
pub async fn remove_domain(
|
||||
pub async fn remove_domain<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
RemoveDomainParams { domain }: RemoveDomainParams,
|
||||
(package, host): (PackageId, HostId),
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package)
|
||||
.or_not_found(&package)?
|
||||
.as_hosts_mut()
|
||||
.as_idx_mut(&host)
|
||||
.or_not_found(&host)?
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_domains_mut()
|
||||
.remove(&domain)
|
||||
})
|
||||
.await?;
|
||||
let service = ctx.services.get(&package).await;
|
||||
let service_ref = service.as_ref().or_not_found(&package)?;
|
||||
service_ref.update_host(host).await?;
|
||||
Kind::sync_host(&ctx, inheritance).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -212,10 +235,10 @@ pub struct OnionParams {
|
||||
pub onion: String,
|
||||
}
|
||||
|
||||
pub async fn add_onion(
|
||||
pub async fn add_onion<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
OnionParams { onion }: OnionParams,
|
||||
(package, host): (PackageId, HostId),
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
let onion = onion
|
||||
.strip_suffix(".onion")
|
||||
@@ -230,28 +253,22 @@ pub async fn add_onion(
|
||||
.mutate(|db| {
|
||||
db.as_private().as_key_store().as_onion().get_key(&onion)?;
|
||||
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package)
|
||||
.or_not_found(&package)?
|
||||
.as_hosts_mut()
|
||||
.as_idx_mut(&host)
|
||||
.or_not_found(&host)?
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_onions_mut()
|
||||
.mutate(|a| Ok(a.insert(onion)))
|
||||
.mutate(|a| Ok(a.insert(onion)))?;
|
||||
check_duplicates(db)
|
||||
})
|
||||
.await?;
|
||||
let service = ctx.services.get(&package).await;
|
||||
let service_ref = service.as_ref().or_not_found(&package)?;
|
||||
service_ref.update_host(host).await?;
|
||||
|
||||
Kind::sync_host(&ctx, inheritance).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_onion(
|
||||
pub async fn remove_onion<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
OnionParams { onion }: OnionParams,
|
||||
(package, host): (PackageId, HostId),
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
let onion = onion
|
||||
.strip_suffix(".onion")
|
||||
@@ -264,40 +281,23 @@ pub async fn remove_onion(
|
||||
.parse::<OnionAddressV3>()?;
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package)
|
||||
.or_not_found(&package)?
|
||||
.as_hosts_mut()
|
||||
.as_idx_mut(&host)
|
||||
.or_not_found(&host)?
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_onions_mut()
|
||||
.mutate(|a| Ok(a.remove(&onion)))
|
||||
})
|
||||
.await?;
|
||||
let service = ctx.services.get(&package).await;
|
||||
let service_ref = service.as_ref().or_not_found(&package)?;
|
||||
service_ref.update_host(host).await?;
|
||||
|
||||
Kind::sync_host(&ctx, inheritance).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_addresses(
|
||||
pub async fn list_addresses<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
(package, host): (PackageId, HostId),
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<Vec<HostAddress>, Error> {
|
||||
Ok(ctx
|
||||
.db
|
||||
.peek()
|
||||
.await
|
||||
.into_public()
|
||||
.into_package_data()
|
||||
.into_idx(&package)
|
||||
.or_not_found(&package)?
|
||||
.into_hosts()
|
||||
.into_idx(&host)
|
||||
.or_not_found(&host)?
|
||||
Ok(Kind::host_for(&inheritance, &mut ctx.db.peek().await)?
|
||||
.de()?
|
||||
.addresses()
|
||||
.collect())
|
||||
|
||||
@@ -3,13 +3,14 @@ use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use clap::Parser;
|
||||
use models::{FromStrParser, HostId, PackageId};
|
||||
use models::{FromStrParser, HostId};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::net::forward::AvailablePorts;
|
||||
use crate::net::host::HostApiKind;
|
||||
use crate::net::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde};
|
||||
@@ -146,17 +147,13 @@ pub struct AddSslOptions {
|
||||
pub alpn: Option<AlpnInfo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
pub struct BindingApiParams {
|
||||
host: HostId,
|
||||
}
|
||||
|
||||
pub fn binding<C: Context>() -> ParentHandler<C, BindingApiParams, PackageId> {
|
||||
ParentHandler::<C, BindingApiParams, PackageId>::new()
|
||||
pub fn binding<C: Context, Kind: HostApiKind>(
|
||||
) -> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
|
||||
ParentHandler::<C, Kind::Params, Kind::InheritedParams>::new()
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_bindings)
|
||||
.with_inherited(|BindingApiParams { host }, package| (package, host))
|
||||
from_fn_async(list_bindings::<Kind>)
|
||||
.with_inherited(Kind::inheritance)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
|
||||
use prettytable::*;
|
||||
@@ -194,30 +191,22 @@ pub fn binding<C: Context>() -> ParentHandler<C, BindingApiParams, PackageId> {
|
||||
)
|
||||
.subcommand(
|
||||
"set-public",
|
||||
from_fn_async(set_public)
|
||||
.with_inherited(|BindingApiParams { host }, package| (package, host))
|
||||
from_fn_async(set_public::<Kind>)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(Kind::inheritance)
|
||||
.no_display()
|
||||
.with_about("Add an binding to this host")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn list_bindings(
|
||||
pub async fn list_bindings<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
(package, host): (PackageId, HostId),
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<BTreeMap<u16, BindInfo>, Error> {
|
||||
ctx.db
|
||||
.peek()
|
||||
.await
|
||||
.into_public()
|
||||
.into_package_data()
|
||||
.into_idx(&package)
|
||||
.or_not_found(&package)?
|
||||
.into_hosts()
|
||||
.into_idx(&host)
|
||||
.or_not_found(&host)?
|
||||
.into_bindings()
|
||||
Kind::host_for(&inheritance, &mut ctx.db.peek().await)?
|
||||
.as_bindings()
|
||||
.de()
|
||||
}
|
||||
|
||||
@@ -230,23 +219,17 @@ pub struct BindingSetPublicParams {
|
||||
public: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn set_public(
|
||||
pub async fn set_public<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
BindingSetPublicParams {
|
||||
internal_port,
|
||||
public,
|
||||
}: BindingSetPublicParams,
|
||||
(package, host): (PackageId, HostId),
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package)
|
||||
.or_not_found(&package)?
|
||||
.as_hosts_mut()
|
||||
.as_idx_mut(&host)
|
||||
.or_not_found(&host)?
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_bindings_mut()
|
||||
.mutate(|b| {
|
||||
b.get_mut(&internal_port)
|
||||
@@ -257,11 +240,5 @@ pub async fn set_public(
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
ctx.services
|
||||
.get(&package)
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(&package)?
|
||||
.update_host(host)
|
||||
.await
|
||||
Kind::sync_host(&ctx, inheritance).await
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::future::Future;
|
||||
use std::panic::RefUnwindSafe;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use models::{HostId, PackageId};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, OrEmpty, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use torut::onion::OnionAddressV3;
|
||||
use ts_rs::TS;
|
||||
@@ -11,7 +14,7 @@ use ts_rs::TS;
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::net::forward::AvailablePorts;
|
||||
use crate::net::host::address::{address, DomainConfig, HostAddress};
|
||||
use crate::net::host::address::{address_api, DomainConfig, HostAddress};
|
||||
use crate::net::host::binding::{binding, BindInfo, BindOptions};
|
||||
use crate::net::service_interface::HostnameInfo;
|
||||
use crate::prelude::*;
|
||||
@@ -19,12 +22,11 @@ use crate::prelude::*;
|
||||
pub mod address;
|
||||
pub mod binding;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct Host {
|
||||
pub kind: HostKind,
|
||||
pub bindings: BTreeMap<u16, BindInfo>,
|
||||
#[ts(type = "string[]")]
|
||||
pub onions: BTreeSet<OnionAddressV3>,
|
||||
@@ -39,14 +41,8 @@ impl AsRef<Host> for Host {
|
||||
}
|
||||
}
|
||||
impl Host {
|
||||
pub fn new(kind: HostKind) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
bindings: BTreeMap::new(),
|
||||
onions: BTreeSet::new(),
|
||||
domains: BTreeMap::new(),
|
||||
hostname_info: BTreeMap::new(),
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a {
|
||||
self.onions
|
||||
@@ -67,15 +63,6 @@ impl Host {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum HostKind {
|
||||
Multi,
|
||||
// Single,
|
||||
// Static,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
@@ -94,10 +81,12 @@ impl Map for Hosts {
|
||||
|
||||
pub fn host_for<'a>(
|
||||
db: &'a mut DatabaseModel,
|
||||
package_id: &PackageId,
|
||||
package_id: Option<&PackageId>,
|
||||
host_id: &HostId,
|
||||
host_kind: HostKind,
|
||||
) -> Result<&'a mut Model<Host>, Error> {
|
||||
let Some(package_id) = package_id else {
|
||||
return Ok(db.as_public_mut().as_server_info_mut().as_host_mut());
|
||||
};
|
||||
fn host_info<'a>(
|
||||
db: &'a mut DatabaseModel,
|
||||
package_id: &PackageId,
|
||||
@@ -121,7 +110,7 @@ pub fn host_for<'a>(
|
||||
None
|
||||
};
|
||||
host_info(db, package_id)?.upsert(host_id, || {
|
||||
let mut h = Host::new(host_kind);
|
||||
let mut h = Host::new();
|
||||
h.onions.insert(
|
||||
tor_key
|
||||
.or_not_found("generated tor key")?
|
||||
@@ -132,12 +121,20 @@ pub fn host_for<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn all_hosts(db: &DatabaseModel) -> impl Iterator<Item = Result<&Model<Host>, Error>> {
|
||||
[Ok(db.as_public().as_server_info().as_host())]
|
||||
.into_iter()
|
||||
.chain(
|
||||
[db.as_public().as_package_data().as_entries()]
|
||||
.into_iter()
|
||||
.flatten_ok()
|
||||
.map(|entry| entry.and_then(|(_, v)| v.as_hosts().as_entries()))
|
||||
.flatten_ok()
|
||||
.map_ok(|(_, v)| v),
|
||||
)
|
||||
}
|
||||
|
||||
impl Model<Host> {
|
||||
pub fn set_kind(&mut self, kind: HostKind) -> Result<(), Error> {
|
||||
match (self.as_kind().de()?, kind) {
|
||||
(HostKind::Multi, HostKind::Multi) => Ok(()),
|
||||
}
|
||||
}
|
||||
pub fn add_binding(
|
||||
&mut self,
|
||||
available_ports: &mut AvailablePorts,
|
||||
@@ -157,16 +154,78 @@ impl Model<Host> {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
pub struct HostParams {
|
||||
pub struct RequiresPackageId {
|
||||
package: PackageId,
|
||||
}
|
||||
|
||||
pub fn host<C: Context>() -> ParentHandler<C, HostParams> {
|
||||
ParentHandler::<C, HostParams>::new()
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
pub struct RequiresHostId {
|
||||
host: HostId,
|
||||
}
|
||||
|
||||
pub trait HostApiKind: 'static {
|
||||
type Params: Send + Sync + 'static;
|
||||
type InheritedParams: Send + Sync + 'static;
|
||||
type Inheritance: RefUnwindSafe + OrEmpty<Self::Inheritance> + Send + Sync + 'static;
|
||||
fn inheritance(params: Self::Params, inherited: Self::InheritedParams) -> Self::Inheritance;
|
||||
fn host_for<'a>(
|
||||
inheritance: &Self::Inheritance,
|
||||
db: &'a mut DatabaseModel,
|
||||
) -> Result<&'a mut Model<Host>, Error>;
|
||||
fn sync_host(
|
||||
ctx: &RpcContext,
|
||||
inheritance: Self::Inheritance,
|
||||
) -> impl Future<Output = Result<(), Error>> + Send;
|
||||
}
|
||||
pub struct ForPackage;
|
||||
impl HostApiKind for ForPackage {
|
||||
type Params = RequiresHostId;
|
||||
type InheritedParams = PackageId;
|
||||
type Inheritance = (PackageId, HostId);
|
||||
fn inheritance(
|
||||
RequiresHostId { host }: Self::Params,
|
||||
package: Self::InheritedParams,
|
||||
) -> Self::Inheritance {
|
||||
(package, host)
|
||||
}
|
||||
fn host_for<'a>(
|
||||
(package, host): &Self::Inheritance,
|
||||
db: &'a mut DatabaseModel,
|
||||
) -> Result<&'a mut Model<Host>, Error> {
|
||||
host_for(db, Some(package), host)
|
||||
}
|
||||
async fn sync_host(ctx: &RpcContext, (package, host): Self::Inheritance) -> Result<(), Error> {
|
||||
let service = ctx.services.get(&package).await;
|
||||
let service_ref = service.as_ref().or_not_found(&package)?;
|
||||
service_ref.sync_host(host).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
pub struct ForServer;
|
||||
impl HostApiKind for ForServer {
|
||||
type Params = Empty;
|
||||
type InheritedParams = Empty;
|
||||
type Inheritance = Empty;
|
||||
fn inheritance(_: Self::Params, _: Self::InheritedParams) -> Self::Inheritance {
|
||||
Empty {}
|
||||
}
|
||||
fn host_for<'a>(
|
||||
_: &Self::Inheritance,
|
||||
db: &'a mut DatabaseModel,
|
||||
) -> Result<&'a mut Model<Host>, Error> {
|
||||
host_for(db, None, &HostId::default())
|
||||
}
|
||||
async fn sync_host(ctx: &RpcContext, _: Self::Inheritance) -> Result<(), Error> {
|
||||
ctx.os_net_service.sync_host(HostId::default()).await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_api<C: Context>() -> ParentHandler<C, RequiresPackageId> {
|
||||
ParentHandler::<C, RequiresPackageId>::new()
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_hosts)
|
||||
.with_inherited(|HostParams { package }, _| package)
|
||||
.with_inherited(|RequiresPackageId { package }, _| package)
|
||||
.with_custom_display_fn(|_, ids| {
|
||||
for id in ids {
|
||||
println!("{id}")
|
||||
@@ -177,14 +236,21 @@ pub fn host<C: Context>() -> ParentHandler<C, HostParams> {
|
||||
)
|
||||
.subcommand(
|
||||
"address",
|
||||
address::<C>().with_inherited(|HostParams { package }, _| package),
|
||||
address_api::<C, ForPackage>()
|
||||
.with_inherited(|RequiresPackageId { package }, _| package),
|
||||
)
|
||||
.subcommand(
|
||||
"binding",
|
||||
binding::<C>().with_inherited(|HostParams { package }, _| package),
|
||||
binding::<C, ForPackage>().with_inherited(|RequiresPackageId { package }, _| package),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn server_host_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::<C>::new()
|
||||
.subcommand("address", address_api::<C, ForServer>())
|
||||
.subcommand("binding", binding::<C, ForServer>())
|
||||
}
|
||||
|
||||
pub async fn list_hosts(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
|
||||
@@ -21,7 +21,9 @@ impl KeyStore {
|
||||
local_certs: CertStore::new(account)?,
|
||||
acme: AcmeCertStore::new(),
|
||||
};
|
||||
res.onion.insert(account.tor_key.clone());
|
||||
for tor_key in account.tor_keys.iter().cloned() {
|
||||
res.onion.insert(tor_key);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use imbl::OrdMap;
|
||||
use imbl_value::InternedString;
|
||||
use ipnet::IpNet;
|
||||
use models::{HostId, OptionExt, PackageId};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -16,8 +18,8 @@ use crate::hostname::Hostname;
|
||||
use crate::net::dns::DnsController;
|
||||
use crate::net::forward::LanPortForwardController;
|
||||
use crate::net::host::address::HostAddress;
|
||||
use crate::net::host::binding::{BindId, BindOptions};
|
||||
use crate::net::host::{host_for, Host, HostKind, Hosts};
|
||||
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
||||
use crate::net::host::{host_for, Host, Hosts};
|
||||
use crate::net::network_interface::NetworkInterfaceController;
|
||||
use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname};
|
||||
use crate::net::tor::TorController;
|
||||
@@ -27,129 +29,44 @@ use crate::prelude::*;
|
||||
use crate::util::serde::MaybeUtf8String;
|
||||
use crate::HOST_IP;
|
||||
|
||||
pub struct PreInitNetController {
|
||||
pub db: TypedPatchDb<Database>,
|
||||
tor: TorController,
|
||||
vhost: VHostController,
|
||||
pub net_iface: Arc<NetworkInterfaceController>,
|
||||
os_bindings: Vec<Arc<()>>,
|
||||
server_hostnames: Vec<Option<InternedString>>,
|
||||
}
|
||||
impl PreInitNetController {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init(
|
||||
db: TypedPatchDb<Database>,
|
||||
tor_control: SocketAddr,
|
||||
tor_socks: SocketAddr,
|
||||
hostname: &Hostname,
|
||||
os_tor_key: TorSecretKeyV3,
|
||||
) -> Result<Self, Error> {
|
||||
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
|
||||
let mut res = Self {
|
||||
db: db.clone(),
|
||||
tor: TorController::new(tor_control, tor_socks),
|
||||
vhost: VHostController::new(db, net_iface.clone()),
|
||||
net_iface,
|
||||
os_bindings: Vec::new(),
|
||||
server_hostnames: Vec::new(),
|
||||
};
|
||||
res.add_os_bindings(hostname, os_tor_key).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn add_os_bindings(
|
||||
&mut self,
|
||||
hostname: &Hostname,
|
||||
tor_key: TorSecretKeyV3,
|
||||
) -> Result<(), Error> {
|
||||
self.server_hostnames = vec![
|
||||
// LAN IP
|
||||
None,
|
||||
// Internal DNS
|
||||
Some("embassy".into()),
|
||||
Some("startos".into()),
|
||||
// localhost
|
||||
Some("localhost".into()),
|
||||
Some(hostname.no_dot_host_name()),
|
||||
// LAN mDNS
|
||||
Some(hostname.local_domain_name()),
|
||||
];
|
||||
|
||||
let vhost_target = TargetInfo {
|
||||
public: false,
|
||||
acme: None,
|
||||
addr: ([127, 0, 0, 1], 80).into(),
|
||||
connect_ssl: Err(AlpnInfo::Specified(vec![
|
||||
MaybeUtf8String("http/1.1".into()),
|
||||
MaybeUtf8String("h2".into()),
|
||||
])),
|
||||
};
|
||||
|
||||
for hostname in self.server_hostnames.iter().cloned() {
|
||||
self.os_bindings
|
||||
.push(self.vhost.add(hostname, 443, vhost_target.clone())?);
|
||||
}
|
||||
|
||||
// Tor
|
||||
self.os_bindings.push(self.vhost.add(
|
||||
Some(InternedString::from_display(
|
||||
&tor_key.public().get_onion_address(),
|
||||
)),
|
||||
443,
|
||||
vhost_target,
|
||||
)?);
|
||||
self.os_bindings.extend(
|
||||
self.tor
|
||||
.add(
|
||||
tor_key,
|
||||
vec![
|
||||
(80, ([127, 0, 0, 1], 80).into()), // http
|
||||
(443, ([127, 0, 0, 1], 443).into()), // https
|
||||
],
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetController {
|
||||
db: TypedPatchDb<Database>,
|
||||
pub(crate) db: TypedPatchDb<Database>,
|
||||
pub(super) tor: TorController,
|
||||
pub(super) vhost: VHostController,
|
||||
pub net_iface: Arc<NetworkInterfaceController>,
|
||||
pub(crate) net_iface: Arc<NetworkInterfaceController>,
|
||||
pub(super) dns: DnsController,
|
||||
pub(super) forward: LanPortForwardController,
|
||||
pub(super) os_bindings: Vec<Arc<()>>,
|
||||
pub(super) server_hostnames: Vec<Option<InternedString>>,
|
||||
}
|
||||
|
||||
impl NetController {
|
||||
pub async fn init(
|
||||
PreInitNetController {
|
||||
db,
|
||||
tor,
|
||||
vhost,
|
||||
net_iface,
|
||||
os_bindings,
|
||||
server_hostnames,
|
||||
}: PreInitNetController,
|
||||
dns_bind: &[SocketAddr],
|
||||
db: TypedPatchDb<Database>,
|
||||
tor_control: SocketAddr,
|
||||
tor_socks: SocketAddr,
|
||||
hostname: &Hostname,
|
||||
) -> Result<Self, Error> {
|
||||
let mut res = Self {
|
||||
db,
|
||||
tor,
|
||||
vhost,
|
||||
dns: DnsController::init(dns_bind).await?,
|
||||
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
|
||||
Ok(Self {
|
||||
db: db.clone(),
|
||||
tor: TorController::new(tor_control, tor_socks),
|
||||
vhost: VHostController::new(db, net_iface.clone()),
|
||||
dns: DnsController::init(net_iface.lxcbr_status()).await?,
|
||||
forward: LanPortForwardController::new(net_iface.subscribe()),
|
||||
net_iface,
|
||||
os_bindings,
|
||||
server_hostnames,
|
||||
};
|
||||
res.os_bindings
|
||||
.push(res.dns.add(None, HOST_IP.into()).await?);
|
||||
Ok(res)
|
||||
server_hostnames: vec![
|
||||
// LAN IP
|
||||
None,
|
||||
// Internal DNS
|
||||
Some("embassy".into()),
|
||||
Some("startos".into()),
|
||||
// localhost
|
||||
Some("localhost".into()),
|
||||
Some(hostname.no_dot_host_name()),
|
||||
// LAN mDNS
|
||||
Some(hostname.local_domain_name()),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
@@ -160,17 +77,48 @@ impl NetController {
|
||||
) -> Result<NetService, Error> {
|
||||
let dns = self.dns.add(Some(package.clone()), ip).await?;
|
||||
|
||||
let mut res = NetService {
|
||||
shutdown: false,
|
||||
id: package,
|
||||
let res = NetService::new(NetServiceData {
|
||||
id: Some(package),
|
||||
ip,
|
||||
dns,
|
||||
controller: Arc::downgrade(self),
|
||||
binds: BTreeMap::new(),
|
||||
};
|
||||
})?;
|
||||
res.clear_bindings(Default::default()).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn os_bindings(self: &Arc<Self>) -> Result<NetService, Error> {
|
||||
let dns = self.dns.add(None, HOST_IP.into()).await?;
|
||||
|
||||
let service = NetService::new(NetServiceData {
|
||||
id: None,
|
||||
ip: [127, 0, 0, 1].into(),
|
||||
dns,
|
||||
controller: Arc::downgrade(self),
|
||||
binds: BTreeMap::new(),
|
||||
})?;
|
||||
service.clear_bindings(Default::default()).await?;
|
||||
service
|
||||
.bind(
|
||||
HostId::default(),
|
||||
80,
|
||||
BindOptions {
|
||||
preferred_external_port: 80,
|
||||
add_ssl: Some(AddSslOptions {
|
||||
preferred_external_port: 443,
|
||||
alpn: Some(AlpnInfo::Specified(vec![
|
||||
MaybeUtf8String("http/1.1".into()),
|
||||
MaybeUtf8String("h2".into()),
|
||||
])),
|
||||
}),
|
||||
secure: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(service)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -180,15 +128,14 @@ struct HostBinds {
|
||||
tor: BTreeMap<OnionAddressV3, (OrdMap<u16, SocketAddr>, Vec<Arc<()>>)>,
|
||||
}
|
||||
|
||||
pub struct NetService {
|
||||
shutdown: bool,
|
||||
id: PackageId,
|
||||
pub struct NetServiceData {
|
||||
id: Option<PackageId>,
|
||||
ip: Ipv4Addr,
|
||||
dns: Arc<()>,
|
||||
controller: Weak<NetController>,
|
||||
binds: BTreeMap<HostId, HostBinds>,
|
||||
}
|
||||
impl NetService {
|
||||
impl NetServiceData {
|
||||
fn net_controller(&self) -> Result<Arc<NetController>, Error> {
|
||||
Weak::upgrade(&self.controller).ok_or_else(|| {
|
||||
Error::new(
|
||||
@@ -198,49 +145,54 @@ impl NetService {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn bind(
|
||||
async fn clear_bindings(
|
||||
&mut self,
|
||||
kind: HostKind,
|
||||
id: HostId,
|
||||
internal_port: u16,
|
||||
options: BindOptions,
|
||||
ctrl: &NetController,
|
||||
except: BTreeSet<BindId>,
|
||||
) -> Result<(), Error> {
|
||||
crate::dbg!("bind", &kind, &id, internal_port, &options);
|
||||
let pkg_id = &self.id;
|
||||
let host = self
|
||||
.net_controller()?
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let mut ports = db.as_private().as_available_ports().de()?;
|
||||
let host = host_for(db, pkg_id, &id, kind)?;
|
||||
host.add_binding(&mut ports, internal_port, options)?;
|
||||
let host = host.de()?;
|
||||
db.as_private_mut().as_available_ports_mut().ser(&ports)?;
|
||||
Ok(host)
|
||||
})
|
||||
.await?;
|
||||
self.update(id, host).await
|
||||
}
|
||||
|
||||
pub async fn clear_bindings(&mut self, except: BTreeSet<BindId>) -> Result<(), Error> {
|
||||
let pkg_id = &self.id;
|
||||
let hosts = self
|
||||
.net_controller()?
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let mut res = Hosts::default();
|
||||
for (host_id, host) in db
|
||||
.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(pkg_id)
|
||||
.or_not_found(pkg_id)?
|
||||
.as_hosts_mut()
|
||||
.as_entries_mut()?
|
||||
{
|
||||
if let Some(pkg_id) = &self.id {
|
||||
let hosts = ctrl
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let mut res = Hosts::default();
|
||||
for (host_id, host) in db
|
||||
.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(pkg_id)
|
||||
.or_not_found(pkg_id)?
|
||||
.as_hosts_mut()
|
||||
.as_entries_mut()?
|
||||
{
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
for (internal_port, info) in b {
|
||||
if !except.contains(&BindId {
|
||||
id: host_id.clone(),
|
||||
internal_port: *internal_port,
|
||||
}) {
|
||||
info.disable();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
res.0.insert(host_id, host.de()?);
|
||||
}
|
||||
Ok(res)
|
||||
})
|
||||
.await?;
|
||||
let mut errors = ErrorCollection::new();
|
||||
for (id, host) in hosts.0 {
|
||||
errors.handle(self.update(ctrl, id, host).await);
|
||||
}
|
||||
errors.into_result()
|
||||
} else {
|
||||
let host = ctrl
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let host = db.as_public_mut().as_server_info_mut().as_host_mut();
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
for (internal_port, info) in b {
|
||||
if !except.contains(&BindId {
|
||||
id: host_id.clone(),
|
||||
id: HostId::default(),
|
||||
internal_port: *internal_port,
|
||||
}) {
|
||||
info.disable();
|
||||
@@ -248,20 +200,14 @@ impl NetService {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
res.0.insert(host_id, host.de()?);
|
||||
}
|
||||
Ok(res)
|
||||
})
|
||||
.await?;
|
||||
let mut errors = ErrorCollection::new();
|
||||
for (id, host) in hosts.0 {
|
||||
errors.handle(self.update(id, host).await);
|
||||
host.de()
|
||||
})
|
||||
.await?;
|
||||
self.update(ctrl, HostId::default(), host).await
|
||||
}
|
||||
errors.into_result()
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> {
|
||||
let ctrl = self.net_controller()?;
|
||||
async fn update(&mut self, ctrl: &NetController, id: HostId, host: Host) -> Result<(), Error> {
|
||||
let mut forwards: BTreeMap<u16, (SocketAddr, bool)> = BTreeMap::new();
|
||||
let mut vhosts: BTreeMap<(Option<InternedString>, u16), TargetInfo> = BTreeMap::new();
|
||||
let mut tor: BTreeMap<OnionAddressV3, (TorSecretKeyV3, OrdMap<u16, SocketAddr>)> =
|
||||
@@ -630,7 +576,7 @@ impl NetService {
|
||||
|
||||
ctrl.db
|
||||
.mutate(|db| {
|
||||
host_for(db, &self.id, &id, host.kind)?
|
||||
host_for(db, self.id.as_ref(), &id)?
|
||||
.as_hostname_info_mut()
|
||||
.ser(&hostname_info)
|
||||
})
|
||||
@@ -638,10 +584,129 @@ impl NetService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_all(&mut self) -> Result<(), Error> {
|
||||
let ctrl = self.net_controller()?;
|
||||
if let Some(id) = &self.id {
|
||||
for (host_id, host) in ctrl
|
||||
.db
|
||||
.peek()
|
||||
.await
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(id)
|
||||
.or_not_found(id)?
|
||||
.as_hosts()
|
||||
.as_entries()?
|
||||
{
|
||||
self.update(&*ctrl, host_id, host.de()?).await?;
|
||||
}
|
||||
} else {
|
||||
self.update(
|
||||
&*ctrl,
|
||||
HostId::default(),
|
||||
ctrl.db
|
||||
.peek()
|
||||
.await
|
||||
.as_public()
|
||||
.as_server_info()
|
||||
.as_host()
|
||||
.de()?,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetService {
|
||||
shutdown: bool,
|
||||
data: Arc<Mutex<NetServiceData>>,
|
||||
sync_task: JoinHandle<()>,
|
||||
}
|
||||
impl NetService {
|
||||
fn dummy() -> Self {
|
||||
Self {
|
||||
shutdown: true,
|
||||
data: Arc::new(Mutex::new(NetServiceData {
|
||||
id: None,
|
||||
ip: Ipv4Addr::new(0, 0, 0, 0),
|
||||
dns: Default::default(),
|
||||
controller: Default::default(),
|
||||
binds: BTreeMap::new(),
|
||||
})),
|
||||
sync_task: tokio::spawn(futures::future::ready(())),
|
||||
}
|
||||
}
|
||||
|
||||
fn new(data: NetServiceData) -> Result<Self, Error> {
|
||||
let mut ip_info = data.net_controller()?.net_iface.subscribe();
|
||||
let data = Arc::new(Mutex::new(data));
|
||||
let thread_data = data.clone();
|
||||
let sync_task = tokio::spawn(async move {
|
||||
loop {
|
||||
if let Err(e) = thread_data.lock().await.update_all().await {
|
||||
tracing::error!("Failed to update network info: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
ip_info.changed().await;
|
||||
}
|
||||
});
|
||||
Ok(Self {
|
||||
shutdown: false,
|
||||
data,
|
||||
sync_task,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn bind(
|
||||
&self,
|
||||
id: HostId,
|
||||
internal_port: u16,
|
||||
options: BindOptions,
|
||||
) -> Result<(), Error> {
|
||||
let mut data = self.data.lock().await;
|
||||
let pkg_id = &data.id;
|
||||
let ctrl = data.net_controller()?;
|
||||
let host = ctrl
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let mut ports = db.as_private().as_available_ports().de()?;
|
||||
let host = host_for(db, pkg_id.as_ref(), &id)?;
|
||||
host.add_binding(&mut ports, internal_port, options)?;
|
||||
let host = host.de()?;
|
||||
db.as_private_mut().as_available_ports_mut().ser(&ports)?;
|
||||
Ok(host)
|
||||
})
|
||||
.await?;
|
||||
data.update(&*ctrl, id, host).await
|
||||
}
|
||||
|
||||
pub async fn clear_bindings(&self, except: BTreeSet<BindId>) -> Result<(), Error> {
|
||||
let mut data = self.data.lock().await;
|
||||
let ctrl = data.net_controller()?;
|
||||
data.clear_bindings(&*ctrl, except).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: HostId, host: Host) -> Result<(), Error> {
|
||||
let mut data = self.data.lock().await;
|
||||
let ctrl = data.net_controller()?;
|
||||
data.update(&*ctrl, id, host).await
|
||||
}
|
||||
|
||||
pub async fn sync_host(&self, id: HostId) -> Result<(), Error> {
|
||||
let mut data = self.data.lock().await;
|
||||
let ctrl = data.net_controller()?;
|
||||
let host = host_for(&mut ctrl.db.peek().await, data.id.as_ref(), &id)?.de()?;
|
||||
data.update(&*ctrl, id, host).await
|
||||
}
|
||||
|
||||
pub async fn remove_all(mut self) -> Result<(), Error> {
|
||||
self.shutdown = true;
|
||||
if let Some(ctrl) = Weak::upgrade(&self.controller) {
|
||||
self.clear_bindings(Default::default()).await?;
|
||||
self.sync_task.abort();
|
||||
let mut data = self.data.lock().await;
|
||||
if let Some(ctrl) = Weak::upgrade(&data.controller) {
|
||||
self.shutdown = true;
|
||||
data.clear_bindings(&*ctrl, Default::default()).await?;
|
||||
|
||||
drop(ctrl);
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -653,26 +718,15 @@ impl NetService {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ip(&self) -> Ipv4Addr {
|
||||
self.ip
|
||||
pub async fn get_ip(&self) -> Ipv4Addr {
|
||||
self.data.lock().await.ip
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NetService {
|
||||
fn drop(&mut self) {
|
||||
if !self.shutdown {
|
||||
tracing::debug!("Dropping NetService for {}", self.id);
|
||||
let svc = std::mem::replace(
|
||||
self,
|
||||
NetService {
|
||||
shutdown: true,
|
||||
id: Default::default(),
|
||||
ip: Ipv4Addr::new(0, 0, 0, 0),
|
||||
dns: Default::default(),
|
||||
controller: Default::default(),
|
||||
binds: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
let svc = std::mem::replace(self, Self::dummy());
|
||||
tokio::spawn(async move { svc.remove_all().await.log_err() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ use zbus::{proxy, Connection};
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
|
||||
use crate::db::model::Database;
|
||||
use crate::net::forward::START9_BRIDGE_IFACE;
|
||||
use crate::net::utils::{ipv6_is_link_local, ipv6_is_local};
|
||||
use crate::prelude::*;
|
||||
use crate::util::future::Until;
|
||||
@@ -319,7 +320,10 @@ impl<'a> StubStream<'a> for SignalStream<'a> {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn watcher(write_to: Watch<BTreeMap<InternedString, NetworkInterfaceInfo>>) {
|
||||
async fn watcher(
|
||||
write_to: Watch<BTreeMap<InternedString, NetworkInterfaceInfo>>,
|
||||
lxcbr_status: Watch<bool>,
|
||||
) {
|
||||
loop {
|
||||
let res: Result<(), Error> = async {
|
||||
let connection = Connection::system().await?;
|
||||
@@ -357,19 +361,27 @@ async fn watcher(write_to: Watch<BTreeMap<InternedString, NetworkInterfaceInfo>>
|
||||
let mut ifaces = BTreeSet::new();
|
||||
let mut jobs = Vec::new();
|
||||
for device in devices {
|
||||
use futures::future::Either;
|
||||
|
||||
let device_proxy =
|
||||
device::DeviceProxy::new(&connection, device.clone()).await?;
|
||||
let iface = InternedString::intern(device_proxy.ip_interface().await?);
|
||||
if iface.is_empty() {
|
||||
continue;
|
||||
} else if &*iface == START9_BRIDGE_IFACE {
|
||||
jobs.push(Either::Left(watch_activated(
|
||||
&connection,
|
||||
device_proxy.clone(),
|
||||
&lxcbr_status,
|
||||
)));
|
||||
}
|
||||
|
||||
jobs.push(watch_ip(
|
||||
jobs.push(Either::Right(watch_ip(
|
||||
&connection,
|
||||
device_proxy.clone(),
|
||||
iface.clone(),
|
||||
&write_to,
|
||||
));
|
||||
)));
|
||||
ifaces.insert(iface);
|
||||
}
|
||||
|
||||
@@ -588,13 +600,49 @@ async fn watch_ip(
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(_connection, device_proxy, write_to))]
|
||||
async fn watch_activated(
|
||||
_connection: &Connection,
|
||||
device_proxy: device::DeviceProxy<'_>,
|
||||
write_to: &Watch<bool>,
|
||||
) -> Result<(), Error> {
|
||||
let mut until = Until::new()
|
||||
.with_stream(
|
||||
device_proxy
|
||||
.receive_active_connection_changed()
|
||||
.await
|
||||
.stub(),
|
||||
)
|
||||
.with_stream(
|
||||
device_proxy
|
||||
.receive_state_changed()
|
||||
.await?
|
||||
.into_inner()
|
||||
.stub(),
|
||||
);
|
||||
|
||||
loop {
|
||||
until
|
||||
.run(async {
|
||||
write_to.send(device_proxy._state().await? == 100);
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetworkInterfaceController {
|
||||
db: TypedPatchDb<Database>,
|
||||
lxcbr_status: Watch<bool>,
|
||||
ip_info: Watch<BTreeMap<InternedString, NetworkInterfaceInfo>>,
|
||||
_watcher: NonDetachingJoinHandle<()>,
|
||||
listeners: SyncMutex<BTreeMap<u16, Weak<()>>>,
|
||||
}
|
||||
impl NetworkInterfaceController {
|
||||
pub fn lxcbr_status(&self) -> Watch<bool> {
|
||||
self.lxcbr_status.clone_unseen()
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> Watch<BTreeMap<InternedString, NetworkInterfaceInfo>> {
|
||||
self.ip_info.clone_unseen()
|
||||
}
|
||||
@@ -665,8 +713,10 @@ impl NetworkInterfaceController {
|
||||
}
|
||||
pub fn new(db: TypedPatchDb<Database>) -> Self {
|
||||
let mut ip_info = Watch::new(BTreeMap::new());
|
||||
let lxcbr_status = Watch::new(false);
|
||||
Self {
|
||||
db: db.clone(),
|
||||
lxcbr_status: lxcbr_status.clone(),
|
||||
ip_info: ip_info.clone(),
|
||||
_watcher: tokio::spawn(async move {
|
||||
match db
|
||||
@@ -688,7 +738,7 @@ impl NetworkInterfaceController {
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
};
|
||||
tokio::join!(watcher(ip_info.clone()), async {
|
||||
tokio::join!(watcher(ip_info.clone(), lxcbr_status), async {
|
||||
let res: Result<(), Error> = async {
|
||||
loop {
|
||||
if let Err(e) = async {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -328,17 +327,23 @@ impl VHostServer {
|
||||
.headers()
|
||||
.get(http::header::HOST)
|
||||
.and_then(|host| host.to_str().ok());
|
||||
let uri = Uri::from_parts({
|
||||
let mut parts = req.uri().to_owned().into_parts();
|
||||
parts.scheme = Some("https".parse()?);
|
||||
parts.authority =
|
||||
host.map(FromStr::from_str).transpose()?;
|
||||
parts
|
||||
})?;
|
||||
Response::builder()
|
||||
.status(http::StatusCode::TEMPORARY_REDIRECT)
|
||||
.header(http::header::LOCATION, uri.to_string())
|
||||
.body(Body::default())
|
||||
if let Some(host) = host {
|
||||
let uri = Uri::from_parts({
|
||||
let mut parts =
|
||||
req.uri().to_owned().into_parts();
|
||||
parts.scheme = Some("https".parse()?);
|
||||
parts.authority = Some(host.parse()?);
|
||||
parts
|
||||
})?;
|
||||
Response::builder()
|
||||
.status(http::StatusCode::TEMPORARY_REDIRECT)
|
||||
.header(http::header::LOCATION, uri.to_string())
|
||||
.body(Body::default())
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(http::StatusCode::BAD_REQUEST)
|
||||
.body(Body::from("Host header required"))
|
||||
}
|
||||
}
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -899,9 +899,8 @@ impl TypedValueParser for CountryCodeParser {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn synchronize_network_manager<P: AsRef<Path>>(
|
||||
main_datadir: P,
|
||||
wifi: &mut WifiInfo,
|
||||
wifi: &WifiInfo,
|
||||
) -> Result<(), Error> {
|
||||
wifi.interface = find_wifi_iface().await?;
|
||||
let persistent = main_datadir.as_ref().join("system-connections");
|
||||
|
||||
if tokio::fs::metadata(&persistent).await.is_err() {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use models::{HostId, PackageId};
|
||||
|
||||
use crate::net::host::binding::{BindId, BindOptions, NetInfo};
|
||||
use crate::net::host::HostKind;
|
||||
use crate::service::effects::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct BindParams {
|
||||
kind: HostKind,
|
||||
id: HostId,
|
||||
internal_port: u16,
|
||||
#[serde(flatten)]
|
||||
@@ -17,15 +15,18 @@ pub struct BindParams {
|
||||
pub async fn bind(
|
||||
context: EffectContext,
|
||||
BindParams {
|
||||
kind,
|
||||
id,
|
||||
internal_port,
|
||||
options,
|
||||
}: BindParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let mut svc = context.seed.persistent_container.net_service.lock().await;
|
||||
svc.bind(kind, id, internal_port, options).await
|
||||
context
|
||||
.seed
|
||||
.persistent_container
|
||||
.net_service
|
||||
.bind(id, internal_port, options)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
@@ -41,8 +42,12 @@ pub async fn clear_bindings(
|
||||
ClearBindingsParams { except }: ClearBindingsParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let mut svc = context.seed.persistent_container.net_service.lock().await;
|
||||
svc.clear_bindings(except.into_iter().collect()).await?;
|
||||
context
|
||||
.seed
|
||||
.persistent_container
|
||||
.net_service
|
||||
.clear_bindings(except.into_iter().collect())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,5 @@ use crate::service::effects::prelude::*;
|
||||
|
||||
pub async fn get_container_ip(context: EffectContext) -> Result<Ipv4Addr, Error> {
|
||||
let context = context.deref()?;
|
||||
let net_service = context.seed.persistent_container.net_service.lock().await;
|
||||
Ok(net_service.get_ip())
|
||||
Ok(context.seed.persistent_container.net_service.get_ip().await)
|
||||
}
|
||||
|
||||
@@ -605,23 +605,12 @@ impl Service {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_host(&self, host_id: HostId) -> Result<(), Error> {
|
||||
let mut service = self.seed.persistent_container.net_service.lock().await;
|
||||
let host = self
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.peek()
|
||||
pub async fn sync_host(&self, host_id: HostId) -> Result<(), Error> {
|
||||
self.seed
|
||||
.persistent_container
|
||||
.net_service
|
||||
.sync_host(host_id)
|
||||
.await
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(&self.seed.id)
|
||||
.or_not_found(&self.seed.id)?
|
||||
.as_hosts()
|
||||
.as_idx(&host_id)
|
||||
.or_not_found(&host_id)?
|
||||
.de()?;
|
||||
service.update(host_id, host).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ pub struct PersistentContainer {
|
||||
pub(super) images: BTreeMap<ImageId, Arc<MountGuard>>,
|
||||
pub(super) subcontainers: Arc<Mutex<BTreeMap<Guid, Subcontainer>>>,
|
||||
pub(super) state: Arc<watch::Sender<ServiceState>>,
|
||||
pub(super) net_service: Mutex<NetService>,
|
||||
pub(super) net_service: NetService,
|
||||
destroyed: bool,
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ impl PersistentContainer {
|
||||
images,
|
||||
subcontainers: Arc::new(Mutex::new(BTreeMap::new())),
|
||||
state: Arc::new(watch::channel(ServiceState::new(start)).0),
|
||||
net_service: Mutex::new(net_service),
|
||||
net_service,
|
||||
destroyed: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,38 +37,6 @@ impl Actor for ServiceActor {
|
||||
}
|
||||
}
|
||||
});
|
||||
let seed = self.0.clone();
|
||||
let mut ip_info = seed.ctx.net_controller.net_iface.subscribe();
|
||||
jobs.add_job(async move {
|
||||
loop {
|
||||
if let Err(e) = async {
|
||||
let mut service = seed.persistent_container.net_service.lock().await;
|
||||
let hosts = seed
|
||||
.ctx
|
||||
.db
|
||||
.peek()
|
||||
.await
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(&seed.id)
|
||||
.or_not_found(&seed.id)?
|
||||
.as_hosts()
|
||||
.de()?;
|
||||
for (host_id, host) in hosts.0 {
|
||||
service.update(host_id, host).await?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error syncronizing net host after network change: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
|
||||
ip_info.changed().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{Future, FutureExt};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::{Future, FutureExt, StreamExt};
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use imbl::OrdMap;
|
||||
use imbl_value::InternedString;
|
||||
@@ -68,8 +69,12 @@ impl ServiceMap {
|
||||
progress.start();
|
||||
let ids = ctx.db.peek().await.as_public().as_package_data().keys()?;
|
||||
progress.set_total(ids.len() as u64);
|
||||
for id in ids {
|
||||
if let Err(e) = self.load(ctx, &id, LoadDisposition::Retry).await {
|
||||
let mut jobs = FuturesUnordered::new();
|
||||
for id in &ids {
|
||||
jobs.push(self.load(ctx, id, LoadDisposition::Retry));
|
||||
}
|
||||
while let Some(res) = jobs.next().await {
|
||||
if let Err(e) = res {
|
||||
tracing::error!("Error loading installed package as service: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
|
||||
use crate::disk::util::{pvscan, recovery_info, DiskInfo, StartOsRecoveryInfo};
|
||||
use crate::disk::REPAIR_DISK_PATH;
|
||||
use crate::init::{init, InitPhases, InitResult};
|
||||
use crate::net::net_controller::PreInitNetController;
|
||||
use crate::net::net_controller::NetController;
|
||||
use crate::net::ssl::root_ca_start_time;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::{FullProgress, PhaseProgressTrackerHandle};
|
||||
@@ -80,10 +80,11 @@ async fn setup_init(
|
||||
ctx: &SetupContext,
|
||||
password: Option<String>,
|
||||
init_phases: InitPhases,
|
||||
) -> Result<(AccountInfo, PreInitNetController), Error> {
|
||||
let InitResult { net_ctrl } = init(&ctx.webserver, &ctx.config, init_phases).await?;
|
||||
) -> Result<(AccountInfo, InitResult), Error> {
|
||||
let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?;
|
||||
|
||||
let account = net_ctrl
|
||||
let account = init_result
|
||||
.net_ctrl
|
||||
.db
|
||||
.mutate(|m| {
|
||||
let mut account = AccountInfo::load(m)?;
|
||||
@@ -99,7 +100,7 @@ async fn setup_init(
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok((account, net_ctrl))
|
||||
Ok((account, init_result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TS)]
|
||||
@@ -452,13 +453,13 @@ async fn fresh_setup(
|
||||
db.put(&ROOT, &Database::init(&account)?).await?;
|
||||
drop(db);
|
||||
|
||||
let InitResult { net_ctrl } = init(&ctx.webserver, &ctx.config, init_phases).await?;
|
||||
let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?;
|
||||
|
||||
let rpc_ctx = RpcContext::init(
|
||||
&ctx.webserver,
|
||||
&ctx.config,
|
||||
guid,
|
||||
Some(net_ctrl),
|
||||
Some(init_result),
|
||||
rpc_ctx_phases,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -77,6 +77,14 @@ impl<T> Watch<T> {
|
||||
pub async fn changed(&mut self) {
|
||||
futures::future::poll_fn(|cx| self.poll_changed(cx)).await
|
||||
}
|
||||
pub async fn wait_for<F: FnMut(&T) -> bool>(&mut self, mut f: F) {
|
||||
loop {
|
||||
if self.peek(&mut f) {
|
||||
break;
|
||||
}
|
||||
self.changed().await;
|
||||
}
|
||||
}
|
||||
pub fn send_if_modified<F: FnOnce(&mut T) -> bool>(&self, modify: F) -> bool {
|
||||
self.shared.mutate(|shared| {
|
||||
let changed = modify(&mut shared.data);
|
||||
|
||||
@@ -30,8 +30,9 @@ mod v0_3_6_alpha_9;
|
||||
|
||||
mod v0_3_6_alpha_10;
|
||||
mod v0_3_6_alpha_11;
|
||||
mod v0_3_6_alpha_12;
|
||||
|
||||
pub type Current = v0_3_6_alpha_11::Version; // VERSION_BUMP
|
||||
pub type Current = v0_3_6_alpha_12::Version; // VERSION_BUMP
|
||||
|
||||
impl Current {
|
||||
#[instrument(skip(self, db))]
|
||||
@@ -113,6 +114,7 @@ enum Version {
|
||||
V0_3_6_alpha_9(Wrapper<v0_3_6_alpha_9::Version>),
|
||||
V0_3_6_alpha_10(Wrapper<v0_3_6_alpha_10::Version>),
|
||||
V0_3_6_alpha_11(Wrapper<v0_3_6_alpha_11::Version>),
|
||||
V0_3_6_alpha_12(Wrapper<v0_3_6_alpha_12::Version>),
|
||||
Other(exver::Version),
|
||||
}
|
||||
|
||||
@@ -148,6 +150,7 @@ impl Version {
|
||||
Self::V0_3_6_alpha_9(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_3_6_alpha_10(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_3_6_alpha_11(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_3_6_alpha_12(v) => DynVersion(Box::new(v.0)),
|
||||
Self::Other(v) => {
|
||||
return Err(Error::new(
|
||||
eyre!("unknown version {v}"),
|
||||
@@ -175,6 +178,7 @@ impl Version {
|
||||
Version::V0_3_6_alpha_9(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_6_alpha_10(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_6_alpha_11(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_6_alpha_12(Wrapper(x)) => x.semver(),
|
||||
Version::Other(x) => x.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ async fn previous_account_info(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<Accoun
|
||||
password: account_query
|
||||
.try_get("password")
|
||||
.with_ctx(|_| (ErrorKind::Database, "password"))?,
|
||||
tor_key: TorSecretKeyV3::try_from(
|
||||
tor_keys: vec![TorSecretKeyV3::try_from(
|
||||
if let Some(bytes) = account_query
|
||||
.try_get::<Option<Vec<u8>>, _>("tor_key")
|
||||
.with_ctx(|_| (ErrorKind::Database, "tor_key"))?
|
||||
@@ -503,7 +503,7 @@ async fn previous_account_info(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<Accoun
|
||||
.with_ctx(|_| (ErrorKind::Database, "password.u8 32"))?,
|
||||
)
|
||||
},
|
||||
)?,
|
||||
)?],
|
||||
server_id: account_query
|
||||
.try_get("server_id")
|
||||
.with_ctx(|_| (ErrorKind::Database, "server_id"))?,
|
||||
|
||||
68
core/startos/src/version/v0_3_6_alpha_12.rs
Normal file
68
core/startos/src/version/v0_3_6_alpha_12.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use exver::{PreReleaseSegment, VersionRange};
|
||||
use imbl_value::json;
|
||||
|
||||
use super::v0_3_5::V0_3_0_COMPAT;
|
||||
use super::{v0_3_6_alpha_11, VersionT};
|
||||
use crate::prelude::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref V0_3_6_alpha_12: exver::Version = exver::Version::new(
|
||||
[0, 3, 6],
|
||||
[PreReleaseSegment::String("alpha".into()), 12.into()]
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Version;
|
||||
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_3_6_alpha_11::Version;
|
||||
type PreUpRes = ();
|
||||
|
||||
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn semver(self) -> exver::Version {
|
||||
V0_3_6_alpha_12.clone()
|
||||
}
|
||||
fn compat(self) -> &'static VersionRange {
|
||||
&V0_3_0_COMPAT
|
||||
}
|
||||
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> {
|
||||
let bindings: BTreeMap<u16, Value> = [(
|
||||
80,
|
||||
json!({
|
||||
"enabled": false,
|
||||
"options": {
|
||||
"preferredExternalPort": 80,
|
||||
"addSsl": {
|
||||
"preferredExternalPort": 443,
|
||||
"alpn": { "specified": [ "http/1.1", "h2" ] },
|
||||
},
|
||||
"secure": null,
|
||||
},
|
||||
"net": {
|
||||
"assignedPort": null,
|
||||
"assignedSslPort": 443,
|
||||
"public": false,
|
||||
}
|
||||
}),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let onion = db["public"]["serverInfo"]["onionAddress"].clone();
|
||||
db["public"]["serverInfo"]["host"] = json!({
|
||||
"bindings": bindings,
|
||||
"onions": [onion],
|
||||
"domains": {},
|
||||
"hostnameInfo": {},
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "start-os",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
2
patch-db
2
patch-db
Submodule patch-db updated: 2600a784a9...36eb59b79e
@@ -77,19 +77,18 @@ type BindOptionsByKnownProtocol =
|
||||
preferredExternalPort?: number
|
||||
addSsl?: AddSslOptions
|
||||
}
|
||||
export type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions
|
||||
|
||||
export type HostKind = BindParams["kind"]
|
||||
export type BindOptionsByProtocol =
|
||||
| BindOptionsByKnownProtocol
|
||||
| (BindOptions & { protocol: null })
|
||||
|
||||
const hasStringProtocol = object({
|
||||
protocol: string,
|
||||
}).test
|
||||
|
||||
export class Host {
|
||||
export class MultiHost {
|
||||
constructor(
|
||||
readonly options: {
|
||||
effects: Effects
|
||||
kind: HostKind
|
||||
id: string
|
||||
},
|
||||
) {}
|
||||
@@ -113,7 +112,7 @@ export class Host {
|
||||
async bindPort(
|
||||
internalPort: number,
|
||||
options: BindOptionsByProtocol,
|
||||
): Promise<Origin<this>> {
|
||||
): Promise<Origin> {
|
||||
if (hasStringProtocol(options)) {
|
||||
return await this.bindPortForKnown(options, internalPort)
|
||||
} else {
|
||||
@@ -130,7 +129,6 @@ export class Host {
|
||||
},
|
||||
) {
|
||||
const binderOptions = {
|
||||
kind: this.options.kind,
|
||||
id: this.options.id,
|
||||
internalPort,
|
||||
...options,
|
||||
@@ -163,7 +161,6 @@ export class Host {
|
||||
const secure: Security | null = !protoInfo.secure ? null : { ssl: false }
|
||||
|
||||
await this.options.effects.bind({
|
||||
kind: this.options.kind,
|
||||
id: this.options.id,
|
||||
internalPort,
|
||||
preferredExternalPort,
|
||||
@@ -190,21 +187,3 @@ function inObject<Key extends string>(
|
||||
): obj is { [K in Key]: unknown } {
|
||||
return key in obj
|
||||
}
|
||||
|
||||
// export class StaticHost extends Host {
|
||||
// constructor(options: { effects: Effects; id: string }) {
|
||||
// super({ ...options, kind: "static" })
|
||||
// }
|
||||
// }
|
||||
|
||||
// export class SingleHost extends Host {
|
||||
// constructor(options: { effects: Effects; id: string }) {
|
||||
// super({ ...options, kind: "single" })
|
||||
// }
|
||||
// }
|
||||
|
||||
export class MultiHost extends Host {
|
||||
constructor(options: { effects: Effects; id: string }) {
|
||||
super({ ...options, kind: "multi" })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { AddressInfo } from "../types"
|
||||
import { AddressReceipt } from "./AddressReceipt"
|
||||
import { Host, Scheme } from "./Host"
|
||||
import { MultiHost, Scheme } from "./Host"
|
||||
import { ServiceInterfaceBuilder } from "./ServiceInterfaceBuilder"
|
||||
|
||||
export class Origin<T extends Host> {
|
||||
export class Origin {
|
||||
constructor(
|
||||
readonly host: T,
|
||||
readonly host: MultiHost,
|
||||
readonly internalPort: number,
|
||||
readonly scheme: string | null,
|
||||
readonly sslScheme: string | null,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AddSslOptions } from "./AddSslOptions"
|
||||
import type { HostId } from "./HostId"
|
||||
import type { HostKind } from "./HostKind"
|
||||
import type { Security } from "./Security"
|
||||
|
||||
export type BindParams = {
|
||||
kind: HostKind
|
||||
id: HostId
|
||||
internalPort: number
|
||||
preferredExternalPort: number
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BindInfo } from "./BindInfo"
|
||||
import type { DomainConfig } from "./DomainConfig"
|
||||
import type { HostKind } from "./HostKind"
|
||||
import type { HostnameInfo } from "./HostnameInfo"
|
||||
|
||||
export type Host = {
|
||||
kind: HostKind
|
||||
bindings: { [key: number]: BindInfo }
|
||||
onions: string[]
|
||||
domains: { [key: string]: DomainConfig }
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HostKind = "multi"
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { AcmeProvider } from "./AcmeProvider"
|
||||
import type { AcmeSettings } from "./AcmeSettings"
|
||||
import type { Governor } from "./Governor"
|
||||
import type { Host } from "./Host"
|
||||
import type { LshwDevice } from "./LshwDevice"
|
||||
import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo"
|
||||
import type { ServerStatus } from "./ServerStatus"
|
||||
@@ -13,16 +14,11 @@ export type ServerInfo = {
|
||||
platform: string
|
||||
id: string
|
||||
hostname: string
|
||||
host: Host
|
||||
version: string
|
||||
packageVersionCompat: string
|
||||
postInitMigrationTodos: string[]
|
||||
lastBackup: string | null
|
||||
lanAddress: string
|
||||
onionAddress: string
|
||||
/**
|
||||
* for backwards compatibility
|
||||
*/
|
||||
torAddress: string
|
||||
networkInterfaces: { [key: string]: NetworkInterfaceInfo }
|
||||
acme: { [key: AcmeProvider]: AcmeSettings }
|
||||
statusInfo: ServerStatus
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SetupResult = {
|
||||
torAddress: string
|
||||
torAddresses: Array<string>
|
||||
hostname: string
|
||||
lanAddress: string
|
||||
rootCa: string
|
||||
|
||||
@@ -4,13 +4,13 @@ export { AcmeSettings } from "./AcmeSettings"
|
||||
export { ActionId } from "./ActionId"
|
||||
export { ActionInput } from "./ActionInput"
|
||||
export { ActionMetadata } from "./ActionMetadata"
|
||||
export { ActionRequest } from "./ActionRequest"
|
||||
export { ActionRequestCondition } from "./ActionRequestCondition"
|
||||
export { ActionRequestEntry } from "./ActionRequestEntry"
|
||||
export { ActionRequestInput } from "./ActionRequestInput"
|
||||
export { ActionRequestTrigger } from "./ActionRequestTrigger"
|
||||
export { ActionRequest } from "./ActionRequest"
|
||||
export { ActionResultMember } from "./ActionResultMember"
|
||||
export { ActionResult } from "./ActionResult"
|
||||
export { ActionResultMember } from "./ActionResultMember"
|
||||
export { ActionResultV0 } from "./ActionResultV0"
|
||||
export { ActionResultV1 } from "./ActionResultV1"
|
||||
export { ActionResultValue } from "./ActionResultValue"
|
||||
@@ -20,13 +20,13 @@ export { AddAdminParams } from "./AddAdminParams"
|
||||
export { AddAssetParams } from "./AddAssetParams"
|
||||
export { AddCategoryParams } from "./AddCategoryParams"
|
||||
export { AddPackageParams } from "./AddPackageParams"
|
||||
export { AddressInfo } from "./AddressInfo"
|
||||
export { AddSslOptions } from "./AddSslOptions"
|
||||
export { AddVersionParams } from "./AddVersionParams"
|
||||
export { AddressInfo } from "./AddressInfo"
|
||||
export { Alerts } from "./Alerts"
|
||||
export { Algorithm } from "./Algorithm"
|
||||
export { AllowedStatuses } from "./AllowedStatuses"
|
||||
export { AllPackageData } from "./AllPackageData"
|
||||
export { AllowedStatuses } from "./AllowedStatuses"
|
||||
export { AlpnInfo } from "./AlpnInfo"
|
||||
export { AnySignature } from "./AnySignature"
|
||||
export { AnySigningKey } from "./AnySigningKey"
|
||||
@@ -38,9 +38,9 @@ export { BackupTargetFS } from "./BackupTargetFS"
|
||||
export { Base64 } from "./Base64"
|
||||
export { BindId } from "./BindId"
|
||||
export { BindInfo } from "./BindInfo"
|
||||
export { BindingSetPublicParams } from "./BindingSetPublicParams"
|
||||
export { BindOptions } from "./BindOptions"
|
||||
export { BindParams } from "./BindParams"
|
||||
export { BindingSetPublicParams } from "./BindingSetPublicParams"
|
||||
export { Blake3Commitment } from "./Blake3Commitment"
|
||||
export { BlockDev } from "./BlockDev"
|
||||
export { BuildArg } from "./BuildArg"
|
||||
@@ -60,11 +60,11 @@ export { CreateSubcontainerFsParams } from "./CreateSubcontainerFsParams"
|
||||
export { CurrentDependencies } from "./CurrentDependencies"
|
||||
export { CurrentDependencyInfo } from "./CurrentDependencyInfo"
|
||||
export { DataUrl } from "./DataUrl"
|
||||
export { DepInfo } from "./DepInfo"
|
||||
export { Dependencies } from "./Dependencies"
|
||||
export { DependencyKind } from "./DependencyKind"
|
||||
export { DependencyMetadata } from "./DependencyMetadata"
|
||||
export { DependencyRequirement } from "./DependencyRequirement"
|
||||
export { DepInfo } from "./DepInfo"
|
||||
export { Description } from "./Description"
|
||||
export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams"
|
||||
export { DeviceFilter } from "./DeviceFilter"
|
||||
@@ -84,8 +84,8 @@ export { GetHostInfoParams } from "./GetHostInfoParams"
|
||||
export { GetOsAssetParams } from "./GetOsAssetParams"
|
||||
export { GetOsVersionParams } from "./GetOsVersionParams"
|
||||
export { GetPackageParams } from "./GetPackageParams"
|
||||
export { GetPackageResponseFull } from "./GetPackageResponseFull"
|
||||
export { GetPackageResponse } from "./GetPackageResponse"
|
||||
export { GetPackageResponseFull } from "./GetPackageResponseFull"
|
||||
export { GetServiceInterfaceParams } from "./GetServiceInterfaceParams"
|
||||
export { GetServicePortForwardParams } from "./GetServicePortForwardParams"
|
||||
export { GetSslCertificateParams } from "./GetSslCertificateParams"
|
||||
@@ -98,22 +98,21 @@ export { Governor } from "./Governor"
|
||||
export { Guid } from "./Guid"
|
||||
export { HardwareRequirements } from "./HardwareRequirements"
|
||||
export { HealthCheckId } from "./HealthCheckId"
|
||||
export { Host } from "./Host"
|
||||
export { HostAddress } from "./HostAddress"
|
||||
export { HostId } from "./HostId"
|
||||
export { HostKind } from "./HostKind"
|
||||
export { HostnameInfo } from "./HostnameInfo"
|
||||
export { Hosts } from "./Hosts"
|
||||
export { Host } from "./Host"
|
||||
export { ImageConfig } from "./ImageConfig"
|
||||
export { ImageId } from "./ImageId"
|
||||
export { ImageMetadata } from "./ImageMetadata"
|
||||
export { ImageSource } from "./ImageSource"
|
||||
export { InitProgressRes } from "./InitProgressRes"
|
||||
export { InstallParams } from "./InstallParams"
|
||||
export { InstalledState } from "./InstalledState"
|
||||
export { InstalledVersionParams } from "./InstalledVersionParams"
|
||||
export { InstallingInfo } from "./InstallingInfo"
|
||||
export { InstallingState } from "./InstallingState"
|
||||
export { InstallParams } from "./InstallParams"
|
||||
export { IpHostname } from "./IpHostname"
|
||||
export { IpInfo } from "./IpInfo"
|
||||
export { ListPackageSignersParams } from "./ListPackageSignersParams"
|
||||
@@ -137,14 +136,14 @@ export { NetworkInterfaceSetPublicParams } from "./NetworkInterfaceSetPublicPara
|
||||
export { NetworkInterfaceType } from "./NetworkInterfaceType"
|
||||
export { OnionHostname } from "./OnionHostname"
|
||||
export { OsIndex } from "./OsIndex"
|
||||
export { OsVersionInfoMap } from "./OsVersionInfoMap"
|
||||
export { OsVersionInfo } from "./OsVersionInfo"
|
||||
export { OsVersionInfoMap } from "./OsVersionInfoMap"
|
||||
export { PackageDataEntry } from "./PackageDataEntry"
|
||||
export { PackageDetailLevel } from "./PackageDetailLevel"
|
||||
export { PackageId } from "./PackageId"
|
||||
export { PackageIndex } from "./PackageIndex"
|
||||
export { PackageInfoShort } from "./PackageInfoShort"
|
||||
export { PackageInfo } from "./PackageInfo"
|
||||
export { PackageInfoShort } from "./PackageInfoShort"
|
||||
export { PackageSignerParams } from "./PackageSignerParams"
|
||||
export { PackageState } from "./PackageState"
|
||||
export { PackageVersionInfo } from "./PackageVersionInfo"
|
||||
@@ -166,18 +165,18 @@ export { Security } from "./Security"
|
||||
export { ServerInfo } from "./ServerInfo"
|
||||
export { ServerSpecs } from "./ServerSpecs"
|
||||
export { ServerStatus } from "./ServerStatus"
|
||||
export { ServiceInterfaceId } from "./ServiceInterfaceId"
|
||||
export { ServiceInterface } from "./ServiceInterface"
|
||||
export { ServiceInterfaceId } from "./ServiceInterfaceId"
|
||||
export { ServiceInterfaceType } from "./ServiceInterfaceType"
|
||||
export { Session } from "./Session"
|
||||
export { SessionList } from "./SessionList"
|
||||
export { Sessions } from "./Sessions"
|
||||
export { Session } from "./Session"
|
||||
export { SetDataVersionParams } from "./SetDataVersionParams"
|
||||
export { SetDependenciesParams } from "./SetDependenciesParams"
|
||||
export { SetHealth } from "./SetHealth"
|
||||
export { SetIconParams } from "./SetIconParams"
|
||||
export { SetMainStatusStatus } from "./SetMainStatusStatus"
|
||||
export { SetMainStatus } from "./SetMainStatus"
|
||||
export { SetMainStatusStatus } from "./SetMainStatusStatus"
|
||||
export { SetNameParams } from "./SetNameParams"
|
||||
export { SetStoreParams } from "./SetStoreParams"
|
||||
export { SetupExecuteParams } from "./SetupExecuteParams"
|
||||
@@ -192,7 +191,7 @@ export { TestSmtpParams } from "./TestSmtpParams"
|
||||
export { UnsetPublicParams } from "./UnsetPublicParams"
|
||||
export { UpdatingState } from "./UpdatingState"
|
||||
export { VerifyCifsParams } from "./VerifyCifsParams"
|
||||
export { VersionSignerParams } from "./VersionSignerParams"
|
||||
export { Version } from "./Version"
|
||||
export { VersionSignerParams } from "./VersionSignerParams"
|
||||
export { VolumeId } from "./VolumeId"
|
||||
export { WifiInfo } from "./WifiInfo"
|
||||
|
||||
@@ -2,63 +2,68 @@ import { Pattern } from "../actions/input/inputSpecTypes"
|
||||
import * as regexes from "./regexes"
|
||||
|
||||
export const ipv6: Pattern = {
|
||||
regex: regexes.ipv6.source,
|
||||
regex: regexes.ipv6.matches(),
|
||||
description: "Must be a valid IPv6 address",
|
||||
}
|
||||
|
||||
export const ipv4: Pattern = {
|
||||
regex: regexes.ipv4.source,
|
||||
regex: regexes.ipv4.matches(),
|
||||
description: "Must be a valid IPv4 address",
|
||||
}
|
||||
|
||||
export const hostname: Pattern = {
|
||||
regex: regexes.hostname.source,
|
||||
regex: regexes.hostname.matches(),
|
||||
description: "Must be a valid hostname",
|
||||
}
|
||||
|
||||
export const localHostname: Pattern = {
|
||||
regex: regexes.localHostname.source,
|
||||
regex: regexes.localHostname.matches(),
|
||||
description: 'Must be a valid ".local" hostname',
|
||||
}
|
||||
|
||||
export const torHostname: Pattern = {
|
||||
regex: regexes.torHostname.source,
|
||||
regex: regexes.torHostname.matches(),
|
||||
description: 'Must be a valid Tor (".onion") hostname',
|
||||
}
|
||||
|
||||
export const url: Pattern = {
|
||||
regex: regexes.url.source,
|
||||
regex: regexes.url.matches(),
|
||||
description: "Must be a valid URL",
|
||||
}
|
||||
|
||||
export const localUrl: Pattern = {
|
||||
regex: regexes.localUrl.source,
|
||||
regex: regexes.localUrl.matches(),
|
||||
description: 'Must be a valid ".local" URL',
|
||||
}
|
||||
|
||||
export const torUrl: Pattern = {
|
||||
regex: regexes.torUrl.source,
|
||||
regex: regexes.torUrl.matches(),
|
||||
description: 'Must be a valid Tor (".onion") URL',
|
||||
}
|
||||
|
||||
export const ascii: Pattern = {
|
||||
regex: regexes.ascii.source,
|
||||
regex: regexes.ascii.matches(),
|
||||
description:
|
||||
"May only contain ASCII characters. See https://www.w3schools.com/charsets/ref_html_ascii.asp",
|
||||
}
|
||||
|
||||
export const domain: Pattern = {
|
||||
regex: regexes.domain.matches(),
|
||||
description: "Must be a valid Fully Qualified Domain Name",
|
||||
}
|
||||
|
||||
export const email: Pattern = {
|
||||
regex: regexes.email.source,
|
||||
regex: regexes.email.matches(),
|
||||
description: "Must be a valid email address",
|
||||
}
|
||||
|
||||
export const emailWithName: Pattern = {
|
||||
regex: regexes.emailWithName.source,
|
||||
regex: regexes.emailWithName.matches(),
|
||||
description: "Must be a valid email address, optionally with a name",
|
||||
}
|
||||
|
||||
export const base64: Pattern = {
|
||||
regex: regexes.base64.source,
|
||||
regex: regexes.base64.matches(),
|
||||
description:
|
||||
"May only contain base64 characters. See https://base64.guru/learn/base64-characters",
|
||||
}
|
||||
|
||||
@@ -1,38 +1,71 @@
|
||||
export class ComposableRegex {
|
||||
readonly regex: RegExp
|
||||
constructor(regex: RegExp | string) {
|
||||
if (regex instanceof RegExp) {
|
||||
this.regex = regex
|
||||
} else {
|
||||
this.regex = new RegExp(regex)
|
||||
}
|
||||
}
|
||||
asExpr(): string {
|
||||
return `(${this.regex.source})`
|
||||
}
|
||||
matches(): string {
|
||||
return `^${this.regex.source}$`
|
||||
}
|
||||
contains(): string {
|
||||
return this.regex.source
|
||||
}
|
||||
}
|
||||
|
||||
// https://ihateregex.io/expr/ipv6/
|
||||
export const ipv6 =
|
||||
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/
|
||||
export const ipv6 = new ComposableRegex(
|
||||
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
|
||||
)
|
||||
|
||||
// https://ihateregex.io/expr/ipv4/
|
||||
export const ipv4 =
|
||||
/(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/
|
||||
export const ipv4 = new ComposableRegex(
|
||||
/(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
|
||||
)
|
||||
|
||||
export const hostname =
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/
|
||||
export const hostname = new ComposableRegex(
|
||||
/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])/,
|
||||
)
|
||||
|
||||
export const localHostname = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/
|
||||
export const localHostname = new ComposableRegex(
|
||||
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/,
|
||||
)
|
||||
|
||||
export const torHostname = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/
|
||||
export const torHostname = new ComposableRegex(
|
||||
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/,
|
||||
)
|
||||
|
||||
// https://ihateregex.io/expr/url/
|
||||
export const url =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
|
||||
export const url = new ComposableRegex(
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
||||
)
|
||||
|
||||
export const localUrl =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
|
||||
export const localUrl = new ComposableRegex(
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
||||
)
|
||||
|
||||
export const torUrl =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
|
||||
export const torUrl = new ComposableRegex(
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
||||
)
|
||||
|
||||
// https://ihateregex.io/expr/ascii/
|
||||
export const ascii = /^[ -~]*$/
|
||||
export const ascii = new ComposableRegex(/[ -~]*/)
|
||||
|
||||
export const domain = new ComposableRegex(/[A-Za-z0-9.-]+\.[A-Za-z]{2,}/)
|
||||
|
||||
// https://www.regular-expressions.info/email.html
|
||||
export const email = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/
|
||||
export const email = new ComposableRegex(`[A-Za-z0-9._%+-]+@${domain.asExpr()}`)
|
||||
|
||||
export const emailWithName = new RegExp(
|
||||
`(${email.source})|([^<]*<(${email.source})>)`,
|
||||
export const emailWithName = new ComposableRegex(
|
||||
`${email.asExpr()}|([^<]*<${email.asExpr()}>)`,
|
||||
)
|
||||
|
||||
//https://rgxdb.com/r/1NUN74O6
|
||||
export const base64 =
|
||||
/^(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))$/
|
||||
export const base64 = new ComposableRegex(
|
||||
/(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))/,
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ emver = { version = "0.1.7", git = "https://github.com/Start9Labs/emver-rs.git",
|
||||
] }
|
||||
failure = "0.1.8"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
imbl-value = { git = "https://github.com/Start9Labs/imbl-value.git" }
|
||||
imbl-value = "0.1.2"
|
||||
itertools = "0.10.0"
|
||||
lazy_static = "1.4"
|
||||
linear-map = { version = "1.2", features = ["serde_impl"] }
|
||||
|
||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.3.6-alpha.11",
|
||||
"version": "0.3.6-alpha.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "startos-ui",
|
||||
"version": "0.3.6-alpha.11",
|
||||
"version": "0.3.6-alpha.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^14.1.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"ts-matches": "^6.1.0",
|
||||
"ts-matches": "^6.2.1",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.3.6-alpha.11",
|
||||
"version": "0.3.6-alpha.12",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
overflow: auto;
|
||||
"
|
||||
>
|
||||
<code id="tor-addr"></code>
|
||||
<code id="tor-addr" style="display:block; white-space:pre-wrap"></code>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export class SuccessPage {
|
||||
{} as ElementRef<HTMLCanvasElement>
|
||||
private ctx: CanvasRenderingContext2D = {} as CanvasRenderingContext2D
|
||||
|
||||
torAddress?: string
|
||||
torAddresses?: string[]
|
||||
lanAddress?: string
|
||||
cert?: string
|
||||
|
||||
@@ -52,7 +52,7 @@ export class SuccessPage {
|
||||
const torAddress = this.document.getElementById('tor-addr')
|
||||
const lanAddress = this.document.getElementById('lan-addr')
|
||||
|
||||
if (torAddress) torAddress.innerHTML = this.torAddress!
|
||||
if (torAddress) torAddress.innerHTML = this.torAddresses!.join('\n')
|
||||
if (lanAddress) lanAddress.innerHTML = this.lanAddress!
|
||||
|
||||
this.document
|
||||
@@ -76,7 +76,9 @@ export class SuccessPage {
|
||||
try {
|
||||
const ret = await this.api.complete()
|
||||
if (!this.isKiosk) {
|
||||
this.torAddress = ret.torAddress.replace(/^https:/, 'http:')
|
||||
this.torAddresses = ret.torAddresses.map(a =>
|
||||
a.replace(/^https:/, 'http:'),
|
||||
)
|
||||
this.lanAddress = ret.lanAddress.replace(/^https:/, 'http:')
|
||||
this.cert = ret.rootCa
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ export class MockApiService extends ApiService {
|
||||
case 3:
|
||||
return {
|
||||
status: 'complete',
|
||||
torAddress: 'https://asdafsadasdasasdasdfasdfasdf.onion',
|
||||
torAddresses: ['https://asdafsadasdasasdasdfasdfasdf.onion'],
|
||||
hostname: 'adjective-noun',
|
||||
lanAddress: 'https://adjective-noun.local',
|
||||
rootCa: encodeBase64(rootCA),
|
||||
@@ -283,7 +283,7 @@ export class MockApiService extends ApiService {
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
torAddress: 'https://asdafsadasdasasdasdfasdfasdf.onion',
|
||||
torAddresses: ['https://asdafsadasdasasdasdfasdfasdf.onion'],
|
||||
hostname: 'adjective-noun',
|
||||
lanAddress: 'https://adjective-noun.local',
|
||||
rootCa: encodeBase64(rootCA),
|
||||
|
||||
@@ -13,7 +13,7 @@ export type WorkspaceConfig = {
|
||||
community: 'https://community-registry.start9.com/'
|
||||
}
|
||||
mocks: {
|
||||
maskAs: 'tor' | 'local' | 'ip' | 'localhost'
|
||||
maskAs: 'tor' | 'local' | 'localhost' | 'ipv4' | 'ipv6' | 'clearnet'
|
||||
// enables local development in secure mode
|
||||
maskAsHttps: boolean
|
||||
skipStartupAlerts: boolean
|
||||
|
||||
@@ -45,7 +45,7 @@ const ICONS = [
|
||||
'eye-off-outline',
|
||||
'eye-outline',
|
||||
'file-tray-stacked-outline',
|
||||
'finger-print-outline',
|
||||
'finger-print',
|
||||
'flash-outline',
|
||||
'folder-open-outline',
|
||||
'globe-outline',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[(ngModel)]="selected"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}*
|
||||
{{ spec.name }} *
|
||||
<select
|
||||
tuiSelect
|
||||
[placeholder]="spec.name"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<ion-item *ngIf="iFace">
|
||||
<ion-icon
|
||||
slot="start"
|
||||
size="large"
|
||||
[name]="
|
||||
iFace.type === 'ui'
|
||||
? 'desktop-outline'
|
||||
: iFace.type === 'api'
|
||||
? 'terminal-outline'
|
||||
: 'people-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ iFace.name }}</h1>
|
||||
<h2>{{ iFace.description }}</h2>
|
||||
<ion-button style="margin-right: 8px" (click)="presentDomainForm()">
|
||||
Add Domain
|
||||
</ion-button>
|
||||
<ion-button
|
||||
[color]="iFace.public ? 'danger' : 'success'"
|
||||
(click)="togglePublic()"
|
||||
>
|
||||
Make {{ iFace.public ? 'Private' : 'Public' }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div *ngIf="iFace" style="padding-left: 64px">
|
||||
<ion-item *ngFor="let address of iFace.addresses">
|
||||
<ion-label>
|
||||
<h2>{{ address.name }}</h2>
|
||||
<p>{{ address.url }}</p>
|
||||
<ion-button
|
||||
*ngIf="address.isDomain"
|
||||
color="danger"
|
||||
(click)="removeStandard(address.url)"
|
||||
>
|
||||
Remove
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="address.isOnion"
|
||||
color="danger"
|
||||
(click)="removeOnion(address.url)"
|
||||
>
|
||||
Remove
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button *ngIf="address.isDomain" (click)="showAcme(address.acme)">
|
||||
<ion-icon name="finger-print"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="iFace.type === 'ui'" (click)="launch(address.url)">
|
||||
<ion-icon name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button (click)="showQR(address.url)">
|
||||
<ion-icon name="qr-code-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button (click)="copy(address.url)">
|
||||
<ion-icon name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-item>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
p {
|
||||
font-family: 'Courier New';
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
import { Component, Inject, Input } from '@angular/core'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import {
|
||||
AlertController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import {
|
||||
copyToClipboard,
|
||||
ErrorService,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { ISB, T, utils } from '@start9labs/start-sdk'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { FormComponent } from 'src/app/components/form.component'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
import { ACME_URL, toAcmeName } from 'src/app/util/acme'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
export type MappedInterface = T.ServiceInterface & {
|
||||
addresses: MappedAddress[]
|
||||
public: boolean
|
||||
}
|
||||
export type MappedAddress = {
|
||||
name: string
|
||||
url: string
|
||||
isDomain: boolean
|
||||
isOnion: boolean
|
||||
acme: string | null
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'interface-info',
|
||||
templateUrl: './interface-info.component.html',
|
||||
styleUrls: ['./interface-info.component.scss'],
|
||||
})
|
||||
export class InterfaceInfoComponent {
|
||||
@Input() pkgId?: string
|
||||
@Input() iFace!: MappedInterface
|
||||
|
||||
constructor(
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly api: ApiService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly config: ConfigService,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {}
|
||||
|
||||
launch(url: string): void {
|
||||
this.windowRef.open(url, '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
async togglePublic() {
|
||||
const loader = this.loader
|
||||
.open(`Making ${this.iFace.public ? 'private' : 'public'}`)
|
||||
.subscribe()
|
||||
|
||||
const params = {
|
||||
internalPort: this.iFace.addressInfo.internalPort,
|
||||
public: !this.iFace.public,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.pkgId) {
|
||||
await this.api.pkgBindingSetPubic({
|
||||
...params,
|
||||
host: this.iFace.addressInfo.hostId,
|
||||
package: this.pkgId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverBindingSetPubic(params)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async presentDomainForm() {
|
||||
const acme = await firstValueFrom(this.patch.watch$('serverInfo', 'acme'))
|
||||
|
||||
const spec = getDomainSpec(Object.keys(acme))
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add Domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(spec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async (val: typeof spec._TYPE) => {
|
||||
if (val.type.selection === 'standard') {
|
||||
return this.saveStandard(
|
||||
val.type.value.domain,
|
||||
val.type.value.acme,
|
||||
)
|
||||
} else {
|
||||
return this.saveTor(val.type.value.key)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async removeStandard(url: string) {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
|
||||
const params = {
|
||||
domain: new URL(url).hostname,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.pkgId) {
|
||||
await this.api.pkgRemoveDomain({
|
||||
...params,
|
||||
package: this.pkgId,
|
||||
host: this.iFace.addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async removeOnion(url: string) {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
|
||||
const params = {
|
||||
onion: new URL(url).hostname,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.pkgId) {
|
||||
await this.api.pkgRemoveOnion({
|
||||
...params,
|
||||
package: this.pkgId,
|
||||
host: this.iFace.addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveOnion(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async showAcme(url: ACME_URL | string | null): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'ACME Provider',
|
||||
message: toAcmeName(url),
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async showQR(text: string): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: QRComponent,
|
||||
componentProps: {
|
||||
text,
|
||||
},
|
||||
cssClass: 'qr-modal',
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async copy(address: string): Promise<void> {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '').then(success => {
|
||||
message = success
|
||||
? 'Copied to clipboard!'
|
||||
: 'Failed to copy to clipboard.'
|
||||
})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
private async saveStandard(domain: string, acme: string) {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
const params = {
|
||||
domain,
|
||||
acme: acme === 'none' ? null : acme,
|
||||
private: false,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.pkgId) {
|
||||
await this.api.pkgAddDomain({
|
||||
...params,
|
||||
package: this.pkgId,
|
||||
host: this.iFace.addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveTor(key: string | null) {
|
||||
const loader = this.loader.open('Creating onion address').subscribe()
|
||||
|
||||
try {
|
||||
let onion = key
|
||||
? await this.api.addTorKey({ key })
|
||||
: await this.api.generateTorKey({})
|
||||
onion = `${onion}.onion`
|
||||
|
||||
if (this.pkgId) {
|
||||
await this.api.pkgAddOnion({
|
||||
onion,
|
||||
package: this.pkgId,
|
||||
host: this.iFace.addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddOnion({ onion })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDomainSpec(acme: string[]) {
|
||||
return ISB.InputSpec.of({
|
||||
type: ISB.Value.union(
|
||||
{ name: 'Type', default: 'standard' },
|
||||
ISB.Variants.of({
|
||||
standard: {
|
||||
name: 'Standard',
|
||||
spec: ISB.InputSpec.of({
|
||||
domain: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description: 'The domain or subdomain you want to use',
|
||||
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`,
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
acme: ISB.Value.select({
|
||||
name: 'ACME Provider',
|
||||
description:
|
||||
'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
|
||||
values: acme.reduce(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAcmeName(url),
|
||||
}),
|
||||
{ none: 'None (use system Root CA)' } as Record<string, string>,
|
||||
),
|
||||
default: '',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
onion: {
|
||||
name: 'Onion',
|
||||
spec: ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: 'Private Key (optional)',
|
||||
description:
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.',
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.base64],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export function getAddresses(
|
||||
serviceInterface: T.ServiceInterface,
|
||||
host: T.Host,
|
||||
config: ConfigService,
|
||||
): MappedAddress[] {
|
||||
const addressInfo = serviceInterface.addressInfo
|
||||
|
||||
let hostnames = host.hostnameInfo[addressInfo.internalPort]
|
||||
|
||||
hostnames = hostnames.filter(
|
||||
h =>
|
||||
config.isLocalhost() ||
|
||||
h.kind !== 'ip' ||
|
||||
h.hostname.kind !== 'ipv6' ||
|
||||
!h.hostname.value.startsWith('fe80::'),
|
||||
)
|
||||
if (config.isLocalhost()) {
|
||||
const local = hostnames.find(
|
||||
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
||||
)
|
||||
if (local) {
|
||||
hostnames.unshift({
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'lo',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
port: local.hostname.port,
|
||||
sslPort: local.hostname.sslPort,
|
||||
value: 'localhost',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
const mappedAddresses = hostnames.flatMap(h => {
|
||||
let name = ''
|
||||
let isDomain = false
|
||||
let isOnion = false
|
||||
let acme: string | null = null
|
||||
|
||||
if (h.kind === 'onion') {
|
||||
name = `Tor`
|
||||
isOnion = true
|
||||
} else {
|
||||
const hostnameKind = h.hostname.kind
|
||||
|
||||
if (hostnameKind === 'domain') {
|
||||
name = 'Domain'
|
||||
isDomain = true
|
||||
acme = host.domains[h.hostname.domain]?.acme
|
||||
} else {
|
||||
name =
|
||||
hostnameKind === 'local'
|
||||
? 'Local'
|
||||
: `${h.networkInterfaceId} (${hostnameKind})`
|
||||
}
|
||||
}
|
||||
|
||||
const addresses = utils.addressHostToUrl(addressInfo, h)
|
||||
if (addresses.length > 1) {
|
||||
return addresses.map(url => ({
|
||||
name: `${name} (${new URL(url).protocol
|
||||
.replace(':', '')
|
||||
.toUpperCase()})`,
|
||||
url,
|
||||
isDomain,
|
||||
isOnion,
|
||||
acme,
|
||||
}))
|
||||
} else {
|
||||
return addresses.map(url => ({
|
||||
name,
|
||||
url,
|
||||
isDomain,
|
||||
isOnion,
|
||||
acme,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
return mappedAddresses.filter(
|
||||
(value, index, self) => index === self.findIndex(t => t.url === value.url),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { InterfaceInfoComponent } from './interface-info.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [InterfaceInfoComponent],
|
||||
imports: [CommonModule, IonicModule],
|
||||
exports: [InterfaceInfoComponent],
|
||||
})
|
||||
export class InterfaceInfoModule {}
|
||||
@@ -1,44 +0,0 @@
|
||||
<ion-item *ngIf="iFace">
|
||||
<ion-icon
|
||||
slot="start"
|
||||
size="large"
|
||||
[name]="
|
||||
iFace.type === 'ui'
|
||||
? 'desktop-outline'
|
||||
: iFace.type === 'api'
|
||||
? 'terminal-outline'
|
||||
: 'people-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ iFace.name }}</h1>
|
||||
<h2>{{ iFace.description }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div *ngIf="iFace" style="padding-left: 64px">
|
||||
<ion-item *ngFor="let address of iFace.addresses">
|
||||
<ion-label>
|
||||
<h2>{{ address.name }}</h2>
|
||||
<p>{{ address.url }}</p>
|
||||
</ion-label>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button
|
||||
*ngIf="iFace.type === 'ui'"
|
||||
fill="clear"
|
||||
(click)="launch(address.url)"
|
||||
>
|
||||
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="showQR(address.url)">
|
||||
<ion-icon
|
||||
size="small"
|
||||
slot="icon-only"
|
||||
name="qr-code-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copy(address.url)">
|
||||
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-item>
|
||||
</div>
|
||||
@@ -3,11 +3,8 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
|
||||
import {
|
||||
AppInterfacesItemComponent,
|
||||
AppInterfacesPage,
|
||||
} from './app-interfaces.page'
|
||||
import { AppInterfacesPage } from './app-interfaces.page'
|
||||
import { InterfaceInfoModule } from 'src/app/components/interface-info/interface-info.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -22,7 +19,8 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedPipesModule,
|
||||
InterfaceInfoModule,
|
||||
],
|
||||
declarations: [AppInterfacesPage, AppInterfacesItemComponent],
|
||||
declarations: [AppInterfacesPage],
|
||||
})
|
||||
export class AppInterfacesPageModule {}
|
||||
|
||||
@@ -13,26 +13,29 @@
|
||||
>
|
||||
<ng-container *ngIf="serviceInterfaces.ui.length">
|
||||
<ion-item-divider>User Interfaces</ion-item-divider>
|
||||
<app-interfaces-item
|
||||
<interface-info
|
||||
*ngFor="let ui of serviceInterfaces.ui"
|
||||
[iFace]="ui"
|
||||
></app-interfaces-item>
|
||||
[pkgId]="pkgId"
|
||||
></interface-info>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="serviceInterfaces.api.length">
|
||||
<ion-item-divider>Application Program Interfaces</ion-item-divider>
|
||||
<app-interfaces-item
|
||||
<interface-info
|
||||
*ngFor="let api of serviceInterfaces.api"
|
||||
[iFace]="api"
|
||||
></app-interfaces-item>
|
||||
[pkgId]="pkgId"
|
||||
></interface-info>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="serviceInterfaces.p2p.length">
|
||||
<ion-item-divider>Peer-To-Peer Interfaces</ion-item-divider>
|
||||
<app-interfaces-item
|
||||
<interface-info
|
||||
*ngFor="let p2p of serviceInterfaces.p2p"
|
||||
[iFace]="p2p"
|
||||
></app-interfaces-item>
|
||||
[pkgId]="pkgId"
|
||||
></interface-info>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
p {
|
||||
font-family: 'Courier New';
|
||||
}
|
||||
@@ -1,21 +1,11 @@
|
||||
import { Component, Inject, Input } from '@angular/core'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ModalController, ToastController } from '@ionic/angular'
|
||||
import { copyToClipboard, getPkgId } from '@start9labs/shared'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { combineLatest, map } from 'rxjs'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
|
||||
type MappedInterface = T.ServiceInterface & {
|
||||
addresses: MappedAddress[]
|
||||
}
|
||||
type MappedAddress = {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
import { getAddresses } from 'src/app/components/interface-info/interface-info.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-interfaces',
|
||||
@@ -46,12 +36,11 @@ export class AppInterfacesPage {
|
||||
iface.name.toLowerCase() > iface.name.toLowerCase() ? -1 : 1,
|
||||
)
|
||||
.map(iface => {
|
||||
const host = hosts[iface.addressInfo.hostId]
|
||||
return {
|
||||
...iface,
|
||||
addresses: getAddresses(
|
||||
iface,
|
||||
hosts[iface.addressInfo.hostId] || {},
|
||||
),
|
||||
public: host.bindings[iface.addressInfo.internalPort].net.public,
|
||||
addresses: getAddresses(iface, host, this.config),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -66,124 +55,6 @@ export class AppInterfacesPage {
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-interfaces-item',
|
||||
templateUrl: './app-interfaces-item.component.html',
|
||||
styleUrls: ['./app-interfaces.page.scss'],
|
||||
})
|
||||
export class AppInterfacesItemComponent {
|
||||
@Input() iFace!: MappedInterface
|
||||
|
||||
constructor(
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {}
|
||||
|
||||
launch(url: string): void {
|
||||
this.windowRef.open(url, '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
async showQR(text: string): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: QRComponent,
|
||||
componentProps: {
|
||||
text,
|
||||
},
|
||||
cssClass: 'qr-modal',
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async copy(address: string): Promise<void> {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '').then(success => {
|
||||
message = success
|
||||
? 'Copied to clipboard!'
|
||||
: 'Failed to copy to clipboard.'
|
||||
})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
}
|
||||
|
||||
function getAddresses(
|
||||
serviceInterface: T.ServiceInterface,
|
||||
host: T.Host,
|
||||
): MappedAddress[] {
|
||||
const addressInfo = serviceInterface.addressInfo
|
||||
|
||||
let hostnames =
|
||||
host.kind === 'multi' ? host.hostnameInfo[addressInfo.internalPort] : []
|
||||
|
||||
hostnames = hostnames.filter(
|
||||
h =>
|
||||
window.location.host === 'localhost' ||
|
||||
h.kind !== 'ip' ||
|
||||
h.hostname.kind !== 'ipv6' ||
|
||||
!h.hostname.value.startsWith('fe80::'),
|
||||
)
|
||||
if (window.location.host === 'localhost') {
|
||||
const local = hostnames.find(
|
||||
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
||||
)
|
||||
if (local) {
|
||||
hostnames.unshift({
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'lo',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
port: local.hostname.port,
|
||||
sslPort: local.hostname.sslPort,
|
||||
value: 'localhost',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
const addressesWithNames = hostnames.flatMap(h => {
|
||||
let name = ''
|
||||
|
||||
if (h.kind === 'onion') {
|
||||
name = `Tor`
|
||||
} else {
|
||||
const hostnameKind = h.hostname.kind
|
||||
|
||||
if (hostnameKind === 'domain') {
|
||||
name = 'Domain'
|
||||
} else {
|
||||
name =
|
||||
hostnameKind === 'local'
|
||||
? 'Local'
|
||||
: `${h.networkInterfaceId} (${hostnameKind})`
|
||||
}
|
||||
}
|
||||
|
||||
const addresses = utils.addressHostToUrl(addressInfo, h)
|
||||
if (addresses.length > 1) {
|
||||
return addresses.map(url => ({
|
||||
name: `${name} (${new URL(url).protocol
|
||||
.replace(':', '')
|
||||
.toUpperCase()})`,
|
||||
url,
|
||||
}))
|
||||
} else {
|
||||
return addresses.map(url => ({
|
||||
name,
|
||||
url,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
return addressesWithNames.filter(
|
||||
(value, index, self) => index === self.findIndex(t => t.url === value.url),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { ACMEPage } from './acme.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ACMEPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, RouterModule.forChild(routes)],
|
||||
declarations: [ACMEPage],
|
||||
})
|
||||
export class ACMEPageModule {}
|
||||
@@ -0,0 +1,56 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="system"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>ACME</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<ion-item-group>
|
||||
<!-- always -->
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>
|
||||
Register with one or more ACME providers such as Let's Encrypt in
|
||||
order to generate SSL (https) certificates on-demand for clearnet
|
||||
hosting
|
||||
<a [href]="docsUrl" target="_blank" rel="noreferrer">
|
||||
View instructions
|
||||
</a>
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Saved Providers</ion-item-divider>
|
||||
|
||||
<ion-item button detail="false" (click)="presentFormAcme()">
|
||||
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
||||
<ion-label>
|
||||
<b>Add Provider</b>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="acme$ | async as acme">
|
||||
<ion-item *ngFor="let provider of acme | keyvalue">
|
||||
<ion-icon slot="start" name="finger-print" size="medium"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ toAcmeName(provider.key) }}</h2>
|
||||
<p *ngFor="let contact of provider.value.contact">
|
||||
Contact: {{ contact }}
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="danger"
|
||||
(click)="removeAcme(provider.key)"
|
||||
>
|
||||
<ion-icon slot="start" name="close"></ion-icon>
|
||||
Remove
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
117
web/projects/ui/src/app/pages/server-routes/acme/acme.page.ts
Normal file
117
web/projects/ui/src/app/pages/server-routes/acme/acme.page.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from '../../../services/patch-db/data-model'
|
||||
import { FormDialogService } from '../../../services/form-dialog.service'
|
||||
import { FormComponent } from '../../../components/form.component'
|
||||
import { configBuilderToSpec } from '../../../util/configBuilderToSpec'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { ACME_Name, ACME_URL, knownACME, toAcmeName } from 'src/app/util/acme'
|
||||
|
||||
@Component({
|
||||
selector: 'acme',
|
||||
templateUrl: 'acme.page.html',
|
||||
styleUrls: ['acme.page.scss'],
|
||||
})
|
||||
export class ACMEPage {
|
||||
readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme'
|
||||
|
||||
acme$ = this.patch.watch$('serverInfo', 'acme')
|
||||
|
||||
toAcmeName = toAcmeName
|
||||
|
||||
constructor(
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
async presentFormAcme() {
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add ACME Provider',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(acmeSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async (val: typeof acmeSpec._TYPE) => this.saveAcme(val),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async removeAcme(provider: string) {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.removeAcme({ provider })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveAcme(val: typeof acmeSpec._TYPE) {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
const rawUrl =
|
||||
val.provider.selection === 'other'
|
||||
? val.provider.value.url
|
||||
: val.provider.selection
|
||||
|
||||
try {
|
||||
await this.api.initAcme({
|
||||
provider: new URL(rawUrl).href,
|
||||
contact: [`mailto:${val.contact}`],
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const acmeSpec = ISB.InputSpec.of({
|
||||
provider: ISB.Value.union(
|
||||
{ name: 'Provider', default: knownACME['Let\'s Encrypt'] as any },
|
||||
ISB.Variants.of({
|
||||
...Object.entries(knownACME).reduce(
|
||||
(obj, [name, url]) => ({
|
||||
...obj,
|
||||
[url]: {
|
||||
name,
|
||||
spec: ISB.InputSpec.of({}),
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
other: {
|
||||
name: 'Other',
|
||||
spec: ISB.InputSpec.of({
|
||||
url: ISB.Value.text({
|
||||
name: 'URL',
|
||||
default: null,
|
||||
required: true,
|
||||
inputmode: 'url',
|
||||
patterns: [utils.Patterns.url],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
contact: ISB.Value.text({
|
||||
name: 'Contact Email',
|
||||
default: null,
|
||||
required: true,
|
||||
inputmode: 'email',
|
||||
patterns: [utils.Patterns.email],
|
||||
}),
|
||||
})
|
||||
@@ -85,6 +85,11 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./email/email.module').then(m => m.EmailPageModule),
|
||||
},
|
||||
{
|
||||
path: 'acme',
|
||||
loadChildren: () =>
|
||||
import('./acme/acme.module').then(m => m.ACMEPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -463,6 +463,15 @@ export class ServerShowPage {
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'ACME',
|
||||
description: `Add ACME providers to create SSL certificates for clearnet access`,
|
||||
icon: 'finger-print',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['acme'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
description: 'Connect to an external SMTP server for sending emails',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ServerSpecsPage } from './server-specs.page'
|
||||
import { ExverPipesModule } from '@start9labs/shared'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { InterfaceInfoModule } from 'src/app/components/interface-info/interface-info.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -22,6 +23,7 @@ const routes: Routes = [
|
||||
QRComponentModule,
|
||||
ExverPipesModule,
|
||||
TuiLetModule,
|
||||
InterfaceInfoModule,
|
||||
],
|
||||
declarations: [ServerSpecsPage],
|
||||
})
|
||||
|
||||
@@ -25,73 +25,6 @@
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Web Addresses</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label class="break-all">
|
||||
<h2>Tor</h2>
|
||||
<p>{{ server.torAddress }}</p>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button fill="clear" (click)="showQR(server.torAddress)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="qr-code-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copy(server.torAddress)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="copy-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label class="break-all">
|
||||
<h2>LAN</h2>
|
||||
<p>{{ server.lanAddress }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copy(server.lanAddress)">
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let iface of server.networkInterfaces | keyvalue">
|
||||
<ng-container *ngIf="(isLocalhost || iface.key !== 'lo') && iface.value.ipInfo">
|
||||
<ng-container *ngFor="let ipnet of iface.value.ipInfo.subnets">
|
||||
<ng-container *ngIf="!ipnet.includes('fe80::') || isLocalhost">
|
||||
<ion-item *tuiLet="ipnet.split('/')[0] as ipAddr">
|
||||
<ion-label>
|
||||
<h2>{{ iface.key }} ({{ ipAddr.includes("::") ? "IPv6" : "IPv4" }})</h2>
|
||||
<p>{{
|
||||
ipAddr.includes("fe80::")
|
||||
? "[" + ipAddr + "%" + iface.value.ipInfo.scopeId + "]"
|
||||
: ipAddr.includes("::")
|
||||
? "[" + ipAddr + "]"
|
||||
: ipAddr
|
||||
}}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copy(ipAddr)">
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ion-item-divider>Device Credentials</ion-item-divider>
|
||||
<!-- <ion-item>
|
||||
<ion-label>
|
||||
<h2>Pubkey</h2>
|
||||
<p>{{ server['pubkey'] }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copy(server['pubkey'])">
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item> -->
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>CA fingerprint</h2>
|
||||
@@ -101,5 +34,9 @@
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Web Addresses</ion-item-divider>
|
||||
|
||||
<interface-info *ngIf="ui$ | async as ui" [iFace]="ui"></interface-info>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ModalController, ToastController } from '@ionic/angular'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { copyToClipboard } from '@start9labs/shared'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { map, Observable } from 'rxjs'
|
||||
import {
|
||||
getAddresses,
|
||||
MappedInterface,
|
||||
} from 'src/app/components/interface-info/interface-info.component'
|
||||
|
||||
const iface = {
|
||||
id: '',
|
||||
name: 'StartOS User Interface',
|
||||
description:
|
||||
'The primary user interface for your StartOS server, accessible from any browser.',
|
||||
type: 'ui' as const,
|
||||
masked: false,
|
||||
addressInfo: {
|
||||
hostId: '',
|
||||
internalPort: 80,
|
||||
scheme: 'http',
|
||||
sslScheme: 'https',
|
||||
suffix: '',
|
||||
username: null,
|
||||
},
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'server-specs',
|
||||
@@ -14,11 +35,17 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
})
|
||||
export class ServerSpecsPage {
|
||||
readonly server$ = this.patch.watch$('serverInfo')
|
||||
readonly isLocalhost = window.location.host === 'localhost'
|
||||
|
||||
readonly ui$: Observable<MappedInterface> = this.server$.pipe(
|
||||
map(server => ({
|
||||
...iface,
|
||||
public: server.host.bindings[iface.addressInfo.internalPort].net.public,
|
||||
addresses: getAddresses(iface, server.host, this.config),
|
||||
})),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
@@ -42,19 +69,4 @@ export class ServerSpecsPage {
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async showQR(text: string): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: QRComponent,
|
||||
componentProps: {
|
||||
text,
|
||||
},
|
||||
cssClass: 'qr-modal',
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1771,8 +1771,21 @@ export module Mock {
|
||||
currentDependencies: {},
|
||||
hosts: {
|
||||
abcdefg: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
80: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
@@ -1857,8 +1870,21 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
8332: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8332,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
@@ -1866,8 +1892,21 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
8333: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8333,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
|
||||
@@ -219,6 +219,80 @@ export module RR {
|
||||
|
||||
// package
|
||||
|
||||
export type InitAcmeReq = {
|
||||
provider: 'letsencrypt' | 'letsencrypt-staging' | string
|
||||
contact: string[]
|
||||
}
|
||||
export type InitAcmeRes = null
|
||||
|
||||
export type RemoveAcmeReq = {
|
||||
provider: string
|
||||
}
|
||||
export type RemoveAcmeRes = null
|
||||
|
||||
export type AddTorKeyReq = {
|
||||
// net.tor.key.add
|
||||
key: string
|
||||
}
|
||||
export type GenerateTorKeyReq = {} // net.tor.key.generate
|
||||
export type AddTorKeyRes = string // onion address without .onion suffix
|
||||
|
||||
export type ServerBindingSetPublicReq = {
|
||||
// server.host.binding.set-public
|
||||
internalPort: number
|
||||
public: boolean | null // default true
|
||||
}
|
||||
export type BindingSetPublicRes = null
|
||||
|
||||
export type ServerAddOnionReq = {
|
||||
// server.host.address.onion.add
|
||||
onion: string // address *with* .onion suffix
|
||||
}
|
||||
export type AddOnionRes = null
|
||||
|
||||
export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove
|
||||
export type RemoveOnionRes = null
|
||||
|
||||
export type ServerAddDomainReq = {
|
||||
// server.host.address.domain.add
|
||||
domain: string // FQDN
|
||||
private: boolean
|
||||
acme: string | null // "letsencrypt" | "letsencrypt-staging" | Url | null
|
||||
}
|
||||
export type AddDomainRes = null
|
||||
|
||||
export type ServerRemoveDomainReq = {
|
||||
// server.host.address.domain.remove
|
||||
domain: string // FQDN
|
||||
}
|
||||
export type RemoveDomainRes = null
|
||||
|
||||
export type PkgBindingSetPublicReq = ServerBindingSetPublicReq & {
|
||||
// package.host.binding.set-public
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type PkgAddOnionReq = ServerAddOnionReq & {
|
||||
// package.host.address.onion.add
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove
|
||||
|
||||
export type PkgAddDomainReq = ServerAddDomainReq & {
|
||||
// package.host.address.domain.add
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type PkgRemoveDomainReq = ServerRemoveDomainReq & {
|
||||
// package.host.address.domain.remove
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = LogsRes
|
||||
|
||||
|
||||
@@ -259,4 +259,48 @@ export abstract class ApiService {
|
||||
): Promise<RR.UninstallPackageRes>
|
||||
|
||||
abstract sideloadPackage(): Promise<RR.SideloadPackageRes>
|
||||
|
||||
abstract initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes>
|
||||
|
||||
abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes>
|
||||
|
||||
abstract addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes>
|
||||
|
||||
abstract generateTorKey(
|
||||
params: RR.GenerateTorKeyReq,
|
||||
): Promise<RR.AddTorKeyRes>
|
||||
|
||||
abstract serverBindingSetPubic(
|
||||
params: RR.ServerBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes>
|
||||
|
||||
abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
abstract serverRemoveOnion(
|
||||
params: RR.ServerRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
|
||||
abstract serverAddDomain(
|
||||
params: RR.ServerAddDomainReq,
|
||||
): Promise<RR.AddDomainRes>
|
||||
|
||||
abstract serverRemoveDomain(
|
||||
params: RR.ServerRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes>
|
||||
|
||||
abstract pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes>
|
||||
|
||||
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
abstract pkgRemoveOnion(
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
|
||||
abstract pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes>
|
||||
|
||||
abstract pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes>
|
||||
}
|
||||
|
||||
@@ -516,6 +516,118 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.acme.delete',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.acme.init',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.tor.key.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.tor.key.generate',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverBindingSetPubic(
|
||||
params: RR.ServerBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.binding.set-public',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.onion.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverRemoveOnion(
|
||||
params: RR.ServerRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.onion.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverAddDomain(
|
||||
params: RR.ServerAddDomainReq,
|
||||
): Promise<RR.AddDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.domain.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverRemoveDomain(
|
||||
params: RR.ServerRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.domain.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.binding.set-public',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.onion.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgRemoveOnion(
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.onion.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.domain.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.domain.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(
|
||||
options: RPCOptions,
|
||||
urlOverride?: string,
|
||||
|
||||
@@ -19,16 +19,7 @@ import {
|
||||
import { CifsBackupTarget, RR } from './api.types'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
import {
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
startWith,
|
||||
Subject,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -38,6 +29,7 @@ import {
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
import { toAcmeUrl } from 'src/app/util/acme'
|
||||
|
||||
const PROGRESS: T.FullProgress = {
|
||||
overall: {
|
||||
@@ -1064,6 +1056,283 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/acme`,
|
||||
value: {
|
||||
[toAcmeUrl(params.provider)]: { contact: [params.contact] },
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const regex = new RegExp('/', 'g')
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/acme/${params.provider.replace(regex, '~1')}`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'vanityabcdefghijklmnop'
|
||||
}
|
||||
|
||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'abcdefghijklmnopqrstuv'
|
||||
}
|
||||
|
||||
async serverBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/host/bindings/${params.internalPort}/net/public`,
|
||||
value: params.public,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/onions/0`,
|
||||
value: params.onion,
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
value: params.onion,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async serverRemoveOnion(
|
||||
params: RR.ServerRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/onions/0`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/-1`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async serverAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/domains`,
|
||||
value: {
|
||||
[params.domain]: { public: !params.private, acme: params.acme },
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
domain: params.domain,
|
||||
subdomain: null,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async serverRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/domains/${params.domain}`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/public`,
|
||||
value: params.public,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/onions/0`,
|
||||
value: params.onion,
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
value: params.onion,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgRemoveOnion(
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/onions/0`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains`,
|
||||
value: {
|
||||
[params.domain]: { public: !params.private, acme: params.acme },
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
domain: params.domain,
|
||||
subdomain: null,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.domain}`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async initProgress(): Promise<T.FullProgress> {
|
||||
const progress = JSON.parse(JSON.stringify(PROGRESS))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Mock } from './api.fixures'
|
||||
import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets'
|
||||
import { knownACME } from 'src/app/util/acme'
|
||||
const version = require('../../../../../../package.json').version
|
||||
|
||||
export const mockPatchData: DataModel = {
|
||||
@@ -35,19 +36,16 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
serverInfo: {
|
||||
arch: 'x86_64',
|
||||
onionAddress: 'myveryownspecialtoraddress',
|
||||
id: 'abcdefgh',
|
||||
version,
|
||||
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||
lanAddress: 'https://adjective-noun.local',
|
||||
torAddress: 'https://myveryownspecialtoraddress.onion',
|
||||
networkInterfaces: {
|
||||
eth0: {
|
||||
public: false,
|
||||
ipInfo: {
|
||||
scopeId: 1,
|
||||
deviceType: 'ethernet',
|
||||
subnets: ['10.0.0.1/24'],
|
||||
subnets: ['10.0.0.2/24'],
|
||||
wanIp: null,
|
||||
ntpServers: [],
|
||||
},
|
||||
@@ -59,14 +57,18 @@ export const mockPatchData: DataModel = {
|
||||
deviceType: 'wireless',
|
||||
subnets: [
|
||||
'10.0.90.12/24',
|
||||
'FE80:CD00:0000:0CDE:1257:0000:211E:729CD/64',
|
||||
'fe80::cd00:0000:0cde:1257:0000:211e:72cd/64',
|
||||
],
|
||||
wanIp: null,
|
||||
ntpServers: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
acme: {},
|
||||
acme: {
|
||||
[Object.keys(knownACME)[0]]: {
|
||||
contact: ['mailto:support@start9.com'],
|
||||
},
|
||||
},
|
||||
unreadNotificationCount: 4,
|
||||
// password is asdfasdf
|
||||
passwordHash:
|
||||
@@ -81,6 +83,108 @@ export const mockPatchData: DataModel = {
|
||||
shuttingDown: false,
|
||||
},
|
||||
hostname: 'random-words',
|
||||
host: {
|
||||
bindings: {
|
||||
80: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: null,
|
||||
assignedSslPort: 443,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
preferredExternalPort: 80,
|
||||
addSsl: {
|
||||
preferredExternalPort: 443,
|
||||
alpn: { specified: ['http/1.1', 'h2'] },
|
||||
},
|
||||
secure: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
domains: {},
|
||||
onions: ['myveryownspecialtoraddress'],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.1',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'myveryownspecialtoraddress.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
||||
caFingerprint: 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15',
|
||||
ntpSynced: false,
|
||||
@@ -201,8 +305,21 @@ export const mockPatchData: DataModel = {
|
||||
currentDependencies: {},
|
||||
hosts: {
|
||||
abcdefg: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
80: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
@@ -257,7 +374,7 @@ export const mockPatchData: DataModel = {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
@@ -269,7 +386,7 @@ export const mockPatchData: DataModel = {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:1234]',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
@@ -287,8 +404,21 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
8332: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8332,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
@@ -296,8 +426,21 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
8333: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8333,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
import { PackageDataEntry } from './patch-db/data-model'
|
||||
|
||||
const {
|
||||
@@ -28,8 +28,7 @@ export class ConfigService {
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
isConsulate = (window as any)['platform'] === 'ios'
|
||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||
supportsWebSockets = !!window.WebSocket
|
||||
|
||||
isTor(): boolean {
|
||||
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
|
||||
@@ -41,10 +40,55 @@ export class ConfigService {
|
||||
: this.hostname.endsWith('.local')
|
||||
}
|
||||
|
||||
isLocalhost(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'localhost'
|
||||
: this.hostname === 'localhost' || this.hostname === '127.0.0.1'
|
||||
}
|
||||
|
||||
isIpv4(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'ipv4'
|
||||
: new RegExp(utils.Patterns.ipv4.regex).test(this.hostname)
|
||||
}
|
||||
|
||||
isLanIpv4(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'ipv4'
|
||||
: new RegExp(utils.Patterns.ipv4.regex).test(this.hostname) &&
|
||||
(this.hostname.startsWith('192.168.') ||
|
||||
this.hostname.startsWith('10.') ||
|
||||
(this.hostname.startsWith('172.') &&
|
||||
!![this.hostname.split('.').map(Number)[1]].filter(
|
||||
n => n >= 16 && n < 32,
|
||||
).length))
|
||||
}
|
||||
|
||||
isIpv6(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'ipv6'
|
||||
: new RegExp(utils.Patterns.ipv6.regex).test(this.hostname)
|
||||
}
|
||||
|
||||
isLanHttp(): boolean {
|
||||
return !this.isTor() && !this.isLocalhost() && !this.isHttps()
|
||||
}
|
||||
|
||||
isClearnet(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'clearnet'
|
||||
: this.isHttps() &&
|
||||
!this.isTor() &&
|
||||
!this.isLocal() &&
|
||||
!this.isLocalhost() &&
|
||||
!this.isLanIpv4() &&
|
||||
!this.isIpv6()
|
||||
}
|
||||
|
||||
isHttps(): boolean {
|
||||
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
|
||||
}
|
||||
|
||||
isSecure(): boolean {
|
||||
return window.isSecureContext || this.isTor()
|
||||
}
|
||||
@@ -59,48 +103,154 @@ export class ConfigService {
|
||||
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
||||
launchableAddress(
|
||||
interfaces: PackageDataEntry['serviceInterfaces'],
|
||||
hosts: PackageDataEntry['hosts'],
|
||||
hosts: T.Hosts,
|
||||
): string {
|
||||
const ui = Object.values(interfaces).find(
|
||||
i =>
|
||||
i.type === 'ui' &&
|
||||
(i.addressInfo.scheme === 'http' ||
|
||||
i.addressInfo.sslScheme === 'https'),
|
||||
) // TODO: select if multiple
|
||||
)
|
||||
|
||||
if (!ui) return ''
|
||||
|
||||
const hostnameInfo =
|
||||
hosts[ui.addressInfo.hostId]?.hostnameInfo[ui.addressInfo.internalPort]
|
||||
const host = hosts[ui.addressInfo.hostId]
|
||||
|
||||
if (!host) return ''
|
||||
|
||||
let hostnameInfo = host.hostnameInfo[ui.addressInfo.internalPort]
|
||||
hostnameInfo = hostnameInfo.filter(
|
||||
h =>
|
||||
this.isLocalhost() ||
|
||||
h.kind !== 'ip' ||
|
||||
h.hostname.kind !== 'ipv6' ||
|
||||
!h.hostname.value.startsWith('fe80::'),
|
||||
)
|
||||
if (this.isLocalhost()) {
|
||||
const local = hostnameInfo.find(
|
||||
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
||||
)
|
||||
if (local) {
|
||||
hostnameInfo.unshift({
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'lo',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
port: local.hostname.port,
|
||||
sslPort: local.hostname.sslPort,
|
||||
value: 'localhost',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!hostnameInfo) return ''
|
||||
|
||||
const addressInfo = ui.addressInfo
|
||||
const scheme = this.isHttps()
|
||||
? ui.addressInfo.sslScheme === 'https'
|
||||
? 'https'
|
||||
: 'http'
|
||||
: ui.addressInfo.scheme === 'http'
|
||||
? 'http'
|
||||
: 'https'
|
||||
const username = addressInfo.username ? addressInfo.username + '@' : ''
|
||||
const suffix = addressInfo.suffix || ''
|
||||
const url = new URL(`${scheme}://${username}placeholder${suffix}`)
|
||||
|
||||
const onionHostname = hostnameInfo.find(h => h.kind === 'onion')
|
||||
?.hostname as T.OnionHostname | undefined
|
||||
|
||||
if (this.isTor() && onionHostname) {
|
||||
url.hostname = onionHostname.value
|
||||
} else {
|
||||
const ipHostname = hostnameInfo.find(h => h.kind === 'ip')?.hostname as
|
||||
| T.IpHostname
|
||||
const url = new URL(`https://${username}placeholder${suffix}`)
|
||||
const use = (hostname: {
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}) => {
|
||||
url.hostname = hostname.value
|
||||
const useSsl =
|
||||
hostname.port && hostname.sslPort ? this.isHttps() : !!hostname.sslPort
|
||||
url.protocol = useSsl
|
||||
? `${addressInfo.sslScheme || 'https'}:`
|
||||
: `${addressInfo.scheme || 'http'}:`
|
||||
const port = useSsl ? hostname.sslPort : hostname.port
|
||||
const omitPort = useSsl
|
||||
? ui.addressInfo.sslScheme === 'https' && port === 443
|
||||
: ui.addressInfo.scheme === 'http' && port === 80
|
||||
if (!omitPort && port) url.port = String(port)
|
||||
}
|
||||
const useFirst = (
|
||||
hostnames: (
|
||||
| {
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
| undefined
|
||||
)[],
|
||||
) => {
|
||||
const first = hostnames.find(h => h)
|
||||
if (first) {
|
||||
use(first)
|
||||
}
|
||||
return !!first
|
||||
}
|
||||
|
||||
if (!ipHostname) return ''
|
||||
const ipHostnames = hostnameInfo
|
||||
.filter(h => h.kind === 'ip')
|
||||
.map(h => h.hostname) as T.IpHostname[]
|
||||
const domainHostname = ipHostnames
|
||||
.filter(h => h.kind === 'domain')
|
||||
.map(h => h as T.IpHostname & { kind: 'domain' })
|
||||
.map(h => ({
|
||||
value: h.domain,
|
||||
sslPort: h.sslPort,
|
||||
port: h.port,
|
||||
}))[0]
|
||||
const wanIpHostname = hostnameInfo
|
||||
.filter(h => h.kind === 'ip' && h.public && h.hostname.kind !== 'domain')
|
||||
.map(h => h.hostname as Exclude<T.IpHostname, { kind: 'domain' }>)
|
||||
.map(h => ({
|
||||
value: h.value,
|
||||
sslPort: h.sslPort,
|
||||
port: h.port,
|
||||
}))[0]
|
||||
const onionHostname = hostnameInfo
|
||||
.filter(h => h.kind === 'onion')
|
||||
.map(h => h as T.HostnameInfo & { kind: 'onion' })
|
||||
.map(h => ({
|
||||
value: h.hostname.value,
|
||||
sslPort: h.hostname.sslPort,
|
||||
port: h.hostname.port,
|
||||
}))[0]
|
||||
const localHostname = ipHostnames
|
||||
.filter(h => h.kind === 'local')
|
||||
.map(h => h as T.IpHostname & { kind: 'local' })
|
||||
.map(h => ({ value: h.value, sslPort: h.sslPort, port: h.port }))[0]
|
||||
|
||||
url.hostname = this.hostname
|
||||
url.port = String(ipHostname.sslPort || ipHostname.port)
|
||||
if (this.isClearnet()) {
|
||||
if (
|
||||
!useFirst([domainHostname, wanIpHostname, onionHostname, localHostname])
|
||||
) {
|
||||
return ''
|
||||
}
|
||||
} else if (this.isTor()) {
|
||||
if (
|
||||
!useFirst([onionHostname, domainHostname, wanIpHostname, localHostname])
|
||||
) {
|
||||
return ''
|
||||
}
|
||||
} else if (this.isIpv6()) {
|
||||
const ipv6Hostname = ipHostnames.find(h => h.kind === 'ipv6') as {
|
||||
kind: 'ipv6'
|
||||
value: string
|
||||
scopeId: number
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
|
||||
if (!useFirst([ipv6Hostname, localHostname])) {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
// ipv4 or .local or localhost
|
||||
|
||||
if (!localHostname) return ''
|
||||
|
||||
use({
|
||||
value: this.hostname,
|
||||
port: localHostname.port,
|
||||
sslPort: localHostname.sslPort,
|
||||
})
|
||||
}
|
||||
|
||||
return url.href
|
||||
@@ -109,16 +259,6 @@ export class ConfigService {
|
||||
getHost(): string {
|
||||
return this.host
|
||||
}
|
||||
|
||||
private isLocalhost(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'localhost'
|
||||
: this.hostname === 'localhost'
|
||||
}
|
||||
|
||||
private isHttps(): boolean {
|
||||
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
|
||||
}
|
||||
}
|
||||
|
||||
export function hasUi(
|
||||
|
||||
@@ -135,7 +135,7 @@ export class FormService {
|
||||
return this.formBuilder.control(value)
|
||||
case 'select':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value)
|
||||
return this.formBuilder.control(value, [Validators.required])
|
||||
case 'multiselect':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value, multiselectValidators(spec))
|
||||
|
||||
21
web/projects/ui/src/app/util/acme.ts
Normal file
21
web/projects/ui/src/app/util/acme.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function toAcmeName(url: ACME_URL | string | null): ACME_Name | string {
|
||||
return (
|
||||
Object.entries(knownACME).find(([_, val]) => val === url)?.[0] ||
|
||||
url ||
|
||||
'System CA'
|
||||
)
|
||||
}
|
||||
|
||||
export function toAcmeUrl(name: ACME_Name | string): ACME_URL | string {
|
||||
return knownACME[name as ACME_Name] || name
|
||||
}
|
||||
|
||||
export const knownACME = {
|
||||
'Let\'s Encrypt': 'https://acme-v02.api.letsencrypt.org/directory',
|
||||
'Let\'s Encrypt (Staging)':
|
||||
'https://acme-staging-v02.api.letsencrypt.org/directory',
|
||||
}
|
||||
|
||||
export type ACME_Name = keyof typeof knownACME
|
||||
|
||||
export type ACME_URL = (typeof knownACME)[ACME_Name]
|
||||
Reference in New Issue
Block a user