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.6"
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//!
59//! ## How host parameters are resolved
60//!
61//! 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>.
62//!
63//! > 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).
64//! >
65//! > 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.
66//!
67//! This means that:
68//!
69//! 1. The first obtained value parsing the configuration top-down will be used
70//! 2. Host specific rules ARE not overriding default ones if they are not the first obtained value
71//! 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 *`.
72//! 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>.
73//!
74//! ### Resolvers examples
75//!
76//! ```ssh
77//! Compression yes
78//!
79//! Host 192.168.1.1
80//! Compression no
81//! ```
82//!
83//! If we get rules for `192.168.1.1`, compression will be `yes`, because it's the first obtained value.
84//!
85//! ```ssh
86//! Host 192.168.1.1
87//! Compression no
88//!
89//! Host *
90//! Compression yes
91//! ```
92//!
93//! If we get rules for `192.168.1.1`, compression will be `no`, because it's the first obtained value.
94//!
95//! If we get rules for `172.168.1.1`, compression will be `yes`, because it's the first obtained value MATCHING the host rule.
96//!
97//! ```ssh
98//!
99//! Host 192.168.1.1
100//! Ciphers +c
101//! ```
102//!
103//! 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.
104//!
105//! ## Configuring default algorithms
106//!
107//! When you invoke [`SshConfig::default`], the default algorithms are set from openssh source code,
108//! which can be seen in the [`default_openssh_algorithms`] function documentation.
109//!
110//! If you want you can use a custom constructor [`SshConfig::default_algorithms`] to set your own default algorithms.
111
112#![doc(html_playground_url = "https://play.rust-lang.org")]
113
114#[macro_use]
115extern crate log;
116
117use std::fmt;
118use std::fs::File;
119use std::io::{self, BufRead, BufReader};
120use std::path::PathBuf;
121use std::time::Duration;
122// -- modules
123mod default_algorithms;
124mod host;
125mod params;
126mod parser;
127mod serializer;
128
129// -- export
130pub use self::default_algorithms::{
131 DefaultAlgorithms, default_algorithms as default_openssh_algorithms,
132};
133pub use self::host::{Host, HostClause};
134pub use self::params::{Algorithms, HostParams};
135pub use self::parser::{ParseRule, SshParserError, SshParserResult};
136
137/// Describes the ssh configuration.
138/// Configuration is described in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
139#[derive(Debug, Clone, PartialEq, Eq, Default)]
140pub struct SshConfig {
141 /// Default algorithms for ssh.
142 default_algorithms: DefaultAlgorithms,
143 /// Rulesets for hosts.
144 /// Default config will be stored with key `*`
145 hosts: Vec<Host>,
146}
147
148impl fmt::Display for SshConfig {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 serializer::SshConfigSerializer::from(self).serialize(f)
151 }
152}
153
154impl SshConfig {
155 /// Constructs a new [`SshConfig`] from a list of [`Host`]s.
156 ///
157 /// You can later also set the [`DefaultAlgorithms`] using [`SshConfig::default_algorithms`].
158 ///
159 /// ```rust
160 /// use ssh2_config::{DefaultAlgorithms, Host, SshConfig};
161 ///
162 /// let config = SshConfig::from_hosts(vec![/* put your hosts here */]).default_algorithms(DefaultAlgorithms::default());
163 /// ```
164 pub fn from_hosts(hosts: Vec<Host>) -> Self {
165 Self {
166 default_algorithms: DefaultAlgorithms::default(),
167 hosts,
168 }
169 }
170
171 /// Query params for a certain host. Returns [`HostParams`] for the host.
172 pub fn query<S: AsRef<str>>(&self, pattern: S) -> HostParams {
173 let mut params = HostParams::new(&self.default_algorithms);
174 // iter keys, overwrite if None top-down
175 for host in self.hosts.iter() {
176 if host.intersects(pattern.as_ref()) {
177 debug!(
178 "Merging params for host: {:?} into params {params:?}",
179 host.pattern
180 );
181 params.overwrite_if_none(&host.params);
182 trace!("Params after merge: {params:?}");
183 }
184 }
185 // return calculated params
186 params
187 }
188
189 /// Get an iterator over the [`Host`]s which intersect with the given host pattern
190 pub fn intersecting_hosts(&self, pattern: &str) -> impl Iterator<Item = &'_ Host> {
191 self.hosts.iter().filter(|host| host.intersects(pattern))
192 }
193
194 /// Set default algorithms for ssh.
195 ///
196 /// If you want to use the default algorithms from the system, you can use the `Default::default()` method.
197 pub fn default_algorithms(mut self, algos: DefaultAlgorithms) -> Self {
198 self.default_algorithms = algos;
199
200 self
201 }
202
203 /// Parse [`SshConfig`] from stream which implements [`BufRead`] and return parsed configuration or parser error
204 ///
205 /// ## Example
206 ///
207 /// ```rust,ignore
208 /// let mut reader = BufReader::new(
209 /// File::open(Path::new("./assets/ssh.config"))
210 /// .expect("Could not open configuration file")
211 /// );
212 ///
213 /// let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");
214 /// ```
215 pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult<Self> {
216 parser::SshConfigParser::parse(&mut self, reader, rules, None).map(|_| self)
217 }
218
219 /// Parse `~/.ssh/config`` file and return parsed configuration [`SshConfig`] or parser error
220 pub fn parse_default_file(rules: ParseRule) -> SshParserResult<Self> {
221 let ssh_folder = dirs::home_dir()
222 .ok_or_else(|| {
223 SshParserError::Io(io::Error::new(
224 io::ErrorKind::NotFound,
225 "Home folder not found",
226 ))
227 })?
228 .join(".ssh");
229
230 let mut reader =
231 BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?);
232
233 Self::default().parse(&mut reader, rules)
234 }
235
236 /// Get list of [`Host`]s in the configuration
237 pub fn get_hosts(&self) -> &Vec<Host> {
238 &self.hosts
239 }
240}
241
242#[cfg(test)]
243fn test_log() {
244 use std::sync::Once;
245
246 static INIT: Once = Once::new();
247
248 INIT.call_once(|| {
249 let _ = env_logger::builder()
250 .filter_level(log::LevelFilter::Trace)
251 .is_test(true)
252 .try_init();
253 });
254}
255
256#[cfg(test)]
257mod tests {
258
259 use pretty_assertions::assert_eq;
260
261 use super::*;
262
263 #[test]
264 fn should_init_ssh_config() {
265 test_log();
266
267 let config = SshConfig::default();
268 assert_eq!(config.hosts.len(), 0);
269 assert_eq!(
270 config.query("192.168.1.2"),
271 HostParams::new(&DefaultAlgorithms::default())
272 );
273 }
274
275 #[test]
276 fn should_parse_default_config() -> Result<(), parser::SshParserError> {
277 test_log();
278
279 let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?;
280 Ok(())
281 }
282
283 #[test]
284 fn should_parse_config() -> Result<(), parser::SshParserError> {
285 test_log();
286
287 use std::fs::File;
288 use std::io::BufReader;
289 use std::path::Path;
290
291 let mut reader = BufReader::new(
292 File::open(Path::new("./assets/ssh.config"))
293 .expect("Could not open configuration file"),
294 );
295
296 SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
297
298 Ok(())
299 }
300
301 #[test]
302 fn should_query_ssh_config() {
303 test_log();
304
305 let mut config = SshConfig::default();
306 // add config
307 let mut params1 = HostParams::new(&DefaultAlgorithms::default());
308 params1.bind_address = Some("0.0.0.0".to_string());
309 config.hosts.push(Host::new(
310 vec![HostClause::new(String::from("192.168.*.*"), false)],
311 params1.clone(),
312 ));
313 let mut params2 = HostParams::new(&DefaultAlgorithms::default());
314 params2.bind_interface = Some(String::from("tun0"));
315 config.hosts.push(Host::new(
316 vec![HostClause::new(String::from("192.168.10.*"), false)],
317 params2.clone(),
318 ));
319
320 let mut params3 = HostParams::new(&DefaultAlgorithms::default());
321 params3.host_name = Some("172.26.104.4".to_string());
322 config.hosts.push(Host::new(
323 vec![
324 HostClause::new(String::from("172.26.*.*"), false),
325 HostClause::new(String::from("172.26.104.4"), true),
326 ],
327 params3.clone(),
328 ));
329 // Query
330 assert_eq!(config.query("192.168.1.32"), params1);
331 // merged case
332 params1.overwrite_if_none(¶ms2);
333 assert_eq!(config.query("192.168.10.1"), params1);
334 // Negated case
335 assert_eq!(config.query("172.26.254.1"), params3);
336 assert_eq!(
337 config.query("172.26.104.4"),
338 HostParams::new(&DefaultAlgorithms::default())
339 );
340 }
341
342 #[test]
343 fn roundtrip() {
344 test_log();
345
346 // Root host
347 let mut default_host_params = HostParams::new(&DefaultAlgorithms::default());
348 default_host_params.add_keys_to_agent = Some(true);
349 let root_host_config = Host::new(
350 vec![HostClause::new(String::from("*"), false)],
351 default_host_params,
352 );
353
354 // A host using proxy jumps
355 let mut host_params = HostParams::new(&DefaultAlgorithms::default());
356 host_params.host_name = Some(String::from("192.168.10.1"));
357 host_params.proxy_jump = Some(vec![String::from("jump.example.com")]);
358 let host_config = Host::new(
359 vec![HostClause::new(String::from("server"), false)],
360 host_params,
361 );
362
363 // Create the overall config and serialise it
364 let config = SshConfig::from_hosts(vec![root_host_config, host_config]);
365 let config_string = config.to_string();
366
367 // Parse the serialised string
368 let mut reader = std::io::BufReader::new(config_string.as_bytes());
369 let config_parsed = SshConfig::default()
370 .parse(&mut reader, ParseRule::STRICT)
371 .expect("Could not parse config.");
372
373 assert_eq!(config, config_parsed);
374 }
375}