diff --git a/core/locales/i18n.yaml b/core/locales/i18n.yaml index 7c7984d63..9b3d32cce 100644 --- a/core/locales/i18n.yaml +++ b/core/locales/i18n.yaml @@ -994,6 +994,20 @@ disk.mount.binding: fr_FR: "Liaison de %{src} à %{dst}" pl_PL: "Wiązanie %{src} do %{dst}" +hostname.empty: + en_US: "Hostname cannot be empty" + de_DE: "Der Hostname darf nicht leer sein" + es_ES: "El nombre de host no puede estar vacío" + fr_FR: "Le nom d'hôte ne peut pas être vide" + pl_PL: "Nazwa hosta nie może być pusta" + +hostname.invalid-character: + en_US: "Invalid character in hostname: %{char}" + de_DE: "Ungültiges Zeichen im Hostnamen: %{char}" + es_ES: "Carácter no válido en el nombre de host: %{char}" + fr_FR: "Caractère invalide dans le nom d'hôte : %{char}" + pl_PL: "Nieprawidłowy znak w nazwie hosta: %{char}" + # init.rs init.running-preinit: en_US: "Running preinit.sh" @@ -5204,6 +5218,13 @@ about.set-country: fr_FR: "Définir le pays" pl_PL: "Ustaw kraj" +about.set-hostname: + en_US: "Set the server hostname" + de_DE: "Den Server-Hostnamen festlegen" + es_ES: "Establecer el nombre de host del servidor" + fr_FR: "Définir le nom d'hôte du serveur" + pl_PL: "Ustaw nazwę hosta serwera" + about.set-gateway-enabled-for-binding: en_US: "Set gateway enabled for binding" de_DE: "Gateway für Bindung aktivieren" diff --git a/core/src/account.rs b/core/src/account.rs index 2216fc361..78fb3ecf3 100644 --- a/core/src/account.rs +++ b/core/src/account.rs @@ -31,9 +31,17 @@ pub struct AccountInfo { pub developer_key: ed25519_dalek::SigningKey, } impl AccountInfo { - pub fn new(password: &str, start_time: SystemTime) -> Result { + pub fn new( + password: &str, + start_time: SystemTime, + hostname: Option, + ) -> Result { let server_id = generate_id(); - let hostname = generate_hostname(); + let hostname = if let Some(h) = hostname { + Hostname::validate(h)? + } else { + generate_hostname() + }; let root_ca_key = gen_nistp256()?; 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( diff --git a/core/src/backup/restore.rs b/core/src/backup/restore.rs index 6e8292275..8e2d978bc 100644 --- a/core/src/backup/restore.rs +++ b/core/src/backup/restore.rs @@ -17,6 +17,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::hostname::Hostname; use crate::init::init; use crate::prelude::*; use crate::progress::ProgressUnits; @@ -90,6 +91,7 @@ pub async fn recover_full_server( server_id: &str, recovery_password: &str, kiosk: Option, + hostname: Option, SetupExecuteProgress { init_phases, restore_phase, @@ -115,6 +117,10 @@ pub async fn recover_full_server( ) .with_kind(ErrorKind::PasswordHashGeneration)?; + if let Some(h) = hostname { + os_backup.account.hostname = Hostname::validate(h)?; + } + let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi"); sync_kiosk(kiosk).await?; diff --git a/core/src/context/rpc.rs b/core/src/context/rpc.rs index 7a4e6cfb9..651f8d744 100644 --- a/core/src/context/rpc.rs +++ b/core/src/context/rpc.rs @@ -165,8 +165,7 @@ impl RpcContext { { (net_ctrl, os_net_service) } else { - let net_ctrl = - Arc::new(NetController::init(db.clone(), &account.hostname, socks_proxy).await?); + let net_ctrl = Arc::new(NetController::init(db.clone(), socks_proxy).await?); webserver.send_modify(|wl| wl.set_ip_info(net_ctrl.net_iface.watcher.subscribe())); let os_net_service = net_ctrl.os_bindings().await?; (net_ctrl, os_net_service) diff --git a/core/src/db/model/mod.rs b/core/src/db/model/mod.rs index 64a9ae4c6..05fc8502d 100644 --- a/core/src/db/model/mod.rs +++ b/core/src/db/model/mod.rs @@ -45,7 +45,12 @@ impl Database { .collect(), ssh_privkey: Pem(account.ssh_key.clone()), ssh_pubkeys: SshKeys::new(), - available_ports: AvailablePorts::new(), + available_ports: { + let mut ports = AvailablePorts::new(); + ports.set_ssl(80, false); + ports.set_ssl(443, true); + ports + }, sessions: Sessions::new(), notifications: Notifications::new(), cifs: CifsTargets::new(), diff --git a/core/src/hostname.rs b/core/src/hostname.rs index 91c10f06c..0e528f03f 100644 --- a/core/src/hostname.rs +++ b/core/src/hostname.rs @@ -1,11 +1,16 @@ +use clap::Parser; use imbl_value::InternedString; use lazy_format::lazy_format; use rand::{Rng, rng}; +use serde::{Deserialize, Serialize}; use tokio::process::Command; use tracing::instrument; +use ts_rs::TS; +use crate::context::RpcContext; +use crate::prelude::*; use crate::util::Invoke; -use crate::{Error, ErrorKind}; + #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)] #[ts(type = "string")] pub struct Hostname(pub InternedString); @@ -21,6 +26,25 @@ impl AsRef for Hostname { } impl Hostname { + pub fn validate(h: InternedString) -> Result { + if h.is_empty() { + return Err(Error::new( + eyre!("{}", t!("hostname.empty")), + ErrorKind::InvalidRequest, + )); + } + if let Some(c) = h + .chars() + .find(|c| !(c.is_ascii_alphanumeric() || c == &'-') || c.is_ascii_uppercase()) + { + return Err(Error::new( + eyre!("{}", t!("hostname.invalid-character", char = c)), + ErrorKind::InvalidRequest, + )); + } + Ok(Self(h)) + } + pub fn lan_address(&self) -> InternedString { InternedString::from_display(&lazy_format!("https://{}.local", self.0)) } @@ -87,3 +111,31 @@ pub async fn sync_hostname(hostname: &Hostname) -> Result<(), Error> { .await?; Ok(()) } + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +#[ts(export)] +pub struct SetServerHostnameParams { + hostname: InternedString, +} + +pub async fn set_hostname_rpc( + ctx: RpcContext, + SetServerHostnameParams { hostname }: SetServerHostnameParams, +) -> Result<(), Error> { + let hostname = Hostname::validate(hostname)?; + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_hostname_mut() + .ser(&hostname.0) + }) + .await + .result?; + ctx.account.mutate(|a| a.hostname = hostname.clone()); + sync_hostname(&hostname).await?; + + Ok(()) +} diff --git a/core/src/init.rs b/core/src/init.rs index e9507ef49..c6b38c3e2 100644 --- a/core/src/init.rs +++ b/core/src/init.rs @@ -211,12 +211,7 @@ pub async fn init( start_net.start(); let net_ctrl = Arc::new( - NetController::init( - db.clone(), - &account.hostname, - cfg.socks_listen.unwrap_or(DEFAULT_SOCKS_LISTEN), - ) - .await?, + NetController::init(db.clone(), cfg.socks_listen.unwrap_or(DEFAULT_SOCKS_LISTEN)).await?, ); webserver.send_modify(|wl| wl.set_ip_info(net_ctrl.net_iface.watcher.subscribe())); let os_net_service = net_ctrl.os_bindings().await?; diff --git a/core/src/lib.rs b/core/src/lib.rs index 9aa4d58ad..820190d91 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -377,6 +377,13 @@ pub fn server() -> ParentHandler { "host", net::host::server_host_api::().with_about("about.commands-host-system-ui"), ) + .subcommand( + "set-hostname", + from_fn_async(hostname::set_hostname_rpc) + .no_display() + .with_about("about.set-hostname") + .with_call_remote::(), + ) .subcommand( "set-ifconfig-url", from_fn_async(system::set_ifconfig_url) diff --git a/core/src/net/forward.rs b/core/src/net/forward.rs index 3f0a8d6db..067b6b484 100644 --- a/core/src/net/forward.rs +++ b/core/src/net/forward.rs @@ -76,6 +76,11 @@ impl AvailablePorts { self.0.insert(port, ssl); Some(port) } + + pub fn set_ssl(&mut self, port: u16, ssl: bool) { + self.0.insert(port, ssl); + } + /// Returns whether a given allocated port is SSL. pub fn is_ssl(&self, port: u16) -> bool { self.0.get(&port).copied().unwrap_or(false) diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index 08491630e..e35f193f9 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -5,14 +5,13 @@ use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; use imbl_value::InternedString; use nix::net::if_::if_nametoindex; +use patch_db::json_ptr::JsonPointer; use tokio::process::Command; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tokio_rustls::rustls::ClientConfig as TlsClientConfig; use tracing::instrument; -use patch_db::json_ptr::JsonPointer; - use crate::db::model::Database; use crate::hostname::Hostname; use crate::net::dns::DnsController; @@ -41,16 +40,11 @@ pub struct NetController { pub(super) dns: DnsController, pub(super) forward: InterfacePortForwardController, pub(super) socks: SocksController, - pub(super) server_hostnames: Vec>, pub(crate) callbacks: Arc, } impl NetController { - pub async fn init( - db: TypedPatchDb, - hostname: &Hostname, - socks_listen: SocketAddr, - ) -> Result { + pub async fn init(db: TypedPatchDb, socks_listen: SocketAddr) -> Result { let net_iface = Arc::new(NetworkInterfaceController::new(db.clone())); let socks = SocksController::new(socks_listen)?; let crypto_provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()); @@ -90,18 +84,6 @@ impl NetController { forward: InterfacePortForwardController::new(net_iface.watcher.subscribe()), net_iface, socks, - 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()), - ], callbacks: Arc::new(ServiceCallbacks::default()), }) } @@ -183,12 +165,7 @@ impl NetServiceData { }) } - async fn update( - &mut self, - ctrl: &NetController, - id: HostId, - host: Host, - ) -> Result<(), Error> { + async fn update(&mut self, ctrl: &NetController, id: HostId, host: Host) -> Result<(), Error> { let mut forwards: BTreeMap = BTreeMap::new(); let mut vhosts: BTreeMap<(Option, u16), ProxyTarget> = BTreeMap::new(); let mut private_dns: BTreeSet = BTreeSet::new(); @@ -247,23 +224,21 @@ impl NetServiceData { .cloned() .collect(); - // Server hostname vhosts (on assigned_ssl_port) + // * vhost (on assigned_ssl_port) if !server_private_ips.is_empty() || !server_public_gateways.is_empty() { - for hostname in ctrl.server_hostnames.iter().cloned() { - vhosts.insert( - (hostname, assigned_ssl_port), - ProxyTarget { - public: server_public_gateways.clone(), - private: server_private_ips.clone(), - acme: None, - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } + vhosts.insert( + (None, assigned_ssl_port), + ProxyTarget { + public: server_public_gateways.clone(), + private: server_private_ips.clone(), + acme: None, + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }, + ); } } @@ -435,7 +410,6 @@ impl NetServiceData { Ok(()) } - } pub struct NetService { @@ -474,9 +448,7 @@ impl NetService { let thread_data = data.clone(); let sync_task = tokio::spawn(async move { if let Some(ref id) = pkg_id { - let ptr: JsonPointer = format!("/public/packageData/{}/hosts", id) - .parse() - .unwrap(); + let ptr: JsonPointer = format!("/public/packageData/{}/hosts", id).parse().unwrap(); let mut watch = db.watch(ptr).await.typed::(); // Outbound gateway enforcement @@ -484,9 +456,12 @@ impl NetService { // Purge any stale rules from a previous instance loop { if Command::new("ip") - .arg("rule").arg("del") - .arg("from").arg(&service_ip) - .arg("priority").arg("100") + .arg("rule") + .arg("del") + .arg("from") + .arg(&service_ip) + .arg("priority") + .arg("100") .invoke(ErrorKind::Network) .await .is_err() @@ -555,10 +530,14 @@ impl NetService { if let Some(old_table) = current_outbound_table.take() { let old_table_str = old_table.to_string(); let _ = Command::new("ip") - .arg("rule").arg("del") - .arg("from").arg(&service_ip) - .arg("lookup").arg(&old_table_str) - .arg("priority").arg("100") + .arg("rule") + .arg("del") + .arg("from") + .arg(&service_ip) + .arg("lookup") + .arg(&old_table_str) + .arg("priority") + .arg("100") .invoke(ErrorKind::Network) .await; } @@ -580,10 +559,14 @@ impl NetService { { let table_str = table_id.to_string(); Command::new("ip") - .arg("rule").arg("add") - .arg("from").arg(&service_ip) - .arg("lookup").arg(&table_str) - .arg("priority").arg("100") + .arg("rule") + .arg("add") + .arg("from") + .arg(&service_ip) + .arg("lookup") + .arg(&table_str) + .arg("priority") + .arg("100") .invoke(ErrorKind::Network) .await .log_err(); @@ -606,10 +589,14 @@ impl NetService { if let Some(table_id) = current_outbound_table { let table_str = table_id.to_string(); let _ = Command::new("ip") - .arg("rule").arg("del") - .arg("from").arg(&service_ip) - .arg("lookup").arg(&table_str) - .arg("priority").arg("100") + .arg("rule") + .arg("del") + .arg("from") + .arg(&service_ip) + .arg("lookup") + .arg(&table_str) + .arg("priority") + .arg("100") .invoke(ErrorKind::Network) .await; } @@ -763,9 +750,12 @@ impl NetService { let service_ip = self.data.lock().await.ip.to_string(); loop { if Command::new("ip") - .arg("rule").arg("del") - .arg("from").arg(&service_ip) - .arg("priority").arg("100") + .arg("rule") + .arg("del") + .arg("from") + .arg(&service_ip) + .arg("priority") + .arg("100") .invoke(ErrorKind::Network) .await .is_err() diff --git a/core/src/setup.rs b/core/src/setup.rs index 95aa276a7..ea6a0fd79 100644 --- a/core/src/setup.rs +++ b/core/src/setup.rs @@ -31,6 +31,7 @@ use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::util::{DiskInfo, StartOsRecoveryInfo, pvscan, recovery_info}; +use crate::hostname::Hostname; use crate::init::{InitPhases, InitResult, init}; use crate::net::ssl::root_ca_start_time; use crate::prelude::*; @@ -115,6 +116,7 @@ async fn setup_init( ctx: &SetupContext, password: Option, kiosk: Option, + hostname: Option, init_phases: InitPhases, ) -> Result<(AccountInfo, InitResult), Error> { let init_result = init(&ctx.webserver, &ctx.config.peek(|c| c.clone()), init_phases).await?; @@ -129,6 +131,9 @@ async fn setup_init( if let Some(password) = &password { account.set_password(password)?; } + if let Some(hostname) = hostname { + account.hostname = Hostname::validate(hostname)?; + } account.save(m)?; let info = m.as_public_mut().as_server_info_mut(); info.as_password_hash_mut().ser(&account.password)?; @@ -171,6 +176,7 @@ pub struct AttachParams { pub guid: InternedString, #[ts(optional)] pub kiosk: Option, + pub hostname: Option, } #[instrument(skip_all)] @@ -180,6 +186,7 @@ pub async fn attach( password, guid: disk_guid, kiosk, + hostname, }: AttachParams, ) -> Result { let setup_ctx = ctx.clone(); @@ -233,7 +240,14 @@ pub async fn attach( } disk_phase.complete(); - let (account, net_ctrl) = setup_init(&setup_ctx, password, kiosk, init_phases).await?; + let (account, net_ctrl) = setup_init( + &setup_ctx, + password, + kiosk, + hostname.filter(|h| !h.is_empty()), + init_phases, + ) + .await?; let rpc_ctx = RpcContext::init( &setup_ctx.webserver, @@ -406,6 +420,7 @@ pub struct SetupExecuteParams { recovery_source: Option>, #[ts(optional)] kiosk: Option, + hostname: Option, } // #[command(rpc_only)] @@ -416,6 +431,7 @@ pub async fn execute( password, recovery_source, kiosk, + hostname, }: SetupExecuteParams, ) -> Result { let password = match password.decrypt(&ctx) { @@ -447,7 +463,16 @@ pub async fn execute( }; let setup_ctx = ctx.clone(); - ctx.run_setup(move || execute_inner(setup_ctx, guid, password, recovery, kiosk))?; + ctx.run_setup(move || { + execute_inner( + setup_ctx, + guid, + password, + recovery, + kiosk, + hostname.filter(|h| !h.is_empty()), + ) + })?; Ok(ctx.progress().await) } @@ -536,6 +561,7 @@ pub async fn execute_inner( password: String, recovery_source: Option>, kiosk: Option, + hostname: Option, ) -> Result<(SetupResult, RpcContext), Error> { let progress = &ctx.progress; let restore_phase = match recovery_source.as_ref() { @@ -570,14 +596,15 @@ pub async fn execute_inner( server_id, recovery_password, kiosk, + hostname, progress, ) .await } Some(RecoverySource::Migrate { guid: old_guid }) => { - migrate(&ctx, guid, &old_guid, password, kiosk, progress).await + migrate(&ctx, guid, &old_guid, password, kiosk, hostname, progress).await } - None => fresh_setup(&ctx, guid, &password, kiosk, progress).await, + None => fresh_setup(&ctx, guid, &password, kiosk, hostname, progress).await, } } @@ -592,13 +619,14 @@ async fn fresh_setup( guid: InternedString, password: &str, kiosk: Option, + hostname: Option, SetupExecuteProgress { init_phases, rpc_ctx_phases, .. }: SetupExecuteProgress, ) -> Result<(SetupResult, RpcContext), Error> { - let account = AccountInfo::new(password, root_ca_start_time().await)?; + let account = AccountInfo::new(password, root_ca_start_time().await, hostname)?; let db = ctx.db().await?; let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi"); sync_kiosk(kiosk).await?; @@ -652,6 +680,7 @@ async fn recover( server_id: String, recovery_password: String, kiosk: Option, + hostname: Option, progress: SetupExecuteProgress, ) -> Result<(SetupResult, RpcContext), Error> { let recovery_source = TmpMountGuard::mount(&recovery_source, ReadWrite).await?; @@ -663,6 +692,7 @@ async fn recover( &server_id, &recovery_password, kiosk, + hostname, progress, ) .await @@ -675,6 +705,7 @@ async fn migrate( old_guid: &str, password: String, kiosk: Option, + hostname: Option, SetupExecuteProgress { init_phases, restore_phase, @@ -753,7 +784,8 @@ async fn migrate( crate::disk::main::export(&old_guid, "/media/startos/migrate").await?; restore_phase.complete(); - let (account, net_ctrl) = setup_init(&ctx, Some(password), kiosk, init_phases).await?; + let (account, net_ctrl) = + setup_init(&ctx, Some(password), kiosk, hostname, init_phases).await?; let rpc_ctx = RpcContext::init( &ctx.webserver,