From 0d05ca8b562f7ac41313e79120dcdef6626eefb9 Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Thu, 19 Dec 2024 10:16:55 +0100 Subject: [PATCH] feat: BREAKING Unsupported fields * Update documentation to reflect unsupported_fields field * Add logic for unsupported fields * Add a comment to the unssupported field logic * fix: improvements to code * feat: breaking 0.3 --------- Co-authored-by: not-jan <61017633+not-jan@users.noreply.github.com> --- CHANGELOG.md | 28 ++++++++++++ Cargo.toml | 2 +- README.md | 20 +++++++++ src/params.rs | 19 +++++--- src/parser/field.rs | 105 ++++++++++++++++++++++++++++++++++++++++++++ src/parser/mod.rs | 55 +++++++++++++++++++++-- 6 files changed, 218 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a686a3e..b83a803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog - [Changelog](#changelog) + - [0.3.0](#030) + - [0.2.3](#023) - [0.2.2](#022) - [0.2.1](#021) - [0.2.0](#020) @@ -14,6 +16,32 @@ --- +## 0.3.0 + +Released on 19/12/2024 + +- thiserror `2.0` +- ‼️ **BREAKING CHANGE**: Added support for unsupported fields: + + `AddressFamily, BatchMode, CanonicalDomains, CanonicalizeFallbackLock, CanonicalizeHostname, CanonicalizeMaxDots, CanonicalizePermittedCNAMEs, CheckHostIP, ClearAllForwardings, ControlMaster, ControlPath, ControlPersist, DynamicForward, EnableSSHKeysign, EscapeChar, ExitOnForwardFailure, FingerprintHash, ForkAfterAuthentication, ForwardAgent, ForwardX11, ForwardX11Timeout, ForwardX11Trusted, GatewayPorts, GlobalKnownHostsFile, GSSAPIAuthentication, GSSAPIDelegateCredentials, HashKnownHosts, HostbasedAcceptedAlgorithms, HostbasedAuthentication, HostKeyAlias, HostbasedKeyTypes, IdentitiesOnly, IdentityAgent, Include, IPQoS, KbdInteractiveAuthentication, KbdInteractiveDevices, KnownHostsCommand, LocalCommand, LocalForward, LogLevel, LogVerbose, NoHostAuthenticationForLocalhost, NumberOfPasswordPrompts, PasswordAuthentication, PermitLocalCommand, PermitRemoteOpen, PKCS11Provider, PreferredAuthentications, ProxyCommand, ProxyJump, ProxyUseFdpass, PubkeyAcceptedKeyTypes, RekeyLimit, RequestTTY, RevokedHostKeys, SecruityKeyProvider, SendEnv, ServerAliveCountMax, SessionType, SetEnv, StdinNull, StreamLocalBindMask, StrictHostKeyChecking, SyslogFacility, UpdateHostKeys, UserKnownHostsFile, VerifyHostKeyDNS, VisualHostKey, XAuthLocation` + + If you want to keep the behaviour as-is, use `ParseRule::STRICT | ParseRule::ALLOW_UNSUPPORTED_FIELDS` when calling `parse()` if you were using `ParseRule::STRICT` before. + + Otherwise you can now access unsupported fields by using the `unsupported_fields` field on the `HostParams` structure like this: + + ```rust + use ssh2_config::{ParseRule, SshConfig}; + use std::fs::File; + use std::io::BufReader; + + let mut reader = BufReader::new(File::open(config_path).expect("Could not open configuration file")); + let config = SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNSUPPORTED_FIELDS).expect("Failed to parse configuration"); + + // Query attributes for a certain host + let params = config.query("192.168.1.2"); + let forwards = params.unsupported_fields.get("dynamicforward"); + ``` + ## 0.2.3 Released on 05/12/2023 diff --git a/Cargo.toml b/Cargo.toml index 54d1d09..6dbb99a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" name = "ssh2-config" readme = "README.md" repository = "https://github.com/veeso/ssh2-config" -version = "0.2.4" +version = "0.3.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 1f2b704..0a08f12 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ - [Exposed attributes](#exposed-attributes) - [Missing features](#missing-features) - [Get started 🚀](#get-started-) + - [Reading unsupported fields](#reading-unsupported-fields) - [Examples](#examples) - [Support the developer ☕](#support-the-developer-) - [Contributing and issues 🤝🏻](#contributing-and-issues-) @@ -193,6 +194,25 @@ fn auth_with_rsakey( ``` +### Reading unsupported fields + +As outlined above, ssh2-config does not support all parameters available in the man page of the SSH configuration file. + +If you require these fields you may still access them through the `unsupported_fields` field on the `HostParams` structure like this: + +```rust +use ssh2_config::{ParseRule, SshConfig}; +use std::fs::File; +use std::io::BufReader; + +let mut reader = BufReader::new(File::open(config_path).expect("Could not open configuration file")); +let config = SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNSUPPORTED_FIELDS).expect("Failed to parse configuration"); + +// Query attributes for a certain host +let params = config.query("192.168.1.2"); +let forwards = params.unsupported_fields.get("dynamicforward"); +``` + ### Examples You can view a working examples of an implementation of ssh2-config with ssh2 in the examples folder at [client.rs](examples/client.rs). diff --git a/src/params.rs b/src/params.rs index 7c80a04..f9f0d93 100644 --- a/src/params.rs +++ b/src/params.rs @@ -60,6 +60,8 @@ pub struct HostParams { pub user: Option, /// fields that the parser wasn't able to parse pub ignored_fields: HashMap>, + /// fields that the parser was able to parse but ignored + pub unsupported_fields: HashMap>, } impl HostParams { @@ -167,12 +169,17 @@ impl HostParams { if let Some(user) = b.user.as_deref() { self.user = Some(user.to_owned()); } - if !b.ignored_fields.is_empty() { - for (ignored_field, args) in &b.ignored_fields { - if !self.ignored_fields.contains_key(ignored_field) { - self.ignored_fields - .insert(ignored_field.to_owned(), args.to_owned()); - } + for (ignored_field, args) in &b.ignored_fields { + if !self.ignored_fields.contains_key(ignored_field) { + self.ignored_fields + .insert(ignored_field.to_owned(), args.to_owned()); + } + } + + for (unsupported_field, args) in &b.unsupported_fields { + if !self.unsupported_fields.contains_key(unsupported_field) { + self.unsupported_fields + .insert(unsupported_field.to_owned(), args.to_owned()); } } } diff --git a/src/parser/field.rs b/src/parser/field.rs index 0ab4dd7..a427f3f 100644 --- a/src/parser/field.rs +++ b/src/parser/field.rs @@ -2,6 +2,7 @@ //! //! Ssh config fields +use std::fmt; use std::str::FromStr; /// Configuration field. @@ -215,6 +216,110 @@ impl FromStr for Field { } } +impl fmt::Display for Field { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::Host => "host", + Self::BindAddress => "bindaddress", + Self::BindInterface => "bindinterface", + Self::CaSignatureAlgorithms => "casignaturealgorithms", + Self::CertificateFile => "certificatefile", + Self::Ciphers => "ciphers", + Self::Compression => "compression", + Self::ConnectionAttempts => "connectionattempts", + Self::ConnectTimeout => "connecttimeout", + Self::HostKeyAlgorithms => "hostkeyalgorithms", + Self::HostName => "hostname", + Self::IdentityFile => "identityfile", + Self::IgnoreUnknown => "ignoreunknown", + Self::KexAlgorithms => "kexalgorithms", + Self::Mac => "macs", + Self::Port => "port", + Self::PubkeyAcceptedAlgorithms => "pubkeyacceptedalgorithms", + Self::PubkeyAuthentication => "pubkeyauthentication", + Self::RemoteForward => "remoteforward", + Self::ServerAliveInterval => "serveraliveinterval", + Self::TcpKeepAlive => "tcpkeepalive", + #[cfg(target_os = "macos")] + Self::UseKeychain => "usekeychain", + Self::User => "user", + // Continuation of the rest of the enum variants + Self::AddKeysToAgent => "addkeystoagent", + Self::AddressFamily => "addressfamily", + Self::BatchMode => "batchmode", + Self::CanonicalDomains => "canonicaldomains", + Self::CanonicalizeFallbackLock => "canonicalizefallbacklock", + Self::CanonicalizeHostname => "canonicalizehostname", + Self::CanonicalizeMaxDots => "canonicalizemaxdots", + Self::CanonicalizePermittedCNAMEs => "canonicalizepermittedcnames", + Self::CheckHostIP => "checkhostip", + Self::ClearAllForwardings => "clearallforwardings", + Self::ControlMaster => "controlmaster", + Self::ControlPath => "controlpath", + Self::ControlPersist => "controlpersist", + Self::DynamicForward => "dynamicforward", + Self::EnableSSHKeysign => "enablesshkeysign", + Self::EscapeChar => "escapechar", + Self::ExitOnForwardFailure => "exitonforwardfailure", + Self::FingerprintHash => "fingerprinthash", + Self::ForkAfterAuthentication => "forkafterauthentication", + Self::ForwardAgent => "forwardagent", + Self::ForwardX11 => "forwardx11", + Self::ForwardX11Timeout => "forwardx11timeout", + Self::ForwardX11Trusted => "forwardx11trusted", + Self::GatewayPorts => "gatewayports", + Self::GlobalKnownHostsFile => "globalknownhostsfile", + Self::GSSAPIAuthentication => "gssapiauthentication", + Self::GSSAPIDelegateCredentials => "gssapidelegatecredentials", + Self::HashKnownHosts => "hashknownhosts", + Self::HostbasedAcceptedAlgorithms => "hostbasedacceptedalgorithms", + Self::HostbasedAuthentication => "hostbasedauthentication", + Self::HostKeyAlias => "hostkeyalias", + Self::HostbasedKeyTypes => "hostbasedkeytypes", + Self::IdentitiesOnly => "identitiesonly", + Self::IdentityAgent => "identityagent", + Self::Include => "include", + Self::IPQoS => "ipqos", + Self::KbdInteractiveAuthentication => "kbdinteractiveauthentication", + Self::KbdInteractiveDevices => "kbdinteractivedevices", + Self::KnownHostsCommand => "knownhostscommand", + Self::LocalCommand => "localcommand", + Self::LocalForward => "localforward", + Self::LogLevel => "loglevel", + Self::LogVerbose => "logverbose", + Self::NoHostAuthenticationForLocalhost => "nohostauthenticationforlocalhost", + Self::NumberOfPasswordPrompts => "numberofpasswordprompts", + Self::PasswordAuthentication => "passwordauthentication", + Self::PermitLocalCommand => "permitlocalcommand", + Self::PermitRemoteOpen => "permitremoteopen", + Self::PKCS11Provider => "pkcs11provider", + Self::PreferredAuthentications => "preferredauthentications", + Self::ProxyCommand => "proxycommand", + Self::ProxyJump => "proxyjump", + Self::ProxyUseFdpass => "proxyusefdpass", + Self::PubkeyAcceptedKeyTypes => "pubkeyacceptedkeytypes", + Self::RekeyLimit => "rekeylimit", + Self::RequestTTY => "requesttty", + Self::RevokedHostKeys => "revokedhostkeys", + Self::SecruityKeyProvider => "secruitykeyprovider", + Self::SendEnv => "sendenv", + Self::ServerAliveCountMax => "serveralivecountmax", + Self::SessionType => "sessiontype", + Self::SetEnv => "setenv", + Self::StdinNull => "stdinnull", + Self::StreamLocalBindMask => "streamlocalbindmask", + Self::StrictHostKeyChecking => "stricthostkeychecking", + Self::SyslogFacility => "syslogfacility", + Self::UpdateHostKeys => "updatehostkeys", + Self::UserKnownHostsFile => "userknownhostsfile", + Self::VerifyHostKeyDNS => "verifyhostkeydns", + Self::VisualHostKey => "visualhostkey", + Self::XAuthLocation => "xauthlocation", + }; + write!(f, "{}", s) + } +} + #[cfg(test)] mod test { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6b7ed2b..7d93741 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -33,6 +33,8 @@ pub enum SshParserError { MissingArgument, #[error("unknown field: {0}")] UnknownField(String, Vec), + #[error("unknown field: {0}")] + UnsupportedField(String, Vec), #[error("IO error: {0}")] Io(IoError), } @@ -45,6 +47,8 @@ bitflags! { const STRICT = 0b00000000; /// Allow unknown field const ALLOW_UNKNOWN_FIELDS = 0b00000001; + /// Allow unsupported fields + const ALLOW_UNSUPPORTED_FIELDS = 0b00000010; } } @@ -113,7 +117,21 @@ impl SshConfigParser { current_host = config.hosts.last_mut().unwrap(); } else { // Update field - Self::update_host(field, args, &mut current_host.params)?; + match Self::update_host(field, args, &mut current_host.params) { + Ok(()) => Ok(()), + // If we're allowing unsupported fields to be parsed, add them to the map + Err(SshParserError::UnsupportedField(field, args)) + if rules.intersects(ParseRule::ALLOW_UNSUPPORTED_FIELDS) => + { + current_host.params.unsupported_fields.insert(field, args); + Ok(()) + } + // Eat the error here to not break the API with this change + // Also it'd be weird to error on correct ssh_config's just because they're + // not supported by this library + Err(SshParserError::UnsupportedField(_, _)) => Ok(()), + e => e, + }?; } } @@ -275,7 +293,9 @@ impl SshConfigParser { | Field::UserKnownHostsFile | Field::VerifyHostKeyDNS | Field::VisualHostKey - | Field::XAuthLocation => { /* Ignore fields */ } + | Field::XAuthLocation => { + return Err(SshParserError::UnsupportedField(field.to_string(), args)) + } } Ok(()) } @@ -881,11 +901,38 @@ mod test { #[test] fn should_not_update_host_if_unknown() -> Result<(), SshParserError> { let mut params = HostParams::default(); - SshConfigParser::update_host( + let result = SshConfigParser::update_host( Field::AddKeysToAgent, vec![String::from("yes")], &mut params, - )?; + ); + + match result { + Ok(()) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()), + e => e, + }?; + + assert_eq!(params, HostParams::default()); + Ok(()) + } + + #[test] + fn should_update_host_if_unsupported() -> Result<(), SshParserError> { + let mut params = HostParams::default(); + let result = SshConfigParser::update_host( + Field::AddKeysToAgent, + vec![String::from("yes")], + &mut params, + ); + + match result { + Err(SshParserError::UnsupportedField(field, _)) => { + assert_eq!(field, "addkeystoagent"); + Ok(()) + } + e => e, + }?; + assert_eq!(params, HostParams::default()); Ok(()) }