jocalsend 1.6.180

A terminal implementation of the LocalSend protocol
Documentation
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<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 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<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))
}

fn deserialize_local_addr<'de, D>(_: D) -> std::result::Result<Ipv4Addr, D::Error>
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
}