From c8c1e7d931558fd4c65f65e756e2f996137c4e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Wed, 14 Apr 2021 13:22:16 +0300 Subject: [PATCH 01/15] Implement getting password from external commands -- not tested --- crates/tiny/src/config.rs | 249 ++++++++++++++++++++++++++++++++++---- crates/tiny/src/main.rs | 9 +- 2 files changed, 231 insertions(+), 27 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index ac64d1b9..4357f265 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -3,15 +3,16 @@ use std::fs; use std::fs::File; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; +use std::process::Command; -#[derive(Clone, Deserialize, Debug, PartialEq, Eq)] -pub(crate) struct SASLAuth { +#[derive(Clone, Deserialize, Debug)] +pub(crate) struct SASLAuth

{ pub(crate) username: String, - pub(crate) password: String, + pub(crate) password: P, } #[derive(Clone, Deserialize)] -pub(crate) struct Server { +pub(crate) struct Server

{ /// Address of the server pub(crate) addr: String, @@ -28,7 +29,7 @@ pub(crate) struct Server { /// Server password (optional) #[serde(default)] - pub(crate) pass: Option, + pub(crate) pass: Option

, /// Real name to be used in connection registration pub(crate) realname: String, @@ -42,11 +43,12 @@ pub(crate) struct Server { pub(crate) join: Vec, /// NickServ identification password. Used on connecting to the server and nick change. - pub(crate) nickserv_ident: Option, + #[serde(default)] + pub(crate) nickserv_ident: Option

, /// Authenication method #[serde(rename = "sasl")] - pub(crate) sasl_auth: Option, + pub(crate) sasl_auth: Option>, } /// Similar to `Server`, but used when connecting via the `/connect` command. @@ -60,34 +62,231 @@ pub(crate) struct Defaults { pub(crate) tls: bool, } +// TODO FIXME: I don't understand why we need `Default` bound here on `P` #[derive(Deserialize)] -pub(crate) struct Config { - pub(crate) servers: Vec, +pub(crate) struct Config { + pub(crate) servers: Vec>, pub(crate) defaults: Defaults, pub(crate) log_dir: Option, } -/// Returns error descriptions. -pub(crate) fn validate_config(config: &Config) -> Vec { - let mut errors = vec![]; +/// A password, or a shell command to run the obtain a password. Used for password (server +/// password, SASL, NickServ) fields of `Config`. +#[derive(Debug, Clone)] +pub(crate) enum PassOrCmd { + /// Password is given directly as plain text + Pass(String), + /// A shell command to run to get the password + Cmd(Vec), +} + +impl PassOrCmd { + fn is_empty_cmd(&self) -> bool { + match self { + PassOrCmd::Cmd(cmd) => cmd.is_empty(), + PassOrCmd::Pass(_) => false, + } + } +} + +impl Default for PassOrCmd { + fn default() -> Self { + // HACK FIXME TODO - For some reason we need `Default` for `PassOrCmd` to be able to + // deserialize `Config`. No idea why. + panic!("default() called for PassOrCmd"); + } +} + +impl<'de> Deserialize<'de> for PassOrCmd { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let str = String::deserialize(deserializer)?; + let trimmed = str.trim(); + if trimmed.starts_with('$') { + let rest = trimmed[1..].trim(); // drop '$' + Ok(PassOrCmd::Cmd( + rest.split_whitespace().map(str::to_owned).collect(), + )) + } else { + Ok(PassOrCmd::Pass(str)) + } + } +} + +fn run_command(command_name: &str, server_addr: &str, args: &[String]) -> Option { + println!( + "Running {} command for server {} ({:?}) ...", + command_name, server_addr, args + ); + + assert!(!args.is_empty()); // should be checked in `validate` + + let mut cmd = Command::new(&args[0]); + cmd.args(args[1..].iter()); + + let output = match cmd.output() { + Err(err) => { + println!("Command failed: {:?}", err); + return None; + } + Ok(output) => output, + }; + + if !output.status.success() { + println!("Command returned non-zero: {:?}", output.status); + if output.stdout.is_empty() { + println!("stdout is empty"); + } else { + println!("stdout:"); + println!("--------------------------------------"); + println!("{}", String::from_utf8_lossy(&output.stdout)); + println!("--------------------------------------"); + } + + if output.stderr.is_empty() { + println!("stderr is empty"); + } else { + println!("stderr:"); + println!("--------------------------------------"); + println!("{}", String::from_utf8_lossy(&output.stderr)); + println!("--------------------------------------"); + } + + return None; + } - // Check that nick lists are not empty - if config.defaults.nicks.is_empty() { - errors.push( - "Default nick list can't be empty, please add at least one defaut nick".to_string(), - ); + if output.stdout.is_empty() { + println!("Command returned zero, but stdout is empty. Aborting."); + return None; } - for server in &config.servers { - if server.nicks.is_empty() { - errors.push(format!( - "Nick list for server '{}' is empty, please add at least one nick", - server.addr - )); + Some(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +impl Config { + /// Returns error descriptions. + pub(crate) fn validate(&self) -> Vec { + let mut errors = vec![]; + + // Check that nick lists are not empty + if self.defaults.nicks.is_empty() { + errors.push( + "Default nick list can't be empty, please add at least one defaut nick".to_string(), + ); } + + for server in &self.servers { + // TODO: Empty nick strings + // TODO: Empty realname strings + if server.nicks.is_empty() { + errors.push(format!( + "Nick list for server '{}' is empty, please add at least one nick", + server.addr + )); + } + + if let Some(ref pass) = server.pass { + if pass.is_empty_cmd() { + errors.push(format!("Empty PASS command for '{}'", server.addr)); + } + } + + if let Some(ref nickserv_ident) = server.nickserv_ident { + if nickserv_ident.is_empty_cmd() { + errors.push(format!( + "Empty NickServ password command for '{}'", + server.addr + )); + } + } + + if let Some(ref sasl_auth) = server.sasl_auth { + if sasl_auth.password.is_empty_cmd() { + errors.push(format!("Empty SASL password command for '{}'", server.addr)); + } + } + } + + errors } - errors + /// Runs password commands and updates the config with plain passwords obtained from the + /// commands. + pub(crate) fn read_passwords(self) -> Option> { + let Config { + servers, + defaults, + log_dir, + } = self; + + let mut servers_: Vec> = Vec::with_capacity(servers.len()); + + for server in servers { + let Server { + addr, + alias, + port, + tls, + pass, + realname, + nicks, + join, + nickserv_ident, + sasl_auth, + } = server; + + let pass = match pass { + None => None, + Some(PassOrCmd::Pass(pass)) => Some(pass), + Some(PassOrCmd::Cmd(cmd)) => Some(run_command("server password", &addr, &cmd)?), + }; + + let nickserv_ident = match nickserv_ident { + None => None, + Some(PassOrCmd::Pass(pass)) => Some(pass), + Some(PassOrCmd::Cmd(cmd)) => Some(run_command("NickServ password", &addr, &cmd)?), + }; + + let sasl_auth = match sasl_auth { + None => None, + Some(SASLAuth { + username, + password: PassOrCmd::Pass(pass), + }) => Some(SASLAuth { + username, + password: pass, + }), + Some(SASLAuth { + username, + password: PassOrCmd::Cmd(cmd), + }) => { + let password = run_command("SASL password", &addr, &cmd)?; + Some(SASLAuth { username, password }) + } + }; + + servers_.push(Server { + addr, + alias, + port, + tls, + pass, + realname, + nicks, + join, + nickserv_ident, + sasl_auth, + }); + } + + Some(Config { + servers: servers_, + defaults, + log_dir, + }) + } } /// Returns tiny config file path. File may or may not exist. @@ -126,7 +325,7 @@ pub(crate) fn get_config_path() -> PathBuf { } } -pub(crate) fn parse_config(config_path: &Path) -> Result { +pub(crate) fn parse_config(config_path: &Path) -> Result, serde_yaml::Error> { let contents = { let mut str = String::new(); let mut file = File::open(config_path).unwrap(); diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs index 00745f54..00bc391d 100644 --- a/crates/tiny/src/main.rs +++ b/crates/tiny/src/main.rs @@ -42,7 +42,7 @@ fn main() { exit(1); } Ok(config) => { - let config_errors = config::validate_config(&config); + let config_errors = config.validate(); if !config_errors.is_empty() { println!("Config file error(s):"); for error in config_errors { @@ -51,6 +51,11 @@ fn main() { exit(1); } + let config = match config.read_passwords() { + None => exit(1), + Some(config) => config, + }; + let config::Config { servers, defaults, @@ -75,7 +80,7 @@ fn main() { const DEBUG_LOG_FILE: &str = "tiny_debug_logs.txt"; fn run( - servers: Vec, + servers: Vec>, defaults: config::Defaults, config_path: PathBuf, log_dir: Option, From 5fe096030d979c6ea827c0f8f09f52200df5ef4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Wed, 14 Apr 2021 14:56:14 +0300 Subject: [PATCH 02/15] Fix test --- crates/tiny/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 4357f265..a991f67f 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -367,7 +367,7 @@ mod tests { #[test] fn parse_default_config() { - match serde_yaml::from_str(&get_default_config_yaml()) { + match serde_yaml::from_str::>(&get_default_config_yaml()) { Err(yaml_err) => { println!("{}", yaml_err); panic!(); From ae37285ee67a98e6ccfb5dbba81bb1cfe2867770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Wed, 14 Dec 2022 09:44:05 +0100 Subject: [PATCH 03/15] Revert unrelated changes --- crates/tiny/src/config.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index d155f98a..bafb8a9b 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -181,10 +181,15 @@ impl Config { pub(crate) fn validate(&self) -> Vec { let mut errors = vec![]; - // Check that nick lists are not empty if self.defaults.nicks.is_empty() { errors.push( - "Default nick list can't be empty, please add at least one defaut nick".to_string(), + "Default nick list can't be empty, please add at least one defaut nick".to_owned(), + ); + } + + if self.defaults.realname.is_empty() { + errors.push( + "realname can't be empty, please update 'realname' field of 'defaults'".to_owned(), ); } From 60d4f8ad09303eb15d0d7505d6d73d119e1c7a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Wed, 14 Dec 2022 09:46:33 +0100 Subject: [PATCH 04/15] Revert more unrelated changes --- crates/tiny/src/config.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index bafb8a9b..be77825f 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -207,6 +207,15 @@ impl Config { )); } + for (nick_idx, nick) in server.nicks.iter().enumerate() { + if nick.is_empty() { + errors.push(format!( + "Nicks can't be empty, please update nick {} for '{}'", + nick_idx, server.addr + )); + } + } + if server.realname.is_empty() { errors.push(format!( "'realname' can't be empty, please update 'realname' field of '{}'", From 700d7d4c520179cb7ca8fc4bee9a7f776f4d39c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Wed, 14 Dec 2022 10:02:19 +0100 Subject: [PATCH 05/15] Fix lint --- crates/tiny/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index be77825f..31e06b6e 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -115,7 +115,7 @@ impl<'de> Deserialize<'de> for PassOrCmd { { let str = String::deserialize(deserializer)?; let trimmed = str.trim(); - if trimmed.starts_with('$') { + if let Some(trimmed) = trimmed.strip_prefix('$') { let rest = trimmed[1..].trim(); // drop '$' Ok(PassOrCmd::Cmd( rest.split_whitespace().map(str::to_owned).collect(), From 0526bea20c9cb9dc1de38ca464479934c0b2d500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Wed, 14 Dec 2022 14:48:42 +0100 Subject: [PATCH 06/15] Fix compile error in a test --- crates/tiny/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 31e06b6e..bdf4fbd8 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -402,7 +402,7 @@ mod tests { #[test] fn parse_default_config() { - match serde_yaml::from_str(&get_default_config_yaml()) { + match serde_yaml::from_str::>(&get_default_config_yaml()) { Err(yaml_err) => { println!("{}", yaml_err); panic!(); From 39b91a695d0564c3ea36aa38d1784f56419cfd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 15 Dec 2022 16:37:13 +0100 Subject: [PATCH 07/15] Minor refactoring, implement escaping initial $ --- crates/tiny/src/config.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index bdf4fbd8..22cb3ea2 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -91,7 +91,7 @@ where /// A password, or a shell command to run the obtain a password. Used for password (server /// password, SASL, NickServ) fields of `Config`. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum PassOrCmd { /// Password is given directly as plain text Pass(String), @@ -116,10 +116,12 @@ impl<'de> Deserialize<'de> for PassOrCmd { let str = String::deserialize(deserializer)?; let trimmed = str.trim(); if let Some(trimmed) = trimmed.strip_prefix('$') { - let rest = trimmed[1..].trim(); // drop '$' + let rest = trimmed[1..].trim(); Ok(PassOrCmd::Cmd( rest.split_whitespace().map(str::to_owned).collect(), )) + } else if let Some(without_dollar) = trimmed.strip_prefix("\\$") { + Ok(PassOrCmd::Pass(format!("${}", without_dollar))) } else { Ok(PassOrCmd::Pass(str)) } @@ -128,7 +130,7 @@ impl<'de> Deserialize<'de> for PassOrCmd { fn run_command(command_name: &str, server_addr: &str, args: &[String]) -> Option { println!( - "Running {} command for server {} ({:?}) ...", + "Running {} command for server {} ({:?})", command_name, server_addr, args ); @@ -457,4 +459,25 @@ mod tests { "'realname' can't be empty, please update 'realname' field of 'my_server'" ); } + + #[test] + fn parse_password_field() { + let field = "$ my pass cmd"; + assert_eq!( + serde_yaml::from_str::(&field).unwrap(), + PassOrCmd::Cmd(vec!["my".to_owned(), "pass".to_owned(), "cmd".to_owned()]) + ); + + let field = "my password"; + assert_eq!( + serde_yaml::from_str::(&field).unwrap(), + PassOrCmd::Pass(field.to_string()) + ); + + let field = "\\$my password"; + assert_eq!( + serde_yaml::from_str::(&field).unwrap(), + PassOrCmd::Pass("$my password".to_string()) + ); + } } From 55f69261bf4a54dcd83d96200a205717aa5118cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 15 Dec 2022 16:59:59 +0100 Subject: [PATCH 08/15] Implement proper shell arg parsing --- Cargo.lock | 7 +++++++ crates/tiny/Cargo.toml | 1 + crates/tiny/src/config.rs | 26 ++++++++++++++++++++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc057d76..f9d2d34d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1171,6 +1171,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1368,6 +1374,7 @@ dependencies = [ "rustc_tools_util", "serde", "serde_yaml", + "shell-words", "term_input", "termbox_simple", "time", diff --git a/crates/tiny/Cargo.toml b/crates/tiny/Cargo.toml index f8249fae..2c1c8e24 100644 --- a/crates/tiny/Cargo.toml +++ b/crates/tiny/Cargo.toml @@ -26,6 +26,7 @@ libtiny_wire = { path = "../libtiny_wire" } log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" +shell-words = "1.1.0" time = "0.1" tokio = { version = "1.17", default-features = false, features = [] } tokio-stream = { version = "0.1", features = [] } diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 22cb3ea2..b37872d5 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -113,13 +113,21 @@ impl<'de> Deserialize<'de> for PassOrCmd { where D: serde::Deserializer<'de>, { + use serde::de::Error; + let str = String::deserialize(deserializer)?; let trimmed = str.trim(); if let Some(trimmed) = trimmed.strip_prefix('$') { - let rest = trimmed[1..].trim(); - Ok(PassOrCmd::Cmd( - rest.split_whitespace().map(str::to_owned).collect(), - )) + let args = match shell_words::split(trimmed) { + Ok(args) => args, + Err(err) => { + return Err(D::Error::custom(format!( + "Unable to parse password field: {}", + err + ))) + } + }; + Ok(PassOrCmd::Cmd(args)) } else if let Some(without_dollar) = trimmed.strip_prefix("\\$") { Ok(PassOrCmd::Pass(format!("${}", without_dollar))) } else { @@ -479,5 +487,15 @@ mod tests { serde_yaml::from_str::(&field).unwrap(), PassOrCmd::Pass("$my password".to_string()) ); + + let field = "$ pass show \"my password\""; + assert_eq!( + serde_yaml::from_str::(&field).unwrap(), + PassOrCmd::Cmd(vec![ + "pass".to_string(), + "show".to_string(), + "my password".to_string() + ]) + ); } } From 6998e8e93e8f6365b9ce939a4de85d3466ad1865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 15 Dec 2022 17:02:16 +0100 Subject: [PATCH 09/15] Tweak error code printing --- crates/tiny/src/config.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index b37872d5..43b9a640 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -156,7 +156,12 @@ fn run_command(command_name: &str, server_addr: &str, args: &[String]) -> Option }; if !output.status.success() { - println!("Command returned non-zero: {:?}", output.status); + print!("Command returned non-zero"); + if let Some(code) = output.status.code() { + println!(": {}", code); + } else { + println!(); + } if output.stdout.is_empty() { println!("stdout is empty"); } else { From dffd2fcc874005ca3fa530655471118cd0a73612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 15 Dec 2022 17:03:59 +0100 Subject: [PATCH 10/15] More tweaks --- crates/tiny/src/config.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 43b9a640..a9342087 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -138,8 +138,10 @@ impl<'de> Deserialize<'de> for PassOrCmd { fn run_command(command_name: &str, server_addr: &str, args: &[String]) -> Option { println!( - "Running {} command for server {} ({:?})", - command_name, server_addr, args + "Running {} command for server {} (`{}`)", + command_name, + server_addr, + shell_words::join(args) ); assert!(!args.is_empty()); // should be checked in `validate` From b9b4fe2c84da2b28b8eca052a4e2f6ffc9431d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 15 Dec 2022 17:11:59 +0100 Subject: [PATCH 11/15] Tweak prints a bit more --- crates/tiny/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index a9342087..cc0fef29 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -138,7 +138,7 @@ impl<'de> Deserialize<'de> for PassOrCmd { fn run_command(command_name: &str, server_addr: &str, args: &[String]) -> Option { println!( - "Running {} command for server {} (`{}`)", + "Running {} command for {} (`{}`)", command_name, server_addr, shell_words::join(args) From fc10628d09e923be063eeaead32870ae5a2247aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 15 Dec 2022 17:30:31 +0100 Subject: [PATCH 12/15] Update CHANGELOG, README, use last line of stdout as password --- CHANGELOG.md | 2 ++ README.md | 19 +++++++++++++++++++ crates/tiny/src/config.rs | 3 ++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72c84dcc..6ee97b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Thanks to @ALEX11BR for contributing to this release. This bug was introduced in 0.10.0 with 33df77e. (#366) - `/close` and `/quit` commands now take optional message parameters to be sent with PART and QUIT messages to the server. (#365, #395) +- Passwords can now be read from external commands (e.g. a password manager). + See README for details. (#246, #315) # 2021/11/07: 0.10.0 diff --git a/README.md b/README.md index 28480779..70143c9c 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,25 @@ like `NickServ`) is better when some of the channels that you automatically join require identification. To use this method enter your nick password to the `pass` field in servers. +### Using external commands for passwords + +When a password field in the config file starts with `$`, tiny uses rest of the +field as the shell command to run to get the password. + +For example, in this config: + +```yaml +sasl: + username: osa1 + password: '$ pass show "my irc password"' +``` + +tiny runs the `pass ...` command and uses last line printed by the command as +the password. + +When your password starts with `$`, the initial `$` character can be escaped +with a `\`: `password: '\$myPass'`. + ## Command line arguments By default (i.e. when no command line arguments passed) tiny connects to all diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index cc0fef29..15ef1063 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -190,7 +190,8 @@ fn run_command(command_name: &str, server_addr: &str, args: &[String]) -> Option return None; } - Some(String::from_utf8_lossy(&output.stdout).into_owned()) + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + Some(stdout.lines().last().unwrap().to_owned()) } impl Config { From 8c70d7948c2aaf6a8633435bfaca1d2200d6a452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Sun, 18 Dec 2022 19:29:15 +0100 Subject: [PATCH 13/15] Update cmd syntax --- README.md | 10 ++++------ crates/tiny/src/config.rs | 42 +++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 70143c9c..06041f33 100644 --- a/README.md +++ b/README.md @@ -108,23 +108,21 @@ join require identification. To use this method enter your nick password to the ### Using external commands for passwords -When a password field in the config file starts with `$`, tiny uses rest of the -field as the shell command to run to get the password. +When a password field in the config file is a map with a `cmd` key, the value +is used as the shell command to run to get the password. For example, in this config: ```yaml sasl: username: osa1 - password: '$ pass show "my irc password"' + password: + cmd: 'pass show "my irc password"' ``` tiny runs the `pass ...` command and uses last line printed by the command as the password. -When your password starts with `$`, the initial `$` character can be escaped -with a `\`: `password: '\$myPass'`. - ## Command line arguments By default (i.e. when no command line arguments passed) tiny connects to all diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 15ef1063..a69a867f 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -95,6 +95,7 @@ where pub(crate) enum PassOrCmd { /// Password is given directly as plain text Pass(String), + /// A shell command to run to get the password Cmd(Vec), } @@ -114,24 +115,23 @@ impl<'de> Deserialize<'de> for PassOrCmd { D: serde::Deserializer<'de>, { use serde::de::Error; - - let str = String::deserialize(deserializer)?; - let trimmed = str.trim(); - if let Some(trimmed) = trimmed.strip_prefix('$') { - let args = match shell_words::split(trimmed) { - Ok(args) => args, - Err(err) => { - return Err(D::Error::custom(format!( + use serde_yaml::Value; + + match Value::deserialize(deserializer)? { + Value::String(str) => Ok(PassOrCmd::Pass(str)), + Value::Mapping(map) => match map.get(&Value::String("cmd".to_owned())) { + Some(Value::String(cmd)) => match shell_words::split(cmd) { + Ok(cmd_parts) => Ok(PassOrCmd::Cmd(cmd_parts)), + Err(err) => Err(D::Error::custom(format!( "Unable to parse password field: {}", err - ))) - } - }; - Ok(PassOrCmd::Cmd(args)) - } else if let Some(without_dollar) = trimmed.strip_prefix("\\$") { - Ok(PassOrCmd::Pass(format!("${}", without_dollar))) - } else { - Ok(PassOrCmd::Pass(str)) + ))), + }, + _ => Err(D::Error::custom( + "Expeted a 'cmd' key in password map with string value", + )), + }, + _ => Err(D::Error::custom("Password field must be a string or map")), } } } @@ -478,7 +478,7 @@ mod tests { #[test] fn parse_password_field() { - let field = "$ my pass cmd"; + let field = "cmd: my pass cmd"; assert_eq!( serde_yaml::from_str::(&field).unwrap(), PassOrCmd::Cmd(vec!["my".to_owned(), "pass".to_owned(), "cmd".to_owned()]) @@ -490,13 +490,7 @@ mod tests { PassOrCmd::Pass(field.to_string()) ); - let field = "\\$my password"; - assert_eq!( - serde_yaml::from_str::(&field).unwrap(), - PassOrCmd::Pass("$my password".to_string()) - ); - - let field = "$ pass show \"my password\""; + let field = "cmd: \"pass show 'my password'\""; assert_eq!( serde_yaml::from_str::(&field).unwrap(), PassOrCmd::Cmd(vec![ From 5f84ad02277404b37645f99fa57d0f4c2f6fa17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Sun, 18 Dec 2022 19:34:36 +0100 Subject: [PATCH 14/15] Rename cmd -> command --- README.md | 6 +++--- crates/tiny/src/config.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 06041f33..15a8b4ad 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,8 @@ join require identification. To use this method enter your nick password to the ### Using external commands for passwords -When a password field in the config file is a map with a `cmd` key, the value -is used as the shell command to run to get the password. +When a password field in the config file is a map with a `command` key, the +value is used as the shell command to run to get the password. For example, in this config: @@ -117,7 +117,7 @@ For example, in this config: sasl: username: osa1 password: - cmd: 'pass show "my irc password"' + command: 'pass show "my irc password"' ``` tiny runs the `pass ...` command and uses last line printed by the command as diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index a69a867f..af1a15a3 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -119,7 +119,7 @@ impl<'de> Deserialize<'de> for PassOrCmd { match Value::deserialize(deserializer)? { Value::String(str) => Ok(PassOrCmd::Pass(str)), - Value::Mapping(map) => match map.get(&Value::String("cmd".to_owned())) { + Value::Mapping(map) => match map.get(&Value::String("command".to_owned())) { Some(Value::String(cmd)) => match shell_words::split(cmd) { Ok(cmd_parts) => Ok(PassOrCmd::Cmd(cmd_parts)), Err(err) => Err(D::Error::custom(format!( From 8f0383dc134bbd44904f602ecabd84c40d10c4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Sun, 18 Dec 2022 19:39:01 +0100 Subject: [PATCH 15/15] Fix test --- crates/tiny/src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index af1a15a3..2b638e9e 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -478,7 +478,7 @@ mod tests { #[test] fn parse_password_field() { - let field = "cmd: my pass cmd"; + let field = "command: my pass cmd"; assert_eq!( serde_yaml::from_str::(&field).unwrap(), PassOrCmd::Cmd(vec!["my".to_owned(), "pass".to_owned(), "cmd".to_owned()]) @@ -490,7 +490,7 @@ mod tests { PassOrCmd::Pass(field.to_string()) ); - let field = "cmd: \"pass show 'my password'\""; + let field = "command: \"pass show 'my password'\""; assert_eq!( serde_yaml::from_str::(&field).unwrap(), PassOrCmd::Cmd(vec![