1use std::fs::File;
6use std::io::{BufRead, BufReader, Error as IoError};
7use std::path::PathBuf;
8use std::str::FromStr;
9use std::time::Duration;
10
11use bitflags::bitflags;
12use glob::glob;
13use thiserror::Error;
14
15use super::{Host, HostClause, HostParams, SshConfig};
16
17mod field;
19use field::Field;
20
21pub type SshParserResult<T> = Result<T, SshParserError>;
22
23#[derive(Debug, Error)]
25pub enum SshParserError {
26 #[error("expected boolean value ('yes', 'no')")]
27 ExpectedBoolean,
28 #[error("expected port number")]
29 ExpectedPort,
30 #[error("expected unsigned value")]
31 ExpectedUnsigned,
32 #[error("expected path")]
33 ExpectedPath,
34 #[error("IO error: {0}")]
35 Io(#[from] IoError),
36 #[error("glob error: {0}")]
37 Glob(#[from] glob::GlobError),
38 #[error("missing argument")]
39 MissingArgument,
40 #[error("pattern error: {0}")]
41 PatternError(#[from] glob::PatternError),
42 #[error("unknown field: {0}")]
43 UnknownField(String, Vec<String>),
44 #[error("unknown field: {0}")]
45 UnsupportedField(String, Vec<String>),
46}
47
48bitflags! {
49 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51 pub struct ParseRule: u8 {
52 const STRICT = 0b00000000;
54 const ALLOW_UNKNOWN_FIELDS = 0b00000001;
56 const ALLOW_UNSUPPORTED_FIELDS = 0b00000010;
58 }
59}
60
61pub struct SshConfigParser;
65
66impl SshConfigParser {
67 pub fn parse(
69 config: &mut SshConfig,
70 reader: &mut impl BufRead,
71 rules: ParseRule,
72 ) -> SshParserResult<()> {
73 config.hosts.push(Host::new(
79 vec![HostClause::new(String::from("*"), false)],
80 HostParams::default(),
81 ));
82
83 let mut current_host = config.hosts.last_mut().unwrap();
85
86 let mut lines = reader.lines();
87 loop {
89 let line = match lines.next() {
90 None => break,
91 Some(Err(err)) => return Err(SshParserError::Io(err)),
92 Some(Ok(line)) => Self::strip_comments(line.trim()),
93 };
94 if line.is_empty() {
95 continue;
96 }
97 let (field, args) = match Self::tokenize(&line) {
99 Ok((field, args)) => (field, args),
100 Err(SshParserError::UnknownField(field, args))
101 if rules.intersects(ParseRule::ALLOW_UNKNOWN_FIELDS)
102 || current_host.params.ignored(&field) =>
103 {
104 current_host.params.ignored_fields.insert(field, args);
105 continue;
106 }
107 Err(SshParserError::UnknownField(field, args)) => {
108 return Err(SshParserError::UnknownField(field, args));
109 }
110 Err(err) => return Err(err),
111 };
112 if field == Field::Host {
114 let mut params = HostParams::default();
116 params.ignore_unknown = config.hosts[0].params.ignore_unknown.clone();
117 let pattern = Self::parse_host(args)?;
118 trace!("Adding new host: {pattern:?}",);
119
120 if let Some(existing) = config.hosts.iter_mut().find(|x| x.pattern == pattern) {
122 current_host = existing;
123 } else {
124 config.hosts.push(Host::new(pattern, params));
126 current_host = config.hosts.last_mut().unwrap();
128 }
129 } else {
130 match Self::update_host(field, args, current_host, rules) {
132 Ok(()) => Ok(()),
133 Err(SshParserError::UnsupportedField(field, args))
135 if rules.intersects(ParseRule::ALLOW_UNSUPPORTED_FIELDS) =>
136 {
137 current_host.params.unsupported_fields.insert(field, args);
138 Ok(())
139 }
140 Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
144 e => e,
145 }?;
146 }
147 }
148
149 Ok(())
150 }
151
152 fn strip_comments(s: &str) -> String {
154 if let Some(pos) = s.find('#') {
155 s[..pos].to_string()
156 } else {
157 s.to_string()
158 }
159 }
160
161 fn update_host(
163 field: Field,
164 args: Vec<String>,
165 host: &mut Host,
166 rules: ParseRule,
167 ) -> SshParserResult<()> {
168 trace!("parsing field {field:?} with args {args:?}",);
169 let params = &mut host.params;
170 match field {
171 Field::BindAddress => {
172 let value = Self::parse_string(args)?;
173 trace!("bind_address: {value}",);
174 params.bind_address = Some(value);
175 }
176 Field::BindInterface => {
177 let value = Self::parse_string(args)?;
178 trace!("bind_interface: {value}",);
179 params.bind_interface = Some(value);
180 }
181 Field::CaSignatureAlgorithms => {
182 let value = Self::parse_comma_separated_list(args)?;
183 trace!("ca_signature_algorithms: {value:?}",);
184 params.ca_signature_algorithms = Some(value);
185 }
186 Field::CertificateFile => {
187 let value = Self::parse_path(args)?;
188 trace!("certificate_file: {value:?}",);
189 params.certificate_file = Some(value);
190 }
191 Field::Ciphers => {
192 let value = Self::parse_comma_separated_list(args)?;
193 trace!("ciphers: {value:?}",);
194 params.ciphers = Some(value);
195 }
196 Field::Compression => {
197 let value = Self::parse_boolean(args)?;
198 trace!("compression: {value}",);
199 params.compression = Some(value);
200 }
201 Field::ConnectTimeout => {
202 let value = Self::parse_duration(args)?;
203 trace!("connect_timeout: {value:?}",);
204 params.connect_timeout = Some(value);
205 }
206 Field::ConnectionAttempts => {
207 let value = Self::parse_unsigned(args)?;
208 trace!("connection_attempts: {value}",);
209 params.connection_attempts = Some(value);
210 }
211 Field::Host => { }
212 Field::HostKeyAlgorithms => {
213 let value = Self::parse_comma_separated_list(args)?;
214 trace!("host_key_algorithm: {value:?}",);
215 params.host_key_algorithms = Some(value);
216 }
217 Field::HostName => {
218 let value = Self::parse_string(args)?;
219 trace!("host_name: {value}",);
220 params.host_name = Some(value);
221 }
222 Field::Include => {
223 Self::include_files(args, host, rules)?;
224 }
225 Field::IdentityFile => {
226 let value = Self::parse_path_list(args)?;
227 trace!("identity_file: {value:?}",);
228 params.identity_file = Some(value);
229 }
230 Field::IgnoreUnknown => {
231 let value = Self::parse_comma_separated_list(args)?;
232 trace!("ignore_unknown: {value:?}",);
233 params.ignore_unknown = Some(value);
234 }
235 Field::KexAlgorithms => {
236 let value = Self::parse_comma_separated_list(args)?;
237 trace!("kex_algorithms: {value:?}",);
238 params.kex_algorithms = Some(value);
239 }
240 Field::Mac => {
241 let value = Self::parse_comma_separated_list(args)?;
242 trace!("mac: {value:?}",);
243 params.mac = Some(value);
244 }
245 Field::Port => {
246 let value = Self::parse_port(args)?;
247 trace!("port: {value}",);
248 params.port = Some(value);
249 }
250 Field::PubkeyAcceptedAlgorithms => {
251 let value = Self::parse_comma_separated_list(args)?;
252 trace!("pubkey_accepted_algorithms: {value:?}",);
253 params.pubkey_accepted_algorithms = Some(value);
254 }
255 Field::PubkeyAuthentication => {
256 let value = Self::parse_boolean(args)?;
257 trace!("pubkey_authentication: {value}",);
258 params.pubkey_authentication = Some(value);
259 }
260 Field::RemoteForward => {
261 let value = Self::parse_port(args)?;
262 trace!("remote_forward: {value}",);
263 params.remote_forward = Some(value);
264 }
265 Field::ServerAliveInterval => {
266 let value = Self::parse_duration(args)?;
267 trace!("server_alive_interval: {value:?}",);
268 params.server_alive_interval = Some(value);
269 }
270 Field::TcpKeepAlive => {
271 let value = Self::parse_boolean(args)?;
272 trace!("tcp_keep_alive: {value}",);
273 params.tcp_keep_alive = Some(value);
274 }
275 #[cfg(target_os = "macos")]
276 Field::UseKeychain => {
277 let value = Self::parse_boolean(args)?;
278 trace!("use_keychain: {value}",);
279 params.use_keychain = Some(value);
280 }
281 Field::User => {
282 let value = Self::parse_string(args)?;
283 trace!("user: {value}",);
284 params.user = Some(value);
285 }
286 Field::AddKeysToAgent
288 | Field::AddressFamily
289 | Field::BatchMode
290 | Field::CanonicalDomains
291 | Field::CanonicalizeFallbackLock
292 | Field::CanonicalizeHostname
293 | Field::CanonicalizeMaxDots
294 | Field::CanonicalizePermittedCNAMEs
295 | Field::CheckHostIP
296 | Field::ClearAllForwardings
297 | Field::ControlMaster
298 | Field::ControlPath
299 | Field::ControlPersist
300 | Field::DynamicForward
301 | Field::EnableSSHKeysign
302 | Field::EscapeChar
303 | Field::ExitOnForwardFailure
304 | Field::FingerprintHash
305 | Field::ForkAfterAuthentication
306 | Field::ForwardAgent
307 | Field::ForwardX11
308 | Field::ForwardX11Timeout
309 | Field::ForwardX11Trusted
310 | Field::GatewayPorts
311 | Field::GlobalKnownHostsFile
312 | Field::GSSAPIAuthentication
313 | Field::GSSAPIDelegateCredentials
314 | Field::HashKnownHosts
315 | Field::HostbasedAcceptedAlgorithms
316 | Field::HostbasedAuthentication
317 | Field::HostKeyAlias
318 | Field::HostbasedKeyTypes
319 | Field::IdentitiesOnly
320 | Field::IdentityAgent
321 | Field::IPQoS
322 | Field::KbdInteractiveAuthentication
323 | Field::KbdInteractiveDevices
324 | Field::KnownHostsCommand
325 | Field::LocalCommand
326 | Field::LocalForward
327 | Field::LogLevel
328 | Field::LogVerbose
329 | Field::NoHostAuthenticationForLocalhost
330 | Field::NumberOfPasswordPrompts
331 | Field::PasswordAuthentication
332 | Field::PermitLocalCommand
333 | Field::PermitRemoteOpen
334 | Field::PKCS11Provider
335 | Field::PreferredAuthentications
336 | Field::ProxyCommand
337 | Field::ProxyJump
338 | Field::ProxyUseFdpass
339 | Field::PubkeyAcceptedKeyTypes
340 | Field::RekeyLimit
341 | Field::RequestTTY
342 | Field::RevokedHostKeys
343 | Field::SecruityKeyProvider
344 | Field::SendEnv
345 | Field::ServerAliveCountMax
346 | Field::SessionType
347 | Field::SetEnv
348 | Field::StdinNull
349 | Field::StreamLocalBindMask
350 | Field::StrictHostKeyChecking
351 | Field::SyslogFacility
352 | Field::UpdateHostKeys
353 | Field::UserKnownHostsFile
354 | Field::VerifyHostKeyDNS
355 | Field::VisualHostKey
356 | Field::XAuthLocation => {
357 return Err(SshParserError::UnsupportedField(field.to_string(), args));
358 }
359 }
360 Ok(())
361 }
362
363 fn include_files(args: Vec<String>, host: &mut Host, rules: ParseRule) -> SshParserResult<()> {
365 let path_match = Self::parse_string(args)?;
366 trace!("include files: {path_match}",);
367 let files = glob(&path_match)?;
368
369 for file in files {
370 let file = file?;
371 trace!("including file: {}", file.display());
372 let mut reader = BufReader::new(File::open(file)?);
373 let mut sub_config = SshConfig::default();
374 Self::parse(&mut sub_config, &mut reader, rules)?;
375
376 for pattern in &host.pattern {
378 if pattern.negated {
379 trace!("excluding sub-config for pattern: {pattern:?}",);
380 continue;
381 }
382 trace!("merging sub-config for pattern: {pattern:?}",);
383 let params = sub_config.query(&pattern.pattern);
384 host.params.merge(¶ms);
385 }
386 }
387
388 Ok(())
389 }
390
391 fn tokenize(line: &str) -> SshParserResult<(Field, Vec<String>)> {
393 let mut tokens = line.split_whitespace();
394 let field = match tokens.next().map(Field::from_str) {
395 Some(Ok(field)) => field,
396 Some(Err(field)) => {
397 return Err(SshParserError::UnknownField(
398 field,
399 tokens.map(|x| x.to_string()).collect(),
400 ));
401 }
402 None => return Err(SshParserError::MissingArgument),
403 };
404 let args = tokens
405 .map(|x| x.trim().to_string())
406 .filter(|x| !x.is_empty())
407 .collect();
408 Ok((field, args))
409 }
410
411 fn parse_boolean(args: Vec<String>) -> SshParserResult<bool> {
415 match args.first().map(|x| x.as_str()) {
416 Some("yes") => Ok(true),
417 Some("no") => Ok(false),
418 Some(_) => Err(SshParserError::ExpectedBoolean),
419 None => Err(SshParserError::MissingArgument),
420 }
421 }
422
423 fn parse_comma_separated_list(args: Vec<String>) -> SshParserResult<Vec<String>> {
425 match args
426 .first()
427 .map(|x| x.split(',').map(|x| x.to_string()).collect())
428 {
429 Some(args) => Ok(args),
430 _ => Err(SshParserError::MissingArgument),
431 }
432 }
433
434 fn parse_duration(args: Vec<String>) -> SshParserResult<Duration> {
436 let value = Self::parse_unsigned(args)?;
437 Ok(Duration::from_secs(value as u64))
438 }
439
440 fn parse_host(args: Vec<String>) -> SshParserResult<Vec<HostClause>> {
442 if args.is_empty() {
443 return Err(SshParserError::MissingArgument);
444 }
445 Ok(args
447 .into_iter()
448 .map(|x| {
449 let tokens: Vec<&str> = x.split('!').collect();
450 if tokens.len() == 2 {
451 HostClause::new(tokens[1].to_string(), true)
452 } else {
453 HostClause::new(tokens[0].to_string(), false)
454 }
455 })
456 .collect())
457 }
458
459 fn parse_path_list(args: Vec<String>) -> SshParserResult<Vec<PathBuf>> {
461 if args.is_empty() {
462 return Err(SshParserError::MissingArgument);
463 }
464 args.iter()
465 .map(|x| Self::parse_path_arg(x.as_str()))
466 .collect()
467 }
468
469 fn parse_path(args: Vec<String>) -> SshParserResult<PathBuf> {
471 if let Some(s) = args.first() {
472 Self::parse_path_arg(s)
473 } else {
474 Err(SshParserError::MissingArgument)
475 }
476 }
477
478 fn parse_path_arg(s: &str) -> SshParserResult<PathBuf> {
480 let s = if s.starts_with('~') {
482 let home_dir = dirs::home_dir()
483 .unwrap_or_else(|| PathBuf::from("~"))
484 .to_string_lossy()
485 .to_string();
486 s.replacen('~', &home_dir, 1)
487 } else {
488 s.to_string()
489 };
490 Ok(PathBuf::from(s))
491 }
492
493 fn parse_port(args: Vec<String>) -> SshParserResult<u16> {
495 match args.first().map(|x| u16::from_str(x)) {
496 Some(Ok(val)) => Ok(val),
497 Some(Err(_)) => Err(SshParserError::ExpectedPort),
498 None => Err(SshParserError::MissingArgument),
499 }
500 }
501
502 fn parse_string(args: Vec<String>) -> SshParserResult<String> {
504 if let Some(s) = args.into_iter().next() {
505 Ok(s)
506 } else {
507 Err(SshParserError::MissingArgument)
508 }
509 }
510
511 fn parse_unsigned(args: Vec<String>) -> SshParserResult<usize> {
513 match args.first().map(|x| usize::from_str(x)) {
514 Some(Ok(val)) => Ok(val),
515 Some(Err(_)) => Err(SshParserError::ExpectedUnsigned),
516 None => Err(SshParserError::MissingArgument),
517 }
518 }
519}
520
521#[cfg(test)]
522mod test {
523
524 use std::fs::File;
525 use std::io::{BufReader, Write};
526 use std::path::Path;
527
528 use pretty_assertions::assert_eq;
529 use tempfile::NamedTempFile;
530
531 use super::*;
532
533 #[test]
534 fn should_parse_configuration() -> Result<(), SshParserError> {
535 crate::test_log();
536 let temp = create_ssh_config();
537 let file = File::open(temp.path()).expect("Failed to open tempfile");
538 let mut reader = BufReader::new(file);
539 let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
540
541 let params = config.query("*");
544 assert_eq!(
545 params.ignore_unknown.as_deref().unwrap(),
546 &["Pippo", "Pluto"]
547 );
548 assert_eq!(params.compression.unwrap(), true);
549 assert_eq!(params.connection_attempts.unwrap(), 10);
550 assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
551 assert_eq!(
552 params.server_alive_interval.unwrap(),
553 Duration::from_secs(40)
554 );
555 assert_eq!(params.tcp_keep_alive.unwrap(), true);
556 assert_eq!(
557 params.ciphers.as_deref().unwrap(),
558 &["a-manella", "blowfish"]
559 );
560 assert_eq!(
561 params.pubkey_accepted_algorithms.as_deref().unwrap(),
562 &["desu", "omar-crypt", "fast-omar-crypt"]
563 );
564
565 assert_eq!(
567 params.ca_signature_algorithms.as_deref().unwrap(),
568 &["random"]
569 );
570 assert_eq!(
571 params.host_key_algorithms.as_deref().unwrap(),
572 &["luigi", "mario",]
573 );
574 assert_eq!(
575 params.kex_algorithms.as_deref().unwrap(),
576 &["desu", "gigi",]
577 );
578 assert_eq!(params.mac.as_deref().unwrap(), &["concorde"]);
579 assert!(params.bind_address.is_none());
580
581 let params = config.query("172.26.104.4");
585
586 assert_eq!(params.compression.unwrap(), true);
588 assert_eq!(params.connection_attempts.unwrap(), 10);
589 assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
590 assert_eq!(params.tcp_keep_alive.unwrap(), true);
591
592 assert_eq!(
594 params.ca_signature_algorithms.as_deref().unwrap(),
595 &["random"]
596 );
597 assert_eq!(
598 params.ciphers.as_deref().unwrap(),
599 &[
600 "a-manella",
601 "blowfish",
602 "coi-piedi",
603 "cazdecan",
604 "triestin-stretto",
605 ]
606 );
607 assert_eq!(params.mac.as_deref().unwrap(), &["spyro", "deoxys"]);
608 assert_eq!(
609 params.pubkey_accepted_algorithms.as_deref().unwrap(),
610 &["desu", "fast-omar-crypt"]
611 );
612 assert_eq!(params.bind_address.as_deref().unwrap(), "10.8.0.10");
613 assert_eq!(params.bind_interface.as_deref().unwrap(), "tun0");
614 assert_eq!(params.port.unwrap(), 2222);
615 assert_eq!(
616 params.identity_file.as_deref().unwrap(),
617 vec![
618 Path::new("/home/root/.ssh/pippo.key"),
619 Path::new("/home/root/.ssh/pluto.key")
620 ]
621 );
622 assert_eq!(params.user.as_deref().unwrap(), "omar");
623
624 let params = config.query("tostapane");
626 assert_eq!(params.compression.unwrap(), false);
627 assert_eq!(params.connection_attempts.unwrap(), 10);
628 assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
629 assert_eq!(params.tcp_keep_alive.unwrap(), true);
630 assert_eq!(params.remote_forward.unwrap(), 88);
631 assert_eq!(params.user.as_deref().unwrap(), "ciro-esposito");
632
633 assert_eq!(
635 params.ca_signature_algorithms.as_deref().unwrap(),
636 &["random"]
637 );
638 assert_eq!(
639 params.ciphers.as_deref().unwrap(),
640 &["a-manella", "blowfish",]
641 );
642 assert_eq!(params.mac.as_deref().unwrap(), &["concorde"]);
643 assert_eq!(
644 params.pubkey_accepted_algorithms.as_deref().unwrap(),
645 &["desu", "omar-crypt", "fast-omar-crypt"]
646 );
647
648 let params = config.query("192.168.1.30");
650
651 assert_eq!(params.user.as_deref().unwrap(), "nutellaro");
653 assert_eq!(params.remote_forward.unwrap(), 123);
654
655 assert_eq!(params.compression.unwrap(), true);
657 assert_eq!(params.connection_attempts.unwrap(), 10);
658 assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
659 assert_eq!(params.tcp_keep_alive.unwrap(), true);
660
661 assert_eq!(
663 params.ca_signature_algorithms.as_deref().unwrap(),
664 &["random"]
665 );
666 assert_eq!(
667 params.ciphers.as_deref().unwrap(),
668 &["a-manella", "blowfish"]
669 );
670 assert_eq!(params.mac.as_deref().unwrap(), &["concorde"]);
671 assert_eq!(
672 params.pubkey_accepted_algorithms.as_deref().unwrap(),
673 &["desu", "omar-crypt", "fast-omar-crypt"]
674 );
675
676 Ok(())
677 }
678
679 #[test]
680 fn should_allow_unknown_field() -> Result<(), SshParserError> {
681 crate::test_log();
682 let temp = create_ssh_config_with_unknown_fields();
683 let file = File::open(temp.path()).expect("Failed to open tempfile");
684 let mut reader = BufReader::new(file);
685 let _config = SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)?;
686
687 Ok(())
688 }
689
690 #[test]
691 fn should_not_allow_unknown_field() {
692 crate::test_log();
693 let temp = create_ssh_config_with_unknown_fields();
694 let file = File::open(temp.path()).expect("Failed to open tempfile");
695 let mut reader = BufReader::new(file);
696 assert!(matches!(
697 SshConfig::default()
698 .parse(&mut reader, ParseRule::STRICT)
699 .unwrap_err(),
700 SshParserError::UnknownField(..)
701 ));
702 }
703
704 #[test]
705 fn should_store_unknown_fields() {
706 crate::test_log();
707 let temp = create_ssh_config_with_unknown_fields();
708 let file = File::open(temp.path()).expect("Failed to open tempfile");
709 let mut reader = BufReader::new(file);
710 let config = SshConfig::default()
711 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
712 .unwrap();
713
714 let host = config.query("cross-platform");
715 assert_eq!(
716 host.ignored_fields.get("Piropero").unwrap(),
717 &vec![String::from("yes")]
718 );
719 }
720
721 #[test]
722 fn should_parse_inversed_ssh_config() {
723 crate::test_log();
724 let temp = create_inverted_ssh_config();
725 let file = File::open(temp.path()).expect("Failed to open tempfile");
726 let mut reader = BufReader::new(file);
727 let config = SshConfig::default()
728 .parse(&mut reader, ParseRule::STRICT)
729 .unwrap();
730
731 let home_dir = dirs::home_dir()
732 .unwrap_or_else(|| PathBuf::from("~"))
733 .to_string_lossy()
734 .to_string();
735
736 let host_params = config.query("remote-host");
737
738 assert_eq!(
740 host_params.identity_file.unwrap()[0].as_path(),
741 Path::new(format!("{home_dir}/.ssh/id_rsa_bad").as_str())
742 );
743
744 assert_eq!(host_params.host_name.unwrap(), "hostname.com");
746 assert_eq!(host_params.user.unwrap(), "user");
747
748 assert_eq!(
750 host_params.connect_timeout.unwrap(),
751 Duration::from_secs(15)
752 );
753 }
754
755 #[test]
756 fn should_parse_configuration_with_hosts() {
757 crate::test_log();
758 let temp = create_ssh_config_with_comments();
759
760 let file = File::open(temp.path()).expect("Failed to open tempfile");
761 let mut reader = BufReader::new(file);
762 let config = SshConfig::default()
763 .parse(&mut reader, ParseRule::STRICT)
764 .unwrap();
765
766 let hostname = config.query("cross-platform").host_name.unwrap();
767 assert_eq!(&hostname, "hostname.com");
768
769 assert!(config.query("this").host_name.is_none());
770 }
771
772 #[test]
773 fn should_update_host_bind_address() -> Result<(), SshParserError> {
774 crate::test_log();
775 let mut host = Host::new(vec![], HostParams::default());
776 SshConfigParser::update_host(
777 Field::BindAddress,
778 vec![String::from("127.0.0.1")],
779 &mut host,
780 ParseRule::ALLOW_UNKNOWN_FIELDS,
781 )?;
782 assert_eq!(host.params.bind_address.as_deref().unwrap(), "127.0.0.1");
783 Ok(())
784 }
785
786 #[test]
787 fn should_update_host_bind_interface() -> Result<(), SshParserError> {
788 crate::test_log();
789 let mut host = Host::new(vec![], HostParams::default());
790 SshConfigParser::update_host(
791 Field::BindInterface,
792 vec![String::from("aaa")],
793 &mut host,
794 ParseRule::ALLOW_UNKNOWN_FIELDS,
795 )?;
796 assert_eq!(host.params.bind_interface.as_deref().unwrap(), "aaa");
797 Ok(())
798 }
799
800 #[test]
801 fn should_update_host_ca_signature_algos() -> Result<(), SshParserError> {
802 crate::test_log();
803 let mut host = Host::new(vec![], HostParams::default());
804 SshConfigParser::update_host(
805 Field::CaSignatureAlgorithms,
806 vec![String::from("a,b,c")],
807 &mut host,
808 ParseRule::ALLOW_UNKNOWN_FIELDS,
809 )?;
810 assert_eq!(
811 host.params.ca_signature_algorithms.as_deref().unwrap(),
812 &["a", "b", "c"]
813 );
814 Ok(())
815 }
816
817 #[test]
818 fn should_update_host_certificate_file() -> Result<(), SshParserError> {
819 crate::test_log();
820 let mut host = Host::new(vec![], HostParams::default());
821 SshConfigParser::update_host(
822 Field::CertificateFile,
823 vec![String::from("/tmp/a.crt")],
824 &mut host,
825 ParseRule::ALLOW_UNKNOWN_FIELDS,
826 )?;
827 assert_eq!(
828 host.params.certificate_file.as_deref().unwrap(),
829 Path::new("/tmp/a.crt")
830 );
831 Ok(())
832 }
833
834 #[test]
835 fn should_update_host_ciphers() -> Result<(), SshParserError> {
836 crate::test_log();
837 let mut host = Host::new(vec![], HostParams::default());
838 SshConfigParser::update_host(
839 Field::Ciphers,
840 vec![String::from("a,b,c")],
841 &mut host,
842 ParseRule::ALLOW_UNKNOWN_FIELDS,
843 )?;
844 assert_eq!(host.params.ciphers.as_deref().unwrap(), &["a", "b", "c"]);
845 Ok(())
846 }
847
848 #[test]
849 fn should_update_host_compression() -> Result<(), SshParserError> {
850 crate::test_log();
851 let mut host = Host::new(vec![], HostParams::default());
852 SshConfigParser::update_host(
853 Field::Compression,
854 vec![String::from("yes")],
855 &mut host,
856 ParseRule::ALLOW_UNKNOWN_FIELDS,
857 )?;
858 assert_eq!(host.params.compression.unwrap(), true);
859 Ok(())
860 }
861
862 #[test]
863 fn should_update_host_connection_attempts() -> Result<(), SshParserError> {
864 crate::test_log();
865 let mut host = Host::new(vec![], HostParams::default());
866 SshConfigParser::update_host(
867 Field::ConnectionAttempts,
868 vec![String::from("4")],
869 &mut host,
870 ParseRule::ALLOW_UNKNOWN_FIELDS,
871 )?;
872 assert_eq!(host.params.connection_attempts.unwrap(), 4);
873 Ok(())
874 }
875
876 #[test]
877 fn should_update_host_connection_timeout() -> Result<(), SshParserError> {
878 crate::test_log();
879 let mut host = Host::new(vec![], HostParams::default());
880 SshConfigParser::update_host(
881 Field::ConnectTimeout,
882 vec![String::from("10")],
883 &mut host,
884 ParseRule::ALLOW_UNKNOWN_FIELDS,
885 )?;
886 assert_eq!(
887 host.params.connect_timeout.unwrap(),
888 Duration::from_secs(10)
889 );
890 Ok(())
891 }
892
893 #[test]
894 fn should_update_host_key_algorithms() -> Result<(), SshParserError> {
895 crate::test_log();
896 let mut host = Host::new(vec![], HostParams::default());
897 SshConfigParser::update_host(
898 Field::HostKeyAlgorithms,
899 vec![String::from("a,b,c")],
900 &mut host,
901 ParseRule::ALLOW_UNKNOWN_FIELDS,
902 )?;
903 assert_eq!(
904 host.params.host_key_algorithms.as_deref().unwrap(),
905 &["a", "b", "c"]
906 );
907 Ok(())
908 }
909
910 #[test]
911 fn should_update_host_host_name() -> Result<(), SshParserError> {
912 crate::test_log();
913 let mut host = Host::new(vec![], HostParams::default());
914 SshConfigParser::update_host(
915 Field::HostName,
916 vec![String::from("192.168.1.1")],
917 &mut host,
918 ParseRule::ALLOW_UNKNOWN_FIELDS,
919 )?;
920 assert_eq!(host.params.host_name.as_deref().unwrap(), "192.168.1.1");
921 Ok(())
922 }
923
924 #[test]
925 fn should_update_host_ignore_unknown() -> Result<(), SshParserError> {
926 crate::test_log();
927 let mut host = Host::new(vec![], HostParams::default());
928 SshConfigParser::update_host(
929 Field::IgnoreUnknown,
930 vec![String::from("a,b,c")],
931 &mut host,
932 ParseRule::ALLOW_UNKNOWN_FIELDS,
933 )?;
934 assert_eq!(
935 host.params.ignore_unknown.as_deref().unwrap(),
936 &["a", "b", "c"]
937 );
938 Ok(())
939 }
940
941 #[test]
942 fn should_update_kex_algorithms() -> Result<(), SshParserError> {
943 crate::test_log();
944 let mut host = Host::new(vec![], HostParams::default());
945 SshConfigParser::update_host(
946 Field::KexAlgorithms,
947 vec![String::from("a,b,c")],
948 &mut host,
949 ParseRule::ALLOW_UNKNOWN_FIELDS,
950 )?;
951 assert_eq!(
952 host.params.kex_algorithms.as_deref().unwrap(),
953 &["a", "b", "c"]
954 );
955 Ok(())
956 }
957
958 #[test]
959 fn should_update_host_mac() -> Result<(), SshParserError> {
960 crate::test_log();
961 let mut host = Host::new(vec![], HostParams::default());
962 SshConfigParser::update_host(
963 Field::Mac,
964 vec![String::from("a,b,c")],
965 &mut host,
966 ParseRule::ALLOW_UNKNOWN_FIELDS,
967 )?;
968 assert_eq!(host.params.mac.as_deref().unwrap(), &["a", "b", "c"]);
969 Ok(())
970 }
971
972 #[test]
973 fn should_update_host_port() -> Result<(), SshParserError> {
974 crate::test_log();
975 let mut host = Host::new(vec![], HostParams::default());
976 SshConfigParser::update_host(
977 Field::Port,
978 vec![String::from("2222")],
979 &mut host,
980 ParseRule::ALLOW_UNKNOWN_FIELDS,
981 )?;
982 assert_eq!(host.params.port.unwrap(), 2222);
983 Ok(())
984 }
985
986 #[test]
987 fn should_update_host_pubkey_accepted_algos() -> Result<(), SshParserError> {
988 crate::test_log();
989 let mut host = Host::new(vec![], HostParams::default());
990 SshConfigParser::update_host(
991 Field::PubkeyAcceptedAlgorithms,
992 vec![String::from("a,b,c")],
993 &mut host,
994 ParseRule::ALLOW_UNKNOWN_FIELDS,
995 )?;
996 assert_eq!(
997 host.params.pubkey_accepted_algorithms.as_deref().unwrap(),
998 &["a", "b", "c"]
999 );
1000 Ok(())
1001 }
1002
1003 #[test]
1004 fn should_update_host_pubkey_authentication() -> Result<(), SshParserError> {
1005 crate::test_log();
1006 let mut host = Host::new(vec![], HostParams::default());
1007 SshConfigParser::update_host(
1008 Field::PubkeyAuthentication,
1009 vec![String::from("yes")],
1010 &mut host,
1011 ParseRule::ALLOW_UNKNOWN_FIELDS,
1012 )?;
1013 assert_eq!(host.params.pubkey_authentication.unwrap(), true);
1014 Ok(())
1015 }
1016
1017 #[test]
1018 fn should_update_host_remote_forward() -> Result<(), SshParserError> {
1019 crate::test_log();
1020 let mut host = Host::new(vec![], HostParams::default());
1021 SshConfigParser::update_host(
1022 Field::RemoteForward,
1023 vec![String::from("3005")],
1024 &mut host,
1025 ParseRule::ALLOW_UNKNOWN_FIELDS,
1026 )?;
1027 assert_eq!(host.params.remote_forward.unwrap(), 3005);
1028 Ok(())
1029 }
1030
1031 #[test]
1032 fn should_update_host_server_alive_interval() -> Result<(), SshParserError> {
1033 crate::test_log();
1034 let mut host = Host::new(vec![], HostParams::default());
1035 SshConfigParser::update_host(
1036 Field::ServerAliveInterval,
1037 vec![String::from("40")],
1038 &mut host,
1039 ParseRule::ALLOW_UNKNOWN_FIELDS,
1040 )?;
1041 assert_eq!(
1042 host.params.server_alive_interval.unwrap(),
1043 Duration::from_secs(40)
1044 );
1045 Ok(())
1046 }
1047
1048 #[test]
1049 fn should_update_host_tcp_keep_alive() -> Result<(), SshParserError> {
1050 crate::test_log();
1051 let mut host = Host::new(vec![], HostParams::default());
1052 SshConfigParser::update_host(
1053 Field::TcpKeepAlive,
1054 vec![String::from("no")],
1055 &mut host,
1056 ParseRule::ALLOW_UNKNOWN_FIELDS,
1057 )?;
1058 assert_eq!(host.params.tcp_keep_alive.unwrap(), false);
1059 Ok(())
1060 }
1061
1062 #[test]
1063 fn should_update_host_user() -> Result<(), SshParserError> {
1064 crate::test_log();
1065 let mut host = Host::new(vec![], HostParams::default());
1066 SshConfigParser::update_host(
1067 Field::User,
1068 vec![String::from("pippo")],
1069 &mut host,
1070 ParseRule::ALLOW_UNKNOWN_FIELDS,
1071 )?;
1072 assert_eq!(host.params.user.as_deref().unwrap(), "pippo");
1073 Ok(())
1074 }
1075
1076 #[test]
1077 fn should_not_update_host_if_unknown() -> Result<(), SshParserError> {
1078 crate::test_log();
1079 let mut host = Host::new(vec![], HostParams::default());
1080 let result = SshConfigParser::update_host(
1081 Field::AddKeysToAgent,
1082 vec![String::from("yes")],
1083 &mut host,
1084 ParseRule::ALLOW_UNKNOWN_FIELDS,
1085 );
1086
1087 match result {
1088 Ok(()) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
1089 e => e,
1090 }?;
1091
1092 assert_eq!(host.params, HostParams::default());
1093 Ok(())
1094 }
1095
1096 #[test]
1097 fn should_update_host_if_unsupported() -> Result<(), SshParserError> {
1098 crate::test_log();
1099 let mut host = Host::new(vec![], HostParams::default());
1100 let result = SshConfigParser::update_host(
1101 Field::AddKeysToAgent,
1102 vec![String::from("yes")],
1103 &mut host,
1104 ParseRule::ALLOW_UNKNOWN_FIELDS,
1105 );
1106
1107 match result {
1108 Err(SshParserError::UnsupportedField(field, _)) => {
1109 assert_eq!(field, "addkeystoagent");
1110 Ok(())
1111 }
1112 e => e,
1113 }?;
1114
1115 assert_eq!(host.params, HostParams::default());
1116 Ok(())
1117 }
1118
1119 #[test]
1120 fn should_tokenize_line() -> Result<(), SshParserError> {
1121 crate::test_log();
1122 assert_eq!(
1123 SshConfigParser::tokenize("HostName 192.168.*.* 172.26.*.*")?,
1124 (
1125 Field::HostName,
1126 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1127 )
1128 );
1129 assert_eq!(
1131 SshConfigParser::tokenize(
1132 " HostName 192.168.*.* 172.26.*.* "
1133 )?,
1134 (
1135 Field::HostName,
1136 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1137 )
1138 );
1139 Ok(())
1140 }
1141
1142 #[test]
1143 fn should_not_tokenize_line() {
1144 crate::test_log();
1145 assert!(matches!(
1146 SshConfigParser::tokenize("Omar yes").unwrap_err(),
1147 SshParserError::UnknownField(..)
1148 ));
1149 }
1150
1151 #[test]
1152 fn should_fail_parsing_field() {
1153 crate::test_log();
1154 assert!(matches!(
1155 SshConfigParser::tokenize(" ").unwrap_err(),
1156 SshParserError::MissingArgument
1157 ));
1158 }
1159
1160 #[test]
1161 fn should_parse_boolean() -> Result<(), SshParserError> {
1162 crate::test_log();
1163 assert_eq!(
1164 SshConfigParser::parse_boolean(vec![String::from("yes")])?,
1165 true
1166 );
1167 assert_eq!(
1168 SshConfigParser::parse_boolean(vec![String::from("no")])?,
1169 false
1170 );
1171 Ok(())
1172 }
1173
1174 #[test]
1175 fn should_fail_parsing_boolean() {
1176 crate::test_log();
1177 assert!(matches!(
1178 SshConfigParser::parse_boolean(vec!["boh".to_string()]).unwrap_err(),
1179 SshParserError::ExpectedBoolean
1180 ));
1181 assert!(matches!(
1182 SshConfigParser::parse_boolean(vec![]).unwrap_err(),
1183 SshParserError::MissingArgument
1184 ));
1185 }
1186
1187 #[test]
1188 fn should_parse_comma_separated_list() -> Result<(), SshParserError> {
1189 crate::test_log();
1190 assert_eq!(
1191 SshConfigParser::parse_comma_separated_list(vec![String::from("a,b,c,d")])?,
1192 vec![
1193 "a".to_string(),
1194 "b".to_string(),
1195 "c".to_string(),
1196 "d".to_string(),
1197 ]
1198 );
1199 assert_eq!(
1200 SshConfigParser::parse_comma_separated_list(vec![String::from("a")])?,
1201 vec!["a".to_string()]
1202 );
1203 Ok(())
1204 }
1205
1206 #[test]
1207 fn should_fail_parsing_comma_separated_list() {
1208 crate::test_log();
1209 assert!(matches!(
1210 SshConfigParser::parse_comma_separated_list(vec![]).unwrap_err(),
1211 SshParserError::MissingArgument
1212 ));
1213 }
1214
1215 #[test]
1216 fn should_parse_duration() -> Result<(), SshParserError> {
1217 crate::test_log();
1218 assert_eq!(
1219 SshConfigParser::parse_duration(vec![String::from("60")])?,
1220 Duration::from_secs(60)
1221 );
1222 Ok(())
1223 }
1224
1225 #[test]
1226 fn should_fail_parsing_duration() {
1227 crate::test_log();
1228 assert!(matches!(
1229 SshConfigParser::parse_duration(vec![String::from("AAA")]).unwrap_err(),
1230 SshParserError::ExpectedUnsigned
1231 ));
1232 assert!(matches!(
1233 SshConfigParser::parse_duration(vec![]).unwrap_err(),
1234 SshParserError::MissingArgument
1235 ));
1236 }
1237
1238 #[test]
1239 fn should_parse_host() -> Result<(), SshParserError> {
1240 crate::test_log();
1241 assert_eq!(
1242 SshConfigParser::parse_host(vec![
1243 String::from("192.168.*.*"),
1244 String::from("!192.168.1.1"),
1245 String::from("172.26.104.*"),
1246 String::from("!172.26.104.10"),
1247 ])?,
1248 vec![
1249 HostClause::new(String::from("192.168.*.*"), false),
1250 HostClause::new(String::from("192.168.1.1"), true),
1251 HostClause::new(String::from("172.26.104.*"), false),
1252 HostClause::new(String::from("172.26.104.10"), true),
1253 ]
1254 );
1255 Ok(())
1256 }
1257
1258 #[test]
1259 fn should_fail_parsing_host() {
1260 crate::test_log();
1261 assert!(matches!(
1262 SshConfigParser::parse_host(vec![]).unwrap_err(),
1263 SshParserError::MissingArgument
1264 ));
1265 }
1266
1267 #[test]
1268 fn should_parse_path() -> Result<(), SshParserError> {
1269 crate::test_log();
1270 assert_eq!(
1271 SshConfigParser::parse_path(vec![String::from("/tmp/a.txt")])?,
1272 PathBuf::from("/tmp/a.txt")
1273 );
1274 Ok(())
1275 }
1276
1277 #[test]
1278 fn should_parse_path_and_resolve_tilde() -> Result<(), SshParserError> {
1279 crate::test_log();
1280 let mut expected = dirs::home_dir().unwrap();
1281 expected.push(".ssh/id_dsa");
1282 assert_eq!(
1283 SshConfigParser::parse_path(vec![String::from("~/.ssh/id_dsa")])?,
1284 expected
1285 );
1286 Ok(())
1287 }
1288
1289 #[test]
1290 fn should_parse_path_list() -> Result<(), SshParserError> {
1291 crate::test_log();
1292 assert_eq!(
1293 SshConfigParser::parse_path_list(vec![
1294 String::from("/tmp/a.txt"),
1295 String::from("/tmp/b.txt")
1296 ])?,
1297 vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")]
1298 );
1299 Ok(())
1300 }
1301
1302 #[test]
1303 fn should_fail_parse_path_list() {
1304 crate::test_log();
1305 assert!(matches!(
1306 SshConfigParser::parse_path_list(vec![]).unwrap_err(),
1307 SshParserError::MissingArgument
1308 ));
1309 }
1310
1311 #[test]
1312 fn should_fail_parsing_path() {
1313 crate::test_log();
1314 assert!(matches!(
1315 SshConfigParser::parse_path(vec![]).unwrap_err(),
1316 SshParserError::MissingArgument
1317 ));
1318 }
1319
1320 #[test]
1321 fn should_parse_port() -> Result<(), SshParserError> {
1322 crate::test_log();
1323 assert_eq!(SshConfigParser::parse_port(vec![String::from("22")])?, 22);
1324 Ok(())
1325 }
1326
1327 #[test]
1328 fn should_fail_parsing_port() {
1329 crate::test_log();
1330 assert!(matches!(
1331 SshConfigParser::parse_port(vec![String::from("1234567")]).unwrap_err(),
1332 SshParserError::ExpectedPort
1333 ));
1334 assert!(matches!(
1335 SshConfigParser::parse_port(vec![]).unwrap_err(),
1336 SshParserError::MissingArgument
1337 ));
1338 }
1339
1340 #[test]
1341 fn should_parse_string() -> Result<(), SshParserError> {
1342 crate::test_log();
1343 assert_eq!(
1344 SshConfigParser::parse_string(vec![String::from("foobar")])?,
1345 String::from("foobar")
1346 );
1347 Ok(())
1348 }
1349
1350 #[test]
1351 fn should_fail_parsing_string() {
1352 crate::test_log();
1353 assert!(matches!(
1354 SshConfigParser::parse_string(vec![]).unwrap_err(),
1355 SshParserError::MissingArgument
1356 ));
1357 }
1358
1359 #[test]
1360 fn should_parse_unsigned() -> Result<(), SshParserError> {
1361 crate::test_log();
1362 assert_eq!(
1363 SshConfigParser::parse_unsigned(vec![String::from("43")])?,
1364 43
1365 );
1366 Ok(())
1367 }
1368
1369 #[test]
1370 fn should_fail_parsing_unsigned() {
1371 crate::test_log();
1372 assert!(matches!(
1373 SshConfigParser::parse_unsigned(vec![String::from("abc")]).unwrap_err(),
1374 SshParserError::ExpectedUnsigned
1375 ));
1376 assert!(matches!(
1377 SshConfigParser::parse_unsigned(vec![]).unwrap_err(),
1378 SshParserError::MissingArgument
1379 ));
1380 }
1381
1382 #[test]
1383 fn should_strip_comments() {
1384 crate::test_log();
1385
1386 assert_eq!(
1387 SshConfigParser::strip_comments("host my_host # this is my fav host").as_str(),
1388 "host my_host "
1389 );
1390 assert_eq!(
1391 SshConfigParser::strip_comments("# this is a comment").as_str(),
1392 ""
1393 );
1394 }
1395
1396 fn create_ssh_config() -> NamedTempFile {
1397 let mut tmpfile: tempfile::NamedTempFile =
1398 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1399 let config = r##"
1400# ssh config
1401# written by veeso
1402
1403
1404 # I put a comment here just to annoy
1405
1406IgnoreUnknown Pippo,Pluto
1407
1408Compression yes
1409ConnectionAttempts 10
1410ConnectTimeout 60
1411ServerAliveInterval 40
1412TcpKeepAlive yes
1413Ciphers +a-manella,blowfish
1414
1415# Let's start defining some hosts
1416
1417Host 192.168.*.* 172.26.*.* !192.168.1.30
1418 User omar
1419 # Forward agent is actually not supported; I just want to see that it wont' fail parsing
1420 ForwardAgent yes
1421 BindAddress 10.8.0.10
1422 BindInterface tun0
1423 Ciphers +coi-piedi,cazdecan,triestin-stretto
1424 IdentityFile /home/root/.ssh/pippo.key /home/root/.ssh/pluto.key
1425 Macs spyro,deoxys
1426 Port 2222
1427 PubkeyAcceptedAlgorithms -omar-crypt
1428
1429Host tostapane
1430 User ciro-esposito
1431 HostName 192.168.24.32
1432 RemoteForward 88
1433 Compression no
1434 Pippo yes
1435 Pluto 56
1436
1437Host 192.168.1.30
1438 User nutellaro
1439 RemoteForward 123
1440
1441Host *
1442 CaSignatureAlgorithms random
1443 HostKeyAlgorithms luigi,mario
1444 KexAlgorithms desu,gigi
1445 Macs concorde
1446 PubkeyAcceptedAlgorithms desu,omar-crypt,fast-omar-crypt
1447"##;
1448 tmpfile.write_all(config.as_bytes()).unwrap();
1449 tmpfile
1450 }
1451
1452 fn create_inverted_ssh_config() -> NamedTempFile {
1453 let mut tmpfile: tempfile::NamedTempFile =
1454 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1455 let config = r##"
1456Host *-host
1457 IdentityFile ~/.ssh/id_rsa_good
1458
1459Host remote-*
1460 HostName hostname.com
1461 User user
1462 IdentityFile ~/.ssh/id_rsa_bad
1463
1464Host *
1465 ConnectTimeout 15
1466 IdentityFile ~/.ssh/id_rsa_ugly
1467 "##;
1468 tmpfile.write_all(config.as_bytes()).unwrap();
1469 tmpfile
1470 }
1471
1472 fn create_ssh_config_with_comments() -> NamedTempFile {
1473 let mut tmpfile: tempfile::NamedTempFile =
1474 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1475 let config = r##"
1476Host cross-platform # this is my fav host
1477 HostName hostname.com
1478 User user
1479 IdentityFile ~/.ssh/id_rsa_good
1480
1481Host *
1482 AddKeysToAgent yes
1483 IdentityFile ~/.ssh/id_rsa_bad
1484 "##;
1485 tmpfile.write_all(config.as_bytes()).unwrap();
1486 tmpfile
1487 }
1488
1489 fn create_ssh_config_with_unknown_fields() -> NamedTempFile {
1490 let mut tmpfile: tempfile::NamedTempFile =
1491 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1492 let config = r##"
1493Host cross-platform # this is my fav host
1494 HostName hostname.com
1495 User user
1496 IdentityFile ~/.ssh/id_rsa_good
1497 Piropero yes
1498
1499Host *
1500 AddKeysToAgent yes
1501 IdentityFile ~/.ssh/id_rsa_bad
1502 "##;
1503 tmpfile.write_all(config.as_bytes()).unwrap();
1504 tmpfile
1505 }
1506
1507 #[test]
1508 fn test_should_parse_config_with_include() {
1509 crate::test_log();
1510
1511 let config = create_include_config();
1512 let file = File::open(config.config.path()).expect("Failed to open tempfile");
1513 let mut reader = BufReader::new(file);
1514
1515 let config = SshConfig::default()
1516 .parse(&mut reader, ParseRule::STRICT)
1517 .expect("Failed to parse config");
1518
1519 let glob_params = config.query("192.168.1.1");
1521 assert_eq!(
1522 glob_params.connect_timeout.unwrap(),
1523 Duration::from_secs(60)
1524 );
1525 assert_eq!(
1526 glob_params.server_alive_interval.unwrap(),
1527 Duration::from_secs(60)
1528 );
1529 assert_eq!(glob_params.tcp_keep_alive.unwrap(), true);
1530 assert_eq!(
1531 glob_params.ciphers.as_deref().unwrap(),
1532 &["a-manella", "blowfish",]
1533 );
1534
1535 let tostapane_params = config.query("tostapane");
1537 assert_eq!(
1538 tostapane_params.connect_timeout.unwrap(),
1539 Duration::from_secs(180)
1540 );
1541 assert_eq!(
1542 tostapane_params.server_alive_interval.unwrap(),
1543 Duration::from_secs(180)
1544 );
1545 assert_eq!(tostapane_params.tcp_keep_alive.unwrap(), true);
1546 assert_eq!(
1548 tostapane_params.ciphers.as_deref().unwrap(),
1549 &[
1550 "a-manella",
1551 "blowfish",
1552 "coi-piedi",
1553 "cazdecan",
1554 "triestin-stretto"
1555 ]
1556 );
1557 }
1558
1559 #[allow(dead_code)]
1560 struct ConfigWithInclude {
1561 config: NamedTempFile,
1562 inc1: NamedTempFile,
1563 inc2: NamedTempFile,
1564 }
1565
1566 fn create_include_config() -> ConfigWithInclude {
1567 let mut config_file: tempfile::NamedTempFile =
1568 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1569 let mut inc1_file: tempfile::NamedTempFile =
1570 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1571 let mut inc2_file: tempfile::NamedTempFile =
1572 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1573
1574 let config = format!(
1575 r##"
1576# ssh config
1577# written by veeso
1578
1579
1580 # I put a comment here just to annoy
1581
1582IgnoreUnknown Pippo,Pluto
1583
1584Compression yes
1585ConnectionAttempts 10
1586ConnectTimeout 60
1587ServerAliveInterval 40
1588Ciphers +a-manella,blowfish
1589Include {inc1}
1590
1591# Let's start defining some hosts
1592
1593Host tostapane
1594 User ciro-esposito
1595 HostName 192.168.24.32
1596 RemoteForward 88
1597 Compression no
1598 Pippo yes
1599 Pluto 56
1600 Include {inc2}
1601"##,
1602 inc1 = inc1_file.path().display(),
1603 inc2 = inc2_file.path().display()
1604 );
1605 config_file.write_all(config.as_bytes()).unwrap();
1606
1607 let inc1 = r##"
1609 ConnectTimeout 60
1610 ServerAliveInterval 60
1611 TcpKeepAlive yes
1612 "##;
1613 inc1_file.write_all(inc1.as_bytes()).unwrap();
1614
1615 let inc2 = r##"
1617 ConnectTimeout 180
1618 ServerAliveInterval 180
1619 Ciphers +a-manella,blowfish,coi-piedi,cazdecan,triestin-stretto
1620 "##;
1621 inc2_file.write_all(inc2.as_bytes()).unwrap();
1622
1623 ConfigWithInclude {
1624 config: config_file,
1625 inc1: inc1_file,
1626 inc2: inc2_file,
1627 }
1628 }
1629}