|
| 1 | +//! Interaction with ssh configuration |
| 2 | +// (c) 2024 Ross Younger |
| 3 | + |
| 4 | +use std::{fs::File, io::BufReader}; |
| 5 | + |
| 6 | +use ssh2_config::{ParseRule, SshConfig}; |
| 7 | +use tracing::{debug, warn}; |
| 8 | + |
| 9 | +use crate::os::{AbstractPlatform as _, Platform}; |
| 10 | + |
| 11 | +/// Attempts to resolve a hostname from a single OpenSSH-style config file |
| 12 | +/// |
| 13 | +/// If `path` is None, uses the default user ssh config file. |
| 14 | +fn resolve_one(path: Option<&str>, host: &str) -> Option<String> { |
| 15 | + let source = path.unwrap_or("~/.ssh/config"); |
| 16 | + let result = match path { |
| 17 | + Some(p) => { |
| 18 | + let mut reader = match File::open(p) { |
| 19 | + Ok(f) => BufReader::new(f), |
| 20 | + Err(e) => { |
| 21 | + // This is not automatically an error, as the file might not exist. |
| 22 | + debug!("Unable to read {p}; continuing without. {e}"); |
| 23 | + return None; |
| 24 | + } |
| 25 | + }; |
| 26 | + SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS) |
| 27 | + } |
| 28 | + None => SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS), |
| 29 | + }; |
| 30 | + let cfg = match result { |
| 31 | + Ok(cfg) => cfg, |
| 32 | + Err(e) => { |
| 33 | + warn!("Unable to parse {source}; continuing without. [{e}]"); |
| 34 | + return None; |
| 35 | + } |
| 36 | + }; |
| 37 | + |
| 38 | + cfg.query(host).host_name.inspect(|h| { |
| 39 | + debug!("Using hostname '{h}' for '{host}' (from {source})"); |
| 40 | + }) |
| 41 | +} |
| 42 | + |
| 43 | +/// Attempts to resolve hostname aliasing from the user's and system's ssh config files to resolve aliasing. |
| 44 | +/// |
| 45 | +/// ## Returns |
| 46 | +/// Some(hostname) if any config file matched. |
| 47 | +/// None if no config files matched. |
| 48 | +/// |
| 49 | +/// ## ssh_config features not currently supported |
| 50 | +/// * Include directives |
| 51 | +/// * Match patterns |
| 52 | +/// * CanonicalizeHostname and friends |
| 53 | +#[must_use] |
| 54 | +pub fn resolve_host_alias(host: &str) -> Option<String> { |
| 55 | + let files = vec![None, Some(Platform::system_ssh_config())]; |
| 56 | + files.into_iter().find_map(|it| resolve_one(it, host)) |
| 57 | +} |
| 58 | + |
| 59 | +#[cfg(test)] |
| 60 | +mod test { |
| 61 | + use super::resolve_one; |
| 62 | + use crate::util::make_test_tempfile; |
| 63 | + |
| 64 | + #[test] |
| 65 | + fn hosts_resolve() { |
| 66 | + let (path, _dir) = make_test_tempfile( |
| 67 | + r" |
| 68 | + Host aaa |
| 69 | + HostName zzz |
| 70 | + Host bbb ccc.ddd |
| 71 | + HostName yyy |
| 72 | + ", |
| 73 | + "test_ssh_config", |
| 74 | + ); |
| 75 | + let f = path.to_string_lossy().to_string(); |
| 76 | + assert!(resolve_one(Some(&f), "nope").is_none()); |
| 77 | + assert_eq!(resolve_one(Some(&f), "aaa").unwrap(), "zzz"); |
| 78 | + assert_eq!(resolve_one(Some(&f), "bbb").unwrap(), "yyy"); |
| 79 | + assert_eq!(resolve_one(Some(&f), "ccc.ddd").unwrap(), "yyy"); |
| 80 | + } |
| 81 | + |
| 82 | + #[test] |
| 83 | + fn wildcards_match() { |
| 84 | + let (path, _dir) = make_test_tempfile( |
| 85 | + r" |
| 86 | + Host *.bar |
| 87 | + HostName baz |
| 88 | + Host 10.11.*.13 |
| 89 | + # this is a silly example but it shows that wildcards match by IP |
| 90 | + HostName wibble |
| 91 | + Host fr?d |
| 92 | + hostname barney |
| 93 | + ", |
| 94 | + "test_ssh_config", |
| 95 | + ); |
| 96 | + let f = path.to_string_lossy().to_string(); |
| 97 | + assert_eq!(resolve_one(Some(&f), "foo.bar").unwrap(), "baz"); |
| 98 | + assert_eq!(resolve_one(Some(&f), "qux.qix.bar").unwrap(), "baz"); |
| 99 | + assert!(resolve_one(Some(&f), "qux.qix").is_none()); |
| 100 | + assert_eq!(resolve_one(Some(&f), "10.11.12.13").unwrap(), "wibble"); |
| 101 | + assert_eq!(resolve_one(Some(&f), "10.11.0.13").unwrap(), "wibble"); |
| 102 | + assert_eq!(resolve_one(Some(&f), "10.11.256.13").unwrap(), "wibble"); // yes I know this isn't a real IP address |
| 103 | + assert!(resolve_one(Some(&f), "10.11.0.130").is_none()); |
| 104 | + |
| 105 | + assert_eq!(resolve_one(Some(&f), "fred").unwrap(), "barney"); |
| 106 | + assert_eq!(resolve_one(Some(&f), "frid").unwrap(), "barney"); |
| 107 | + assert!(resolve_one(Some(&f), "freed").is_none()); |
| 108 | + assert!(resolve_one(Some(&f), "fredd").is_none()); |
| 109 | + } |
| 110 | +} |
0 commit comments