1
0
Fork 0

Adding upstream version 0.5.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-06 06:52:03 +02:00
parent 6dc19540ee
commit 19551ac12c
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
23 changed files with 6571 additions and 0 deletions

32
src/default_algorithms.rs Normal file
View file

@ -0,0 +1,32 @@
mod openssh;
/// Default algorithms for ssh.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DefaultAlgorithms {
pub ca_signature_algorithms: Vec<String>,
pub ciphers: Vec<String>,
pub host_key_algorithms: Vec<String>,
pub kex_algorithms: Vec<String>,
pub mac: Vec<String>,
pub pubkey_accepted_algorithms: Vec<String>,
}
impl Default for DefaultAlgorithms {
fn default() -> Self {
self::openssh::defaults()
}
}
impl DefaultAlgorithms {
/// Create a new instance of [`DefaultAlgorithms`] with empty fields.
pub fn empty() -> Self {
Self {
ca_signature_algorithms: vec![],
ciphers: vec![],
host_key_algorithms: vec![],
kex_algorithms: vec![],
mac: vec![],
pubkey_accepted_algorithms: vec![],
}
}
}

View file

@ -0,0 +1,130 @@
//! This file is autogenerated at build-time when `RELOAD_SSH_ALGO` is set to environment.
use crate::DefaultAlgorithms;
/// Default algorithms for ssh.
pub fn defaults() -> DefaultAlgorithms {
DefaultAlgorithms {
ca_signature_algorithms: vec![
"ssh-ed25519".to_string(),
"ecdsa-sha2-nistp256".to_string(),
"ecdsa-sha2-nistp384".to_string(),
"ecdsa-sha2-nistp521".to_string(),
"sk-ssh-ed25519@openssh.com".to_string(),
"sk-ecdsa-sha2-nistp256@openssh.com".to_string(),
"rsa-sha2-512".to_string(),
"rsa-sha2-256".to_string(),
],
ciphers: vec![
"chacha20-poly1305@openssh.com".to_string(),
"aes128-ctr,aes192-ctr,aes256-ctr".to_string(),
"aes128-gcm@openssh.com,aes256-gcm@openssh.com".to_string(),
],
host_key_algorithms: vec![
"ssh-ed25519-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp384-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp521-cert-v01@openssh.com".to_string(),
"sk-ssh-ed25519-cert-v01@openssh.com".to_string(),
"sk-ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(),
"rsa-sha2-512-cert-v01@openssh.com".to_string(),
"rsa-sha2-256-cert-v01@openssh.com".to_string(),
"ssh-ed25519".to_string(),
"ecdsa-sha2-nistp256".to_string(),
"ecdsa-sha2-nistp384".to_string(),
"ecdsa-sha2-nistp521".to_string(),
"sk-ssh-ed25519@openssh.com".to_string(),
"sk-ecdsa-sha2-nistp256@openssh.com".to_string(),
"rsa-sha2-512".to_string(),
"rsa-sha2-256".to_string(),
],
kex_algorithms: vec![
"sntrup761x25519-sha512".to_string(),
"sntrup761x25519-sha512@openssh.com".to_string(),
"mlkem768x25519-sha256".to_string(),
"curve25519-sha256".to_string(),
"curve25519-sha256@libssh.org".to_string(),
"ecdh-sha2-nistp256".to_string(),
"ecdh-sha2-nistp384".to_string(),
"ecdh-sha2-nistp521".to_string(),
"diffie-hellman-group-exchange-sha256".to_string(),
"diffie-hellman-group16-sha512".to_string(),
"diffie-hellman-group18-sha512".to_string(),
"diffie-hellman-group14-sha256".to_string(),
"ssh-ed25519-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp384-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp521-cert-v01@openssh.com".to_string(),
"sk-ssh-ed25519-cert-v01@openssh.com".to_string(),
"sk-ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(),
"rsa-sha2-512-cert-v01@openssh.com".to_string(),
"rsa-sha2-256-cert-v01@openssh.com".to_string(),
"ssh-ed25519".to_string(),
"ecdsa-sha2-nistp256".to_string(),
"ecdsa-sha2-nistp384".to_string(),
"ecdsa-sha2-nistp521".to_string(),
"sk-ssh-ed25519@openssh.com".to_string(),
"sk-ecdsa-sha2-nistp256@openssh.com".to_string(),
"rsa-sha2-512".to_string(),
"rsa-sha2-256".to_string(),
"chacha20-poly1305@openssh.com".to_string(),
"aes128-ctr,aes192-ctr,aes256-ctr".to_string(),
"aes128-gcm@openssh.com,aes256-gcm@openssh.com".to_string(),
"chacha20-poly1305@openssh.com".to_string(),
"aes128-ctr,aes192-ctr,aes256-ctr".to_string(),
"aes128-gcm@openssh.com,aes256-gcm@openssh.com".to_string(),
"umac-64-etm@openssh.com".to_string(),
"umac-128-etm@openssh.com".to_string(),
"hmac-sha2-256-etm@openssh.com".to_string(),
"hmac-sha2-512-etm@openssh.com".to_string(),
"hmac-sha1-etm@openssh.com".to_string(),
"umac-64@openssh.com".to_string(),
"umac-128@openssh.com".to_string(),
"hmac-sha2-256".to_string(),
"hmac-sha2-512".to_string(),
"hmac-sha1".to_string(),
"umac-64-etm@openssh.com".to_string(),
"umac-128-etm@openssh.com".to_string(),
"hmac-sha2-256-etm@openssh.com".to_string(),
"hmac-sha2-512-etm@openssh.com".to_string(),
"hmac-sha1-etm@openssh.com".to_string(),
"umac-64@openssh.com".to_string(),
"umac-128@openssh.com".to_string(),
"hmac-sha2-256".to_string(),
"hmac-sha2-512".to_string(),
"hmac-sha1".to_string(),
"none,zlib@openssh.com".to_string(),
"none,zlib@openssh.com".to_string(),
],
mac: vec![
"umac-64-etm@openssh.com".to_string(),
"umac-128-etm@openssh.com".to_string(),
"hmac-sha2-256-etm@openssh.com".to_string(),
"hmac-sha2-512-etm@openssh.com".to_string(),
"hmac-sha1-etm@openssh.com".to_string(),
"umac-64@openssh.com".to_string(),
"umac-128@openssh.com".to_string(),
"hmac-sha2-256".to_string(),
"hmac-sha2-512".to_string(),
"hmac-sha1".to_string(),
],
pubkey_accepted_algorithms: vec![
"ssh-ed25519-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp384-cert-v01@openssh.com".to_string(),
"ecdsa-sha2-nistp521-cert-v01@openssh.com".to_string(),
"sk-ssh-ed25519-cert-v01@openssh.com".to_string(),
"sk-ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(),
"rsa-sha2-512-cert-v01@openssh.com".to_string(),
"rsa-sha2-256-cert-v01@openssh.com".to_string(),
"ssh-ed25519".to_string(),
"ecdsa-sha2-nistp256".to_string(),
"ecdsa-sha2-nistp384".to_string(),
"ecdsa-sha2-nistp521".to_string(),
"sk-ssh-ed25519@openssh.com".to_string(),
"sk-ecdsa-sha2-nistp256@openssh.com".to_string(),
"rsa-sha2-512".to_string(),
"rsa-sha2-256".to_string(),
],
}
}

138
src/host.rs Normal file
View file

@ -0,0 +1,138 @@
//! # host
//!
//! Ssh host type
use std::fmt;
use wildmatch::WildMatch;
use super::HostParams;
/// Describes the rules to be used for a certain host
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Host {
/// List of hosts for which params are valid. String is string pattern, bool is whether condition is negated
pub pattern: Vec<HostClause>,
pub params: HostParams,
}
impl Host {
pub fn new(pattern: Vec<HostClause>, params: HostParams) -> Self {
Self { pattern, params }
}
/// Returns whether `host` argument intersects the host clauses
pub fn intersects(&self, host: &str) -> bool {
let mut has_matched = false;
for entry in self.pattern.iter() {
let matches = entry.intersects(host);
// If the entry is negated and it matches we can stop searching
if matches && entry.negated {
return false;
}
has_matched |= matches;
}
has_matched
}
}
/// Describes a single clause to match host
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostClause {
pub pattern: String,
pub negated: bool,
}
impl fmt::Display for HostClause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.negated {
write!(f, "!{}", self.pattern)
} else {
write!(f, "{}", self.pattern)
}
}
}
impl HostClause {
/// Creates a new `HostClause` from arguments
pub fn new(pattern: String, negated: bool) -> Self {
Self { pattern, negated }
}
/// Returns whether `host` argument intersects the clause
pub fn intersects(&self, host: &str) -> bool {
WildMatch::new(self.pattern.as_str()).matches(host)
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
use crate::DefaultAlgorithms;
#[test]
fn should_build_host_clause() {
let clause = HostClause::new("192.168.1.1".to_string(), false);
assert_eq!(clause.pattern.as_str(), "192.168.1.1");
assert_eq!(clause.negated, false);
}
#[test]
fn should_intersect_host_clause() {
let clause = HostClause::new("192.168.*.*".to_string(), false);
assert!(clause.intersects("192.168.2.30"));
let clause = HostClause::new("192.168.?0.*".to_string(), false);
assert!(clause.intersects("192.168.40.28"));
}
#[test]
fn should_not_intersect_host_clause() {
let clause = HostClause::new("192.168.*.*".to_string(), false);
assert_eq!(clause.intersects("172.26.104.4"), false);
}
#[test]
fn should_init_host() {
let host = Host::new(
vec![HostClause::new("192.168.*.*".to_string(), false)],
HostParams::new(&DefaultAlgorithms::default()),
);
assert_eq!(host.pattern.len(), 1);
}
#[test]
fn should_intersect_clause() {
let host = Host::new(
vec![
HostClause::new("192.168.*.*".to_string(), false),
HostClause::new("172.26.*.*".to_string(), false),
HostClause::new("10.8.*.*".to_string(), false),
HostClause::new("10.8.0.8".to_string(), true),
],
HostParams::new(&DefaultAlgorithms::default()),
);
assert!(host.intersects("192.168.1.32"));
assert!(host.intersects("172.26.104.4"));
assert!(host.intersects("10.8.0.10"));
}
#[test]
fn should_not_intersect_clause() {
let host = Host::new(
vec![
HostClause::new("192.168.*.*".to_string(), false),
HostClause::new("172.26.*.*".to_string(), false),
HostClause::new("10.8.*.*".to_string(), false),
HostClause::new("10.8.0.8".to_string(), true),
],
HostParams::new(&DefaultAlgorithms::default()),
);
assert_eq!(host.intersects("192.169.1.32"), false);
assert_eq!(host.intersects("172.28.104.4"), false);
assert_eq!(host.intersects("10.9.0.8"), false);
assert_eq!(host.intersects("10.8.0.8"), false);
}
}

445
src/lib.rs Normal file
View file

@ -0,0 +1,445 @@
#![crate_name = "ssh2_config"]
#![crate_type = "lib"]
//! # ssh2-config
//!
//! ssh2-config a library which provides a parser for the SSH configuration file,
//! to be used in pair with the [ssh2](https://github.com/alexcrichton/ssh2-rs) crate.
//!
//! This library provides a method to parse the configuration file and returns the
//! configuration parsed into a structure.
//! The `SshConfig` structure provides all the attributes which **can** be used to configure the **ssh2 Session**
//! and to resolve the host, port and username.
//!
//! Once the configuration has been parsed you can use the `query(&str)`
//! method to query configuration for a certain host, based on the configured patterns.
//! Even if many attributes are not exposed, since not supported, there is anyway a validation of the configuration,
//! so invalid configuration will result in a parsing error.
//!
//! ## Get started
//!
//! First of you need to add **ssh2-config** to your project dependencies:
//!
//! ```toml
//! ssh2-config = "^0.5"
//! ```
//!
//! ## Example
//!
//! Here is a basic example:
//!
//! ```rust
//!
//! use ssh2::Session;
//! use ssh2_config::{HostParams, ParseRule, SshConfig};
//! use std::fs::File;
//! use std::io::BufReader;
//! use std::path::Path;
//!
//! let mut reader = BufReader::new(
//! File::open(Path::new("./assets/ssh.config"))
//! .expect("Could not open configuration file")
//! );
//!
//! let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");
//!
//! // Query parameters for your host
//! // If there's no rule for your host, default params are returned
//! let params = config.query("192.168.1.2");
//!
//! // ...
//!
//! // serialize configuration to string
//! let s = config.to_string();
//!
//! ```
//!
//! ---
//!
//! ## How host parameters are resolved
//!
//! This topic has been debated a lot over the years, so finally since 0.5 this has been fixed to follow the official ssh configuration file rules, as described in the MAN <https://man.openbsd.org/OpenBSD-current/man5/ssh_config.5#DESCRIPTION>.
//!
//! > Unless noted otherwise, for each parameter, the first obtained value will be used. The configuration files contain sections separated by Host specifications, and that section is only applied for hosts that match one of the patterns given in the specification. The matched host name is usually the one given on the command line (see the CanonicalizeHostname option for exceptions).
//! >
//! > Since the first obtained value for each parameter is used, more host-specific declarations should be given near the beginning of the file, and general defaults at the end.
//!
//! This means that:
//!
//! 1. The first obtained value parsing the configuration top-down will be used
//! 2. Host specific rules ARE not overriding default ones if they are not the first obtained value
//! 3. If you want to achieve default values to be less specific than host specific ones, you should put the default values at the end of the configuration file using `Host *`.
//! 4. Algorithms, so `KexAlgorithms`, `Ciphers`, `MACs` and `HostKeyAlgorithms` use a different resolvers which supports appending, excluding and heading insertions, as described in the man page at ciphers: <https://man.openbsd.org/OpenBSD-current/man5/ssh_config.5#Ciphers>.
//!
//! ### Resolvers examples
//!
//! ```ssh
//! Compression yes
//!
//! Host 192.168.1.1
//! Compression no
//! ```
//!
//! If we get rules for `192.168.1.1`, compression will be `yes`, because it's the first obtained value.
//!
//! ```ssh
//! Host 192.168.1.1
//! Compression no
//!
//! Host *
//! Compression yes
//! ```
//!
//! If we get rules for `192.168.1.1`, compression will be `no`, because it's the first obtained value.
//!
//! If we get rules for `172.168.1.1`, compression will be `yes`, because it's the first obtained value MATCHING the host rule.
//!
//! ```ssh
//!
//! Host 192.168.1.1
//! Ciphers +c
//! ```
//!
//! If we get rules for `192.168.1.1`, ciphers will be `c` appended to default algorithms, which can be specified in the [`SshConfig`] constructor.
//!
//! ## Configuring default algorithms
//!
//! When you invoke [`SshConfig::default`], the default algorithms are set from openssh source code, which are the following:
//!
//! ```txt
//! ca_signature_algorithms:
//! "ssh-ed25519",
//! "ecdsa-sha2-nistp256",
//! "ecdsa-sha2-nistp384",
//! "ecdsa-sha2-nistp521",
//! "sk-ssh-ed25519@openssh.com",
//! "sk-ecdsa-sha2-nistp256@openssh.com",
//! "rsa-sha2-512",
//! "rsa-sha2-256",
//!
//! ciphers:
//! "chacha20-poly1305@openssh.com",
//! "aes128-ctr,aes192-ctr,aes256-ctr",
//! "aes128-gcm@openssh.com,aes256-gcm@openssh.com",
//!
//! host_key_algorithms:
//! "ssh-ed25519-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp256-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp384-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp521-cert-v01@openssh.com",
//! "sk-ssh-ed25519-cert-v01@openssh.com",
//! "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
//! "rsa-sha2-512-cert-v01@openssh.com",
//! "rsa-sha2-256-cert-v01@openssh.com",
//! "ssh-ed25519",
//! "ecdsa-sha2-nistp256",
//! "ecdsa-sha2-nistp384",
//! "ecdsa-sha2-nistp521",
//! "sk-ssh-ed25519@openssh.com",
//! "sk-ecdsa-sha2-nistp256@openssh.com",
//! "rsa-sha2-512",
//! "rsa-sha2-256",
//!
//! kex_algorithms:
//! "sntrup761x25519-sha512",
//! "sntrup761x25519-sha512@openssh.com",
//! "mlkem768x25519-sha256",
//! "curve25519-sha256",
//! "curve25519-sha256@libssh.org",
//! "ecdh-sha2-nistp256",
//! "ecdh-sha2-nistp384",
//! "ecdh-sha2-nistp521",
//! "diffie-hellman-group-exchange-sha256",
//! "diffie-hellman-group16-sha512",
//! "diffie-hellman-group18-sha512",
//! "diffie-hellman-group14-sha256",
//! "ssh-ed25519-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp256-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp384-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp521-cert-v01@openssh.com",
//! "sk-ssh-ed25519-cert-v01@openssh.com",
//! "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
//! "rsa-sha2-512-cert-v01@openssh.com",
//! "rsa-sha2-256-cert-v01@openssh.com",
//! "ssh-ed25519",
//! "ecdsa-sha2-nistp256",
//! "ecdsa-sha2-nistp384",
//! "ecdsa-sha2-nistp521",
//! "sk-ssh-ed25519@openssh.com",
//! "sk-ecdsa-sha2-nistp256@openssh.com",
//! "rsa-sha2-512",
//! "rsa-sha2-256",
//! "chacha20-poly1305@openssh.com",
//! "aes128-ctr,aes192-ctr,aes256-ctr",
//! "aes128-gcm@openssh.com,aes256-gcm@openssh.com",
//! "chacha20-poly1305@openssh.com",
//! "aes128-ctr,aes192-ctr,aes256-ctr",
//! "aes128-gcm@openssh.com,aes256-gcm@openssh.com",
//! "umac-64-etm@openssh.com",
//! "umac-128-etm@openssh.com",
//! "hmac-sha2-256-etm@openssh.com",
//! "hmac-sha2-512-etm@openssh.com",
//! "hmac-sha1-etm@openssh.com",
//! "umac-64@openssh.com",
//! "umac-128@openssh.com",
//! "hmac-sha2-256",
//! "hmac-sha2-512",
//! "hmac-sha1",
//! "umac-64-etm@openssh.com",
//! "umac-128-etm@openssh.com",
//! "hmac-sha2-256-etm@openssh.com",
//! "hmac-sha2-512-etm@openssh.com",
//! "hmac-sha1-etm@openssh.com",
//! "umac-64@openssh.com",
//! "umac-128@openssh.com",
//! "hmac-sha2-256",
//! "hmac-sha2-512",
//! "hmac-sha1",
//! "none,zlib@openssh.com",
//! "none,zlib@openssh.com",
//!
//! mac:
//! "umac-64-etm@openssh.com",
//! "umac-128-etm@openssh.com",
//! "hmac-sha2-256-etm@openssh.com",
//! "hmac-sha2-512-etm@openssh.com",
//! "hmac-sha1-etm@openssh.com",
//! "umac-64@openssh.com",
//! "umac-128@openssh.com",
//! "hmac-sha2-256",
//! "hmac-sha2-512",
//! "hmac-sha1",
//!
//! pubkey_accepted_algorithms:
//! "ssh-ed25519-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp256-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp384-cert-v01@openssh.com",
//! "ecdsa-sha2-nistp521-cert-v01@openssh.com",
//! "sk-ssh-ed25519-cert-v01@openssh.com",
//! "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
//! "rsa-sha2-512-cert-v01@openssh.com",
//! "rsa-sha2-256-cert-v01@openssh.com",
//! "ssh-ed25519",
//! "ecdsa-sha2-nistp256",
//! "ecdsa-sha2-nistp384",
//! "ecdsa-sha2-nistp521",
//! "sk-ssh-ed25519@openssh.com",
//! "sk-ecdsa-sha2-nistp256@openssh.com",
//! "rsa-sha2-512",
//! "rsa-sha2-256",
//! ```
//!
//! If you want you can use a custom constructor [`SshConfig::default_algorithms`] to set your own default algorithms.
#![doc(html_playground_url = "https://play.rust-lang.org")]
#[macro_use]
extern crate log;
use std::fmt;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::PathBuf;
use std::time::Duration;
// -- modules
mod default_algorithms;
mod host;
mod params;
mod parser;
mod serializer;
// -- export
pub use self::default_algorithms::DefaultAlgorithms;
pub use self::host::{Host, HostClause};
pub use self::params::{Algorithms, HostParams};
pub use self::parser::{ParseRule, SshParserError, SshParserResult};
/// Describes the ssh configuration.
/// Configuration is described in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SshConfig {
/// Default algorithms for ssh.
default_algorithms: DefaultAlgorithms,
/// Rulesets for hosts.
/// Default config will be stored with key `*`
hosts: Vec<Host>,
}
impl fmt::Display for SshConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
serializer::SshConfigSerializer::from(self).serialize(f)
}
}
impl SshConfig {
/// Query params for a certain host. Returns [`HostParams`] for the host.
pub fn query<S: AsRef<str>>(&self, pattern: S) -> HostParams {
let mut params = HostParams::new(&self.default_algorithms);
// iter keys, overwrite if None top-down
for host in self.hosts.iter() {
if host.intersects(pattern.as_ref()) {
debug!(
"Merging params for host: {:?} into params {params:?}",
host.pattern
);
params.overwrite_if_none(&host.params);
trace!("Params after merge: {params:?}");
}
}
// return calculated params
params
}
/// Get an iterator over the [`Host`]s which intersect with the given host pattern
pub fn intersecting_hosts(&self, pattern: &str) -> impl Iterator<Item = &'_ Host> {
self.hosts.iter().filter(|host| host.intersects(pattern))
}
/// Set default algorithms for ssh.
///
/// If you want to use the default algorithms from the system, you can use the `Default::default()` method.
pub fn default_algorithms(mut self, algos: DefaultAlgorithms) -> Self {
self.default_algorithms = algos;
self
}
/// Parse [`SshConfig`] from stream which implements [`BufRead`] and return parsed configuration or parser error
///
/// ## Example
///
/// ```rust,ignore
/// let mut reader = BufReader::new(
/// File::open(Path::new("./assets/ssh.config"))
/// .expect("Could not open configuration file")
/// );
///
/// let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");
/// ```
pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult<Self> {
parser::SshConfigParser::parse(&mut self, reader, rules).map(|_| self)
}
/// Parse `~/.ssh/config`` file and return parsed configuration [`SshConfig`] or parser error
pub fn parse_default_file(rules: ParseRule) -> SshParserResult<Self> {
let ssh_folder = dirs::home_dir()
.ok_or_else(|| {
SshParserError::Io(io::Error::new(
io::ErrorKind::NotFound,
"Home folder not found",
))
})?
.join(".ssh");
let mut reader =
BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?);
Self::default().parse(&mut reader, rules)
}
/// Get list of [`Host`]s in the configuration
pub fn get_hosts(&self) -> &Vec<Host> {
&self.hosts
}
}
#[cfg(test)]
fn test_log() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
});
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn should_init_ssh_config() {
test_log();
let config = SshConfig::default();
assert_eq!(config.hosts.len(), 0);
assert_eq!(
config.query("192.168.1.2"),
HostParams::new(&DefaultAlgorithms::default())
);
}
#[test]
fn should_parse_default_config() -> Result<(), parser::SshParserError> {
test_log();
let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?;
Ok(())
}
#[test]
fn should_parse_config() -> Result<(), parser::SshParserError> {
test_log();
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
let mut reader = BufReader::new(
File::open(Path::new("./assets/ssh.config"))
.expect("Could not open configuration file"),
);
SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
Ok(())
}
#[test]
fn should_query_ssh_config() {
test_log();
let mut config = SshConfig::default();
// add config
let mut params1 = HostParams::new(&DefaultAlgorithms::default());
params1.bind_address = Some("0.0.0.0".to_string());
config.hosts.push(Host::new(
vec![HostClause::new(String::from("192.168.*.*"), false)],
params1.clone(),
));
let mut params2 = HostParams::new(&DefaultAlgorithms::default());
params2.bind_interface = Some(String::from("tun0"));
config.hosts.push(Host::new(
vec![HostClause::new(String::from("192.168.10.*"), false)],
params2.clone(),
));
let mut params3 = HostParams::new(&DefaultAlgorithms::default());
params3.host_name = Some("172.26.104.4".to_string());
config.hosts.push(Host::new(
vec![
HostClause::new(String::from("172.26.*.*"), false),
HostClause::new(String::from("172.26.104.4"), true),
],
params3.clone(),
));
// Query
assert_eq!(config.query("192.168.1.32"), params1);
// merged case
params1.overwrite_if_none(&params2);
assert_eq!(config.query("192.168.10.1"), params1);
// Negated case
assert_eq!(config.query("172.26.254.1"), params3);
assert_eq!(
config.query("172.26.104.4"),
HostParams::new(&DefaultAlgorithms::default())
);
}
}

257
src/params.rs Normal file
View file

@ -0,0 +1,257 @@
//! # params
//!
//! Ssh config params for host rule
mod algos;
use std::collections::HashMap;
pub use self::algos::Algorithms;
pub(crate) use self::algos::AlgorithmsRule;
use super::{Duration, PathBuf};
use crate::DefaultAlgorithms;
/// Describes the ssh configuration.
/// Configuration is describes in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
/// Only arguments supported by libssh2 are implemented
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostParams {
/// Specifies to use the specified address on the local machine as the source address of the connection
pub bind_address: Option<String>,
/// Use the specified address on the local machine as the source address of the connection
pub bind_interface: Option<String>,
/// Specifies which algorithms are allowed for signing of certificates by certificate authorities
pub ca_signature_algorithms: Algorithms,
/// Specifies a file from which the user's certificate is read
pub certificate_file: Option<PathBuf>,
/// Specifies the ciphers allowed for protocol version 2 in order of preference
pub ciphers: Algorithms,
/// Specifies whether to use compression
pub compression: Option<bool>,
/// Specifies the number of attempts to make before exiting
pub connection_attempts: Option<usize>,
/// Specifies the timeout used when connecting to the SSH server
pub connect_timeout: Option<Duration>,
/// Specifies the host key signature algorithms that the client wants to use in order of preference
pub host_key_algorithms: Algorithms,
/// Specifies the real host name to log into
pub host_name: Option<String>,
/// Specifies the path of the identity file to be used when authenticating.
/// More than one file can be specified.
/// If more than one file is specified, they will be read in order
pub identity_file: Option<Vec<PathBuf>>,
/// Specifies a pattern-list of unknown options to be ignored if they are encountered in configuration parsing
pub ignore_unknown: Option<Vec<String>>,
/// Specifies the available KEX (Key Exchange) algorithms
pub kex_algorithms: Algorithms,
/// Specifies the MAC (message authentication code) algorithms in order of preference
pub mac: Algorithms,
/// Specifies the port number to connect on the remote host.
pub port: Option<u16>,
/// Specifies the signature algorithms that will be used for public key authentication
pub pubkey_accepted_algorithms: Algorithms,
/// Specifies whether to try public key authentication using SSH keys
pub pubkey_authentication: Option<bool>,
/// Specifies that a TCP port on the remote machine be forwarded over the secure channel
pub remote_forward: Option<u16>,
/// Sets a timeout interval in seconds after which if no data has been received from the server, keep alive will be sent
pub server_alive_interval: Option<Duration>,
/// Specifies whether to send TCP keepalives to the other side
pub tcp_keep_alive: Option<bool>,
#[cfg(target_os = "macos")]
/// specifies whether the system should search for passphrases in the user's keychain when attempting to use a particular key
pub use_keychain: Option<bool>,
/// Specifies the user to log in as.
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 {
/// Create a new [`HostParams`] object with the [`DefaultAlgorithms`]
pub fn new(default_algorithms: &DefaultAlgorithms) -> Self {
Self {
bind_address: None,
bind_interface: None,
ca_signature_algorithms: Algorithms::new(&default_algorithms.ca_signature_algorithms),
certificate_file: None,
ciphers: Algorithms::new(&default_algorithms.ciphers),
compression: None,
connection_attempts: None,
connect_timeout: None,
host_key_algorithms: Algorithms::new(&default_algorithms.host_key_algorithms),
host_name: None,
identity_file: None,
ignore_unknown: None,
kex_algorithms: Algorithms::new(&default_algorithms.kex_algorithms),
mac: Algorithms::new(&default_algorithms.mac),
port: None,
pubkey_accepted_algorithms: Algorithms::new(
&default_algorithms.pubkey_accepted_algorithms,
),
pubkey_authentication: None,
remote_forward: None,
server_alive_interval: None,
tcp_keep_alive: None,
#[cfg(target_os = "macos")]
use_keychain: None,
user: None,
ignored_fields: HashMap::new(),
unsupported_fields: HashMap::new(),
}
}
/// Return whether a certain `param` is in the ignored list
pub(crate) fn ignored(&self, param: &str) -> bool {
self.ignore_unknown
.as_ref()
.map(|x| x.iter().any(|x| x.as_str() == param))
.unwrap_or(false)
}
/// Given a [`HostParams`] object `b`, it will overwrite all the params from `self` only if they are [`None`]
pub fn overwrite_if_none(&mut self, b: &Self) {
self.bind_address = self.bind_address.clone().or_else(|| b.bind_address.clone());
self.bind_interface = self
.bind_interface
.clone()
.or_else(|| b.bind_interface.clone());
self.certificate_file = self
.certificate_file
.clone()
.or_else(|| b.certificate_file.clone());
self.compression = self.compression.or(b.compression);
self.connection_attempts = self.connection_attempts.or(b.connection_attempts);
self.connect_timeout = self.connect_timeout.or(b.connect_timeout);
self.host_name = self.host_name.clone().or_else(|| b.host_name.clone());
self.identity_file = self
.identity_file
.clone()
.or_else(|| b.identity_file.clone());
self.ignore_unknown = self
.ignore_unknown
.clone()
.or_else(|| b.ignore_unknown.clone());
self.port = self.port.or(b.port);
self.pubkey_authentication = self.pubkey_authentication.or(b.pubkey_authentication);
self.remote_forward = self.remote_forward.or(b.remote_forward);
self.server_alive_interval = self.server_alive_interval.or(b.server_alive_interval);
#[cfg(target_os = "macos")]
{
self.use_keychain = self.use_keychain.or(b.use_keychain);
}
self.tcp_keep_alive = self.tcp_keep_alive.or(b.tcp_keep_alive);
self.user = self.user.clone().or_else(|| b.user.clone());
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());
}
}
// merge algos if default and b is not default
if self.ca_signature_algorithms.is_default() && !b.ca_signature_algorithms.is_default() {
self.ca_signature_algorithms = b.ca_signature_algorithms.clone();
}
if self.ciphers.is_default() && !b.ciphers.is_default() {
self.ciphers = b.ciphers.clone();
}
if self.host_key_algorithms.is_default() && !b.host_key_algorithms.is_default() {
self.host_key_algorithms = b.host_key_algorithms.clone();
}
if self.kex_algorithms.is_default() && !b.kex_algorithms.is_default() {
self.kex_algorithms = b.kex_algorithms.clone();
}
if self.mac.is_default() && !b.mac.is_default() {
self.mac = b.mac.clone();
}
if self.pubkey_accepted_algorithms.is_default()
&& !b.pubkey_accepted_algorithms.is_default()
{
self.pubkey_accepted_algorithms = b.pubkey_accepted_algorithms.clone();
}
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use pretty_assertions::assert_eq;
use super::*;
use crate::params::algos::AlgorithmsRule;
#[test]
fn should_initialize_params() {
let params = HostParams::new(&DefaultAlgorithms::default());
assert!(params.bind_address.is_none());
assert!(params.bind_interface.is_none());
assert_eq!(
params.ca_signature_algorithms.algorithms(),
DefaultAlgorithms::default().ca_signature_algorithms
);
assert!(params.certificate_file.is_none());
assert_eq!(
params.ciphers.algorithms(),
DefaultAlgorithms::default().ciphers
);
assert!(params.compression.is_none());
assert!(params.connection_attempts.is_none());
assert!(params.connect_timeout.is_none());
assert_eq!(
params.host_key_algorithms.algorithms(),
DefaultAlgorithms::default().host_key_algorithms
);
assert!(params.host_name.is_none());
assert!(params.identity_file.is_none());
assert!(params.ignore_unknown.is_none());
assert_eq!(
params.kex_algorithms.algorithms(),
DefaultAlgorithms::default().kex_algorithms
);
assert_eq!(params.mac.algorithms(), DefaultAlgorithms::default().mac);
assert!(params.port.is_none());
assert_eq!(
params.pubkey_accepted_algorithms.algorithms(),
DefaultAlgorithms::default().pubkey_accepted_algorithms
);
assert!(params.pubkey_authentication.is_none());
assert!(params.remote_forward.is_none());
assert!(params.server_alive_interval.is_none());
#[cfg(target_os = "macos")]
assert!(params.use_keychain.is_none());
assert!(params.tcp_keep_alive.is_none());
}
#[test]
fn test_should_overwrite_if_none() {
let mut params = HostParams::new(&DefaultAlgorithms::default());
params.bind_address = Some(String::from("pippo"));
let mut b = HostParams::new(&DefaultAlgorithms::default());
b.bind_address = Some(String::from("pluto"));
b.bind_interface = Some(String::from("tun0"));
b.ciphers
.apply(AlgorithmsRule::from_str("c,d").expect("parse error"));
params.overwrite_if_none(&b);
assert_eq!(params.bind_address.unwrap(), "pippo");
assert_eq!(params.bind_interface.unwrap(), "tun0");
// algos
assert_eq!(
params.ciphers.algorithms(),
vec!["c".to_string(), "d".to_string()]
);
}
}

366
src/params/algos.rs Normal file
View file

@ -0,0 +1,366 @@
use std::fmt;
use std::str::FromStr;
use crate::SshParserError;
const ID_APPEND: char = '+';
const ID_HEAD: char = '^';
const ID_EXCLUDE: char = '-';
/// List of algorithms to be used.
/// The algorithms can be appended to the default set, placed at the head of the list,
/// excluded from the default set, or set as the default set.
///
/// # Configuring SSH Algorithms
///
/// In order to configure ssh you should use the `to_string()` method to get the string representation
/// with the correct format for ssh2.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Algorithms {
/// Algorithms to be used.
algos: Vec<String>,
/// whether the default algorithms have been overridden
overridden: bool,
/// applied rule
rule: Option<AlgorithmsRule>,
}
impl Algorithms {
/// Create a new instance of [`Algorithms`] with the given default algorithms.
///
/// ## Example
///
/// ```rust
/// use ssh2_config::Algorithms;
///
/// let algos = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
/// ```
pub fn new<I, S>(default: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self {
algos: default
.into_iter()
.map(|s| s.as_ref().to_string())
.collect(),
overridden: false,
rule: None,
}
}
}
/// List of algorithms to be used.
/// The algorithms can be appended to the default set, placed at the head of the list,
/// excluded from the default set, or set as the default set.
///
/// # Configuring SSH Algorithms
///
/// In order to configure ssh you should use the `to_string()` method to get the string representation
/// with the correct format for ssh2.
///
/// # Algorithms vector
///
/// Otherwise you can access the inner [`Vec`] of algorithms with the [`Algorithms::algos`] method.
///
/// Beware though, that you must **TAKE CARE of the current variant**.
///
/// For instance in case the variant is [`Algorithms::Exclude`] the algos contained in the vec are the ones **to be excluded**.
///
/// While in case of [`Algorithms::Append`] the algos contained in the vec are the ones to be appended to the default ones.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AlgorithmsRule {
/// Append the given algorithms to the default set.
Append(Vec<String>),
/// Place the given algorithms at the head of the list.
Head(Vec<String>),
/// Exclude the given algorithms from the default set.
Exclude(Vec<String>),
/// Set the given algorithms as the default set.
Set(Vec<String>),
}
/// Rule applied; used to format algorithms
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AlgorithmsOp {
Append,
Head,
Exclude,
Set,
}
impl Algorithms {
/// Returns whether the default algorithms are being used.
pub fn is_default(&self) -> bool {
!self.overridden
}
/// Returns algorithms to be used.
pub fn algorithms(&self) -> &[String] {
&self.algos
}
/// Apply an [`AlgorithmsRule`] to the [`Algorithms`] instance.
///
/// If defaults haven't been overridden, apply changes from incoming rule;
/// otherwise keep as-is.
pub fn apply(&mut self, rule: AlgorithmsRule) {
if self.overridden {
// don't apply changes if defaults have been overridden
return;
}
let mut current_algos = self.algos.clone();
match rule.clone() {
AlgorithmsRule::Append(algos) => {
// append but exclude duplicates
for algo in algos {
if !current_algos.iter().any(|s| s == &algo) {
current_algos.push(algo);
}
}
}
AlgorithmsRule::Head(algos) => {
current_algos = algos;
current_algos.extend(self.algorithms().iter().map(|s| s.to_string()));
}
AlgorithmsRule::Exclude(exclude) => {
current_algos = current_algos
.iter()
.filter(|algo| !exclude.contains(algo))
.map(|s| s.to_string())
.collect();
}
AlgorithmsRule::Set(algos) => {
// override default with new set
current_algos = algos;
}
}
// apply changes
self.rule = Some(rule);
self.algos = current_algos;
self.overridden = true;
}
}
impl AlgorithmsRule {
fn op(&self) -> AlgorithmsOp {
match self {
Self::Append(_) => AlgorithmsOp::Append,
Self::Head(_) => AlgorithmsOp::Head,
Self::Exclude(_) => AlgorithmsOp::Exclude,
Self::Set(_) => AlgorithmsOp::Set,
}
}
}
impl FromStr for AlgorithmsRule {
type Err = SshParserError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(SshParserError::ExpectedAlgorithms);
}
// get first char
let (op, start) = match s.chars().next().expect("can't be empty") {
ID_APPEND => (AlgorithmsOp::Append, 1),
ID_HEAD => (AlgorithmsOp::Head, 1),
ID_EXCLUDE => (AlgorithmsOp::Exclude, 1),
_ => (AlgorithmsOp::Set, 0),
};
let algos = s[start..]
.split(',')
.map(|s| s.trim().to_string())
.collect::<Vec<String>>();
match op {
AlgorithmsOp::Append => Ok(Self::Append(algos)),
AlgorithmsOp::Head => Ok(Self::Head(algos)),
AlgorithmsOp::Exclude => Ok(Self::Exclude(algos)),
AlgorithmsOp::Set => Ok(Self::Set(algos)),
}
}
}
impl fmt::Display for AlgorithmsRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let op = self.op();
write!(f, "{op}")
}
}
impl fmt::Display for AlgorithmsOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::Append => write!(f, "{ID_APPEND}"),
Self::Head => write!(f, "{ID_HEAD}"),
Self::Exclude => write!(f, "{ID_EXCLUDE}"),
Self::Set => write!(f, ""),
}
}
}
impl fmt::Display for Algorithms {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(rule) = self.rule.as_ref() {
write!(f, "{rule}",)
} else {
write!(f, "{}", self.algos.join(","))
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_should_parse_algos_set() {
let algo =
AlgorithmsRule::from_str("aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse");
assert_eq!(
algo,
AlgorithmsRule::Set(vec![
"aes128-ctr".to_string(),
"aes192-ctr".to_string(),
"aes256-ctr".to_string()
])
);
}
#[test]
fn test_should_parse_algos_append() {
let algo =
AlgorithmsRule::from_str("+aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse");
assert_eq!(
algo,
AlgorithmsRule::Append(vec![
"aes128-ctr".to_string(),
"aes192-ctr".to_string(),
"aes256-ctr".to_string()
])
);
}
#[test]
fn test_should_parse_algos_head() {
let algo =
AlgorithmsRule::from_str("^aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse");
assert_eq!(
algo,
AlgorithmsRule::Head(vec![
"aes128-ctr".to_string(),
"aes192-ctr".to_string(),
"aes256-ctr".to_string()
])
);
}
#[test]
fn test_should_parse_algos_exclude() {
let algo =
AlgorithmsRule::from_str("-aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse");
assert_eq!(
algo,
AlgorithmsRule::Exclude(vec![
"aes128-ctr".to_string(),
"aes192-ctr".to_string(),
"aes256-ctr".to_string()
])
);
}
#[test]
fn test_should_apply_append() {
let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
let algo2 = AlgorithmsRule::from_str("+aes256-ctr").expect("failed to parse");
algo1.apply(algo2);
assert_eq!(
algo1.algorithms(),
vec![
"aes128-ctr".to_string(),
"aes192-ctr".to_string(),
"aes256-ctr".to_string()
]
);
}
#[test]
fn test_should_merge_append_if_undefined() {
let algos: Vec<String> = vec![];
let mut algo1 = Algorithms::new(algos);
let algo2 = AlgorithmsRule::from_str("+aes256-ctr").expect("failed to parse");
algo1.apply(algo2);
assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]);
}
#[test]
fn test_should_merge_head() {
let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
let algo2 = AlgorithmsRule::from_str("^aes256-ctr").expect("failed to parse");
algo1.apply(algo2);
assert_eq!(
algo1.algorithms(),
vec![
"aes256-ctr".to_string(),
"aes128-ctr".to_string(),
"aes192-ctr".to_string()
]
);
}
#[test]
fn test_should_apply_head() {
let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
let algo2 = AlgorithmsRule::from_str("^aes256-ctr").expect("failed to parse");
algo1.apply(algo2);
assert_eq!(
algo1.algorithms(),
vec![
"aes256-ctr".to_string(),
"aes128-ctr".to_string(),
"aes192-ctr".to_string()
]
);
}
#[test]
fn test_should_merge_exclude() {
let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr", "aes256-ctr"]);
let algo2 = AlgorithmsRule::from_str("-aes192-ctr").expect("failed to parse");
algo1.apply(algo2);
assert_eq!(
algo1.algorithms(),
vec!["aes128-ctr".to_string(), "aes256-ctr".to_string()]
);
}
#[test]
fn test_should_merge_set() {
let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
let algo2 = AlgorithmsRule::from_str("aes256-ctr").expect("failed to parse");
algo1.apply(algo2);
assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]);
}
#[test]
fn test_should_not_apply_twice() {
let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
let algo2 = AlgorithmsRule::from_str("aes256-ctr").expect("failed to parse");
algo1.apply(algo2);
assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string(),]);
let algo3 = AlgorithmsRule::from_str("aes128-ctr").expect("failed to parse");
algo1.apply(algo3);
assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]);
assert_eq!(algo1.overridden, true);
}
}

1814
src/parser.rs Normal file

File diff suppressed because it is too large Load diff

673
src/parser/field.rs Normal file
View file

@ -0,0 +1,673 @@
//! # field
//!
//! Ssh config fields
use std::fmt;
use std::str::FromStr;
/// Configuration field.
/// This enum defines ALL THE SUPPORTED fields in ssh config,
/// as described at <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>.
/// Only a few of them are implemented, as described in `HostParams` struct.
#[derive(Debug, Eq, PartialEq)]
pub enum Field {
Host,
BindAddress,
BindInterface,
CaSignatureAlgorithms,
CertificateFile,
Ciphers,
Compression,
ConnectionAttempts,
ConnectTimeout,
HostKeyAlgorithms,
HostName,
IdentityFile,
IgnoreUnknown,
KexAlgorithms,
Mac,
Port,
PubkeyAcceptedAlgorithms,
PubkeyAuthentication,
RemoteForward,
ServerAliveInterval,
TcpKeepAlive,
#[cfg(target_os = "macos")]
UseKeychain,
User,
// -- not implemented
AddKeysToAgent,
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,
HostbasedKeyTypes,
HostKeyAlias,
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,
}
impl FromStr for Field {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"host" => Ok(Self::Host),
"bindaddress" => Ok(Self::BindAddress),
"bindinterface" => Ok(Self::BindInterface),
"casignaturealgorithms" => Ok(Self::CaSignatureAlgorithms),
"certificatefile" => Ok(Self::CertificateFile),
"ciphers" => Ok(Self::Ciphers),
"compression" => Ok(Self::Compression),
"connectionattempts" => Ok(Self::ConnectionAttempts),
"connecttimeout" => Ok(Self::ConnectTimeout),
"hostkeyalgorithms" => Ok(Self::HostKeyAlgorithms),
"hostname" => Ok(Self::HostName),
"identityfile" => Ok(Self::IdentityFile),
"ignoreunknown" => Ok(Self::IgnoreUnknown),
"kexalgorithms" => Ok(Self::KexAlgorithms),
"macs" => Ok(Self::Mac),
"port" => Ok(Self::Port),
"pubkeyacceptedalgorithms" => Ok(Self::PubkeyAcceptedAlgorithms),
"pubkeyauthentication" => Ok(Self::PubkeyAuthentication),
"remoteforward" => Ok(Self::RemoteForward),
"serveraliveinterval" => Ok(Self::ServerAliveInterval),
"tcpkeepalive" => Ok(Self::TcpKeepAlive),
#[cfg(target_os = "macos")]
"usekeychain" => Ok(Self::UseKeychain),
"user" => Ok(Self::User),
// -- not implemented fields
"addkeystoagent" => Ok(Self::AddKeysToAgent),
"addressfamily" => Ok(Self::AddressFamily),
"batchmode" => Ok(Self::BatchMode),
"canonicaldomains" => Ok(Self::CanonicalDomains),
"canonicalizefallbacklock" => Ok(Self::CanonicalizeFallbackLock),
"canonicalizehostname" => Ok(Self::CanonicalizeHostname),
"canonicalizemaxdots" => Ok(Self::CanonicalizeMaxDots),
"canonicalizepermittedcnames" => Ok(Self::CanonicalizePermittedCNAMEs),
"checkhostip" => Ok(Self::CheckHostIP),
"clearallforwardings" => Ok(Self::ClearAllForwardings),
"controlmaster" => Ok(Self::ControlMaster),
"controlpath" => Ok(Self::ControlPath),
"controlpersist" => Ok(Self::ControlPersist),
"dynamicforward" => Ok(Self::DynamicForward),
"enablesshkeysign" => Ok(Self::EnableSSHKeysign),
"escapechar" => Ok(Self::EscapeChar),
"exitonforwardfailure" => Ok(Self::ExitOnForwardFailure),
"fingerprinthash" => Ok(Self::FingerprintHash),
"forkafterauthentication" => Ok(Self::ForkAfterAuthentication),
"forwardagent" => Ok(Self::ForwardAgent),
"forwardx11" => Ok(Self::ForwardX11),
"forwardx11timeout" => Ok(Self::ForwardX11Timeout),
"forwardx11trusted" => Ok(Self::ForwardX11Trusted),
"gatewayports" => Ok(Self::GatewayPorts),
"globalknownhostsfile" => Ok(Self::GlobalKnownHostsFile),
"gssapiauthentication" => Ok(Self::GSSAPIAuthentication),
"gssapidelegatecredentials" => Ok(Self::GSSAPIDelegateCredentials),
"hashknownhosts" => Ok(Self::HashKnownHosts),
"hostbasedacceptedalgorithms" => Ok(Self::HostbasedAcceptedAlgorithms),
"hostbasedauthentication" => Ok(Self::HostbasedAuthentication),
"hostkeyalias" => Ok(Self::HostKeyAlias),
"hostbasedkeytypes" => Ok(Self::HostbasedKeyTypes),
"identitiesonly" => Ok(Self::IdentitiesOnly),
"identityagent" => Ok(Self::IdentityAgent),
"include" => Ok(Self::Include),
"ipqos" => Ok(Self::IPQoS),
"kbdinteractiveauthentication" => Ok(Self::KbdInteractiveAuthentication),
"kbdinteractivedevices" => Ok(Self::KbdInteractiveDevices),
"knownhostscommand" => Ok(Self::KnownHostsCommand),
"localcommand" => Ok(Self::LocalCommand),
"localforward" => Ok(Self::LocalForward),
"loglevel" => Ok(Self::LogLevel),
"logverbose" => Ok(Self::LogVerbose),
"nohostauthenticationforlocalhost" => Ok(Self::NoHostAuthenticationForLocalhost),
"numberofpasswordprompts" => Ok(Self::NumberOfPasswordPrompts),
"passwordauthentication" => Ok(Self::PasswordAuthentication),
"permitlocalcommand" => Ok(Self::PermitLocalCommand),
"permitremoteopen" => Ok(Self::PermitRemoteOpen),
"pkcs11provider" => Ok(Self::PKCS11Provider),
"preferredauthentications" => Ok(Self::PreferredAuthentications),
"proxycommand" => Ok(Self::ProxyCommand),
"proxyjump" => Ok(Self::ProxyJump),
"proxyusefdpass" => Ok(Self::ProxyUseFdpass),
"pubkeyacceptedkeytypes" => Ok(Self::PubkeyAcceptedKeyTypes),
"rekeylimit" => Ok(Self::RekeyLimit),
"requesttty" => Ok(Self::RequestTTY),
"revokedhostkeys" => Ok(Self::RevokedHostKeys),
"secruitykeyprovider" => Ok(Self::SecruityKeyProvider),
"sendenv" => Ok(Self::SendEnv),
"serveralivecountmax" => Ok(Self::ServerAliveCountMax),
"sessiontype" => Ok(Self::SessionType),
"setenv" => Ok(Self::SetEnv),
"stdinnull" => Ok(Self::StdinNull),
"streamlocalbindmask" => Ok(Self::StreamLocalBindMask),
"stricthostkeychecking" => Ok(Self::StrictHostKeyChecking),
"syslogfacility" => Ok(Self::SyslogFacility),
"updatehostkeys" => Ok(Self::UpdateHostKeys),
"userknownhostsfile" => Ok(Self::UserKnownHostsFile),
"verifyhostkeydns" => Ok(Self::VerifyHostKeyDNS),
"visualhostkey" => Ok(Self::VisualHostKey),
"xauthlocation" => Ok(Self::XAuthLocation),
// -- unknwon field
_ => Err(s.to_string()),
}
}
}
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 {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn should_parse_field_from_string() {
assert_eq!(Field::from_str("Host").ok().unwrap(), Field::Host);
assert_eq!(
Field::from_str("BindAddress").ok().unwrap(),
Field::BindAddress
);
assert_eq!(
Field::from_str("BindInterface").ok().unwrap(),
Field::BindInterface
);
assert_eq!(
Field::from_str("CaSignatureAlgorithms").ok().unwrap(),
Field::CaSignatureAlgorithms
);
assert_eq!(
Field::from_str("CertificateFile").ok().unwrap(),
Field::CertificateFile
);
assert_eq!(Field::from_str("Ciphers").ok().unwrap(), Field::Ciphers);
assert_eq!(
Field::from_str("Compression").ok().unwrap(),
Field::Compression
);
assert_eq!(
Field::from_str("ConnectionAttempts").ok().unwrap(),
Field::ConnectionAttempts
);
assert_eq!(
Field::from_str("ConnectTimeout").ok().unwrap(),
Field::ConnectTimeout
);
assert_eq!(Field::from_str("HostName").ok().unwrap(), Field::HostName);
assert_eq!(
Field::from_str("IdentityFile").ok().unwrap(),
Field::IdentityFile
);
assert_eq!(
Field::from_str("IgnoreUnknown").ok().unwrap(),
Field::IgnoreUnknown
);
assert_eq!(Field::from_str("Macs").ok().unwrap(), Field::Mac);
assert_eq!(
Field::from_str("PubkeyAcceptedAlgorithms").ok().unwrap(),
Field::PubkeyAcceptedAlgorithms
);
assert_eq!(
Field::from_str("PubkeyAuthentication").ok().unwrap(),
Field::PubkeyAuthentication
);
assert_eq!(
Field::from_str("RemoteForward").ok().unwrap(),
Field::RemoteForward
);
assert_eq!(
Field::from_str("TcpKeepAlive").ok().unwrap(),
Field::TcpKeepAlive
);
#[cfg(target_os = "macos")]
assert_eq!(
Field::from_str("UseKeychain").ok().unwrap(),
Field::UseKeychain
);
assert_eq!(Field::from_str("User").ok().unwrap(), Field::User);
assert_eq!(
Field::from_str("AddKeysToAgent").ok().unwrap(),
Field::AddKeysToAgent
);
assert_eq!(
Field::from_str("AddressFamily").ok().unwrap(),
Field::AddressFamily
);
assert_eq!(Field::from_str("BatchMode").ok().unwrap(), Field::BatchMode);
assert_eq!(
Field::from_str("CanonicalDomains").ok().unwrap(),
Field::CanonicalDomains
);
assert_eq!(
Field::from_str("CanonicalizeFallbackLock").ok().unwrap(),
Field::CanonicalizeFallbackLock
);
assert_eq!(
Field::from_str("CanonicalizeHostname").ok().unwrap(),
Field::CanonicalizeHostname
);
assert_eq!(
Field::from_str("CanonicalizeMaxDots").ok().unwrap(),
Field::CanonicalizeMaxDots
);
assert_eq!(
Field::from_str("CanonicalizePermittedCNAMEs").ok().unwrap(),
Field::CanonicalizePermittedCNAMEs
);
assert_eq!(
Field::from_str("CheckHostIP").ok().unwrap(),
Field::CheckHostIP
);
assert_eq!(
Field::from_str("ClearAllForwardings").ok().unwrap(),
Field::ClearAllForwardings
);
assert_eq!(
Field::from_str("ControlMaster").ok().unwrap(),
Field::ControlMaster
);
assert_eq!(
Field::from_str("ControlPath").ok().unwrap(),
Field::ControlPath
);
assert_eq!(
Field::from_str("ControlPersist").ok().unwrap(),
Field::ControlPersist
);
assert_eq!(
Field::from_str("DynamicForward").ok().unwrap(),
Field::DynamicForward
);
assert_eq!(
Field::from_str("EnableSSHKeysign").ok().unwrap(),
Field::EnableSSHKeysign
);
assert_eq!(
Field::from_str("EscapeChar").ok().unwrap(),
Field::EscapeChar
);
assert_eq!(
Field::from_str("ExitOnForwardFailure").ok().unwrap(),
Field::ExitOnForwardFailure
);
assert_eq!(
Field::from_str("FingerprintHash").ok().unwrap(),
Field::FingerprintHash
);
assert_eq!(
Field::from_str("ForkAfterAuthentication").ok().unwrap(),
Field::ForkAfterAuthentication
);
assert_eq!(
Field::from_str("ForwardAgent").ok().unwrap(),
Field::ForwardAgent
);
assert_eq!(
Field::from_str("ForwardX11").ok().unwrap(),
Field::ForwardX11
);
assert_eq!(
Field::from_str("ForwardX11Timeout").ok().unwrap(),
Field::ForwardX11Timeout
);
assert_eq!(
Field::from_str("ForwardX11Trusted").ok().unwrap(),
Field::ForwardX11Trusted,
);
assert_eq!(
Field::from_str("GatewayPorts").ok().unwrap(),
Field::GatewayPorts
);
assert_eq!(
Field::from_str("GlobalKnownHostsFile").ok().unwrap(),
Field::GlobalKnownHostsFile
);
assert_eq!(
Field::from_str("GSSAPIAuthentication").ok().unwrap(),
Field::GSSAPIAuthentication
);
assert_eq!(
Field::from_str("GSSAPIDelegateCredentials").ok().unwrap(),
Field::GSSAPIDelegateCredentials
);
assert_eq!(
Field::from_str("HashKnownHosts").ok().unwrap(),
Field::HashKnownHosts
);
assert_eq!(
Field::from_str("HostbasedAcceptedAlgorithms").ok().unwrap(),
Field::HostbasedAcceptedAlgorithms
);
assert_eq!(
Field::from_str("HostbasedAuthentication").ok().unwrap(),
Field::HostbasedAuthentication
);
assert_eq!(
Field::from_str("HostKeyAlgorithms").ok().unwrap(),
Field::HostKeyAlgorithms
);
assert_eq!(
Field::from_str("HostKeyAlias").ok().unwrap(),
Field::HostKeyAlias
);
assert_eq!(
Field::from_str("HostbasedKeyTypes").ok().unwrap(),
Field::HostbasedKeyTypes
);
assert_eq!(
Field::from_str("IdentitiesOnly").ok().unwrap(),
Field::IdentitiesOnly
);
assert_eq!(
Field::from_str("IdentityAgent").ok().unwrap(),
Field::IdentityAgent
);
assert_eq!(Field::from_str("Include").ok().unwrap(), Field::Include);
assert_eq!(Field::from_str("IPQoS").ok().unwrap(), Field::IPQoS);
assert_eq!(
Field::from_str("KbdInteractiveAuthentication")
.ok()
.unwrap(),
Field::KbdInteractiveAuthentication
);
assert_eq!(
Field::from_str("KbdInteractiveDevices").ok().unwrap(),
Field::KbdInteractiveDevices
);
assert_eq!(
Field::from_str("KnownHostsCommand").ok().unwrap(),
Field::KnownHostsCommand
);
assert_eq!(
Field::from_str("LocalCommand").ok().unwrap(),
Field::LocalCommand
);
assert_eq!(
Field::from_str("LocalForward").ok().unwrap(),
Field::LocalForward
);
assert_eq!(Field::from_str("LogLevel").ok().unwrap(), Field::LogLevel);
assert_eq!(
Field::from_str("LogVerbose").ok().unwrap(),
Field::LogVerbose
);
assert_eq!(
Field::from_str("NoHostAuthenticationForLocalhost")
.ok()
.unwrap(),
Field::NoHostAuthenticationForLocalhost
);
assert_eq!(
Field::from_str("NumberOfPasswordPrompts").ok().unwrap(),
Field::NumberOfPasswordPrompts
);
assert_eq!(
Field::from_str("PasswordAuthentication").ok().unwrap(),
Field::PasswordAuthentication
);
assert_eq!(
Field::from_str("PermitLocalCommand").ok().unwrap(),
Field::PermitLocalCommand
);
assert_eq!(
Field::from_str("PermitRemoteOpen").ok().unwrap(),
Field::PermitRemoteOpen
);
assert_eq!(
Field::from_str("PKCS11Provider").ok().unwrap(),
Field::PKCS11Provider
);
assert_eq!(Field::from_str("Port").ok().unwrap(), Field::Port);
assert_eq!(
Field::from_str("PreferredAuthentications").ok().unwrap(),
Field::PreferredAuthentications
);
assert_eq!(
Field::from_str("ProxyCommand").ok().unwrap(),
Field::ProxyCommand
);
assert_eq!(Field::from_str("ProxyJump").ok().unwrap(), Field::ProxyJump);
assert_eq!(
Field::from_str("ProxyUseFdpass").ok().unwrap(),
Field::ProxyUseFdpass
);
assert_eq!(
Field::from_str("PubkeyAcceptedKeyTypes").ok().unwrap(),
Field::PubkeyAcceptedKeyTypes
);
assert_eq!(
Field::from_str("RekeyLimit").ok().unwrap(),
Field::RekeyLimit
);
assert_eq!(
Field::from_str("RequestTTY").ok().unwrap(),
Field::RequestTTY
);
assert_eq!(
Field::from_str("RevokedHostKeys").ok().unwrap(),
Field::RevokedHostKeys
);
assert_eq!(
Field::from_str("SecruityKeyProvider").ok().unwrap(),
Field::SecruityKeyProvider
);
assert_eq!(Field::from_str("SendEnv").ok().unwrap(), Field::SendEnv);
assert_eq!(
Field::from_str("ServerAliveCountMax").ok().unwrap(),
Field::ServerAliveCountMax
);
assert_eq!(
Field::from_str("ServerAliveInterval").ok().unwrap(),
Field::ServerAliveInterval
);
assert_eq!(
Field::from_str("SessionType").ok().unwrap(),
Field::SessionType
);
assert_eq!(Field::from_str("SetEnv").ok().unwrap(), Field::SetEnv);
assert_eq!(Field::from_str("StdinNull").ok().unwrap(), Field::StdinNull);
assert_eq!(
Field::from_str("StreamLocalBindMask").ok().unwrap(),
Field::StreamLocalBindMask
);
assert_eq!(
Field::from_str("StrictHostKeyChecking").ok().unwrap(),
Field::StrictHostKeyChecking
);
assert_eq!(
Field::from_str("SyslogFacility").ok().unwrap(),
Field::SyslogFacility
);
assert_eq!(
Field::from_str("UpdateHostKeys").ok().unwrap(),
Field::UpdateHostKeys
);
assert_eq!(
Field::from_str("UserKnownHostsFile").ok().unwrap(),
Field::UserKnownHostsFile
);
assert_eq!(
Field::from_str("VerifyHostKeyDNS").ok().unwrap(),
Field::VerifyHostKeyDNS
);
assert_eq!(
Field::from_str("VisualHostKey").ok().unwrap(),
Field::VisualHostKey
);
assert_eq!(
Field::from_str("XAuthLocation").ok().unwrap(),
Field::XAuthLocation
);
}
#[test]
fn should_fail_parsing_field() {
assert!(Field::from_str("CristinaDavena").is_err());
}
}

211
src/serializer.rs Normal file
View file

@ -0,0 +1,211 @@
//! SSH Config serializer
use std::fmt;
use crate::{Host, HostParams, SshConfig};
pub struct SshConfigSerializer<'a>(&'a SshConfig);
impl SshConfigSerializer<'_> {
pub fn serialize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.hosts.is_empty() {
return Ok(());
}
// serialize default host
let root = self.0.hosts.first().unwrap();
Self::serialize_host_params(f, &root.params, false)?;
// serialize other hosts
for host in self.0.hosts.iter().skip(1) {
Self::serialize_host(f, host)?;
}
Ok(())
}
fn serialize_host(f: &mut fmt::Formatter<'_>, host: &Host) -> fmt::Result {
for pattern in &host.pattern {
writeln!(f, "Host {pattern}",)?;
Self::serialize_host_params(f, &host.params, true)?;
writeln!(f,)?;
}
Ok(())
}
fn serialize_host_params(
f: &mut fmt::Formatter<'_>,
params: &HostParams,
nested: bool,
) -> fmt::Result {
let padding = if nested { " " } else { "" };
if let Some(value) = params.bind_address.as_ref() {
writeln!(f, "{padding}Hostname {value}",)?;
}
if let Some(value) = params.bind_interface.as_ref() {
writeln!(f, "{padding}BindAddress {value}",)?;
}
if !params.ca_signature_algorithms.is_default() {
writeln!(
f,
"{padding}CASignatureAlgorithms {ca_signature_algorithms}",
padding = padding,
ca_signature_algorithms = params.ca_signature_algorithms
)?;
}
if let Some(certificate_file) = params.certificate_file.as_ref() {
writeln!(f, "{padding}CertificateFile {}", certificate_file.display())?;
}
if !params.ciphers.is_default() {
writeln!(
f,
"{padding}Ciphers {ciphers}",
padding = padding,
ciphers = params.ciphers
)?;
}
if let Some(value) = params.compression.as_ref() {
writeln!(
f,
"{padding}Compression {}",
if *value { "yes" } else { "no" }
)?;
}
if let Some(connection_attempts) = params.connection_attempts {
writeln!(f, "{padding}ConnectionAttempts {connection_attempts}",)?;
}
if let Some(connect_timeout) = params.connect_timeout {
writeln!(f, "{padding}ConnectTimeout {}", connect_timeout.as_secs())?;
}
if !params.host_key_algorithms.is_default() {
writeln!(
f,
"{padding}HostKeyAlgorithms {host_key_algorithms}",
padding = padding,
host_key_algorithms = params.host_key_algorithms
)?;
}
if let Some(host_name) = params.host_name.as_ref() {
writeln!(f, "{padding}HostName {host_name}",)?;
}
if let Some(identity_file) = params.identity_file.as_ref() {
writeln!(
f,
"{padding}IdentityFile {}",
identity_file
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(",")
)?;
}
if let Some(ignore_unknown) = params.ignore_unknown.as_ref() {
writeln!(
f,
"{padding}IgnoreUnknown {}",
ignore_unknown
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(",")
)?;
}
if !params.kex_algorithms.is_default() {
writeln!(
f,
"{padding}KexAlgorithms {kex_algorithms}",
padding = padding,
kex_algorithms = params.kex_algorithms
)?;
}
if !params.mac.is_default() {
writeln!(
f,
"{padding}MACs {mac}",
padding = padding,
mac = params.mac
)?;
}
if let Some(port) = params.port {
writeln!(f, "{padding}Port {port}", port = port)?;
}
if !params.pubkey_accepted_algorithms.is_default() {
writeln!(
f,
"{padding}PubkeyAcceptedAlgorithms {pubkey_accepted_algorithms}",
padding = padding,
pubkey_accepted_algorithms = params.pubkey_accepted_algorithms
)?;
}
if let Some(pubkey_authentication) = params.pubkey_authentication.as_ref() {
writeln!(
f,
"{padding}PubkeyAuthentication {}",
if *pubkey_authentication { "yes" } else { "no" }
)?;
}
if let Some(remote_forward) = params.remote_forward.as_ref() {
writeln!(f, "{padding}RemoteForward {remote_forward}",)?;
}
if let Some(server_alive_interval) = params.server_alive_interval {
writeln!(
f,
"{padding}ServerAliveInterval {}",
server_alive_interval.as_secs()
)?;
}
if let Some(tcp_keep_alive) = params.tcp_keep_alive.as_ref() {
writeln!(
f,
"{padding}TCPKeepAlive {}",
if *tcp_keep_alive { "yes" } else { "no" }
)?;
}
#[cfg(target_os = "macos")]
if let Some(use_keychain) = params.use_keychain.as_ref() {
writeln!(
f,
"{padding}UseKeychain {}",
if *use_keychain { "yes" } else { "no" }
)?;
}
if let Some(user) = params.user.as_ref() {
writeln!(f, "{padding}User {user}",)?;
}
for (field, value) in &params.ignored_fields {
writeln!(
f,
"{padding}{field} {value}",
field = field,
value = value
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" ")
)?;
}
for (field, value) in &params.unsupported_fields {
writeln!(
f,
"{padding}{field} {value}",
field = field,
value = value
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" ")
)?;
}
Ok(())
}
}
impl<'a> From<&'a SshConfig> for SshConfigSerializer<'a> {
fn from(config: &'a SshConfig) -> Self {
SshConfigSerializer(config)
}
}