1#![crate_name = "ssh2_config"]
2#![crate_type = "lib"]
3
4#![doc(html_playground_url = "https://play.rust-lang.org")]
117
118#[macro_use]
119extern crate log;
120
121use std::fmt;
122use std::fs::File;
123use std::io::{self, BufRead, BufReader};
124use std::path::PathBuf;
125use std::time::Duration;
126mod default_algorithms;
128mod host;
129mod params;
130mod parser;
131mod serializer;
132
133pub use self::default_algorithms::{
135 DefaultAlgorithms, default_algorithms as default_openssh_algorithms,
136};
137pub use self::host::{Host, HostClause};
138pub use self::params::{Algorithms, HostParams};
139pub use self::parser::{ParseRule, SshParserError, SshParserResult};
140
141#[derive(Debug, Clone, PartialEq, Eq, Default)]
144pub struct SshConfig {
145 default_algorithms: DefaultAlgorithms,
147 hosts: Vec<Host>,
150}
151
152impl fmt::Display for SshConfig {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 serializer::SshConfigSerializer::from(self).serialize(f)
155 }
156}
157
158impl SshConfig {
159 pub fn from_hosts(hosts: Vec<Host>) -> Self {
169 Self {
170 default_algorithms: DefaultAlgorithms::default(),
171 hosts,
172 }
173 }
174
175 pub fn query<S: AsRef<str>>(&self, pattern: S) -> HostParams {
177 let mut params = HostParams::new(&self.default_algorithms);
178 for host in self.hosts.iter() {
180 if host.intersects(pattern.as_ref()) {
181 debug!(
182 "Merging params for host: {:?} into params {params:?}",
183 host.pattern
184 );
185 params.overwrite_if_none(&host.params);
186 trace!("Params after merge: {params:?}");
187 }
188 }
189 params
191 }
192
193 pub fn intersecting_hosts(&self, pattern: &str) -> impl Iterator<Item = &'_ Host> {
195 self.hosts.iter().filter(|host| host.intersects(pattern))
196 }
197
198 pub fn default_algorithms(mut self, algos: DefaultAlgorithms) -> Self {
202 self.default_algorithms = algos;
203
204 self
205 }
206
207 pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult<Self> {
220 parser::SshConfigParser::parse(&mut self, reader, rules, None).map(|_| self)
221 }
222
223 pub fn parse_default_file(rules: ParseRule) -> SshParserResult<Self> {
225 let ssh_folder = dirs::home_dir()
226 .ok_or_else(|| {
227 SshParserError::Io(io::Error::new(
228 io::ErrorKind::NotFound,
229 "Home folder not found",
230 ))
231 })?
232 .join(".ssh");
233
234 let mut reader =
235 BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?);
236
237 Self::default().parse(&mut reader, rules)
238 }
239
240 pub fn get_hosts(&self) -> &Vec<Host> {
242 &self.hosts
243 }
244}
245
246#[cfg(test)]
247fn test_log() {
248 use std::sync::Once;
249
250 static INIT: Once = Once::new();
251
252 INIT.call_once(|| {
253 let _ = env_logger::builder()
254 .filter_level(log::LevelFilter::Trace)
255 .is_test(true)
256 .try_init();
257 });
258}
259
260#[cfg(test)]
261mod tests {
262
263 use pretty_assertions::assert_eq;
264
265 use super::*;
266
267 #[test]
268 fn should_init_ssh_config() {
269 test_log();
270
271 let config = SshConfig::default();
272 assert_eq!(config.hosts.len(), 0);
273 assert_eq!(
274 config.query("192.168.1.2"),
275 HostParams::new(&DefaultAlgorithms::default())
276 );
277 }
278
279 #[test]
280 fn should_parse_default_config() -> Result<(), parser::SshParserError> {
281 test_log();
282
283 let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?;
284 Ok(())
285 }
286
287 #[test]
288 fn should_parse_config() -> Result<(), parser::SshParserError> {
289 test_log();
290
291 use std::fs::File;
292 use std::io::BufReader;
293 use std::path::Path;
294
295 let mut reader = BufReader::new(
296 File::open(Path::new("./assets/ssh.config"))
297 .expect("Could not open configuration file"),
298 );
299
300 SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
301
302 Ok(())
303 }
304
305 #[test]
306 fn should_query_ssh_config() {
307 test_log();
308
309 let mut config = SshConfig::default();
310 let mut params1 = HostParams::new(&DefaultAlgorithms::default());
312 params1.bind_address = Some("0.0.0.0".to_string());
313 config.hosts.push(Host::new(
314 vec![HostClause::new(String::from("192.168.*.*"), false)],
315 params1.clone(),
316 ));
317 let mut params2 = HostParams::new(&DefaultAlgorithms::default());
318 params2.bind_interface = Some(String::from("tun0"));
319 config.hosts.push(Host::new(
320 vec![HostClause::new(String::from("192.168.10.*"), false)],
321 params2.clone(),
322 ));
323
324 let mut params3 = HostParams::new(&DefaultAlgorithms::default());
325 params3.host_name = Some("172.26.104.4".to_string());
326 config.hosts.push(Host::new(
327 vec![
328 HostClause::new(String::from("172.26.*.*"), false),
329 HostClause::new(String::from("172.26.104.4"), true),
330 ],
331 params3.clone(),
332 ));
333 assert_eq!(config.query("192.168.1.32"), params1);
335 params1.overwrite_if_none(¶ms2);
337 assert_eq!(config.query("192.168.10.1"), params1);
338 assert_eq!(config.query("172.26.254.1"), params3);
340 assert_eq!(
341 config.query("172.26.104.4"),
342 HostParams::new(&DefaultAlgorithms::default())
343 );
344 }
345
346 #[test]
347 fn roundtrip() {
348 test_log();
349
350 let mut default_host_params = HostParams::new(&DefaultAlgorithms::default());
352 default_host_params.add_keys_to_agent = Some(true);
353 let root_host_config = Host::new(
354 vec![HostClause::new(String::from("*"), false)],
355 default_host_params,
356 );
357
358 let mut host_params = HostParams::new(&DefaultAlgorithms::default());
360 host_params.host_name = Some(String::from("192.168.10.1"));
361 host_params.proxy_jump = Some(vec![String::from("jump.example.com")]);
362 let host_config = Host::new(
363 vec![HostClause::new(String::from("server"), false)],
364 host_params,
365 );
366
367 let config = SshConfig::from_hosts(vec![root_host_config, host_config]);
369 let config_string = config.to_string();
370
371 let mut reader = std::io::BufReader::new(config_string.as_bytes());
373 let config_parsed = SshConfig::default()
374 .parse(&mut reader, ParseRule::STRICT)
375 .expect("Could not parse config.");
376
377 assert_eq!(config, config_parsed);
378 }
379
380 #[test]
381 fn should_get_intersecting_hosts() {
382 test_log();
383
384 let mut config = SshConfig::default();
385 let mut params1 = HostParams::new(&DefaultAlgorithms::default());
386 params1.bind_address = Some("0.0.0.0".to_string());
387 config.hosts.push(Host::new(
388 vec![HostClause::new(String::from("192.168.*.*"), false)],
389 params1,
390 ));
391 let mut params2 = HostParams::new(&DefaultAlgorithms::default());
392 params2.bind_interface = Some(String::from("tun0"));
393 config.hosts.push(Host::new(
394 vec![HostClause::new(String::from("192.168.10.*"), false)],
395 params2,
396 ));
397 let mut params3 = HostParams::new(&DefaultAlgorithms::default());
398 params3.host_name = Some("172.26.104.4".to_string());
399 config.hosts.push(Host::new(
400 vec![HostClause::new(String::from("172.26.*.*"), false)],
401 params3,
402 ));
403
404 let matching: Vec<_> = config.intersecting_hosts("192.168.10.1").collect();
406 assert_eq!(matching.len(), 2);
407
408 let matching: Vec<_> = config.intersecting_hosts("192.168.1.1").collect();
409 assert_eq!(matching.len(), 1);
410
411 let matching: Vec<_> = config.intersecting_hosts("172.26.0.1").collect();
412 assert_eq!(matching.len(), 1);
413
414 let matching: Vec<_> = config.intersecting_hosts("10.0.0.1").collect();
416 assert_eq!(matching.len(), 0);
417 }
418
419 #[test]
420 fn should_set_default_algorithms() {
421 test_log();
422
423 let custom_algos = DefaultAlgorithms {
424 ca_signature_algorithms: vec!["custom-algo".to_string()],
425 ciphers: vec!["custom-cipher".to_string()],
426 host_key_algorithms: vec!["custom-hostkey".to_string()],
427 kex_algorithms: vec!["custom-kex".to_string()],
428 mac: vec!["custom-mac".to_string()],
429 pubkey_accepted_algorithms: vec!["custom-pubkey".to_string()],
430 };
431
432 let config = SshConfig::default().default_algorithms(custom_algos.clone());
433
434 assert_eq!(config.default_algorithms, custom_algos);
435 }
436
437 #[test]
438 fn should_create_config_from_hosts() {
439 test_log();
440
441 let mut params = HostParams::new(&DefaultAlgorithms::default());
442 params.host_name = Some("example.com".to_string());
443 let host = Host::new(
444 vec![HostClause::new(String::from("example"), false)],
445 params,
446 );
447
448 let config = SshConfig::from_hosts(vec![host.clone()]);
449 assert_eq!(config.get_hosts().len(), 1);
450 assert_eq!(config.get_hosts()[0], host);
451 }
452
453 #[test]
454 fn should_query_empty_config() {
455 test_log();
456
457 let config = SshConfig::default();
458 let params = config.query("any-host");
459
460 assert!(params.host_name.is_none());
462 assert!(params.port.is_none());
463 }
464
465 #[test]
466 fn should_display_empty_config() {
467 test_log();
468
469 let config = SshConfig::default();
470 let output = config.to_string();
471 assert!(output.is_empty());
472 }
473}