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..6b6ab231 100644 --- a/russh-config/src/lib.rs +++ b/russh-config/src/lib.rs @@ -8,6 +8,7 @@ use std::io::Read; use std::net::ToSocketAddrs; use std::path::Path; +use globset::Glob; use log::debug; use thiserror::*; @@ -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,30 @@ 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 +115,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.first().unwrap_or(&""), tokens.get(1).unwrap_or(&"")); let lower = key.to_lowercase(); if let Some(ref mut config) = config { match lower.as_str() { @@ -116,10 +126,7 @@ pub fn parse(file: &str, host: &str) -> Result { config.user.clear(); config.user.push_str(value.trim_start()); } - "hostname" => { - config.host_name.clear(); - config.host_name.push_str(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 @@ -148,6 +155,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 +166,11 @@ 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() + .any(|x| check_host_against_glob_pattern(host, x)) + { let mut c = Config::default(host); c.port = 22; config = Some(c) @@ -171,3 +183,11 @@ 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, + } +}