Skip to content

Commit

Permalink
feat: BREAKING Unsupported fields
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
veeso and not-jan authored Dec 19, 2024
1 parent 1f884c8 commit 0d05ca8
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 11 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-)
Expand Down Expand Up @@ -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).
Expand Down
19 changes: 13 additions & 6 deletions src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub struct HostParams {
pub user: Option<String>,
/// fields that the parser wasn't able to parse
pub ignored_fields: HashMap<String, Vec<String>>,
/// fields that the parser was able to parse but ignored
pub unsupported_fields: HashMap<String, Vec<String>>,
}

impl HostParams {
Expand Down Expand Up @@ -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());
}
}
}
Expand Down
105 changes: 105 additions & 0 deletions src/parser/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! Ssh config fields
use std::fmt;
use std::str::FromStr;

/// Configuration field.
Expand Down Expand Up @@ -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 {

Expand Down
55 changes: 51 additions & 4 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub enum SshParserError {
MissingArgument,
#[error("unknown field: {0}")]
UnknownField(String, Vec<String>),
#[error("unknown field: {0}")]
UnsupportedField(String, Vec<String>),
#[error("IO error: {0}")]
Io(IoError),
}
Expand All @@ -45,6 +47,8 @@ bitflags! {
const STRICT = 0b00000000;
/// Allow unknown field
const ALLOW_UNKNOWN_FIELDS = 0b00000001;
/// Allow unsupported fields
const ALLOW_UNSUPPORTED_FIELDS = 0b00000010;
}
}

Expand Down Expand Up @@ -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,
}?;
}
}

Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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(())
}
Expand Down

0 comments on commit 0d05ca8

Please sign in to comment.