diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f48a6e0..41b047cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ tiny does not update permissions of existing log files. - Add new server configuration option `autoconnect` to disable automatically connecting to a server on startup. (#443) +- Expand tildes and shell variables in `pem` (SASL authentication) and + `log_dir` config fields. (#192, #463) # 2025/01/01: 0.13.0 diff --git a/Cargo.lock b/Cargo.lock index c6eda0ac..6f2e771b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -867,6 +867,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1062,6 +1071,7 @@ dependencies = [ "serde", "serde_yaml", "shell-words", + "shellexpand", "term_input", "termbox_simple", "time 0.1.45", diff --git a/crates/tiny/Cargo.toml b/crates/tiny/Cargo.toml index 70095dc5..f800e1f0 100644 --- a/crates/tiny/Cargo.toml +++ b/crates/tiny/Cargo.toml @@ -27,6 +27,7 @@ log = "0.4" serde = { version = "1.0.196", features = ["derive"] } serde_yaml = "0.8" shell-words = "1.1.0" +shellexpand = "3.1.2" time = "0.1" tokio = { version = "1.36", default-features = false, features = [] } tokio-stream = { version = "0.1", features = [] } diff --git a/crates/tiny/config.yml b/crates/tiny/config.yml index 8e2b9777..6131ab46 100644 --- a/crates/tiny/config.yml +++ b/crates/tiny/config.yml @@ -49,7 +49,9 @@ servers: # sasl: # username: tiny_user # password: hunter2 - # pem: "/home/user/.config/tiny/oftc.pem" + + # sasl: + # pem: "$HOME/.config/tiny/oftc.pem" # nickserv_ident: hunter2 diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 6dbc403c..adbe8475 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -1,6 +1,8 @@ use libtiny_client::SASLAuth as ClientSASLAuth; use serde::{Deserialize, Deserializer}; +use shellexpand::LookupError; +use std::env::VarError; use std::fs; use std::fs::File; use std::io::{Read, Write}; @@ -314,6 +316,29 @@ impl Config { errors } + pub(crate) fn expand_fields( + &mut self, + home_dir: impl Fn() -> Option, + env_var: impl Fn(&str) -> Result, VarError>, + ) -> Result<(), LookupError> { + for server in &mut self.servers { + server.sasl_auth = match &mut server.sasl_auth { + None => None, + Some(other @ SASLAuth::Plain { .. }) => Some(other.clone()), + Some(SASLAuth::External { pem }) => Some(SASLAuth::External { + pem: expand_path(pem.to_path_buf(), &home_dir, &env_var)?, + }), + }; + } + + self.log_dir = match &self.log_dir { + None => None, + Some(dir) => Some(expand_path(dir.to_path_buf(), &home_dir, &env_var)?), + }; + + Ok(()) + } + /// Runs password commands and updates the config with plain passwords obtained from the /// commands. pub(crate) fn read_passwords(self) -> Option> { @@ -396,6 +421,16 @@ impl Config { } } +fn expand_path( + path: PathBuf, + home_dir: &impl Fn() -> Option, + env_var: &impl Fn(&str) -> Result, VarError>, +) -> Result> { + let path_str = path.to_string_lossy(); + let expanded = shellexpand::full_with_context(&path_str, home_dir, env_var)?; + Ok(PathBuf::from(expanded.as_ref())) +} + /// Returns tiny config file path. File may or may not exist. /// /// Places to look: (in priority order) @@ -559,4 +594,76 @@ mod tests { ]) ); } + + #[test] + fn config_shell_expansion() { + let mut config: Config = Config { + servers: vec![Server { + addr: "my_server".to_owned(), + alias: None, + port: 123, + tls: false, + pass: None, + autoconnect: true, + user: None, + realname: "".to_owned(), + nicks: vec!["".to_owned()], + join: vec![], + nickserv_ident: None, + sasl_auth: Some(SASLAuth::External { + pem: "~/a/$SASL/b".into(), + }), + }], + defaults: Defaults { + nicks: vec!["".to_owned()], + realname: "".to_owned(), + join: vec![], + tls: false, + }, + log_dir: Some("~/b/$LOG/c".into()), + }; + config + .expand_fields( + || Some("/home/test".to_string()), + |s| match s { + "SASL" => Ok(Some("sasl_val".to_string())), + "LOG" => Ok(Some("log_val".to_string())), + _ => Err(VarError::NotPresent), + }, + ) + .unwrap(); + + assert_eq!( + config.servers[0].sasl_auth, + Some(SASLAuth::External { + pem: PathBuf::from("/home/test/a/sasl_val/b"), + }) + ); + assert_eq!( + config.log_dir, + Some(PathBuf::from("/home/test/b/log_val/c")) + ); + } + + #[test] + fn config_shell_expansion_var_fail() { + let mut config: Config = Config { + servers: vec![], + defaults: Defaults { + nicks: vec!["nick".to_owned()], + realname: "real".to_owned(), + join: vec![], + tls: false, + }, + log_dir: Some("~/logs/$MISSING/data".into()), + }; + let err = config + .expand_fields( + || Some("/home/test".to_string()), + |_| Err(VarError::NotPresent), + ) + .unwrap_err(); + assert_eq!(err.var_name, "MISSING"); + assert_eq!(err.cause, VarError::NotPresent); + } } diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs index f0a28fe7..0c7cfab1 100644 --- a/crates/tiny/src/main.rs +++ b/crates/tiny/src/main.rs @@ -41,7 +41,7 @@ fn main() { println!("{yaml_err}"); exit(1); } - Ok(config) => { + Ok(mut config) => { let config_errors = config.validate(); if !config_errors.is_empty() { println!( @@ -54,6 +54,15 @@ fn main() { exit(1); } + if let Err(var_error) = config.expand_fields( + || dirs::home_dir().and_then(|p| p.into_os_string().into_string().ok()), + |s| std::env::var(s).map(Some), + ) { + println!("Config file error:"); + println!("- Cannot expand variable: {var_error}"); + exit(1); + }; + let config = match config.read_passwords() { None => exit(1), Some(config) => config,