diff --git a/CHANGELOG.md b/CHANGELOG.md index 864d73e..af96b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Released on ?? - **Changed keybindings for BACKTAB**: backtab will now change the explorer tab - To active the LOG PANEL, use `P` - **Yes/No dialogs** are now answerable by pressing `Y` or `N` on your keyboard ([Issue 121](https://github.com/veeso/termscp/issues/121)) +- **Use ssh2 config IdentityFile** as fallback for key based authentication - **Bugfix** - Fixed [Issue 122](https://github.com/veeso/termscp/issues/122) - Fixed version comparison when going above 0.9 @@ -44,6 +45,8 @@ Released on ?? - Bump `keyring` to `1.2.0` - Bump `open` to `3.0.2` - Changed `regex` to `lazy-regex 2.3.0` + - Bump `remotefs-ssh` to `0.1.2` + - Bump `ssh2-config` to `0.1.3` - Bump `tuirealm` to `1.8.0` - Bump `tui-realm-stdlib` to `1.1.7` - Added `version-compare 0.1.0` diff --git a/Cargo.lock b/Cargo.lock index 2fe8286..effa48e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1719,6 +1719,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696" +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + [[package]] name = "pathdiff" version = "0.2.1" @@ -1938,7 +1944,7 @@ checksum = "9939849e0b3895b07f258458d0fa5cd4632c78ffc517e66e1f758ddf23990542" dependencies = [ "chrono", "log", - "path-slash", + "path-slash 0.1.5", "remotefs", "rust-s3", "thiserror", @@ -1952,7 +1958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "359fd989a6ad50fa6defae771e453695412cbfdb5476f49e42d1bb1d51b5c096" dependencies = [ "log", - "path-slash", + "path-slash 0.1.5", "remotefs", "suppaftp", "users", @@ -1960,14 +1966,14 @@ dependencies = [ [[package]] name = "remotefs-ssh" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2ac5a7f475c1b63ffd2aaab57ca4e37e6a6ed23461046d4fb1554835e6088a" +checksum = "190106b53b839cf094172d2f1bc14e8d2846a69de06b239f11c2fc353080e9e0" dependencies = [ "chrono", "lazy_static", "log", - "path-slash", + "path-slash 0.2.1", "regex", "remotefs", "ssh2", @@ -2511,6 +2517,7 @@ dependencies = [ "serde", "serial_test", "simplelog", + "ssh2-config", "tempfile", "thiserror", "toml", diff --git a/Cargo.toml b/Cargo.toml index 919db11..fc3bb87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,11 +51,12 @@ rand = "0.8.5" remotefs = "^0.2.0" remotefs-aws-s3 = "^0.2.0" remotefs-ftp = { version = "^0.1.0", features = [ "secure" ] } -remotefs-ssh = { version = "^0.1.1", features = [ "ssh2-vendored" ] } +remotefs-ssh = { version = "^0.1.2", features = [ "ssh2-vendored" ] } rpassword = "6.0.1" self_update = { version = "0.30.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] } serde = { version = "^1.0.0", features = [ "derive" ] } simplelog = "0.12.0" +ssh2-config = "^0.1.3" tempfile = "3.2.0" thiserror = "^1.0.0" toml = "0.5.0" diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index 1b3cace..b607b8a 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -5,12 +5,16 @@ // Locals use super::config_client::ConfigClient; // Ext -use remotefs_ssh::SshKeyStorage as SshKeyStorageT; +use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait; +use ssh2_config::SshConfig; use std::collections::HashMap; use std::path::{Path, PathBuf}; pub struct SshKeyStorage { - hosts: HashMap, // Association between {user}@{host} and RSA key path + /// Association between {user}@{host} and RSA key path + hosts: HashMap, + /// Ssh2 configuration + ssh_config: Option, } impl SshKeyStorage { @@ -19,6 +23,7 @@ impl SshKeyStorage { pub fn empty() -> Self { SshKeyStorage { hosts: HashMap::new(), + ssh_config: None, } } @@ -34,21 +39,67 @@ impl SshKeyStorage { let key: String = Self::make_mapkey(host, username); self.hosts.insert(key, p); } -} -impl SshKeyStorageT for SshKeyStorage { - fn resolve(&self, host: &str, username: &str) -> Option<&Path> { + /// Parse ssh2 config + fn parse_ssh2_config(path: &str) -> Result { + use std::fs::File; + use std::io::BufReader; + + let mut reader = File::open(path) + .map_err(|e| format!("failed to open {}: {}", path, e)) + .map(BufReader::new)?; + SshConfig::default() + .parse(&mut reader) + .map_err(|e| format!("Failed to parse ssh2 config: {}", e)) + } + + /// Resolve host via termscp ssh keys storage + fn resolve_host_in_termscp_storage(&self, host: &str, username: &str) -> Option<&Path> { let key: String = Self::make_mapkey(host, username); self.hosts.get(&key).map(|x| x.as_path()) } + + /// Resolve host via ssh2 configuration + fn resolve_host_in_ssh2_configuration(&self, host: &str) -> Option { + if let Some(config) = self.ssh_config.as_ref() { + let params = config.query(host); + params + .identity_file + .as_ref() + .and_then(|x| x.get(0).cloned()) + } else { + debug!("ssh2 config is not available; no key has been found"); + None + } + } +} + +impl SshKeyStorageTrait for SshKeyStorage { + fn resolve(&self, host: &str, username: &str) -> Option { + // search in termscp keys + if let Some(path) = self.resolve_host_in_termscp_storage(host, username) { + return Some(path.to_path_buf()); + } + debug!( + "couldn't find any ssh key associated to {} at {}. Trying with ssh2 config", + username, host + ); + // otherwise search in configuration + self.resolve_host_in_ssh2_configuration(host) + } } impl From<&ConfigClient> for SshKeyStorage { fn from(cfg_client: &ConfigClient) -> Self { + // read ssh2 config + let ssh_config = cfg_client.get_ssh_config().and_then(|x| { + debug!("reading ssh config at {}", x); + Self::parse_ssh2_config(x).ok() + }); let mut hosts: HashMap = HashMap::with_capacity(cfg_client.iter_ssh_keys().count()); debug!("Setting up SSH key storage"); - // Iterate over keys + // Iterate over keys in storage for key in cfg_client.iter_ssh_keys() { match cfg_client.get_ssh_key(key) { Ok(host) => match host { @@ -66,7 +117,7 @@ impl From<&ConfigClient> for SshKeyStorage { info!("Got SSH key for {}", key); } // Return storage - SshKeyStorage { hosts } + SshKeyStorage { hosts, ssh_config } } } @@ -75,6 +126,7 @@ mod tests { use super::*; use crate::system::config_client::ConfigClient; + use crate::utils::test_helpers; use pretty_assertions::assert_eq; use std::path::Path; @@ -103,6 +155,35 @@ mod tests { assert!(storage.resolve("deskichup", "veeso").is_none()); } + #[test] + fn sould_resolve_key_from_ssh2_config() { + let rsa_key = test_helpers::create_sample_file_with_content("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a"); + let ssh_config_file = test_helpers::create_sample_file_with_content(format!( + r#" +Host test + HostName 127.0.0.1 + Port 2222 + User test + IdentityFile {} + StrictHostKeyChecking no + UserKnownHostsFile /dev/null +"#, + rsa_key.path().display() + )); + // make storage + let tmp_dir: tempfile::TempDir = tempfile::TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path()) + .ok() + .unwrap(); + client.set_ssh_config(Some(ssh_config_file.path().to_string_lossy().to_string())); + let storage: SshKeyStorage = SshKeyStorage::from(&client); + assert_eq!( + storage.resolve("test", "pi").unwrap().as_path(), + rsa_key.path() + ); + } + #[test] fn test_system_sshkey_storage_empty() { let storage: SshKeyStorage = SshKeyStorage::empty(); diff --git a/src/utils/test_helpers.rs b/src/utils/test_helpers.rs index fe21f02..2aa39c8 100644 --- a/src/utils/test_helpers.rs +++ b/src/utils/test_helpers.rs @@ -21,14 +21,15 @@ pub fn create_sample_file_entry() -> (File, NamedTempFile) { ) } +/// Create sample file with default lorem ipsum content pub fn create_sample_file() -> NamedTempFile { - // Write + create_sample_file_with_content("Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus.") +} + +/// Create sample file with provided content +pub fn create_sample_file_with_content(content: impl std::fmt::Display) -> NamedTempFile { let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); - writeln!( - tmpfile, - "Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus." - ) - .unwrap(); + writeln!(tmpfile, "{}", content).unwrap(); tmpfile }