use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use chrono::{DateTime, Utc}; use exver::{Version, VersionRange}; use imbl::{OrdMap, OrdSet}; use imbl_value::InternedString; use ipnet::IpNet; use isocountry::CountryCode; use itertools::Itertools; use lazy_static::lazy_static; use models::{GatewayId, PackageId}; use openssl::hash::MessageDigest; use patch_db::{HasModel, Value}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::account::AccountInfo; use crate::db::model::package::AllPackageData; use crate::net::acme::AcmeProvider; use crate::net::forward::START9_BRIDGE_IFACE; use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo}; use crate::net::host::Host; use crate::net::utils::ipv6_is_local; 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, HOST_IP, PLATFORM}; #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct Public { pub server_info: ServerInfo, pub package_data: AllPackageData, #[ts(type = "unknown")] pub ui: Value, } impl Public { pub fn init(account: &AccountInfo, kiosk: Option) -> Result { Ok(Self { server_info: ServerInfo { arch: get_arch(), platform: get_platform(), id: account.server_id.clone(), version: Current::default().semver(), hostname: account.hostname.no_dot_host_name(), last_backup: None, package_version_compat: Current::default().compat().clone(), post_init_migration_todos: BTreeMap::new(), network: NetworkInfo { 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), private_disabled: OrdSet::new(), public_enabled: OrdSet::new(), }, }, )] .into_iter() .collect(), onions: account.tor_keys.iter().map(|k| k.onion_address()).collect(), public_domains: BTreeMap::new(), private_domains: BTreeSet::new(), hostname_info: BTreeMap::new(), }, wifi: WifiInfo { enabled: true, ..Default::default() }, gateways: OrdMap::new(), acme: BTreeMap::new(), dns: Default::default(), }, status_info: ServerStatus { backup_progress: None, updated: false, update_progress: None, shutting_down: false, restarting: false, }, unread_notification_count: 0, password_hash: account.password.clone(), pubkey: ssh_key::PublicKey::from(&account.ssh_key) .to_openssh() .unwrap(), ca_fingerprint: account .root_ca_cert .digest(MessageDigest::sha256()) .unwrap() .iter() .map(|x| format!("{x:X}")) .join(":"), ntp_synced: false, zram: true, governor: None, smtp: None, ram: 0, devices: Vec::new(), kiosk, }, package_data: AllPackageData::default(), ui: serde_json::from_str(include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/../../web/patchdb-ui-seed.json" ))) .with_kind(ErrorKind::Deserialization)?, }) } } fn get_arch() -> InternedString { (*ARCH).into() } fn get_platform() -> InternedString { (&*PLATFORM).into() } #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct ServerInfo { #[serde(default = "get_arch")] #[ts(type = "string")] pub arch: InternedString, #[serde(default = "get_platform")] #[ts(type = "string")] pub platform: InternedString, pub id: String, #[ts(type = "string")] pub hostname: InternedString, #[ts(type = "string")] pub version: Version, #[ts(type = "string")] pub package_version_compat: VersionRange, #[ts(type = "Record")] pub post_init_migration_todos: BTreeMap, #[ts(type = "string | null")] pub last_backup: Option>, pub network: NetworkInfo, #[serde(default)] pub status_info: ServerStatus, #[ts(type = "number")] pub unread_notification_count: u64, pub password_hash: String, pub pubkey: String, pub ca_fingerprint: String, #[serde(default)] pub ntp_synced: bool, #[serde(default)] pub zram: bool, pub governor: Option, pub smtp: Option, #[ts(type = "number")] pub ram: u64, pub devices: Vec, pub kiosk: Option, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct NetworkInfo { pub wifi: WifiInfo, pub host: Host, #[ts(as = "BTreeMap::")] #[serde(default)] pub gateways: OrdMap, #[serde(default)] pub acme: BTreeMap, #[serde(default)] pub dns: DnsSettings, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct DnsSettings { #[ts(type = "string[]")] pub dhcp_servers: VecDeque, #[ts(type = "string[] | null")] pub static_servers: Option>, } #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct NetworkInterfaceInfo { pub name: Option, pub public: Option, pub secure: Option, pub ip_info: Option, } impl NetworkInterfaceInfo { pub fn loopback() -> (&'static GatewayId, &'static Self) { lazy_static! { static ref LO: GatewayId = GatewayId::from(InternedString::intern("lo")); static ref LOOPBACK: NetworkInterfaceInfo = NetworkInterfaceInfo { name: Some(InternedString::from_static("Loopback")), public: Some(false), secure: Some(true), ip_info: Some(IpInfo { name: "lo".into(), scope_id: 1, device_type: None, subnets: [ IpNet::new(Ipv4Addr::LOCALHOST.into(), 8).unwrap(), IpNet::new(Ipv6Addr::LOCALHOST.into(), 128).unwrap(), ] .into_iter() .collect(), lan_ip: [ IpAddr::from(Ipv4Addr::LOCALHOST), IpAddr::from(Ipv6Addr::LOCALHOST) ] .into_iter() .collect(), wan_ip: None, ntp_servers: Default::default(), dns_servers: Default::default(), }), }; } (&*LO, &*LOOPBACK) } pub fn public(&self) -> bool { self.public.unwrap_or_else(|| { !self.ip_info.as_ref().map_or(true, |ip_info| { let ip4s = ip_info .subnets .iter() .filter_map(|ipnet| { if let IpAddr::V4(ip4) = ipnet.addr() { Some(ip4) } else { None } }) .collect::>(); if !ip4s.is_empty() { return ip4s .iter() .all(|ip4| ip4.is_loopback() || ip4.is_private() || ip4.is_link_local()); } ip_info.subnets.iter().all(|ipnet| { if let IpAddr::V6(ip6) = ipnet.addr() { ipv6_is_local(ip6) } else { true } }) }) }) } pub fn secure(&self) -> bool { self.secure.unwrap_or_else(|| { self.ip_info.as_ref().map_or(false, |ip_info| { ip_info.device_type == Some(NetworkInterfaceType::Wireguard) }) }) } } #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, TS, HasModel)] #[ts(export)] #[serde(rename_all = "camelCase")] #[model = "Model"] pub struct IpInfo { #[ts(type = "string")] pub name: InternedString, pub scope_id: u32, pub device_type: Option, #[ts(type = "string[]")] pub subnets: OrdSet, #[ts(type = "string[]")] pub lan_ip: OrdSet, pub wan_ip: Option, #[ts(type = "string[]")] pub ntp_servers: OrdSet, #[ts(type = "string[]")] pub dns_servers: OrdSet, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "kebab-case")] pub enum NetworkInterfaceType { Ethernet, Wireless, Bridge, Wireguard, Loopback, } #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct AcmeSettings { pub contact: Vec, } #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct DomainSettings { pub gateway: GatewayId, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[model = "Model"] #[ts(export)] pub struct BackupProgress { pub complete: bool, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct ServerStatus { pub backup_progress: Option>, pub updated: bool, pub update_progress: Option, #[serde(default)] pub shutting_down: bool, #[serde(default)] pub restarting: bool, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct WifiInfo { pub enabled: bool, pub interface: Option, pub ssids: BTreeSet, pub selected: Option, #[ts(type = "string | null")] pub last_region: Option, } #[derive(Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct ServerSpecs { pub cpu: String, pub disk: String, pub memory: String, }