Skip to content

Commit 1eaadfb

Browse files
packetsourceAdam Chappell
andauthored
Add support for glob pattern matching in Host directives (#306)
- 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 --------- Co-authored-by: Adam Chappell <Adam.Chappell@gtt.net>
1 parent 3f4646a commit 1eaadfb

File tree

2 files changed

+36
-15
lines changed

2 files changed

+36
-15
lines changed

russh-config/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ rust-version = "1.65"
1313
[dependencies]
1414
dirs-next = "2.0"
1515
futures = { workspace = true }
16+
globset = "0.4.14"
1617
log = { workspace = true }
1718
thiserror = { workspace = true }
1819
tokio = { workspace = true, features = ["io-util", "net", "macros", "process"] }

russh-config/src/lib.rs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::io::Read;
88
use std::net::ToSocketAddrs;
99
use std::path::Path;
1010

11+
use globset::Glob;
1112
use log::debug;
1213
use thiserror::*;
1314

@@ -34,6 +35,7 @@ pub struct Config {
3435
pub port: u16,
3536
pub identity_file: Option<String>,
3637
pub proxy_command: Option<String>,
38+
pub proxy_jump: Option<String>,
3739
pub add_keys_to_agent: AddKeysToAgent,
3840
}
3941

@@ -45,22 +47,30 @@ impl Config {
4547
port: 22,
4648
identity_file: None,
4749
proxy_command: None,
50+
proxy_jump: None,
4851
add_keys_to_agent: AddKeysToAgent::default(),
4952
}
5053
}
5154
}
5255

5356
impl Config {
54-
fn update_proxy_command(&mut self) {
55-
if let Some(ref mut prox) = self.proxy_command {
56-
*prox = prox.replace("%h", &self.host_name);
57-
*prox = prox.replace("%p", &format!("{}", self.port));
58-
}
57+
// Look for any of the ssh_config(5) percent-style tokens and expand them
58+
// based on current data in the struct, returning a new String. This function
59+
// can be employed late/lazy eg just before establishing a stream using ProxyCommand
60+
// but also can be used to modify Hostname as config parse time
61+
fn expand_tokens(&self, original: &str) -> String {
62+
let mut string = original.to_string();
63+
string = string.replace("%u", &self.user);
64+
string = string.replace("%h", &self.host_name); // remote hostname (from context "host")
65+
string = string.replace("%H", &self.host_name); // remote hostname (from context "host")
66+
string = string.replace("%p", &format!("{}", self.port)); // original typed hostname (from context "host")
67+
string = string.replace("%%", "%");
68+
string
5969
}
6070

61-
pub async fn stream(&mut self) -> Result<Stream, Error> {
62-
self.update_proxy_command();
71+
pub async fn stream(&self) -> Result<Stream, Error> {
6372
if let Some(ref proxy_command) = self.proxy_command {
73+
let proxy_command = self.expand_tokens(proxy_command);
6474
let cmd: Vec<&str> = proxy_command.split(' ').collect();
6575
Stream::proxy_command(cmd.first().unwrap_or(&""), cmd.get(1..).unwrap_or(&[]))
6676
.await
@@ -105,9 +115,9 @@ pub enum AddKeysToAgent {
105115
pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
106116
let mut config: Option<Config> = None;
107117
for line in file.lines() {
108-
let line = line.trim();
109-
if let Some(n) = line.find(' ') {
110-
let (key, value) = line.split_at(n);
118+
let tokens = line.trim().splitn(2, ' ').collect::<Vec<&str>>();
119+
if tokens.len() == 2 {
120+
let (key, value) = (tokens.first().unwrap_or(&""), tokens.get(1).unwrap_or(&""));
111121
let lower = key.to_lowercase();
112122
if let Some(ref mut config) = config {
113123
match lower.as_str() {
@@ -116,10 +126,7 @@ pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
116126
config.user.clear();
117127
config.user.push_str(value.trim_start());
118128
}
119-
"hostname" => {
120-
config.host_name.clear();
121-
config.host_name.push_str(value.trim_start())
122-
}
129+
"hostname" => config.host_name = config.expand_tokens(value.trim_start()),
123130
"port" => {
124131
if let Ok(port) = value.trim_start().parse() {
125132
config.port = port
@@ -148,6 +155,7 @@ pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
148155
}
149156
}
150157
"proxycommand" => config.proxy_command = Some(value.trim_start().to_string()),
158+
"proxyjump" => config.proxy_jump = Some(value.trim_start().to_string()),
151159
"addkeystoagent" => match value.to_lowercase().as_str() {
152160
"yes" => config.add_keys_to_agent = AddKeysToAgent::Yes,
153161
"confirm" => config.add_keys_to_agent = AddKeysToAgent::Confirm,
@@ -158,7 +166,11 @@ pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
158166
debug!("{:?}", key);
159167
}
160168
}
161-
} else if lower.as_str() == "host" && value.trim_start() == host {
169+
} else if lower.as_str() == "host"
170+
&& value
171+
.split_whitespace()
172+
.any(|x| check_host_against_glob_pattern(host, x))
173+
{
162174
let mut c = Config::default(host);
163175
c.port = 22;
164176
config = Some(c)
@@ -171,3 +183,11 @@ pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
171183
Err(Error::HostNotFound)
172184
}
173185
}
186+
187+
fn check_host_against_glob_pattern(candidate: &str, glob_pattern: &str) -> bool {
188+
dbg!(candidate, glob_pattern);
189+
match Glob::new(glob_pattern) {
190+
Ok(glob) => glob.compile_matcher().is_match(candidate),
191+
_ => false,
192+
}
193+
}

0 commit comments

Comments
 (0)