use std::{ net::{IpAddr, Ipv4Addr, SocketAddrV4}, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, }; use figment::{ Figment, providers::{Format, Serialized, Toml}, }; use local_ip_address::local_ip; use serde::{Deserialize, Deserializer, Serialize}; use crate::{ DEFAULT_PORT, MULTICAST_IP, error::{LocalSendError, Result}, models::Device, }; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub multicast_addr: SocketAddrV4, pub download_dir: PathBuf, pub data_dir: PathBuf, #[serde(deserialize_with = "deserialize_local_addr")] pub local_ip_addr: Ipv4Addr, pub device: Device, } impl Default for Config { fn default() -> Self { Self { multicast_addr: SocketAddrV4::new(MULTICAST_IP, DEFAULT_PORT), download_dir: Default::default(), data_dir: Default::default(), local_ip_addr: Ipv4Addr::from_bits(0), device: Default::default(), } } } impl Config { pub fn new() -> Result { let dirs = directories::BaseDirs::new().ok_or(LocalSendError::NoHomeDir)?; let download_dir = dirs.home_dir().join("jocalsend-downloads"); let config_file = dirs.config_dir().join("jocalsend.toml"); let data_dir = dirs.data_local_dir().join("jocalsend"); let key = data_dir.join("key.pem"); let cert = data_dir.join("cert.pem"); let fingerprint = if data_dir.exists() { if !(key.exists() && cert.exists()) { gen_ssl(&key, &cert)? } else { let key = std::fs::read(key)?; sha256::digest(key) } } else { std::fs::create_dir_all(data_dir.as_path())?; gen_ssl(&key, &cert)? }; let config = Self { multicast_addr: SocketAddrV4::new(MULTICAST_IP, DEFAULT_PORT), local_ip_addr: get_local_ip_addr(), download_dir, data_dir, device: Device { alias: rustix::system::uname() .nodename() .to_string_lossy() .to_string(), fingerprint, ..Default::default() }, }; let config = if !config_file.exists() { log::info!("creating config file at {config_file:?}"); std::fs::write(&config_file, toml::to_string(&config)?)?; config } else { log::info!("reading config from {config_file:?}"); Figment::from(Serialized::defaults(config)) .merge(Toml::file(config_file)) .extract() .map_err(Box::new)? // boxed because the error size from figment is large }; log::debug!("using config: {config:?}"); Ok(config) } /// Returns (key, cert) paths pub fn ssl(&self) -> (PathBuf, PathBuf) { let key = self.data_dir.join("key.pem"); let cert = self.data_dir.join("cert.pem"); (key, cert) } } fn gen_ssl(key: &Path, cert: &Path) -> Result { let cert_key = rcgen::generate_simple_self_signed(vec!["*".into()])?; let cert_text = cert_key.cert.pem(); let key_text = cert_key.signing_key.serialize_pem(); std::fs::write(key, key_text.clone())?; std::fs::set_permissions(key, std::fs::Permissions::from_mode(0o400u32))?; std::fs::write(cert, cert_text)?; Ok(sha256::digest(key_text)) } fn deserialize_local_addr<'de, D>(_: D) -> std::result::Result where D: Deserializer<'de>, { Ok(get_local_ip_addr()) } fn get_local_ip_addr() -> Ipv4Addr { let IpAddr::V4(local_ip_addr) = local_ip().unwrap_or(IpAddr::V4(Ipv4Addr::from_bits(0))) else { panic!("IPv6 is unsupported"); }; local_ip_addr }