joecalsend/src/config.rs
2025-08-08 11:10:14 -07:00

120 lines
3.6 KiB
Rust

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, 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(skip_serializing)]
pub local_ip_addr: Option<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: None,
device: Default::default(),
}
}
}
impl Config {
pub fn new() -> Result<Self> {
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 IpAddr::V4(local_ip_addr) = local_ip()? else {
return Err(LocalSendError::IPv6Unsupported);
};
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 mut config = Self {
multicast_addr: SocketAddrV4::new(MULTICAST_IP, DEFAULT_PORT),
download_dir,
local_ip_addr: Some(local_ip_addr),
data_dir,
device: Device {
alias: rustix::system::uname()
.nodename()
.to_string_lossy()
.to_string(),
fingerprint,
..Default::default()
},
};
let mut 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
};
if config.local_ip_addr.is_none() {
config.local_ip_addr = Some(local_ip_addr);
}
log::info!("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<String> {
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))
}