use clap::Parser; use imbl_value::InternedString; use lazy_format::lazy_format; use serde::{Deserialize, Serialize}; use tokio::process::Command; use tracing::instrument; use ts_rs::TS; use crate::context::RpcContext; use crate::db::model::public::ServerInfo; use crate::prelude::*; use crate::util::Invoke; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)] #[ts(type = "string")] pub struct ServerHostname(InternedString); impl std::ops::Deref for ServerHostname { type Target = InternedString; fn deref(&self) -> &Self::Target { &self.0 } } impl AsRef for ServerHostname { fn as_ref(&self) -> &str { &***self } } impl ServerHostname { fn validate(&self) -> Result<(), Error> { if self.0.is_empty() { return Err(Error::new( eyre!("{}", t!("hostname.empty")), ErrorKind::InvalidRequest, )); } if let Some(c) = self .0 .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(()) } pub fn new(hostname: InternedString) -> Result { let res = Self(hostname); res.validate()?; Ok(res) } pub fn lan_address(&self) -> InternedString { InternedString::from_display(&lazy_format!("https://{}.local", self.0)) } pub fn local_domain_name(&self) -> InternedString { InternedString::from_display(&lazy_format!("{}.local", self.0)) } pub fn load(server_info: &Model) -> Result { Ok(Self(server_info.as_hostname().de()?)) } pub fn save(&self, server_info: &mut Model) -> Result<(), Error> { server_info.as_hostname_mut().ser(&**self) } } #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)] #[ts(type = "string")] pub struct ServerHostnameInfo { pub name: InternedString, pub hostname: ServerHostname, } lazy_static::lazy_static! { static ref ADJECTIVES: Vec = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect(); static ref NOUNS: Vec = include_str!("./assets/nouns.txt").lines().map(|x| x.to_string()).collect(); } impl AsRef for ServerHostnameInfo { fn as_ref(&self) -> &str { &self.hostname } } fn normalize(s: &str) -> InternedString { let mut prev_was_dash = true; let mut normalized = s .chars() .filter_map(|c| { if c.is_alphanumeric() { prev_was_dash = false; Some(c.to_ascii_lowercase()) } else if (c == '-' || c.is_whitespace()) && !prev_was_dash { prev_was_dash = true; Some('-') } else { None } }) .collect::(); while normalized.ends_with('-') { normalized.pop(); } if normalized.len() < 4 { generate_hostname().0 } else { normalized.into() } } fn denormalize(s: &str) -> InternedString { let mut cap = true; s.chars() .map(|c| { if c == '-' { cap = true; ' ' } else if cap { cap = false; c.to_ascii_uppercase() } else { c } }) .collect::() .into() } impl ServerHostnameInfo { pub fn new( name: Option, hostname: Option, ) -> Result { Self::new_opt(name, hostname) .map(|h| h.unwrap_or_else(|| ServerHostnameInfo::from_hostname(generate_hostname()))) } pub fn new_opt( name: Option, hostname: Option, ) -> Result, Error> { let name = name.filter(|n| !n.is_empty()); let hostname = hostname.filter(|h| !h.is_empty()); Ok(match (name, hostname) { (Some(name), Some(hostname)) => Some(ServerHostnameInfo { name, hostname: ServerHostname::new(hostname)?, }), (Some(name), None) => Some(ServerHostnameInfo::from_name(name)), (None, Some(hostname)) => Some(ServerHostnameInfo::from_hostname(ServerHostname::new( hostname, )?)), (None, None) => None, }) } pub fn from_hostname(hostname: ServerHostname) -> Self { Self { name: denormalize(&**hostname), hostname, } } pub fn from_name(name: InternedString) -> Self { Self { hostname: ServerHostname(normalize(&*name)), name, } } pub fn load(server_info: &Model) -> Result { Ok(Self { name: server_info.as_name().de()?, hostname: ServerHostname::load(server_info)?, }) } pub fn save(&self, server_info: &mut Model) -> Result<(), Error> { server_info.as_name_mut().ser(&self.name)?; self.hostname.save(server_info) } } pub fn generate_hostname() -> ServerHostname { let num = rand::random::(); ServerHostname(InternedString::from_display(&lazy_format!( "startos-{num:04x}" ))) } pub fn generate_id() -> String { let id = uuid::Uuid::new_v4(); id.to_string() } #[instrument(skip_all)] pub async fn get_current_hostname() -> Result { let out = Command::new("hostname") .invoke(ErrorKind::ParseSysInfo) .await?; let out_string = String::from_utf8(out)?; Ok(out_string.trim().into()) } #[instrument(skip_all)] pub async fn set_hostname(hostname: &ServerHostname) -> Result<(), Error> { let hostname = &***hostname; Command::new("hostnamectl") .arg("--static") .arg("set-hostname") .arg(hostname) .invoke(ErrorKind::ParseSysInfo) .await?; Command::new("sed") .arg("-i") .arg(format!( "s/\\(\\s\\)localhost\\( {hostname}\\)\\?/\\1localhost {hostname}/g" )) .arg("/etc/hosts") .invoke(ErrorKind::ParseSysInfo) .await?; Ok(()) } #[instrument(skip_all)] pub async fn sync_hostname(hostname: &ServerHostname) -> Result<(), Error> { set_hostname(hostname).await?; Command::new("systemctl") .arg("restart") .arg("avahi-daemon") .invoke(crate::ErrorKind::Network) .await?; Ok(()) } #[derive(Deserialize, Serialize, Parser, TS)] #[group(skip)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] #[ts(export)] pub struct SetServerHostnameParams { name: Option, hostname: Option, } pub async fn set_hostname_rpc( ctx: RpcContext, SetServerHostnameParams { name, hostname }: SetServerHostnameParams, ) -> Result<(), Error> { let name = name.filter(|n| !n.is_empty()); let hostname = hostname .filter(|h| !h.is_empty()) .map(ServerHostname::new) .transpose()?; if name.is_none() && hostname.is_none() { return Err(Error::new( eyre!("{}", t!("hostname.must-provide-name-or-hostname")), ErrorKind::InvalidRequest, )); }; let info = ctx .db .mutate(|db| { let server_info = db.as_public_mut().as_server_info_mut(); if let Some(name) = name { server_info.as_name_mut().ser(&name)?; } if let Some(hostname) = &hostname { hostname.save(server_info)?; } ServerHostnameInfo::load(server_info) }) .await .result?; ctx.account.mutate(|a| a.hostname = info.clone()); if let Some(h) = hostname { sync_hostname(&h).await?; } Ok(()) } #[test] fn test_generate_hostname() { assert_eq!(dbg!(generate_hostname().0).len(), 12); }