diff --git a/Cargo.lock b/Cargo.lock index a776204..0255fb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -415,6 +415,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -560,7 +569,7 @@ dependencies = [ "pear", "serde", "tempfile", - "toml", + "toml 0.8.23", "uncased", "version_check", ] @@ -1168,12 +1177,16 @@ dependencies = [ "network-interface", "ratatui", "ratatui-explorer", + "rcgen", "reqwest", + "rustix 1.0.8", "serde", "serde_json", "sha256", "thiserror", "tokio", + "tokio-rustls", + "toml 0.9.5", "tower-http", "tui-input", "tui-logger", @@ -1381,6 +1394,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -1513,6 +1532,16 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1561,6 +1590,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1668,6 +1703,19 @@ dependencies = [ "ratatui", ] +[[package]] +name = "rcgen" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0068c5b3cab1d4e271e0bb6539c87563c43411cad90b057b15c79958fbeb41f7" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -1809,6 +1857,7 @@ version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ + "log", "once_cell", "rustls-pki-types", "rustls-webpki", @@ -1937,6 +1986,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2173,6 +2231,25 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + [[package]] name = "tinystr" version = "0.8.1" @@ -2252,11 +2329,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -2266,6 +2358,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2274,18 +2375,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tower" version = "0.5.2" @@ -2865,6 +2981,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 44456d2..63531ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,16 @@ mime_guess = "2" network-interface = { version = "2", features = ["serde"] } ratatui = "0.29" ratatui-explorer = "0.2" +rcgen = "0.14.3" reqwest = { version = "0.12", features = ["json"] } +rustix = { version = "1.0.8", default-features = false, features = ["system"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha256 = "1.6" thiserror = "2" tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] } +tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "logging"] } +toml = "0.9.5" tower-http = { version = "0.6", features = ["limit"] } tui-input = "0.14" tui-logger = { version = "0.17", features = ["crossterm"] } diff --git a/src/app/widgets.rs b/src/app/widgets.rs index cc61d57..512ea6d 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -68,7 +68,7 @@ static CONTENT_SEND_FILE_MENU: LazyLock = LazyLock::new(|| { "".blue().bold(), " Select Next ".into(), "".blue().bold(), - " Select ".into(), + " Send File ".into(), "".blue().bold(), " Parent Dir ".into(), "".blue().bold(), @@ -91,7 +91,7 @@ static CONTENT_SEND_PEERS_MENU: LazyLock = LazyLock::new(|| { "".blue().bold(), " Select Next ".into(), "".blue().bold(), - " Select ".into(), + " Send to Peer ".into(), "".blue().bold(), " Enter Text ".into(), "".blue().bold(), diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..7b1a70b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,101 @@ +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: Ipv4Addr, + pub device: Device, +} + +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 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 config = Self { + multicast_addr: SocketAddrV4::new(MULTICAST_IP, DEFAULT_PORT), + download_dir, + local_ip_addr, + 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 + }; + + Ok(config) + } + + pub fn ssl(&self) -> (PathBuf, PathBuf) { + let key = self.data_dir.join("jocalsend").join("key.pem"); + let cert = self.data_dir.join("jocalsend").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)) +} diff --git a/src/error.rs b/src/error.rs index 0e38274..a0adfdf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -50,6 +50,18 @@ pub enum LocalSendError { #[error("Error getting network interface")] NetworkInterfaceError(#[from] network_interface::Error), + + #[error("Error: could not get $HOME value")] + NoHomeDir, + + #[error("Could not generate SSL certs")] + SslGenFail(#[from] rcgen::Error), + + #[error("Could not serialize config")] + ConfigSerializationFail(#[from] toml::ser::Error), + + #[error("Could not parse config file")] + ConfigParseError(#[from] Box), } pub type Result = std::result::Result; diff --git a/src/http_server.rs b/src/http_server.rs index 2bc8e03..f92ce63 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -18,7 +18,7 @@ impl JocalService { pub async fn start_http_server(&self, stop_rx: mpsc::Receiver<()>) -> crate::error::Result<()> { let app = self.create_router(); // TODO: make addr config - let addr = SocketAddr::from(([0, 0, 0, 0], self.config.port)); + let addr = SocketAddr::from(([0, 0, 0, 0], self.config.device.port)); let listener = TcpListener::bind(&addr).await?; axum::serve( diff --git a/src/lib.rs b/src/lib.rs index a0c1c25..23c0634 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod discovery; pub mod error; pub mod http_server; @@ -7,16 +8,14 @@ pub mod transfer; use std::{ collections::BTreeMap, fmt::Debug, - net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, sync::{Arc, OnceLock}, }; -use error::LocalSendError; +pub use config::Config; use julid::Julid; -use local_ip_address::local_ip; use log::error; use models::{Device, FileMetadata}; -use serde::{Deserialize, Serialize}; use tokio::{ net::UdpSocket, sync::{ @@ -191,28 +190,3 @@ impl Default for RunningState { Self::Running } } - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - pub multicast_addr: SocketAddrV4, - pub port: u16, - pub download_dir: String, - pub local_ip_addr: Ipv4Addr, -} - -impl Config { - pub fn new() -> error::Result { - let home = std::env::home_dir().unwrap_or("/tmp".into()); - let dd = home.join("jocalsend-downloads"); - let IpAddr::V4(local_ip_addr) = local_ip()? else { - return Err(LocalSendError::IPv6Unsupported); - }; - - Ok(Self { - multicast_addr: SocketAddrV4::new(MULTICAST_IP, DEFAULT_PORT), - port: DEFAULT_PORT, - download_dir: dd.to_string_lossy().into(), - local_ip_addr, - }) - } -} diff --git a/src/models.rs b/src/models.rs index e2281b6..5626e9f 100644 --- a/src/models.rs +++ b/src/models.rs @@ -125,7 +125,7 @@ impl Default for Device { device_model: None, device_type: Some(DeviceType::Headless), fingerprint: Julid::new().to_string(), - port: 53317, + port: crate::DEFAULT_PORT, protocol: "http".to_string(), download: false, announce: Some(true), diff --git a/src/transfer.rs b/src/transfer.rs index ff6d1c0..6df6e0c 100644 --- a/src/transfer.rs +++ b/src/transfer.rs @@ -350,7 +350,7 @@ pub async fn receive_upload( } // Create file path - let file_path = format!("{}/{}", download_dir, file_metadata.file_name); + let file_path = service.config.download_dir.join(&file_metadata.file_name); // Write file if let Err(e) = tokio::fs::write(&file_path, body).await {