From 7712d72e8860b64b485b9c7a4db73f72456466e9 Mon Sep 17 00:00:00 2001 From: Adam Chappell Date: Fri, 28 Jun 2024 11:07:50 +0200 Subject: [PATCH 1/2] - update parse logic to perform host glob matching (using globset) - generalise token expansion so it can be used to qualify hosts as well as localising ProxyCommand - add proxyjump to the config structure --- russh-config/Cargo.toml | 1 + russh-config/src/lib.rs | 52 ++++++++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/russh-config/Cargo.toml b/russh-config/Cargo.toml index 1e99ddf2..096dacfe 100644 --- a/russh-config/Cargo.toml +++ b/russh-config/Cargo.toml @@ -13,6 +13,7 @@ rust-version = "1.65" [dependencies] dirs-next = "2.0" futures = { workspace = true } +globset = "0.4.14" log = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["io-util", "net", "macros", "process"] } diff --git a/russh-config/src/lib.rs b/russh-config/src/lib.rs index 0b09dab6..e21d4ef8 100644 --- a/russh-config/src/lib.rs +++ b/russh-config/src/lib.rs @@ -10,6 +10,7 @@ use std::path::Path; use log::debug; use thiserror::*; +use globset::Glob; #[derive(Debug, Error)] /// anyhow::Errors. @@ -34,6 +35,7 @@ pub struct Config { pub port: u16, pub identity_file: Option, pub proxy_command: Option, + pub proxy_jump: Option, pub add_keys_to_agent: AddKeysToAgent, } @@ -45,22 +47,31 @@ impl Config { port: 22, identity_file: None, proxy_command: None, + proxy_jump: None, add_keys_to_agent: AddKeysToAgent::default(), } } } impl Config { - fn update_proxy_command(&mut self) { - if let Some(ref mut prox) = self.proxy_command { - *prox = prox.replace("%h", &self.host_name); - *prox = prox.replace("%p", &format!("{}", self.port)); - } + + // Look for any of the ssh_config(5) percent-style tokens and expand them + // based on current data in the struct, returning a new String. This function + // can be employed late/lazy eg just before establishing a stream using ProxyCommand + // but also can be used to modify Hostname as config parse time + fn expand_tokens(&self, original: &str) -> String { + let mut string = original.to_string(); + string = string.replace("%u", &self.user); + string = string.replace("%h", &self.host_name); // remote hostname (from context "host") + string = string.replace("%H", &self.host_name); // remote hostname (from context "host") + string = string.replace("%p", &format!("{}", self.port)); // original typed hostname (from context "host") + string = string.replace("%%", "%"); + string } - pub async fn stream(&mut self) -> Result { - self.update_proxy_command(); + pub async fn stream(&self) -> Result { if let Some(ref proxy_command) = self.proxy_command { + let proxy_command = self.expand_tokens(proxy_command); let cmd: Vec<&str> = proxy_command.split(' ').collect(); Stream::proxy_command(cmd.first().unwrap_or(&""), cmd.get(1..).unwrap_or(&[])) .await @@ -105,9 +116,9 @@ pub enum AddKeysToAgent { pub fn parse(file: &str, host: &str) -> Result { let mut config: Option = None; for line in file.lines() { - let line = line.trim(); - if let Some(n) = line.find(' ') { - let (key, value) = line.split_at(n); + let tokens = line.trim().splitn(2, ' ').collect::>(); + if tokens.len() == 2 { + let (key, value) = (tokens[0], tokens[1]); let lower = key.to_lowercase(); if let Some(ref mut config) = config { match lower.as_str() { @@ -117,8 +128,7 @@ pub fn parse(file: &str, host: &str) -> Result { config.user.push_str(value.trim_start()); } "hostname" => { - config.host_name.clear(); - config.host_name.push_str(value.trim_start()) + config.host_name = config.expand_tokens(value.trim_start()) } "port" => { if let Ok(port) = value.trim_start().parse() { @@ -148,6 +158,7 @@ pub fn parse(file: &str, host: &str) -> Result { } } "proxycommand" => config.proxy_command = Some(value.trim_start().to_string()), + "proxyjump" => config.proxy_jump = Some(value.trim_start().to_string()), "addkeystoagent" => match value.to_lowercase().as_str() { "yes" => config.add_keys_to_agent = AddKeysToAgent::Yes, "confirm" => config.add_keys_to_agent = AddKeysToAgent::Confirm, @@ -158,7 +169,9 @@ pub fn parse(file: &str, host: &str) -> Result { debug!("{:?}", key); } } - } else if lower.as_str() == "host" && value.trim_start() == host { + } else if lower.as_str() == "host" && value.split_whitespace().find(|x| { + check_host_against_glob_pattern(host, x) + }).is_some() { let mut c = Config::default(host); c.port = 22; config = Some(c) @@ -171,3 +184,16 @@ pub fn parse(file: &str, host: &str) -> Result { Err(Error::HostNotFound) } } + +fn check_host_against_glob_pattern(candidate: &str, glob_pattern: &str) -> bool { + dbg!(candidate, glob_pattern); + match Glob::new(glob_pattern) { + Ok(glob) => { + glob.compile_matcher().is_match(candidate) + }, + _ => { + false + } + } +} + From 47899f6350235fccf3ee56a6f700ad85ffe4bea9 Mon Sep 17 00:00:00 2001 From: Adam Chappell Date: Sat, 29 Jun 2024 14:35:38 +0200 Subject: [PATCH 2/2] cargo fmt / cargo clippy --- russh-config/src/lib.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/russh-config/src/lib.rs b/russh-config/src/lib.rs index e21d4ef8..6b6ab231 100644 --- a/russh-config/src/lib.rs +++ b/russh-config/src/lib.rs @@ -8,9 +8,9 @@ use std::io::Read; use std::net::ToSocketAddrs; use std::path::Path; +use globset::Glob; use log::debug; use thiserror::*; -use globset::Glob; #[derive(Debug, Error)] /// anyhow::Errors. @@ -54,7 +54,6 @@ impl Config { } impl Config { - // Look for any of the ssh_config(5) percent-style tokens and expand them // based on current data in the struct, returning a new String. This function // can be employed late/lazy eg just before establishing a stream using ProxyCommand @@ -118,7 +117,7 @@ pub fn parse(file: &str, host: &str) -> Result { for line in file.lines() { let tokens = line.trim().splitn(2, ' ').collect::>(); if tokens.len() == 2 { - let (key, value) = (tokens[0], tokens[1]); + let (key, value) = (tokens.first().unwrap_or(&""), tokens.get(1).unwrap_or(&"")); let lower = key.to_lowercase(); if let Some(ref mut config) = config { match lower.as_str() { @@ -127,9 +126,7 @@ pub fn parse(file: &str, host: &str) -> Result { config.user.clear(); config.user.push_str(value.trim_start()); } - "hostname" => { - config.host_name = config.expand_tokens(value.trim_start()) - } + "hostname" => config.host_name = config.expand_tokens(value.trim_start()), "port" => { if let Ok(port) = value.trim_start().parse() { config.port = port @@ -169,9 +166,11 @@ pub fn parse(file: &str, host: &str) -> Result { debug!("{:?}", key); } } - } else if lower.as_str() == "host" && value.split_whitespace().find(|x| { - check_host_against_glob_pattern(host, x) - }).is_some() { + } else if lower.as_str() == "host" + && value + .split_whitespace() + .any(|x| check_host_against_glob_pattern(host, x)) + { let mut c = Config::default(host); c.port = 22; config = Some(c) @@ -188,12 +187,7 @@ pub fn parse(file: &str, host: &str) -> Result { fn check_host_against_glob_pattern(candidate: &str, glob_pattern: &str) -> bool { dbg!(candidate, glob_pattern); match Glob::new(glob_pattern) { - Ok(glob) => { - glob.compile_matcher().is_match(candidate) - }, - _ => { - false - } + Ok(glob) => glob.compile_matcher().is_match(candidate), + _ => false, } } -