ssh2_config/
lib.rs

1#![crate_name = "ssh2_config"]
2#![crate_type = "lib"]
3
4//! # ssh2-config
5//!
6//! ssh2-config a library which provides a parser for the SSH configuration file,
7//! to be used in pair with the [ssh2](https://github.com/alexcrichton/ssh2-rs) crate.
8//!
9//! This library provides a method to parse the configuration file and returns the
10//! configuration parsed into a structure.
11//! The `SshConfig` structure provides all the attributes which **can** be used to configure the **ssh2 Session**
12//! and to resolve the host, port and username.
13//!
14//! Once the configuration has been parsed you can use the `query(&str)`
15//! method to query configuration for a certain host, based on the configured patterns.
16//! Even if many attributes are not exposed, since not supported, there is anyway a validation of the configuration,
17//! so invalid configuration will result in a parsing error.
18//!
19//! ## Get started
20//!
21//! First of you need to add **ssh2-config** to your project dependencies:
22//!
23//! ```toml
24//! ssh2-config = "^0.2.0"
25//! ```
26//!
27//! ## Example
28//!
29//! Here is a basic example:
30//!
31//! ```rust
32//!
33//! use ssh2::Session;
34//! use ssh2_config::{HostParams, ParseRule, SshConfig};
35//! use std::fs::File;
36//! use std::io::BufReader;
37//! use std::path::Path;
38//!
39//! let mut reader = BufReader::new(
40//!     File::open(Path::new("./assets/ssh.config"))
41//!         .expect("Could not open configuration file")
42//! );
43//!
44//! let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");
45//!
46//! // Query parameters for your host
47//! // If there's no rule for your host, default params are returned
48//! let params = config.query("192.168.1.2");
49//!
50//! ```
51//!
52
53#![doc(html_playground_url = "https://play.rust-lang.org")]
54
55use std::fs::File;
56use std::io::{self, BufRead, BufReader};
57use std::path::PathBuf;
58use std::time::Duration;
59// -- modules
60mod host;
61mod params;
62mod parser;
63
64// -- export
65pub use host::{Host, HostClause};
66pub use params::HostParams;
67pub use parser::{ParseRule, SshParserError, SshParserResult};
68
69/// Describes the ssh configuration.
70/// Configuration is describes in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
71#[derive(Debug, Clone, PartialEq, Eq, Default)]
72pub struct SshConfig {
73    /// Rulesets for hosts.
74    /// Default config will be stored with key `*`
75    hosts: Vec<Host>,
76}
77
78impl SshConfig {
79    /// Query params for a certain host
80    pub fn query<S: AsRef<str>>(&self, host: S) -> HostParams {
81        let mut params = HostParams::default();
82        // iter keys, merge from lowest to highest precedence
83        for cfg_host in self.hosts.iter().rev() {
84            if cfg_host.intersects(host.as_ref()) {
85                params.merge(&cfg_host.params);
86            }
87        }
88        // return calculated params
89        params
90    }
91
92    /// Parse stream and return parsed configuration or parser error
93    pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult<Self> {
94        parser::SshConfigParser::parse(&mut self, reader, rules).map(|_| self)
95    }
96
97    #[cfg(target_family = "unix")]
98    /// Parse ~/.ssh/config file and return parsed configuration or parser error
99    pub fn parse_default_file(rules: ParseRule) -> SshParserResult<Self> {
100        let ssh_folder = dirs::home_dir()
101            .ok_or_else(|| {
102                SshParserError::Io(io::Error::new(
103                    io::ErrorKind::NotFound,
104                    "Home folder not found",
105                ))
106            })?
107            .join(".ssh");
108
109        let mut reader =
110            BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?);
111
112        Self::default().parse(&mut reader, rules)
113    }
114
115    pub fn get_hosts(&self) -> &Vec<Host> {
116        &self.hosts
117    }
118}
119
120#[cfg(test)]
121mod test {
122
123    use pretty_assertions::assert_eq;
124
125    use super::*;
126
127    #[test]
128    fn should_init_ssh_config() {
129        let config = SshConfig::default();
130        assert_eq!(config.hosts.len(), 0);
131        assert_eq!(config.query("192.168.1.2"), HostParams::default());
132    }
133
134    #[test]
135    #[cfg(target_family = "unix")]
136    fn should_parse_default_config() -> Result<(), parser::SshParserError> {
137        let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?;
138        Ok(())
139    }
140
141    #[test]
142    fn should_parse_config() -> Result<(), parser::SshParserError> {
143        use std::fs::File;
144        use std::io::BufReader;
145        use std::path::Path;
146
147        let mut reader = BufReader::new(
148            File::open(Path::new("./assets/ssh.config"))
149                .expect("Could not open configuration file"),
150        );
151
152        SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
153
154        Ok(())
155    }
156
157    #[test]
158    fn should_query_ssh_config() {
159        let mut config = SshConfig::default();
160        // add config
161        let mut params1 = HostParams {
162            bind_address: Some(String::from("0.0.0.0")),
163            ..Default::default()
164        };
165        config.hosts.push(Host::new(
166            vec![HostClause::new(String::from("192.168.*.*"), false)],
167            params1.clone(),
168        ));
169        let params2 = HostParams {
170            bind_interface: Some(String::from("tun0")),
171            ..Default::default()
172        };
173        config.hosts.push(Host::new(
174            vec![HostClause::new(String::from("192.168.10.*"), false)],
175            params2.clone(),
176        ));
177        let params3 = HostParams {
178            host_name: Some(String::from("172.26.104.4")),
179            ..Default::default()
180        };
181        config.hosts.push(Host::new(
182            vec![
183                HostClause::new(String::from("172.26.*.*"), false),
184                HostClause::new(String::from("172.26.104.4"), true),
185            ],
186            params3.clone(),
187        ));
188        // Query
189        assert_eq!(config.query("192.168.1.32"), params1);
190        // merged case
191        params1.merge(&params2);
192        assert_eq!(config.query("192.168.10.1"), params1);
193        // Negated case
194        assert_eq!(config.query("172.26.254.1"), params3);
195        assert_eq!(config.query("172.26.104.4"), HostParams::default());
196    }
197}