add config file and data dirs with SSL keys

This commit is contained in:
Joe Ardent 2025-08-07 15:36:25 -07:00
parent 514dee438f
commit 1cf07a4f97
9 changed files with 255 additions and 39 deletions

135
Cargo.lock generated
View file

@ -415,6 +415,15 @@ dependencies = [
"syn 2.0.104", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -560,7 +569,7 @@ dependencies = [
"pear", "pear",
"serde", "serde",
"tempfile", "tempfile",
"toml", "toml 0.8.23",
"uncased", "uncased",
"version_check", "version_check",
] ]
@ -1168,12 +1177,16 @@ dependencies = [
"network-interface", "network-interface",
"ratatui", "ratatui",
"ratatui-explorer", "ratatui-explorer",
"rcgen",
"reqwest", "reqwest",
"rustix 1.0.8",
"serde", "serde",
"serde_json", "serde_json",
"sha256", "sha256",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-rustls",
"toml 0.9.5",
"tower-http", "tower-http",
"tui-input", "tui-input",
"tui-logger", "tui-logger",
@ -1381,6 +1394,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1513,6 +1532,16 @@ dependencies = [
"syn 2.0.104", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -1561,6 +1590,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -1668,6 +1703,19 @@ dependencies = [
"ratatui", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.17" version = "0.5.17"
@ -1809,6 +1857,7 @@ version = "0.23.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
dependencies = [ dependencies = [
"log",
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki", "rustls-webpki",
@ -1937,6 +1986,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -2173,6 +2231,25 @@ dependencies = [
"syn 2.0.104", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.1"
@ -2252,11 +2329,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned 0.6.9",
"toml_datetime", "toml_datetime 0.6.11",
"toml_edit", "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]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.11" version = "0.6.11"
@ -2266,6 +2358,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.27" version = "0.22.27"
@ -2274,18 +2375,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
"serde_spanned", "serde_spanned 0.6.9",
"toml_datetime", "toml_datetime 0.6.11",
"toml_write", "toml_write",
"winnow", "winnow",
] ]
[[package]]
name = "toml_parser"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [
"winnow",
]
[[package]] [[package]]
name = "toml_write" name = "toml_write"
version = "0.1.2" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@ -2865,6 +2981,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yasna"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.0" version = "0.8.0"

View file

@ -19,12 +19,16 @@ mime_guess = "2"
network-interface = { version = "2", features = ["serde"] } network-interface = { version = "2", features = ["serde"] }
ratatui = "0.29" ratatui = "0.29"
ratatui-explorer = "0.2" ratatui-explorer = "0.2"
rcgen = "0.14.3"
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
rustix = { version = "1.0.8", default-features = false, features = ["system"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
sha256 = "1.6" sha256 = "1.6"
thiserror = "2" thiserror = "2"
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] } 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"] } tower-http = { version = "0.6", features = ["limit"] }
tui-input = "0.14" tui-input = "0.14"
tui-logger = { version = "0.17", features = ["crossterm"] } tui-logger = { version = "0.17", features = ["crossterm"] }

View file

@ -68,7 +68,7 @@ static CONTENT_SEND_FILE_MENU: LazyLock<Line> = LazyLock::new(|| {
"<UP>".blue().bold(), "<UP>".blue().bold(),
" Select Next ".into(), " Select Next ".into(),
"<DOWN>".blue().bold(), "<DOWN>".blue().bold(),
" Select ".into(), " Send File ".into(),
"<ENTER>".blue().bold(), "<ENTER>".blue().bold(),
" Parent Dir ".into(), " Parent Dir ".into(),
"<LEFT>".blue().bold(), "<LEFT>".blue().bold(),
@ -91,7 +91,7 @@ static CONTENT_SEND_PEERS_MENU: LazyLock<Line> = LazyLock::new(|| {
"<UP>".blue().bold(), "<UP>".blue().bold(),
" Select Next ".into(), " Select Next ".into(),
"<DOWN>".blue().bold(), "<DOWN>".blue().bold(),
" Select ".into(), " Send to Peer ".into(),
"<ENTER>".blue().bold(), "<ENTER>".blue().bold(),
" Enter Text ".into(), " Enter Text ".into(),
"<T>".blue().bold(), "<T>".blue().bold(),

101
src/config.rs Normal file
View file

@ -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<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 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<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))
}

View file

@ -50,6 +50,18 @@ pub enum LocalSendError {
#[error("Error getting network interface")] #[error("Error getting network interface")]
NetworkInterfaceError(#[from] network_interface::Error), 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<figment::Error>),
} }
pub type Result<T> = std::result::Result<T, LocalSendError>; pub type Result<T> = std::result::Result<T, LocalSendError>;

View file

@ -18,7 +18,7 @@ impl JocalService {
pub async fn start_http_server(&self, stop_rx: mpsc::Receiver<()>) -> crate::error::Result<()> { pub async fn start_http_server(&self, stop_rx: mpsc::Receiver<()>) -> crate::error::Result<()> {
let app = self.create_router(); let app = self.create_router();
// TODO: make addr config // 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?; let listener = TcpListener::bind(&addr).await?;
axum::serve( axum::serve(

View file

@ -1,3 +1,4 @@
pub mod config;
pub mod discovery; pub mod discovery;
pub mod error; pub mod error;
pub mod http_server; pub mod http_server;
@ -7,16 +8,14 @@ pub mod transfer;
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
fmt::Debug, fmt::Debug,
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, net::{Ipv4Addr, SocketAddr, SocketAddrV4},
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
}; };
use error::LocalSendError; pub use config::Config;
use julid::Julid; use julid::Julid;
use local_ip_address::local_ip;
use log::error; use log::error;
use models::{Device, FileMetadata}; use models::{Device, FileMetadata};
use serde::{Deserialize, Serialize};
use tokio::{ use tokio::{
net::UdpSocket, net::UdpSocket,
sync::{ sync::{
@ -191,28 +190,3 @@ impl Default for RunningState {
Self::Running 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<Self> {
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,
})
}
}

View file

@ -125,7 +125,7 @@ impl Default for Device {
device_model: None, device_model: None,
device_type: Some(DeviceType::Headless), device_type: Some(DeviceType::Headless),
fingerprint: Julid::new().to_string(), fingerprint: Julid::new().to_string(),
port: 53317, port: crate::DEFAULT_PORT,
protocol: "http".to_string(), protocol: "http".to_string(),
download: false, download: false,
announce: Some(true), announce: Some(true),

View file

@ -350,7 +350,7 @@ pub async fn receive_upload(
} }
// Create file path // 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 // Write file
if let Err(e) = tokio::fs::write(&file_path, body).await { if let Err(e) = tokio::fs::write(&file_path, body).await {