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