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.4"
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//! // serialize configuration to string
53//! let s = config.to_string();
54//!
55//! ```
56//!
57
58#![doc(html_playground_url = "https://play.rust-lang.org")]
59
60#[macro_use]
61extern crate log;
62
63use std::fmt;
64use std::fs::File;
65use std::io::{self, BufRead, BufReader};
66use std::path::PathBuf;
67use std::time::Duration;
68// -- modules
69mod host;
70mod params;
71mod parser;
72mod serializer;
73
74// -- export
75pub use host::{Host, HostClause};
76pub use params::HostParams;
77pub use parser::{ParseRule, SshParserError, SshParserResult};
78
79/// Describes the ssh configuration.
80/// Configuration is describes in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
81#[derive(Debug, Clone, PartialEq, Eq, Default)]
82pub struct SshConfig {
83    /// Rulesets for hosts.
84    /// Default config will be stored with key `*`
85    hosts: Vec<Host>,
86}
87
88impl fmt::Display for SshConfig {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        serializer::SshConfigSerializer::from(self).serialize(f)
91    }
92}
93
94impl SshConfig {
95    /// Query params for a certain host
96    pub fn query<S: AsRef<str>>(&self, host: S) -> HostParams {
97        let mut params = HostParams::default();
98        // iter keys, merge from lowest to highest precedence
99        for cfg_host in self.hosts.iter() {
100            if cfg_host.intersects(host.as_ref()) {
101                params.merge(&cfg_host.params);
102            }
103        }
104        // return calculated params
105        params
106    }
107
108    /// Parse stream and return parsed configuration or parser error
109    pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult<Self> {
110        parser::SshConfigParser::parse(&mut self, reader, rules).map(|_| self)
111    }
112
113    /// Parse ~/.ssh/config file and return parsed configuration or parser error
114    pub fn parse_default_file(rules: ParseRule) -> SshParserResult<Self> {
115        let ssh_folder = dirs::home_dir()
116            .ok_or_else(|| {
117                SshParserError::Io(io::Error::new(
118                    io::ErrorKind::NotFound,
119                    "Home folder not found",
120                ))
121            })?
122            .join(".ssh");
123
124        let mut reader =
125            BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?);
126
127        Self::default().parse(&mut reader, rules)
128    }
129
130    pub fn get_hosts(&self) -> &Vec<Host> {
131        &self.hosts
132    }
133}
134
135#[cfg(test)]
136fn test_log() {
137    use std::sync::Once;
138
139    static INIT: Once = Once::new();
140
141    INIT.call_once(|| {
142        let _ = env_logger::builder()
143            .filter_level(log::LevelFilter::Trace)
144            .is_test(true)
145            .try_init();
146    });
147}
148
149#[cfg(test)]
150mod test {
151
152    use pretty_assertions::assert_eq;
153
154    use super::*;
155
156    #[test]
157    fn should_init_ssh_config() {
158        test_log();
159
160        let config = SshConfig::default();
161        assert_eq!(config.hosts.len(), 0);
162        assert_eq!(config.query("192.168.1.2"), HostParams::default());
163    }
164
165    #[test]
166    fn should_parse_default_config() -> Result<(), parser::SshParserError> {
167        test_log();
168
169        let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?;
170        Ok(())
171    }
172
173    #[test]
174    fn should_parse_config() -> Result<(), parser::SshParserError> {
175        test_log();
176
177        use std::fs::File;
178        use std::io::BufReader;
179        use std::path::Path;
180
181        let mut reader = BufReader::new(
182            File::open(Path::new("./assets/ssh.config"))
183                .expect("Could not open configuration file"),
184        );
185
186        SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
187
188        Ok(())
189    }
190
191    #[test]
192    fn should_query_ssh_config() {
193        test_log();
194
195        let mut config = SshConfig::default();
196        // add config
197        let mut params1 = HostParams {
198            bind_address: Some(String::from("0.0.0.0")),
199            ..Default::default()
200        };
201        config.hosts.push(Host::new(
202            vec![HostClause::new(String::from("192.168.*.*"), false)],
203            params1.clone(),
204        ));
205        let params2 = HostParams {
206            bind_interface: Some(String::from("tun0")),
207            ..Default::default()
208        };
209        config.hosts.push(Host::new(
210            vec![HostClause::new(String::from("192.168.10.*"), false)],
211            params2.clone(),
212        ));
213        let params3 = HostParams {
214            host_name: Some(String::from("172.26.104.4")),
215            ..Default::default()
216        };
217        config.hosts.push(Host::new(
218            vec![
219                HostClause::new(String::from("172.26.*.*"), false),
220                HostClause::new(String::from("172.26.104.4"), true),
221            ],
222            params3.clone(),
223        ));
224        // Query
225        assert_eq!(config.query("192.168.1.32"), params1);
226        // merged case
227        params1.merge(&params2);
228        assert_eq!(config.query("192.168.10.1"), params1);
229        // Negated case
230        assert_eq!(config.query("172.26.254.1"), params3);
231        assert_eq!(config.query("172.26.104.4"), HostParams::default());
232    }
233}