Skip to content

Commit

Permalink
Add support for StrictHostKeyChecking and UserKnownHostsFile (#386)
Browse files Browse the repository at this point in the history
  • Loading branch information
snaggen authored Nov 22, 2024
1 parent 33e6b72 commit 68fff93
Showing 1 changed file with 139 additions and 20 deletions.
159 changes: 139 additions & 20 deletions russh-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub struct Config {
pub proxy_command: Option<String>,
pub proxy_jump: Option<String>,
pub add_keys_to_agent: AddKeysToAgent,
pub user_known_hosts_file: Option<String>,
pub strict_host_key_checking: bool,
}

impl Config {
Expand All @@ -49,6 +51,8 @@ impl Config {
proxy_command: None,
proxy_jump: None,
add_keys_to_agent: AddKeysToAgent::default(),
user_known_hosts_file: None,
strict_host_key_checking: true,
}
}
}
Expand Down Expand Up @@ -138,26 +142,8 @@ pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
}
}
"identityfile" => {
let id = value.trim_start();
if id.starts_with("~/") {
if let Some(mut home) = home::home_dir() {
home.push(id.split_at(2).1);
config.identity_file = Some(
home.to_str()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to convert home directory to string",
)
})?
.to_string(),
);
} else {
return Err(Error::NoHome);
}
} else {
config.identity_file = Some(id.to_string())
}
config.identity_file =
Some(value.trim_start().strip_quotes().expand_home()?);
}
"proxycommand" => config.proxy_command = Some(value.trim_start().to_string()),
"proxyjump" => config.proxy_jump = Some(value.trim_start().to_string()),
Expand All @@ -167,6 +153,14 @@ pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
"ask" => config.add_keys_to_agent = AddKeysToAgent::Ask,
_ => config.add_keys_to_agent = AddKeysToAgent::No,
},
"userknownhostsfile" => {
config.user_known_hosts_file =
Some(value.trim_start().strip_quotes().expand_home()?);
}
"stricthostkeychecking" => match value.to_lowercase().as_str() {
"no" => config.strict_host_key_checking = false,
_ => config.strict_host_key_checking = true,
},
key => {
debug!("{:?}", key);
}
Expand All @@ -183,3 +177,128 @@ fn check_host_against_glob_pattern(candidate: &str, glob_pattern: &str) -> bool
_ => false,
}
}

trait SshConfigStrExt {
fn strip_quotes(&self) -> Self;
fn expand_home(&self) -> Result<String, Error>;
}

impl SshConfigStrExt for &str {
fn strip_quotes(&self) -> Self {
if self.len() > 1
&& ((self.starts_with('\'') && self.ends_with('\''))
|| (self.starts_with('\"') && self.ends_with('\"')))
{
&self[1..self.len() - 1]
} else {
self
}
}

fn expand_home(&self) -> Result<String, Error> {
if self.starts_with("~/") {
if let Some(mut home) = home::home_dir() {
home.push(self.split_at(2).1);
Ok(home
.to_str()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to convert home directory to string",
)
})?
.to_string())
} else {
Err(Error::NoHome)
}
} else {
Ok(self.to_string())
}
}
}

#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use crate::{parse, AddKeysToAgent, Config, SshConfigStrExt};

#[test]
fn strip_quotes() {
let value = "'this is a test'";
assert_eq!("this is a test", value.strip_quotes());
let value = "\"this is a test\"";
assert_eq!("this is a test", value.strip_quotes());
let value = "'this is a test\"";
assert_eq!("'this is a test\"", value.strip_quotes());
let value = "'this is a test";
assert_eq!("'this is a test", value.strip_quotes());
let value = "this is a test'";
assert_eq!("this is a test'", value.strip_quotes());
let value = "this is a test";
assert_eq!("this is a test", value.strip_quotes());
let value = "";
assert_eq!("", value.strip_quotes());
let value = "'";
assert_eq!("'", value.strip_quotes());
let value = "''";
assert_eq!("", value.strip_quotes());
}

#[test]
fn expand_home() {
let value = "~/some/folder".expand_home().expect("expand_home");
assert_eq!(
format!(
"{}{}",
home::home_dir().expect("homedir").to_str().expect("to_str"),
"/some/folder"
),
value
);
}

#[test]
fn default_config() {
let config: Config = Config::default("some_host");
assert_eq!(whoami::username(), config.user);
assert_eq!("some_host", config.host_name);
assert_eq!(22, config.port);
assert_eq!(None, config.identity_file);
assert_eq!(None, config.proxy_command);
assert_eq!(None, config.proxy_jump);
assert_eq!(AddKeysToAgent::No, config.add_keys_to_agent);
assert_eq!(None, config.user_known_hosts_file);
assert!(config.strict_host_key_checking);
}

#[test]
fn basic_config() {
let value = r"#
Host test_host
IdentityFile '~/.ssh/id_ed25519'
User trinity
Hostname foo.com
Port 23
UserKnownHostsFile /some/special/host_file
StrictHostKeyChecking no
#";
let identity_file = format!(
"{}{}",
home::home_dir().expect("homedir").to_str().expect("to_str"),
"/.ssh/id_ed25519"
);
let config = parse(value, "test_host").expect("parse");
assert_eq!("trinity", config.user);
assert_eq!("foo.com", config.host_name);
assert_eq!(23, config.port);
assert_eq!(Some(identity_file), config.identity_file);
assert_eq!(None, config.proxy_command);
assert_eq!(None, config.proxy_jump);
assert_eq!(AddKeysToAgent::No, config.add_keys_to_agent);
assert_eq!(
Some("/some/special/host_file"),
config.user_known_hosts_file.as_deref()
);
assert!(!config.strict_host_key_checking);
}
}

0 comments on commit 68fff93

Please sign in to comment.