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, PartialEq, Eq)]
27enum UpdateHost {
28 UpdateHost,
30 NewHosts(Vec<Host>),
32}
33
34#[derive(Debug, Error)]
36pub enum SshParserError {
37 #[error("expected boolean value ('yes', 'no')")]
38 ExpectedBoolean,
39 #[error("expected port number")]
40 ExpectedPort,
41 #[error("expected unsigned value")]
42 ExpectedUnsigned,
43 #[error("expected algorithms")]
44 ExpectedAlgorithms,
45 #[error("expected path")]
46 ExpectedPath,
47 #[error("IO error: {0}")]
48 Io(#[from] IoError),
49 #[error("glob error: {0}")]
50 Glob(#[from] glob::GlobError),
51 #[error("invalid quotes")]
52 InvalidQuotes,
53 #[error("missing argument")]
54 MissingArgument,
55 #[error("pattern error: {0}")]
56 PatternError(#[from] glob::PatternError),
57 #[error("unknown field: {0}")]
58 UnknownField(String, Vec<String>),
59 #[error("unknown field: {0}")]
60 UnsupportedField(String, Vec<String>),
61}
62
63bitflags! {
64 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66 pub struct ParseRule: u8 {
67 const STRICT = 0b00000000;
69 const ALLOW_UNKNOWN_FIELDS = 0b00000001;
71 const ALLOW_UNSUPPORTED_FIELDS = 0b00000010;
73 }
74}
75
76pub(crate) struct SshConfigParser;
80
81impl SshConfigParser {
82 pub(crate) fn parse(
84 config: &mut SshConfig,
85 reader: &mut impl BufRead,
86 rules: ParseRule,
87 ignore_unknown: Option<Vec<String>>,
88 ) -> SshParserResult<()> {
89 let mut default_params = HostParams::new(&config.default_algorithms);
95 default_params.ignore_unknown = ignore_unknown;
96 config.hosts.push(Host::new(
97 vec![HostClause::new(String::from("*"), false)],
98 default_params,
99 ));
100
101 let mut current_host = config.hosts.last_mut().unwrap();
103
104 let mut lines = reader.lines();
105 loop {
107 let line = match lines.next() {
108 None => break,
109 Some(Err(err)) => return Err(SshParserError::Io(err)),
110 Some(Ok(line)) => Self::strip_comments(line.trim()),
111 };
112 if line.is_empty() {
113 continue;
114 }
115 let (field, args) = match Self::tokenize_line(&line) {
117 Ok((field, args)) => (field, args),
118 Err(SshParserError::UnknownField(field, args))
119 if rules.intersects(ParseRule::ALLOW_UNKNOWN_FIELDS)
120 || current_host.params.ignored(&field) =>
121 {
122 current_host.params.ignored_fields.insert(field, args);
123 continue;
124 }
125 Err(SshParserError::UnknownField(field, args)) => {
126 return Err(SshParserError::UnknownField(field, args));
127 }
128 Err(err) => return Err(err),
129 };
130 if field == Field::Host {
132 let mut params = HostParams::new(&config.default_algorithms);
134 params.ignore_unknown = config.hosts[0].params.ignore_unknown.clone();
135 let pattern = Self::parse_host(args)?;
136 trace!("Adding new host: {pattern:?}",);
137
138 config.hosts.push(Host::new(pattern, params));
140 current_host = config.hosts.last_mut().expect("Just added hosts");
142 } else {
143 match Self::update_host(
145 field,
146 args,
147 current_host,
148 rules,
149 &config.default_algorithms,
150 ) {
151 Ok(UpdateHost::UpdateHost) => Ok(()),
152 Ok(UpdateHost::NewHosts(new_hosts)) => {
153 trace!("Adding new hosts from 'UpdateHost::NewHosts': {new_hosts:?}",);
154 config.hosts.extend(new_hosts);
155 current_host = config.hosts.last_mut().expect("Just added hosts");
156 Ok(())
157 }
158 Err(SshParserError::UnsupportedField(field, args))
160 if rules.intersects(ParseRule::ALLOW_UNSUPPORTED_FIELDS) =>
161 {
162 current_host.params.unsupported_fields.insert(field, args);
163 Ok(())
164 }
165 Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
169 Err(e) => Err(e),
170 }?;
171 }
172 }
173
174 Ok(())
175 }
176
177 fn strip_comments(s: &str) -> String {
179 let mut in_quotes = false;
180 let mut result = String::new();
181
182 for c in s.chars() {
183 match c {
184 '"' => {
185 in_quotes = !in_quotes;
186 result.push(c);
187 }
188 '#' if !in_quotes => {
189 break;
191 }
192 _ => {
193 result.push(c);
194 }
195 }
196 }
197
198 result
199 }
200
201 fn count_unescaped_quotes(s: &str) -> usize {
204 let mut count = 0;
205 let chars: Vec<char> = s.chars().collect();
206 let mut i = 0;
207 while i < chars.len() {
208 if chars[i] == '\\' && i + 1 < chars.len() {
209 i += 2;
211 } else if chars[i] == '"' {
212 count += 1;
213 i += 1;
214 } else {
215 i += 1;
216 }
217 }
218 count
219 }
220
221 fn ends_with_unescaped_quote(s: &str) -> bool {
223 if !s.ends_with('"') {
224 return false;
225 }
226 let chars: Vec<char> = s.chars().collect();
228 let mut backslash_count = 0;
229 for i in (0..chars.len() - 1).rev() {
230 if chars[i] == '\\' {
231 backslash_count += 1;
232 } else {
233 break;
234 }
235 }
236 backslash_count % 2 == 0
238 }
239
240 fn unescape_string(s: &str) -> String {
244 let mut result = String::with_capacity(s.len());
245 let chars: Vec<char> = s.chars().collect();
246 let mut i = 0;
247 while i < chars.len() {
248 if chars[i] == '\\' && i + 1 < chars.len() {
249 let next = chars[i + 1];
250 match next {
251 '"' | '\\' | '\'' => {
252 result.push(next);
254 i += 2;
255 }
256 _ => {
257 result.push(chars[i]);
259 i += 1;
260 }
261 }
262 } else {
263 result.push(chars[i]);
264 i += 1;
265 }
266 }
267 result
268 }
269
270 fn update_host(
272 field: Field,
273 args: Vec<String>,
274 host: &mut Host,
275 rules: ParseRule,
276 default_algos: &DefaultAlgorithms,
277 ) -> SshParserResult<UpdateHost> {
278 trace!("parsing field {field:?} with args {args:?}",);
279 let params = &mut host.params;
280 match field {
281 Field::AddKeysToAgent => {
282 let value = Self::parse_boolean(args)?;
283 trace!("add_keys_to_agent: {value}",);
284 params.add_keys_to_agent = Some(value);
285 }
286 Field::BindAddress => {
287 let value = Self::parse_string(args)?;
288 trace!("bind_address: {value}",);
289 params.bind_address = Some(value);
290 }
291 Field::BindInterface => {
292 let value = Self::parse_string(args)?;
293 trace!("bind_interface: {value}",);
294 params.bind_interface = Some(value);
295 }
296 Field::CaSignatureAlgorithms => {
297 let rule = Self::parse_algos(args)?;
298 trace!("ca_signature_algorithms: {rule:?}",);
299 params.ca_signature_algorithms.apply(rule);
300 }
301 Field::CertificateFile => {
302 let value = Self::parse_path(args)?;
303 trace!("certificate_file: {value:?}",);
304 params.certificate_file = Some(value);
305 }
306 Field::Ciphers => {
307 let rule = Self::parse_algos(args)?;
308 trace!("ciphers: {rule:?}",);
309 params.ciphers.apply(rule);
310 }
311 Field::Compression => {
312 let value = Self::parse_boolean(args)?;
313 trace!("compression: {value}",);
314 params.compression = Some(value);
315 }
316 Field::ConnectTimeout => {
317 let value = Self::parse_duration(args)?;
318 trace!("connect_timeout: {value:?}",);
319 params.connect_timeout = Some(value);
320 }
321 Field::ConnectionAttempts => {
322 let value = Self::parse_unsigned(args)?;
323 trace!("connection_attempts: {value}",);
324 params.connection_attempts = Some(value);
325 }
326 Field::ForwardAgent => {
327 let value = Self::parse_boolean(args)?;
328 trace!("forward_agent: {value}",);
329 params.forward_agent = Some(value);
330 }
331 Field::Host => { }
332 Field::HostKeyAlgorithms => {
333 let rule = Self::parse_algos(args)?;
334 trace!("host_key_algorithm: {rule:?}",);
335 params.host_key_algorithms.apply(rule);
336 }
337 Field::HostName => {
338 let value = Self::parse_string(args)?;
339 trace!("host_name: {value}",);
340 params.host_name = Some(value);
341 }
342 Field::Include => {
343 return Self::include_files(
344 args,
345 host,
346 rules,
347 default_algos,
348 host.params.ignore_unknown.clone(),
349 )
350 .map(UpdateHost::NewHosts);
351 }
352 Field::IdentityFile => {
353 let value = Self::parse_path_list(args)?;
354 trace!("identity_file: {value:?}",);
355 if let Some(existing) = &mut params.identity_file {
356 existing.extend(value);
357 } else {
358 params.identity_file = Some(value);
359 }
360 }
361 Field::IgnoreUnknown => {
362 let value = Self::parse_comma_separated_list(args)?;
363 trace!("ignore_unknown: {value:?}",);
364 params.ignore_unknown = Some(value);
365 }
366 Field::KexAlgorithms => {
367 let rule = Self::parse_algos(args)?;
368 trace!("kex_algorithms: {rule:?}",);
369 params.kex_algorithms.apply(rule);
370 }
371 Field::Mac => {
372 let rule = Self::parse_algos(args)?;
373 trace!("mac: {rule:?}",);
374 params.mac.apply(rule);
375 }
376 Field::Port => {
377 let value = Self::parse_port(args)?;
378 trace!("port: {value}",);
379 params.port = Some(value);
380 }
381 Field::ProxyJump => {
382 let rule = Self::parse_comma_separated_list(args)?;
383 trace!("proxy_jump: {rule:?}",);
384 params.proxy_jump = Some(rule);
385 }
386 Field::PubkeyAcceptedAlgorithms => {
387 let rule = Self::parse_algos(args)?;
388 trace!("pubkey_accepted_algorithms: {rule:?}",);
389 params.pubkey_accepted_algorithms.apply(rule);
390 }
391 Field::PubkeyAuthentication => {
392 let value = Self::parse_boolean(args)?;
393 trace!("pubkey_authentication: {value}",);
394 params.pubkey_authentication = Some(value);
395 }
396 Field::RemoteForward => {
397 let value = Self::parse_port(args)?;
398 trace!("remote_forward: {value}",);
399 params.remote_forward = Some(value);
400 }
401 Field::ServerAliveInterval => {
402 let value = Self::parse_duration(args)?;
403 trace!("server_alive_interval: {value:?}",);
404 params.server_alive_interval = Some(value);
405 }
406 Field::TcpKeepAlive => {
407 let value = Self::parse_boolean(args)?;
408 trace!("tcp_keep_alive: {value}",);
409 params.tcp_keep_alive = Some(value);
410 }
411 #[cfg(target_os = "macos")]
412 Field::UseKeychain => {
413 let value = Self::parse_boolean(args)?;
414 trace!("use_keychain: {value}",);
415 params.use_keychain = Some(value);
416 }
417 Field::User => {
418 let value = Self::parse_string(args)?;
419 trace!("user: {value}",);
420 params.user = Some(value);
421 }
422 Field::AddressFamily
424 | Field::BatchMode
425 | Field::CanonicalDomains
426 | Field::CanonicalizeFallbackLock
427 | Field::CanonicalizeHostname
428 | Field::CanonicalizeMaxDots
429 | Field::CanonicalizePermittedCNAMEs
430 | Field::CheckHostIP
431 | Field::ClearAllForwardings
432 | Field::ControlMaster
433 | Field::ControlPath
434 | Field::ControlPersist
435 | Field::DynamicForward
436 | Field::EnableSSHKeysign
437 | Field::EscapeChar
438 | Field::ExitOnForwardFailure
439 | Field::FingerprintHash
440 | Field::ForkAfterAuthentication
441 | Field::ForwardX11
442 | Field::ForwardX11Timeout
443 | Field::ForwardX11Trusted
444 | Field::GatewayPorts
445 | Field::GlobalKnownHostsFile
446 | Field::GSSAPIAuthentication
447 | Field::GSSAPIDelegateCredentials
448 | Field::HashKnownHosts
449 | Field::HostbasedAcceptedAlgorithms
450 | Field::HostbasedAuthentication
451 | Field::HostKeyAlias
452 | Field::HostbasedKeyTypes
453 | Field::IdentitiesOnly
454 | Field::IdentityAgent
455 | Field::IPQoS
456 | Field::KbdInteractiveAuthentication
457 | Field::KbdInteractiveDevices
458 | Field::KnownHostsCommand
459 | Field::LocalCommand
460 | Field::LocalForward
461 | Field::LogLevel
462 | Field::LogVerbose
463 | Field::NoHostAuthenticationForLocalhost
464 | Field::NumberOfPasswordPrompts
465 | Field::PasswordAuthentication
466 | Field::PermitLocalCommand
467 | Field::PermitRemoteOpen
468 | Field::PKCS11Provider
469 | Field::PreferredAuthentications
470 | Field::ProxyCommand
471 | Field::ProxyUseFdpass
472 | Field::PubkeyAcceptedKeyTypes
473 | Field::RekeyLimit
474 | Field::RequestTTY
475 | Field::RevokedHostKeys
476 | Field::SecruityKeyProvider
477 | Field::SendEnv
478 | Field::ServerAliveCountMax
479 | Field::SessionType
480 | Field::SetEnv
481 | Field::StdinNull
482 | Field::StreamLocalBindMask
483 | Field::StrictHostKeyChecking
484 | Field::SyslogFacility
485 | Field::UpdateHostKeys
486 | Field::UserKnownHostsFile
487 | Field::VerifyHostKeyDNS
488 | Field::VisualHostKey
489 | Field::XAuthLocation => {
490 return Err(SshParserError::UnsupportedField(field.to_string(), args));
491 }
492 }
493 Ok(UpdateHost::UpdateHost)
494 }
495
496 fn resolve_include_path(path_match: &str) -> String {
501 #[cfg(windows)]
502 const PATH_SEPARATOR: &str = "\\";
503 #[cfg(unix)]
504 const PATH_SEPARATOR: &str = "/";
505
506 if path_match.starts_with(PATH_SEPARATOR) {
508 path_match.to_string()
509 } else {
510 let home_dir = dirs::home_dir().unwrap_or(PathBuf::from(PATH_SEPARATOR));
511 if let Some(stripped) = path_match.strip_prefix("~") {
513 format!("{dir}{PATH_SEPARATOR}{stripped}", dir = home_dir.display())
514 } else {
515 format!(
517 "{dir}{PATH_SEPARATOR}{path_match}",
518 dir = home_dir.join(".ssh").display()
519 )
520 }
521 }
522 }
523
524 fn include_files(
526 args: Vec<String>,
527 host: &mut Host,
528 rules: ParseRule,
529 default_algos: &DefaultAlgorithms,
530 ignore_unknown: Option<Vec<String>>,
531 ) -> SshParserResult<Vec<Host>> {
532 let path_match = Self::resolve_include_path(&Self::parse_string(args)?);
533
534 trace!("include files: {path_match}",);
535 let files = glob(&path_match)?;
536
537 let mut new_hosts = vec![];
538
539 for file in files {
540 let file = file?;
541 trace!("including file: {}", file.display());
542 let mut reader = BufReader::new(File::open(file)?);
543 let mut sub_config = SshConfig::default().default_algorithms(default_algos.clone());
544 Self::parse(&mut sub_config, &mut reader, rules, ignore_unknown.clone())?;
545
546 for pattern in &host.pattern {
548 if pattern.negated {
549 trace!("excluding sub-config for pattern: {pattern:?}",);
550 continue;
551 }
552 trace!("merging sub-config for pattern: {pattern:?}",);
553 let params = sub_config.query(&pattern.pattern);
554 host.params.overwrite_if_none(¶ms);
555 }
556
557 for sub_host in sub_config.hosts.into_iter().skip(1) {
559 trace!("adding sub-host: {sub_host:?}",);
560 new_hosts.push(sub_host);
561 }
562 }
563
564 Ok(new_hosts)
565 }
566
567 fn tokenize_line(line: &str) -> SshParserResult<(Field, Vec<String>)> {
580 let trimmed_line = line.trim();
582 let (field, other_tokens) = if trimmed_line.find('=').unwrap_or(usize::MAX)
584 < trimmed_line.find(char::is_whitespace).unwrap_or(usize::MAX)
585 {
586 trimmed_line
587 .split_once('=')
588 .ok_or(SshParserError::MissingArgument)?
589 } else {
590 trimmed_line
591 .split_once(char::is_whitespace)
592 .ok_or(SshParserError::MissingArgument)?
593 };
594
595 trace!("tokenized line '{line}' - field '{field}' with args '{other_tokens}'",);
596
597 let other_tokens = other_tokens.trim().trim_start_matches('=').trim();
599 trace!("other tokens trimmed: '{other_tokens}'",);
600
601 let unescaped_quote_count = Self::count_unescaped_quotes(other_tokens);
603 if unescaped_quote_count % 2 != 0 {
604 return Err(SshParserError::InvalidQuotes);
605 }
606
607 let args = if other_tokens.starts_with('"') && Self::ends_with_unescaped_quote(other_tokens)
609 {
610 trace!("quoted args: '{other_tokens}'",);
611 let content = &other_tokens[1..other_tokens.len() - 1];
612 vec![Self::unescape_string(content)]
613 } else {
614 trace!("splitting args (non-quoted): '{other_tokens}'",);
615 let tokens = other_tokens.split_whitespace();
617
618 tokens
619 .map(|x| x.trim().to_string())
620 .filter(|x| !x.is_empty())
621 .collect()
622 };
623
624 match Field::from_str(field) {
625 Ok(field) => Ok((field, args)),
626 Err(_) => Err(SshParserError::UnknownField(field.to_string(), args)),
627 }
628 }
629
630 fn parse_boolean(args: Vec<String>) -> SshParserResult<bool> {
634 match args.first().map(|x| x.as_str()) {
635 Some("yes") => Ok(true),
636 Some("no") => Ok(false),
637 Some(_) => Err(SshParserError::ExpectedBoolean),
638 None => Err(SshParserError::MissingArgument),
639 }
640 }
641
642 fn parse_algos(args: Vec<String>) -> SshParserResult<AlgorithmsRule> {
644 let first = args.first().ok_or(SshParserError::MissingArgument)?;
645
646 AlgorithmsRule::from_str(first)
647 }
648
649 fn parse_comma_separated_list(args: Vec<String>) -> SshParserResult<Vec<String>> {
651 match args
652 .first()
653 .map(|x| x.split(',').map(|x| x.to_string()).collect())
654 {
655 Some(args) => Ok(args),
656 _ => Err(SshParserError::MissingArgument),
657 }
658 }
659
660 fn parse_duration(args: Vec<String>) -> SshParserResult<Duration> {
662 let value = Self::parse_unsigned(args)?;
663 Ok(Duration::from_secs(value as u64))
664 }
665
666 fn parse_host(args: Vec<String>) -> SshParserResult<Vec<HostClause>> {
670 if args.is_empty() {
671 return Err(SshParserError::MissingArgument);
672 }
673 Ok(args
675 .into_iter()
676 .map(|x| {
677 if let Some(pattern) = x.strip_prefix('!') {
678 HostClause::new(pattern.to_string(), true)
679 } else {
680 HostClause::new(x, false)
681 }
682 })
683 .collect())
684 }
685
686 fn parse_path_list(args: Vec<String>) -> SshParserResult<Vec<PathBuf>> {
688 if args.is_empty() {
689 return Err(SshParserError::MissingArgument);
690 }
691 args.iter()
692 .map(|x| Self::parse_path_arg(x.as_str()))
693 .collect()
694 }
695
696 fn parse_path(args: Vec<String>) -> SshParserResult<PathBuf> {
698 if let Some(s) = args.first() {
699 Self::parse_path_arg(s)
700 } else {
701 Err(SshParserError::MissingArgument)
702 }
703 }
704
705 fn parse_path_arg(s: &str) -> SshParserResult<PathBuf> {
707 let s = if s.starts_with('~') {
709 let home_dir = dirs::home_dir()
710 .unwrap_or_else(|| PathBuf::from("~"))
711 .to_string_lossy()
712 .to_string();
713 s.replacen('~', &home_dir, 1)
714 } else {
715 s.to_string()
716 };
717 Ok(PathBuf::from(s))
718 }
719
720 fn parse_port(args: Vec<String>) -> SshParserResult<u16> {
722 match args.first().map(|x| u16::from_str(x)) {
723 Some(Ok(val)) => Ok(val),
724 Some(Err(_)) => Err(SshParserError::ExpectedPort),
725 None => Err(SshParserError::MissingArgument),
726 }
727 }
728
729 fn parse_string(args: Vec<String>) -> SshParserResult<String> {
731 if let Some(s) = args.into_iter().next() {
732 Ok(s)
733 } else {
734 Err(SshParserError::MissingArgument)
735 }
736 }
737
738 fn parse_unsigned(args: Vec<String>) -> SshParserResult<usize> {
740 match args.first().map(|x| usize::from_str(x)) {
741 Some(Ok(val)) => Ok(val),
742 Some(Err(_)) => Err(SshParserError::ExpectedUnsigned),
743 None => Err(SshParserError::MissingArgument),
744 }
745 }
746}
747
748#[cfg(test)]
749mod tests {
750
751 use std::fs::File;
752 use std::io::{BufReader, Write};
753 use std::path::Path;
754
755 use pretty_assertions::assert_eq;
756 use tempfile::NamedTempFile;
757
758 use super::*;
759 use crate::DefaultAlgorithms;
760
761 #[test]
762 fn should_parse_configuration() -> Result<(), SshParserError> {
763 crate::test_log();
764 let temp = create_ssh_config();
765 let file = File::open(temp.path()).expect("Failed to open tempfile");
766 let mut reader = BufReader::new(file);
767 let config = SshConfig::default()
768 .default_algorithms(DefaultAlgorithms {
769 ca_signature_algorithms: vec![],
770 ciphers: vec![],
771 host_key_algorithms: vec![],
772 kex_algorithms: vec![],
773 mac: vec![],
774 pubkey_accepted_algorithms: vec!["omar-crypt".to_string()],
775 })
776 .parse(&mut reader, ParseRule::STRICT)?;
777
778 let params = config.query("*");
781 assert_eq!(
782 params.ignore_unknown.as_deref().unwrap(),
783 &["Pippo", "Pluto"]
784 );
785 assert_eq!(params.compression.unwrap(), true);
786 assert_eq!(params.connection_attempts.unwrap(), 10);
787 assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
788 assert_eq!(
789 params.server_alive_interval.unwrap(),
790 Duration::from_secs(40)
791 );
792 assert_eq!(params.tcp_keep_alive.unwrap(), true);
793 assert_eq!(params.ciphers.algorithms(), &["a-manella", "blowfish"]);
794 assert_eq!(
795 params.pubkey_accepted_algorithms.algorithms(),
796 &["desu", "omar-crypt", "fast-omar-crypt"]
797 );
798
799 assert_eq!(params.ca_signature_algorithms.algorithms(), &["random"]);
801 assert_eq!(
802 params.host_key_algorithms.algorithms(),
803 &["luigi", "mario",]
804 );
805 assert_eq!(params.kex_algorithms.algorithms(), &["desu", "gigi",]);
806 assert_eq!(params.mac.algorithms(), &["concorde"]);
807 assert!(params.bind_address.is_none());
808
809 let params_172_26_104_4 = config.query("172.26.104.4");
813
814 assert_eq!(params_172_26_104_4.add_keys_to_agent.unwrap(), true);
816 assert_eq!(params_172_26_104_4.compression.unwrap(), true);
817 assert_eq!(params_172_26_104_4.connection_attempts.unwrap(), 10);
818 assert_eq!(
819 params_172_26_104_4.connect_timeout.unwrap(),
820 Duration::from_secs(60)
821 );
822 assert_eq!(params_172_26_104_4.tcp_keep_alive.unwrap(), true);
823
824 assert_eq!(
826 params_172_26_104_4.ca_signature_algorithms.algorithms(),
827 &["random"]
828 );
829 assert_eq!(
830 params_172_26_104_4.ciphers.algorithms(),
831 &["a-manella", "blowfish",]
832 );
833 assert_eq!(params_172_26_104_4.mac.algorithms(), &["spyro", "deoxys"]); assert_eq!(
835 params_172_26_104_4.proxy_jump.unwrap(),
836 &["jump.example.com"]
837 ); assert_eq!(
839 params_172_26_104_4
840 .pubkey_accepted_algorithms
841 .algorithms()
842 .is_empty(), true
844 );
845 assert_eq!(
846 params_172_26_104_4.bind_address.as_deref().unwrap(),
847 "10.8.0.10"
848 );
849 assert_eq!(
850 params_172_26_104_4.bind_interface.as_deref().unwrap(),
851 "tun0"
852 );
853 assert_eq!(params_172_26_104_4.port.unwrap(), 2222);
854 assert_eq!(
855 params_172_26_104_4.identity_file.as_deref().unwrap(),
856 vec![
857 Path::new("/home/root/.ssh/pippo.key"),
858 Path::new("/home/root/.ssh/pluto.key")
859 ]
860 );
861 assert_eq!(params_172_26_104_4.user.as_deref().unwrap(), "omar");
862
863 let params_tostapane = config.query("tostapane");
865 assert_eq!(params_tostapane.compression.unwrap(), true); assert_eq!(params_tostapane.connection_attempts.unwrap(), 10);
867 assert_eq!(
868 params_tostapane.connect_timeout.unwrap(),
869 Duration::from_secs(60)
870 );
871 assert_eq!(params_tostapane.tcp_keep_alive.unwrap(), true);
872 assert_eq!(params_tostapane.remote_forward.unwrap(), 88);
873 assert_eq!(params_tostapane.user.as_deref().unwrap(), "ciro-esposito");
874
875 assert_eq!(
877 params_tostapane.ca_signature_algorithms.algorithms(),
878 &["random"]
879 );
880 assert_eq!(
881 params_tostapane.ciphers.algorithms(),
882 &["a-manella", "blowfish",]
883 );
884 assert_eq!(
885 params_tostapane.mac.algorithms(),
886 vec!["spyro".to_string(), "deoxys".to_string(),]
887 );
888 assert_eq!(
889 params_tostapane.proxy_jump.unwrap(),
890 vec![
891 "jump1.example.com".to_string(),
892 "jump2.example.com".to_string(),
893 ]
894 );
895 assert_eq!(
896 params_tostapane.pubkey_accepted_algorithms.algorithms(),
897 &["desu", "omar-crypt", "fast-omar-crypt"]
898 );
899
900 let params_192_168_1_30 = config.query("192.168.1.30");
902
903 assert_eq!(params_192_168_1_30.user.as_deref().unwrap(), "nutellaro");
905 assert_eq!(params_192_168_1_30.remote_forward.unwrap(), 123);
906
907 assert_eq!(params_192_168_1_30.compression.unwrap(), true);
909 assert_eq!(params_192_168_1_30.connection_attempts.unwrap(), 10);
910 assert_eq!(
911 params_192_168_1_30.connect_timeout.unwrap(),
912 Duration::from_secs(60)
913 );
914 assert_eq!(params_192_168_1_30.tcp_keep_alive.unwrap(), true);
915
916 assert_eq!(
918 params_192_168_1_30.ca_signature_algorithms.algorithms(),
919 &["random"]
920 );
921 assert_eq!(
922 params_192_168_1_30.ciphers.algorithms(),
923 &["a-manella", "blowfish"]
924 );
925 assert_eq!(params_192_168_1_30.mac.algorithms(), &["concorde"]);
926 assert_eq!(
927 params_192_168_1_30.pubkey_accepted_algorithms.algorithms(),
928 &["desu", "omar-crypt", "fast-omar-crypt"]
929 );
930
931 Ok(())
932 }
933
934 #[test]
935 fn should_allow_unknown_field() -> Result<(), SshParserError> {
936 crate::test_log();
937 let temp = create_ssh_config_with_unknown_fields();
938 let file = File::open(temp.path()).expect("Failed to open tempfile");
939 let mut reader = BufReader::new(file);
940 let _config = SshConfig::default()
941 .default_algorithms(DefaultAlgorithms::empty())
942 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)?;
943
944 Ok(())
945 }
946
947 #[test]
948 fn should_not_allow_unknown_field() {
949 crate::test_log();
950 let temp = create_ssh_config_with_unknown_fields();
951 let file = File::open(temp.path()).expect("Failed to open tempfile");
952 let mut reader = BufReader::new(file);
953 assert!(matches!(
954 SshConfig::default()
955 .default_algorithms(DefaultAlgorithms::empty())
956 .parse(&mut reader, ParseRule::STRICT)
957 .unwrap_err(),
958 SshParserError::UnknownField(..)
959 ));
960 }
961
962 #[test]
963 fn should_store_unknown_fields() {
964 crate::test_log();
965 let temp = create_ssh_config_with_unknown_fields();
966 let file = File::open(temp.path()).expect("Failed to open tempfile");
967 let mut reader = BufReader::new(file);
968 let config = SshConfig::default()
969 .default_algorithms(DefaultAlgorithms::empty())
970 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
971 .unwrap();
972
973 let host = config.query("cross-platform");
974 assert_eq!(
975 host.ignored_fields.get("Piropero").unwrap(),
976 &vec![String::from("yes")]
977 );
978 }
979
980 #[test]
981 fn should_parse_inversed_ssh_config() {
982 crate::test_log();
983 let temp = create_inverted_ssh_config();
984 let file = File::open(temp.path()).expect("Failed to open tempfile");
985 let mut reader = BufReader::new(file);
986 let config = SshConfig::default()
987 .default_algorithms(DefaultAlgorithms::empty())
988 .parse(&mut reader, ParseRule::STRICT)
989 .unwrap();
990
991 let home_dir = dirs::home_dir()
992 .unwrap_or_else(|| PathBuf::from("~"))
993 .to_string_lossy()
994 .to_string();
995
996 let remote_host = config.query("remote-host");
997
998 assert_eq!(
1000 remote_host.identity_file.unwrap()[0].as_path(),
1001 Path::new(format!("{home_dir}/.ssh/id_rsa_good").as_str()) );
1003
1004 assert_eq!(remote_host.host_name.unwrap(), "hostname.com");
1006 assert_eq!(remote_host.user.unwrap(), "user");
1007
1008 assert_eq!(
1010 remote_host.connect_timeout.unwrap(),
1011 Duration::from_secs(15)
1012 );
1013 }
1014
1015 #[test]
1016 fn should_parse_configuration_with_hosts() {
1017 crate::test_log();
1018 let temp = create_ssh_config_with_comments();
1019
1020 let file = File::open(temp.path()).expect("Failed to open tempfile");
1021 let mut reader = BufReader::new(file);
1022 let config = SshConfig::default()
1023 .default_algorithms(DefaultAlgorithms::empty())
1024 .parse(&mut reader, ParseRule::STRICT)
1025 .unwrap();
1026
1027 let hostname = config.query("cross-platform").host_name.unwrap();
1028 assert_eq!(&hostname, "hostname.com");
1029
1030 assert!(config.query("this").host_name.is_none());
1031 }
1032
1033 #[test]
1034 fn should_update_host_bind_address() -> Result<(), SshParserError> {
1035 crate::test_log();
1036 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1037 SshConfigParser::update_host(
1038 Field::BindAddress,
1039 vec![String::from("127.0.0.1")],
1040 &mut host,
1041 ParseRule::ALLOW_UNKNOWN_FIELDS,
1042 &DefaultAlgorithms::empty(),
1043 )?;
1044 assert_eq!(host.params.bind_address.as_deref().unwrap(), "127.0.0.1");
1045 Ok(())
1046 }
1047
1048 #[test]
1049 fn should_update_host_bind_interface() -> Result<(), SshParserError> {
1050 crate::test_log();
1051 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1052 SshConfigParser::update_host(
1053 Field::BindInterface,
1054 vec![String::from("aaa")],
1055 &mut host,
1056 ParseRule::ALLOW_UNKNOWN_FIELDS,
1057 &DefaultAlgorithms::empty(),
1058 )?;
1059 assert_eq!(host.params.bind_interface.as_deref().unwrap(), "aaa");
1060 Ok(())
1061 }
1062
1063 #[test]
1064 fn should_update_host_ca_signature_algos() -> Result<(), SshParserError> {
1065 crate::test_log();
1066 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1067 SshConfigParser::update_host(
1068 Field::CaSignatureAlgorithms,
1069 vec![String::from("a,b,c")],
1070 &mut host,
1071 ParseRule::ALLOW_UNKNOWN_FIELDS,
1072 &DefaultAlgorithms::empty(),
1073 )?;
1074 assert_eq!(
1075 host.params.ca_signature_algorithms.algorithms(),
1076 &["a", "b", "c"]
1077 );
1078 Ok(())
1079 }
1080
1081 #[test]
1082 fn should_update_host_certificate_file() -> Result<(), SshParserError> {
1083 crate::test_log();
1084 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1085 SshConfigParser::update_host(
1086 Field::CertificateFile,
1087 vec![String::from("/tmp/a.crt")],
1088 &mut host,
1089 ParseRule::ALLOW_UNKNOWN_FIELDS,
1090 &DefaultAlgorithms::empty(),
1091 )?;
1092 assert_eq!(
1093 host.params.certificate_file.as_deref().unwrap(),
1094 Path::new("/tmp/a.crt")
1095 );
1096 Ok(())
1097 }
1098
1099 #[test]
1100 fn should_update_host_ciphers() -> Result<(), SshParserError> {
1101 crate::test_log();
1102 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1103 SshConfigParser::update_host(
1104 Field::Ciphers,
1105 vec![String::from("a,b,c")],
1106 &mut host,
1107 ParseRule::ALLOW_UNKNOWN_FIELDS,
1108 &DefaultAlgorithms::empty(),
1109 )?;
1110 assert_eq!(host.params.ciphers.algorithms(), &["a", "b", "c"]);
1111 Ok(())
1112 }
1113
1114 #[test]
1115 fn should_update_host_compression() -> Result<(), SshParserError> {
1116 crate::test_log();
1117 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1118 SshConfigParser::update_host(
1119 Field::Compression,
1120 vec![String::from("yes")],
1121 &mut host,
1122 ParseRule::ALLOW_UNKNOWN_FIELDS,
1123 &DefaultAlgorithms::empty(),
1124 )?;
1125 assert_eq!(host.params.compression.unwrap(), true);
1126 Ok(())
1127 }
1128
1129 #[test]
1130 fn should_update_host_connection_attempts() -> Result<(), SshParserError> {
1131 crate::test_log();
1132 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1133 SshConfigParser::update_host(
1134 Field::ConnectionAttempts,
1135 vec![String::from("4")],
1136 &mut host,
1137 ParseRule::ALLOW_UNKNOWN_FIELDS,
1138 &DefaultAlgorithms::empty(),
1139 )?;
1140 assert_eq!(host.params.connection_attempts.unwrap(), 4);
1141 Ok(())
1142 }
1143
1144 #[test]
1145 fn should_update_host_connection_timeout() -> Result<(), SshParserError> {
1146 crate::test_log();
1147 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1148 SshConfigParser::update_host(
1149 Field::ConnectTimeout,
1150 vec![String::from("10")],
1151 &mut host,
1152 ParseRule::ALLOW_UNKNOWN_FIELDS,
1153 &DefaultAlgorithms::empty(),
1154 )?;
1155 assert_eq!(
1156 host.params.connect_timeout.unwrap(),
1157 Duration::from_secs(10)
1158 );
1159 Ok(())
1160 }
1161
1162 #[test]
1163 fn should_update_host_key_algorithms() -> Result<(), SshParserError> {
1164 crate::test_log();
1165 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1166 SshConfigParser::update_host(
1167 Field::HostKeyAlgorithms,
1168 vec![String::from("a,b,c")],
1169 &mut host,
1170 ParseRule::ALLOW_UNKNOWN_FIELDS,
1171 &DefaultAlgorithms::empty(),
1172 )?;
1173 assert_eq!(
1174 host.params.host_key_algorithms.algorithms(),
1175 &["a", "b", "c"]
1176 );
1177 Ok(())
1178 }
1179
1180 #[test]
1181 fn should_update_host_host_name() -> Result<(), SshParserError> {
1182 crate::test_log();
1183 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1184 SshConfigParser::update_host(
1185 Field::HostName,
1186 vec![String::from("192.168.1.1")],
1187 &mut host,
1188 ParseRule::ALLOW_UNKNOWN_FIELDS,
1189 &DefaultAlgorithms::empty(),
1190 )?;
1191 assert_eq!(host.params.host_name.as_deref().unwrap(), "192.168.1.1");
1192 Ok(())
1193 }
1194
1195 #[test]
1196 fn should_update_host_ignore_unknown() -> Result<(), SshParserError> {
1197 crate::test_log();
1198 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1199 SshConfigParser::update_host(
1200 Field::IgnoreUnknown,
1201 vec![String::from("a,b,c")],
1202 &mut host,
1203 ParseRule::ALLOW_UNKNOWN_FIELDS,
1204 &DefaultAlgorithms::empty(),
1205 )?;
1206 assert_eq!(
1207 host.params.ignore_unknown.as_deref().unwrap(),
1208 &["a", "b", "c"]
1209 );
1210 Ok(())
1211 }
1212
1213 #[test]
1214 fn should_update_kex_algorithms() -> Result<(), SshParserError> {
1215 crate::test_log();
1216 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1217 SshConfigParser::update_host(
1218 Field::KexAlgorithms,
1219 vec![String::from("a,b,c")],
1220 &mut host,
1221 ParseRule::ALLOW_UNKNOWN_FIELDS,
1222 &DefaultAlgorithms::empty(),
1223 )?;
1224 assert_eq!(host.params.kex_algorithms.algorithms(), &["a", "b", "c"]);
1225 Ok(())
1226 }
1227
1228 #[test]
1229 fn should_update_host_mac() -> Result<(), SshParserError> {
1230 crate::test_log();
1231 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1232 SshConfigParser::update_host(
1233 Field::Mac,
1234 vec![String::from("a,b,c")],
1235 &mut host,
1236 ParseRule::ALLOW_UNKNOWN_FIELDS,
1237 &DefaultAlgorithms::empty(),
1238 )?;
1239 assert_eq!(host.params.mac.algorithms(), &["a", "b", "c"]);
1240 Ok(())
1241 }
1242
1243 #[test]
1244 fn should_update_host_port() -> Result<(), SshParserError> {
1245 crate::test_log();
1246 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1247 SshConfigParser::update_host(
1248 Field::Port,
1249 vec![String::from("2222")],
1250 &mut host,
1251 ParseRule::ALLOW_UNKNOWN_FIELDS,
1252 &DefaultAlgorithms::empty(),
1253 )?;
1254 assert_eq!(host.params.port.unwrap(), 2222);
1255 Ok(())
1256 }
1257
1258 #[test]
1259 fn should_update_host_pubkey_accepted_algos() -> Result<(), SshParserError> {
1260 crate::test_log();
1261 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1262 SshConfigParser::update_host(
1263 Field::PubkeyAcceptedAlgorithms,
1264 vec![String::from("a,b,c")],
1265 &mut host,
1266 ParseRule::ALLOW_UNKNOWN_FIELDS,
1267 &DefaultAlgorithms::empty(),
1268 )?;
1269 assert_eq!(
1270 host.params.pubkey_accepted_algorithms.algorithms(),
1271 &["a", "b", "c"]
1272 );
1273 Ok(())
1274 }
1275
1276 #[test]
1277 fn should_update_host_pubkey_authentication() -> Result<(), SshParserError> {
1278 crate::test_log();
1279 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1280 SshConfigParser::update_host(
1281 Field::PubkeyAuthentication,
1282 vec![String::from("yes")],
1283 &mut host,
1284 ParseRule::ALLOW_UNKNOWN_FIELDS,
1285 &DefaultAlgorithms::empty(),
1286 )?;
1287 assert_eq!(host.params.pubkey_authentication.unwrap(), true);
1288 Ok(())
1289 }
1290
1291 #[test]
1292 fn should_update_host_remote_forward() -> Result<(), SshParserError> {
1293 crate::test_log();
1294 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1295 SshConfigParser::update_host(
1296 Field::RemoteForward,
1297 vec![String::from("3005")],
1298 &mut host,
1299 ParseRule::ALLOW_UNKNOWN_FIELDS,
1300 &DefaultAlgorithms::empty(),
1301 )?;
1302 assert_eq!(host.params.remote_forward.unwrap(), 3005);
1303 Ok(())
1304 }
1305
1306 #[test]
1307 fn should_update_host_server_alive_interval() -> Result<(), SshParserError> {
1308 crate::test_log();
1309 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1310 SshConfigParser::update_host(
1311 Field::ServerAliveInterval,
1312 vec![String::from("40")],
1313 &mut host,
1314 ParseRule::ALLOW_UNKNOWN_FIELDS,
1315 &DefaultAlgorithms::empty(),
1316 )?;
1317 assert_eq!(
1318 host.params.server_alive_interval.unwrap(),
1319 Duration::from_secs(40)
1320 );
1321 Ok(())
1322 }
1323
1324 #[test]
1325 fn should_update_host_tcp_keep_alive() -> Result<(), SshParserError> {
1326 crate::test_log();
1327 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1328 SshConfigParser::update_host(
1329 Field::TcpKeepAlive,
1330 vec![String::from("no")],
1331 &mut host,
1332 ParseRule::ALLOW_UNKNOWN_FIELDS,
1333 &DefaultAlgorithms::empty(),
1334 )?;
1335 assert_eq!(host.params.tcp_keep_alive.unwrap(), false);
1336 Ok(())
1337 }
1338
1339 #[test]
1340 fn should_update_host_user() -> Result<(), SshParserError> {
1341 crate::test_log();
1342 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1343 SshConfigParser::update_host(
1344 Field::User,
1345 vec![String::from("pippo")],
1346 &mut host,
1347 ParseRule::ALLOW_UNKNOWN_FIELDS,
1348 &DefaultAlgorithms::empty(),
1349 )?;
1350 assert_eq!(host.params.user.as_deref().unwrap(), "pippo");
1351 Ok(())
1352 }
1353
1354 #[test]
1355 fn should_not_update_host_if_unknown() -> Result<(), SshParserError> {
1356 crate::test_log();
1357 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1358 let result = SshConfigParser::update_host(
1359 Field::PasswordAuthentication,
1360 vec![String::from("yes")],
1361 &mut host,
1362 ParseRule::ALLOW_UNKNOWN_FIELDS,
1363 &DefaultAlgorithms::empty(),
1364 );
1365
1366 match result {
1367 Ok(_) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
1368 Err(e) => Err(e),
1369 }?;
1370
1371 assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1372 Ok(())
1373 }
1374
1375 #[test]
1376 fn should_update_host_if_unsupported() -> Result<(), SshParserError> {
1377 crate::test_log();
1378 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1379 let result = SshConfigParser::update_host(
1380 Field::PasswordAuthentication,
1381 vec![String::from("yes")],
1382 &mut host,
1383 ParseRule::ALLOW_UNKNOWN_FIELDS,
1384 &DefaultAlgorithms::empty(),
1385 );
1386
1387 match result {
1388 Err(SshParserError::UnsupportedField(field, _)) => {
1389 assert_eq!(field, "passwordauthentication");
1390 Ok(())
1391 }
1392 Ok(_) => Ok(()),
1393 Err(e) => Err(e),
1394 }?;
1395
1396 assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1397 Ok(())
1398 }
1399
1400 #[test]
1401 fn should_tokenize_line() -> Result<(), SshParserError> {
1402 crate::test_log();
1403 assert_eq!(
1404 SshConfigParser::tokenize_line("HostName 192.168.*.* 172.26.*.*")?,
1405 (
1406 Field::HostName,
1407 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1408 )
1409 );
1410 assert_eq!(
1412 SshConfigParser::tokenize_line(
1413 " HostName 192.168.*.* 172.26.*.* "
1414 )?,
1415 (
1416 Field::HostName,
1417 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1418 )
1419 );
1420 Ok(())
1421 }
1422
1423 #[test]
1424 fn should_not_tokenize_line() {
1425 crate::test_log();
1426 assert!(matches!(
1427 SshConfigParser::tokenize_line("Omar yes").unwrap_err(),
1428 SshParserError::UnknownField(..)
1429 ));
1430 }
1431
1432 #[test]
1433 fn should_fail_parsing_field() {
1434 crate::test_log();
1435
1436 assert!(matches!(
1437 SshConfigParser::tokenize_line(" ").unwrap_err(),
1438 SshParserError::MissingArgument
1439 ));
1440 }
1441
1442 #[test]
1443 fn should_fail_on_mismatched_quotes() {
1444 crate::test_log();
1445
1446 assert!(matches!(
1448 SshConfigParser::tokenize_line(r#"Hostname "example.com"#).unwrap_err(),
1449 SshParserError::InvalidQuotes
1450 ));
1451 assert!(matches!(
1453 SshConfigParser::tokenize_line(r#"Hostname example.com""#).unwrap_err(),
1454 SshParserError::InvalidQuotes
1455 ));
1456 assert!(matches!(
1458 SshConfigParser::tokenize_line(r#"Hostname foo "bar"#).unwrap_err(),
1459 SshParserError::InvalidQuotes
1460 ));
1461 }
1462
1463 #[test]
1464 fn should_parse_boolean() -> Result<(), SshParserError> {
1465 crate::test_log();
1466 assert_eq!(
1467 SshConfigParser::parse_boolean(vec![String::from("yes")])?,
1468 true
1469 );
1470 assert_eq!(
1471 SshConfigParser::parse_boolean(vec![String::from("no")])?,
1472 false
1473 );
1474 Ok(())
1475 }
1476
1477 #[test]
1478 fn should_fail_parsing_boolean() {
1479 crate::test_log();
1480 assert!(matches!(
1481 SshConfigParser::parse_boolean(vec!["boh".to_string()]).unwrap_err(),
1482 SshParserError::ExpectedBoolean
1483 ));
1484 assert!(matches!(
1485 SshConfigParser::parse_boolean(vec![]).unwrap_err(),
1486 SshParserError::MissingArgument
1487 ));
1488 }
1489
1490 #[test]
1491 fn should_parse_algos() -> Result<(), SshParserError> {
1492 crate::test_log();
1493 assert_eq!(
1494 SshConfigParser::parse_algos(vec![String::from("a,b,c,d")])?,
1495 AlgorithmsRule::Set(vec![
1496 "a".to_string(),
1497 "b".to_string(),
1498 "c".to_string(),
1499 "d".to_string(),
1500 ])
1501 );
1502
1503 assert_eq!(
1504 SshConfigParser::parse_algos(vec![String::from("a")])?,
1505 AlgorithmsRule::Set(vec!["a".to_string()])
1506 );
1507
1508 assert_eq!(
1509 SshConfigParser::parse_algos(vec![String::from("+a,b")])?,
1510 AlgorithmsRule::Append(vec!["a".to_string(), "b".to_string()])
1511 );
1512
1513 Ok(())
1514 }
1515
1516 #[test]
1517 fn should_parse_comma_separated_list() -> Result<(), SshParserError> {
1518 crate::test_log();
1519 assert_eq!(
1520 SshConfigParser::parse_comma_separated_list(vec![String::from("a,b,c,d")])?,
1521 vec![
1522 "a".to_string(),
1523 "b".to_string(),
1524 "c".to_string(),
1525 "d".to_string(),
1526 ]
1527 );
1528 assert_eq!(
1529 SshConfigParser::parse_comma_separated_list(vec![String::from("a")])?,
1530 vec!["a".to_string()]
1531 );
1532 Ok(())
1533 }
1534
1535 #[test]
1536 fn should_fail_parsing_comma_separated_list() {
1537 crate::test_log();
1538 assert!(matches!(
1539 SshConfigParser::parse_comma_separated_list(vec![]).unwrap_err(),
1540 SshParserError::MissingArgument
1541 ));
1542 }
1543
1544 #[test]
1545 fn should_parse_duration() -> Result<(), SshParserError> {
1546 crate::test_log();
1547 assert_eq!(
1548 SshConfigParser::parse_duration(vec![String::from("60")])?,
1549 Duration::from_secs(60)
1550 );
1551 Ok(())
1552 }
1553
1554 #[test]
1555 fn should_fail_parsing_duration() {
1556 crate::test_log();
1557 assert!(matches!(
1558 SshConfigParser::parse_duration(vec![String::from("AAA")]).unwrap_err(),
1559 SshParserError::ExpectedUnsigned
1560 ));
1561 assert!(matches!(
1562 SshConfigParser::parse_duration(vec![]).unwrap_err(),
1563 SshParserError::MissingArgument
1564 ));
1565 }
1566
1567 #[test]
1568 fn should_parse_host() -> Result<(), SshParserError> {
1569 crate::test_log();
1570 assert_eq!(
1571 SshConfigParser::parse_host(vec![
1572 String::from("192.168.*.*"),
1573 String::from("!192.168.1.1"),
1574 String::from("172.26.104.*"),
1575 String::from("!172.26.104.10"),
1576 ])?,
1577 vec![
1578 HostClause::new(String::from("192.168.*.*"), false),
1579 HostClause::new(String::from("192.168.1.1"), true),
1580 HostClause::new(String::from("172.26.104.*"), false),
1581 HostClause::new(String::from("172.26.104.10"), true),
1582 ]
1583 );
1584 Ok(())
1585 }
1586
1587 #[test]
1588 fn should_fail_parsing_host() {
1589 crate::test_log();
1590 assert!(matches!(
1591 SshConfigParser::parse_host(vec![]).unwrap_err(),
1592 SshParserError::MissingArgument
1593 ));
1594 }
1595
1596 #[test]
1597 fn should_parse_path() -> Result<(), SshParserError> {
1598 crate::test_log();
1599 assert_eq!(
1600 SshConfigParser::parse_path(vec![String::from("/tmp/a.txt")])?,
1601 PathBuf::from("/tmp/a.txt")
1602 );
1603 Ok(())
1604 }
1605
1606 #[test]
1607 fn should_parse_path_and_resolve_tilde() -> Result<(), SshParserError> {
1608 crate::test_log();
1609 let mut expected = dirs::home_dir().unwrap();
1610 expected.push(".ssh/id_dsa");
1611 assert_eq!(
1612 SshConfigParser::parse_path(vec![String::from("~/.ssh/id_dsa")])?,
1613 expected
1614 );
1615 Ok(())
1616 }
1617
1618 #[test]
1619 fn should_parse_path_list() -> Result<(), SshParserError> {
1620 crate::test_log();
1621 assert_eq!(
1622 SshConfigParser::parse_path_list(vec![
1623 String::from("/tmp/a.txt"),
1624 String::from("/tmp/b.txt")
1625 ])?,
1626 vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")]
1627 );
1628 Ok(())
1629 }
1630
1631 #[test]
1632 fn should_fail_parse_path_list() {
1633 crate::test_log();
1634 assert!(matches!(
1635 SshConfigParser::parse_path_list(vec![]).unwrap_err(),
1636 SshParserError::MissingArgument
1637 ));
1638 }
1639
1640 #[test]
1641 fn should_fail_parsing_path() {
1642 crate::test_log();
1643 assert!(matches!(
1644 SshConfigParser::parse_path(vec![]).unwrap_err(),
1645 SshParserError::MissingArgument
1646 ));
1647 }
1648
1649 #[test]
1650 fn should_parse_port() -> Result<(), SshParserError> {
1651 crate::test_log();
1652 assert_eq!(SshConfigParser::parse_port(vec![String::from("22")])?, 22);
1653 Ok(())
1654 }
1655
1656 #[test]
1657 fn should_fail_parsing_port() {
1658 crate::test_log();
1659 assert!(matches!(
1660 SshConfigParser::parse_port(vec![String::from("1234567")]).unwrap_err(),
1661 SshParserError::ExpectedPort
1662 ));
1663 assert!(matches!(
1664 SshConfigParser::parse_port(vec![]).unwrap_err(),
1665 SshParserError::MissingArgument
1666 ));
1667 }
1668
1669 #[test]
1670 fn should_parse_string() -> Result<(), SshParserError> {
1671 crate::test_log();
1672 assert_eq!(
1673 SshConfigParser::parse_string(vec![String::from("foobar")])?,
1674 String::from("foobar")
1675 );
1676 Ok(())
1677 }
1678
1679 #[test]
1680 fn should_fail_parsing_string() {
1681 crate::test_log();
1682 assert!(matches!(
1683 SshConfigParser::parse_string(vec![]).unwrap_err(),
1684 SshParserError::MissingArgument
1685 ));
1686 }
1687
1688 #[test]
1689 fn should_parse_unsigned() -> Result<(), SshParserError> {
1690 crate::test_log();
1691 assert_eq!(
1692 SshConfigParser::parse_unsigned(vec![String::from("43")])?,
1693 43
1694 );
1695 Ok(())
1696 }
1697
1698 #[test]
1699 fn should_fail_parsing_unsigned() {
1700 crate::test_log();
1701 assert!(matches!(
1702 SshConfigParser::parse_unsigned(vec![String::from("abc")]).unwrap_err(),
1703 SshParserError::ExpectedUnsigned
1704 ));
1705 assert!(matches!(
1706 SshConfigParser::parse_unsigned(vec![]).unwrap_err(),
1707 SshParserError::MissingArgument
1708 ));
1709 }
1710
1711 #[test]
1712 fn should_strip_comments() {
1713 crate::test_log();
1714
1715 assert_eq!(
1716 SshConfigParser::strip_comments("host my_host # this is my fav host").as_str(),
1717 "host my_host "
1718 );
1719 assert_eq!(
1720 SshConfigParser::strip_comments("# this is a comment").as_str(),
1721 ""
1722 );
1723 }
1724
1725 #[test]
1726 fn should_preserve_hash_inside_quoted_strings() {
1727 crate::test_log();
1728
1729 assert_eq!(
1731 SshConfigParser::strip_comments(r#"Ciphers "aes256-ctr # not a comment""#).as_str(),
1732 r#"Ciphers "aes256-ctr # not a comment""#
1733 );
1734 assert_eq!(
1736 SshConfigParser::strip_comments(r#"Ciphers "aes256-ctr" # this is a comment"#).as_str(),
1737 r#"Ciphers "aes256-ctr" "#
1738 );
1739 assert_eq!(
1741 SshConfigParser::strip_comments(r#"ProxyCommand "ssh # hop" -W "dest # host""#)
1742 .as_str(),
1743 r#"ProxyCommand "ssh # hop" -W "dest # host""#
1744 );
1745 assert_eq!(
1747 SshConfigParser::strip_comments(r#"Key "val1" "val2" # comment"#).as_str(),
1748 r#"Key "val1" "val2" "#
1749 );
1750 }
1751
1752 #[test]
1753 fn test_should_parse_config_with_quotes_and_eq() {
1754 crate::test_log();
1755
1756 let config = create_ssh_config_with_quotes_and_eq();
1757 let file = File::open(config.path()).expect("Failed to open tempfile");
1758 let mut reader = BufReader::new(file);
1759
1760 let config = SshConfig::default()
1761 .default_algorithms(DefaultAlgorithms::empty())
1762 .parse(&mut reader, ParseRule::STRICT)
1763 .expect("Failed to parse config");
1764
1765 let params = config.query("foo");
1766
1767 assert_eq!(
1769 params.connect_timeout.expect("unspec connect timeout"),
1770 Duration::from_secs(15)
1771 );
1772 assert_eq!(
1773 params
1774 .ignore_unknown
1775 .as_deref()
1776 .expect("unspec ignore unknown"),
1777 &["Pippo", "Pluto"]
1778 );
1779 assert_eq!(
1780 params
1781 .ciphers
1782 .algorithms()
1783 .iter()
1784 .map(|x| x.as_str())
1785 .collect::<Vec<&str>>(),
1786 &["Pepperoni Pizza", "Margherita Pizza", "Hawaiian Pizza"]
1787 );
1788 assert_eq!(
1789 params
1790 .mac
1791 .algorithms()
1792 .iter()
1793 .map(|x| x.as_str())
1794 .collect::<Vec<&str>>(),
1795 &["Pasta Carbonara", "Pasta con tonno"]
1796 );
1797 }
1798
1799 #[test]
1800 fn test_should_resolve_absolute_include_path() {
1801 crate::test_log();
1802
1803 let expected = PathBuf::from("/tmp/config.local");
1804
1805 let s = "/tmp/config.local";
1806 let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
1807 assert_eq!(resolved, expected);
1808 }
1809
1810 #[test]
1811 fn test_should_resolve_relative_include_path() {
1812 crate::test_log();
1813
1814 let expected = dirs::home_dir()
1815 .unwrap_or_else(|| PathBuf::from("~"))
1816 .join(".ssh")
1817 .join("config.local");
1818
1819 let s = "config.local";
1820 let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
1821 assert_eq!(resolved, expected);
1822 }
1823
1824 #[test]
1825 fn test_should_resolve_include_path_with_tilde() {
1826 let p = "~/.ssh/config.local";
1827 let resolved = SshConfigParser::resolve_include_path(p);
1828 let mut expected = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
1829 expected.push(".ssh");
1830 expected.push("config.local");
1831 assert_eq!(PathBuf::from(resolved), expected);
1832 }
1833
1834 #[test]
1835 fn should_fail_parsing_algos_missing_arg() {
1836 crate::test_log();
1837 assert!(matches!(
1838 SshConfigParser::parse_algos(vec![]).unwrap_err(),
1839 SshParserError::MissingArgument
1840 ));
1841 }
1842
1843 #[test]
1844 fn should_parse_duration_zero() {
1845 crate::test_log();
1846 assert_eq!(
1847 SshConfigParser::parse_duration(vec![String::from("0")]).unwrap(),
1848 Duration::from_secs(0)
1849 );
1850 }
1851
1852 #[test]
1853 fn should_parse_port_boundary() {
1854 crate::test_log();
1855 assert_eq!(
1857 SshConfigParser::parse_port(vec![String::from("1")]).unwrap(),
1858 1
1859 );
1860 assert_eq!(
1862 SshConfigParser::parse_port(vec![String::from("65535")]).unwrap(),
1863 65535
1864 );
1865 }
1866
1867 #[test]
1868 fn should_update_host_add_keys_to_agent() {
1869 crate::test_log();
1870 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1871 SshConfigParser::update_host(
1872 Field::AddKeysToAgent,
1873 vec![String::from("yes")],
1874 &mut host,
1875 ParseRule::STRICT,
1876 &DefaultAlgorithms::empty(),
1877 )
1878 .unwrap();
1879 assert_eq!(host.params.add_keys_to_agent.unwrap(), true);
1880
1881 let mut host2 = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1882 SshConfigParser::update_host(
1883 Field::AddKeysToAgent,
1884 vec![String::from("no")],
1885 &mut host2,
1886 ParseRule::STRICT,
1887 &DefaultAlgorithms::empty(),
1888 )
1889 .unwrap();
1890 assert_eq!(host2.params.add_keys_to_agent.unwrap(), false);
1891 }
1892
1893 #[test]
1894 fn should_update_host_forward_agent() {
1895 crate::test_log();
1896 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1897 SshConfigParser::update_host(
1898 Field::ForwardAgent,
1899 vec![String::from("yes")],
1900 &mut host,
1901 ParseRule::STRICT,
1902 &DefaultAlgorithms::empty(),
1903 )
1904 .unwrap();
1905 assert_eq!(host.params.forward_agent.unwrap(), true);
1906 }
1907
1908 #[test]
1909 fn should_update_host_proxy_jump() {
1910 crate::test_log();
1911 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1912 SshConfigParser::update_host(
1913 Field::ProxyJump,
1914 vec![String::from("jump1,jump2,jump3")],
1915 &mut host,
1916 ParseRule::STRICT,
1917 &DefaultAlgorithms::empty(),
1918 )
1919 .unwrap();
1920 assert_eq!(
1921 host.params.proxy_jump.unwrap(),
1922 vec![
1923 "jump1".to_string(),
1924 "jump2".to_string(),
1925 "jump3".to_string()
1926 ]
1927 );
1928 }
1929
1930 #[test]
1931 fn should_update_host_identity_file() {
1932 crate::test_log();
1933 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1934 SshConfigParser::update_host(
1935 Field::IdentityFile,
1936 vec![String::from("/path/to/key1"), String::from("/path/to/key2")],
1937 &mut host,
1938 ParseRule::STRICT,
1939 &DefaultAlgorithms::empty(),
1940 )
1941 .unwrap();
1942 assert_eq!(
1943 host.params.identity_file.unwrap(),
1944 vec![
1945 PathBuf::from("/path/to/key1"),
1946 PathBuf::from("/path/to/key2")
1947 ]
1948 );
1949 }
1950
1951 #[test]
1952 fn test_should_allow_and_append_multiple_identity_files_directives() {
1953 crate::test_log();
1954 let config = r##"
1955Host test
1956 IdentityFile /path/to/key1 /path/to/key2
1957 IdentityFile /path/to/key3
1958"##;
1959 let mut reader = BufReader::new(config.as_bytes());
1960 let config = SshConfig::default()
1961 .default_algorithms(DefaultAlgorithms::empty())
1962 .parse(&mut reader, ParseRule::STRICT)
1963 .expect("Failed to parse config");
1964
1965 let params = config.query("test");
1966 assert_eq!(
1967 params.identity_file.as_ref().unwrap(),
1968 &vec![
1969 PathBuf::from("/path/to/key1"),
1970 PathBuf::from("/path/to/key2"),
1971 PathBuf::from("/path/to/key3"),
1972 ]
1973 );
1974 }
1975
1976 #[test]
1977 fn test_should_accumulate_identity_files_across_host_blocks() {
1978 crate::test_log();
1979 let config = r##"
1980Host test
1981 IdentityFile /path/to/specific_key
1982
1983Host *
1984 IdentityFile /path/to/default_key
1985"##;
1986 let mut reader = BufReader::new(config.as_bytes());
1987 let config = SshConfig::default()
1988 .default_algorithms(DefaultAlgorithms::empty())
1989 .parse(&mut reader, ParseRule::STRICT)
1990 .expect("Failed to parse config");
1991
1992 let params = config.query("test");
1993 assert_eq!(
1995 params.identity_file.as_ref().unwrap(),
1996 &vec![
1997 PathBuf::from("/path/to/specific_key"),
1998 PathBuf::from("/path/to/default_key"),
1999 ]
2000 );
2001 }
2002
2003 #[test]
2004 fn should_store_unsupported_fields_when_allowed() {
2005 crate::test_log();
2006
2007 let config = r##"
2008Host test
2009 PasswordAuthentication yes
2010"##;
2011 let mut reader = BufReader::new(config.as_bytes());
2012 let config = SshConfig::default()
2013 .default_algorithms(DefaultAlgorithms::empty())
2014 .parse(&mut reader, ParseRule::ALLOW_UNSUPPORTED_FIELDS)
2015 .unwrap();
2016
2017 let params = config.query("test");
2018 assert!(
2019 params
2020 .unsupported_fields
2021 .contains_key("passwordauthentication")
2022 );
2023 }
2024
2025 #[test]
2026 fn should_tokenize_line_with_equals_separator() {
2027 crate::test_log();
2028 let (field, args) = SshConfigParser::tokenize_line("HostName=example.com").unwrap();
2029 assert_eq!(field, Field::HostName);
2030 assert_eq!(args, vec!["example.com".to_string()]);
2031 }
2032
2033 #[test]
2034 fn should_tokenize_line_with_quoted_args() {
2035 crate::test_log();
2036 let (field, args) =
2037 SshConfigParser::tokenize_line("Ciphers \"aes256-ctr,aes128-ctr\"").unwrap();
2038 assert_eq!(field, Field::Ciphers);
2039 assert_eq!(args, vec!["aes256-ctr,aes128-ctr".to_string()]);
2040 }
2041
2042 #[test]
2043 fn should_tokenize_line_with_equals_and_quoted_args() {
2044 crate::test_log();
2045 let (field, args) =
2046 SshConfigParser::tokenize_line("Ciphers=\"aes256-ctr,aes128-ctr\"").unwrap();
2047 assert_eq!(field, Field::Ciphers);
2048 assert_eq!(args, vec!["aes256-ctr,aes128-ctr".to_string()]);
2049 }
2050
2051 #[test]
2052 fn should_unescape_quoted_args() {
2053 crate::test_log();
2054
2055 let (field, args) =
2057 SshConfigParser::tokenize_line(r#"HostName "gateway\"server""#).unwrap();
2058 assert_eq!(field, Field::HostName);
2059 assert_eq!(args, vec![r#"gateway"server"#.to_string()]);
2060
2061 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "path\\to\\host""#).unwrap();
2063 assert_eq!(field, Field::HostName);
2064 assert_eq!(args, vec![r#"path\to\host"#.to_string()]);
2065
2066 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "it\'s a test""#).unwrap();
2068 assert_eq!(field, Field::HostName);
2069 assert_eq!(args, vec!["it's a test".to_string()]);
2070
2071 let (field, args) =
2073 SshConfigParser::tokenize_line(r#"HostName "say \"hello\" and \\go""#).unwrap();
2074 assert_eq!(field, Field::HostName);
2075 assert_eq!(args, vec![r#"say "hello" and \go"#.to_string()]);
2076
2077 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "test\nvalue""#).unwrap();
2079 assert_eq!(field, Field::HostName);
2080 assert_eq!(args, vec![r#"test\nvalue"#.to_string()]);
2081 }
2082
2083 #[test]
2084 fn should_count_unescaped_quotes() {
2085 crate::test_log();
2086
2087 assert_eq!(SshConfigParser::count_unescaped_quotes("hello"), 0);
2089
2090 assert_eq!(SshConfigParser::count_unescaped_quotes(r#""hello""#), 2);
2092
2093 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\"hello\""#), 0);
2095
2096 assert_eq!(
2098 SshConfigParser::count_unescaped_quotes(r#""hello\"world""#),
2099 2
2100 );
2101
2102 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\\""#), 1);
2104
2105 assert_eq!(SshConfigParser::count_unescaped_quotes(""), 0);
2107
2108 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\""#), 0);
2110 }
2111
2112 #[test]
2113 fn should_detect_ends_with_unescaped_quote() {
2114 crate::test_log();
2115
2116 assert!(SshConfigParser::ends_with_unescaped_quote(r#""hello""#));
2118
2119 assert!(!SshConfigParser::ends_with_unescaped_quote(r#""hello\""#));
2121
2122 assert!(SshConfigParser::ends_with_unescaped_quote(r#""hello\\""#));
2124
2125 assert!(!SshConfigParser::ends_with_unescaped_quote(r#""hello\\\""#));
2127
2128 assert!(!SshConfigParser::ends_with_unescaped_quote("hello"));
2130
2131 assert!(SshConfigParser::ends_with_unescaped_quote(r#"""#));
2133
2134 assert!(!SshConfigParser::ends_with_unescaped_quote(r#"\""#));
2136 }
2137
2138 #[test]
2139 fn should_unescape_string() {
2140 crate::test_log();
2141
2142 assert_eq!(
2144 SshConfigParser::unescape_string(r#"hello\"world"#),
2145 r#"hello"world"#
2146 );
2147
2148 assert_eq!(
2150 SshConfigParser::unescape_string(r#"path\\to\\file"#),
2151 r#"path\to\file"#
2152 );
2153
2154 assert_eq!(SshConfigParser::unescape_string(r#"it\'s"#), "it's");
2156
2157 assert_eq!(
2159 SshConfigParser::unescape_string(r#"say \"hi\" and \\go"#),
2160 r#"say "hi" and \go"#
2161 );
2162
2163 assert_eq!(
2165 SshConfigParser::unescape_string(r#"test\nvalue"#),
2166 r#"test\nvalue"#
2167 );
2168
2169 assert_eq!(SshConfigParser::unescape_string("plain text"), "plain text");
2171
2172 assert_eq!(SshConfigParser::unescape_string(""), "");
2174
2175 assert_eq!(SshConfigParser::unescape_string(r#"test\"#), r#"test\"#);
2177
2178 assert_eq!(SshConfigParser::unescape_string(r#"\\\\"#), r#"\\"#);
2180 }
2181
2182 #[test]
2183 fn should_parse_host_with_single_pattern() {
2184 crate::test_log();
2185 let result = SshConfigParser::parse_host(vec![String::from("example.com")]).unwrap();
2186 assert_eq!(result.len(), 1);
2187 assert_eq!(result[0].pattern, "example.com");
2188 assert!(!result[0].negated);
2189 }
2190
2191 #[test]
2192 fn should_parse_host_with_exclamation_in_pattern() {
2193 crate::test_log();
2194
2195 let result = SshConfigParser::parse_host(vec![String::from("host!name")]).unwrap();
2197 assert_eq!(result.len(), 1);
2198 assert_eq!(result[0].pattern, "host!name");
2199 assert!(!result[0].negated);
2200
2201 let result = SshConfigParser::parse_host(vec![String::from("!host!name")]).unwrap();
2203 assert_eq!(result.len(), 1);
2204 assert_eq!(result[0].pattern, "host!name");
2205 assert!(result[0].negated);
2206
2207 let result = SshConfigParser::parse_host(vec![String::from("!a!b!c")]).unwrap();
2209 assert_eq!(result.len(), 1);
2210 assert_eq!(result[0].pattern, "a!b!c");
2211 assert!(result[0].negated);
2212
2213 let result = SshConfigParser::parse_host(vec![String::from("a!b")]).unwrap();
2215 assert_eq!(result.len(), 1);
2216 assert_eq!(result[0].pattern, "a!b");
2217 assert!(!result[0].negated);
2218 }
2219
2220 #[cfg(target_os = "macos")]
2221 #[test]
2222 fn should_update_host_use_keychain() {
2223 crate::test_log();
2224 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2225 SshConfigParser::update_host(
2226 Field::UseKeychain,
2227 vec![String::from("yes")],
2228 &mut host,
2229 ParseRule::STRICT,
2230 &DefaultAlgorithms::empty(),
2231 )
2232 .unwrap();
2233 assert_eq!(host.params.use_keychain.unwrap(), true);
2234 }
2235
2236 fn create_ssh_config_with_quotes_and_eq() -> NamedTempFile {
2237 let mut tmpfile: tempfile::NamedTempFile =
2238 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2239 let config = r##"
2240# ssh config
2241# written by veeso
2242
2243
2244# I put a comment here just to annoy
2245
2246IgnoreUnknown=Pippo,Pluto
2247ConnectTimeout = 15
2248Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza"
2249Macs="Pasta Carbonara,Pasta con tonno"
2250"##;
2251 tmpfile.write_all(config.as_bytes()).unwrap();
2252 tmpfile
2253 }
2254
2255 fn create_ssh_config() -> NamedTempFile {
2256 let mut tmpfile: tempfile::NamedTempFile =
2257 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2258 let config = r##"
2259# ssh config
2260# written by veeso
2261
2262
2263 # I put a comment here just to annoy
2264
2265IgnoreUnknown Pippo,Pluto
2266
2267Compression yes
2268ConnectionAttempts 10
2269ConnectTimeout 60
2270ServerAliveInterval 40
2271TcpKeepAlive yes
2272Ciphers +a-manella,blowfish
2273
2274# Let's start defining some hosts
2275
2276Host 192.168.*.* 172.26.*.* !192.168.1.30
2277 User omar
2278 # ForwardX11 is actually not supported; I just want to see that it wont' fail parsing
2279 ForwardX11 yes
2280 BindAddress 10.8.0.10
2281 BindInterface tun0
2282 AddKeysToAgent yes
2283 Ciphers +coi-piedi,cazdecan,triestin-stretto
2284 IdentityFile /home/root/.ssh/pippo.key /home/root/.ssh/pluto.key
2285 Macs spyro,deoxys
2286 Port 2222
2287 PubkeyAcceptedAlgorithms -omar-crypt
2288 ProxyJump jump.example.com
2289
2290Host tostapane
2291 User ciro-esposito
2292 HostName 192.168.24.32
2293 RemoteForward 88
2294 Compression no
2295 Pippo yes
2296 Pluto 56
2297 ProxyJump jump1.example.com,jump2.example.com
2298 Macs +spyro,deoxys
2299
2300Host 192.168.1.30
2301 User nutellaro
2302 RemoteForward 123
2303
2304Host *
2305 CaSignatureAlgorithms random
2306 HostKeyAlgorithms luigi,mario
2307 KexAlgorithms desu,gigi
2308 Macs concorde
2309 PubkeyAcceptedAlgorithms desu,omar-crypt,fast-omar-crypt
2310"##;
2311 tmpfile.write_all(config.as_bytes()).unwrap();
2312 tmpfile
2313 }
2314
2315 fn create_inverted_ssh_config() -> NamedTempFile {
2316 let mut tmpfile: tempfile::NamedTempFile =
2317 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2318 let config = r##"
2319Host *-host
2320 IdentityFile ~/.ssh/id_rsa_good
2321
2322Host remote-*
2323 HostName hostname.com
2324 User user
2325 IdentityFile ~/.ssh/id_rsa_bad
2326
2327Host *
2328 ConnectTimeout 15
2329 IdentityFile ~/.ssh/id_rsa_ugly
2330 "##;
2331 tmpfile.write_all(config.as_bytes()).unwrap();
2332 tmpfile
2333 }
2334
2335 fn create_ssh_config_with_comments() -> NamedTempFile {
2336 let mut tmpfile: tempfile::NamedTempFile =
2337 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2338 let config = r##"
2339Host cross-platform # this is my fav host
2340 HostName hostname.com
2341 User user
2342 IdentityFile ~/.ssh/id_rsa_good
2343
2344Host *
2345 AddKeysToAgent yes
2346 IdentityFile ~/.ssh/id_rsa_bad
2347 "##;
2348 tmpfile.write_all(config.as_bytes()).unwrap();
2349 tmpfile
2350 }
2351
2352 fn create_ssh_config_with_unknown_fields() -> NamedTempFile {
2353 let mut tmpfile: tempfile::NamedTempFile =
2354 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2355 let config = r##"
2356Host cross-platform # this is my fav host
2357 HostName hostname.com
2358 User user
2359 IdentityFile ~/.ssh/id_rsa_good
2360 Piropero yes
2361
2362Host *
2363 AddKeysToAgent yes
2364 IdentityFile ~/.ssh/id_rsa_bad
2365 "##;
2366 tmpfile.write_all(config.as_bytes()).unwrap();
2367 tmpfile
2368 }
2369
2370 #[test]
2371 fn test_should_parse_config_with_include() {
2372 crate::test_log();
2373
2374 let config = create_include_config();
2375 let file = File::open(config.config.path()).expect("Failed to open tempfile");
2376 let mut reader = BufReader::new(file);
2377
2378 let config = SshConfig::default()
2379 .default_algorithms(DefaultAlgorithms::empty())
2380 .parse(&mut reader, ParseRule::STRICT)
2381 .expect("Failed to parse config");
2382
2383 let default_params = config.query("unknown-host");
2384 assert_eq!(
2386 default_params.connect_timeout.unwrap(),
2387 Duration::from_secs(60) );
2389 assert_eq!(
2390 default_params.server_alive_interval.unwrap(),
2391 Duration::from_secs(40) );
2393 assert_eq!(default_params.tcp_keep_alive.unwrap(), true);
2394 assert_eq!(default_params.ciphers.algorithms().is_empty(), true);
2395 assert_eq!(
2396 default_params.ignore_unknown.as_deref().unwrap(),
2397 &["Pippo", "Pluto"]
2398 );
2399 assert_eq!(default_params.compression.unwrap(), true);
2400 assert_eq!(default_params.connection_attempts.unwrap(), 10);
2401
2402 let glob_params = config.query("192.168.1.1");
2404 assert_eq!(
2405 glob_params.connect_timeout.unwrap(),
2406 Duration::from_secs(60)
2407 );
2408 assert_eq!(
2409 glob_params.server_alive_interval.unwrap(),
2410 Duration::from_secs(40) );
2412 assert_eq!(glob_params.tcp_keep_alive.unwrap(), true);
2413 assert_eq!(glob_params.ciphers.algorithms().is_empty(), true);
2414
2415 let tostapane_params = config.query("tostapane");
2417 assert_eq!(
2418 tostapane_params.connect_timeout.unwrap(),
2419 Duration::from_secs(60) );
2421 assert_eq!(
2422 tostapane_params.server_alive_interval.unwrap(),
2423 Duration::from_secs(40) );
2425 assert_eq!(tostapane_params.tcp_keep_alive.unwrap(), true);
2426 assert_eq!(
2428 tostapane_params.ciphers.algorithms(),
2429 &[
2430 "a-manella",
2431 "blowfish",
2432 "coi-piedi",
2433 "cazdecan",
2434 "triestin-stretto"
2435 ]
2436 );
2437
2438 let microwave_params = config.query("microwave");
2440 assert_eq!(
2441 microwave_params.connect_timeout.unwrap(),
2442 Duration::from_secs(60) );
2444 assert_eq!(
2445 microwave_params.server_alive_interval.unwrap(),
2446 Duration::from_secs(40) );
2448 assert_eq!(
2449 microwave_params.port.unwrap(),
2450 345 );
2452 assert_eq!(microwave_params.tcp_keep_alive.unwrap(), true);
2453 assert_eq!(microwave_params.ciphers.algorithms().is_empty(), true);
2454 assert_eq!(microwave_params.user.as_deref().unwrap(), "mario-rossi");
2455 assert_eq!(
2456 microwave_params.host_name.as_deref().unwrap(),
2457 "192.168.24.33"
2458 );
2459 assert_eq!(microwave_params.remote_forward.unwrap(), 88);
2460 assert_eq!(microwave_params.compression.unwrap(), true);
2461
2462 let fridge_params = config.query("fridge");
2464 assert_eq!(
2465 fridge_params.connect_timeout.unwrap(),
2466 Duration::from_secs(60)
2467 ); assert_eq!(
2469 fridge_params.server_alive_interval.unwrap(),
2470 Duration::from_secs(40)
2471 ); assert_eq!(fridge_params.tcp_keep_alive.unwrap(), true);
2473 assert_eq!(fridge_params.ciphers.algorithms().is_empty(), true);
2474 assert_eq!(fridge_params.user.as_deref().unwrap(), "luigi-verdi");
2475 assert_eq!(fridge_params.host_name.as_deref().unwrap(), "192.168.24.34");
2476 }
2477
2478 #[allow(dead_code)]
2479 struct ConfigWithInclude {
2480 config: NamedTempFile,
2481 inc1: NamedTempFile,
2482 inc2: NamedTempFile,
2483 inc3: NamedTempFile,
2484 inc4: NamedTempFile,
2485 }
2486
2487 fn create_include_config() -> ConfigWithInclude {
2488 let mut config_file: tempfile::NamedTempFile =
2489 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2490 let mut inc1_file: tempfile::NamedTempFile =
2491 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2492 let mut inc2_file: tempfile::NamedTempFile =
2493 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2494 let mut inc3_file: tempfile::NamedTempFile =
2495 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2496 let mut inc4_file: tempfile::NamedTempFile =
2497 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2498
2499 let config = format!(
2500 r##"
2501# ssh config
2502# written by veeso
2503
2504
2505 # I put a comment here just to annoy
2506
2507IgnoreUnknown Pippo,Pluto
2508
2509Compression yes
2510ConnectionAttempts 10
2511ConnectTimeout 60
2512ServerAliveInterval 40
2513Include {inc1}
2514
2515# Let's start defining some hosts
2516
2517Host tostapane
2518 User ciro-esposito
2519 HostName 192.168.24.32
2520 RemoteForward 88
2521 Compression no
2522 # Ignore unknown fields should be inherited from the global section
2523 Pippo yes
2524 Pluto 56
2525 Include {inc2}
2526
2527Include {inc3}
2528Include {inc4}
2529"##,
2530 inc1 = inc1_file.path().display(),
2531 inc2 = inc2_file.path().display(),
2532 inc3 = inc3_file.path().display(),
2533 inc4 = inc4_file.path().display(),
2534 );
2535 config_file.write_all(config.as_bytes()).unwrap();
2536
2537 let inc1 = r##"
2539 ConnectTimeout 60
2540 ServerAliveInterval 60
2541 TcpKeepAlive yes
2542 "##;
2543 inc1_file.write_all(inc1.as_bytes()).unwrap();
2544
2545 let inc2 = r##"
2547 ConnectTimeout 180
2548 ServerAliveInterval 180
2549 Ciphers +a-manella,blowfish,coi-piedi,cazdecan,triestin-stretto
2550 "##;
2551 inc2_file.write_all(inc2.as_bytes()).unwrap();
2552
2553 let inc3 = r##"
2555Host microwave
2556 User mario-rossi
2557 HostName 192.168.24.33
2558 RemoteForward 88
2559 Compression no
2560 # Ignore unknown fields should be inherited from the global section
2561 Pippo yes
2562 Pluto 56
2563"##;
2564 inc3_file.write_all(inc3.as_bytes()).unwrap();
2565
2566 let inc4 = r##"
2568 # Update microwave
2569 ServerAliveInterval 30
2570 Port 345
2571
2572# Force microwave update (it won't work)
2573Host microwave
2574 ConnectTimeout 30
2575
2576Host fridge
2577 User luigi-verdi
2578 HostName 192.168.24.34
2579 RemoteForward 88
2580 Compression no
2581"##;
2582 inc4_file.write_all(inc4.as_bytes()).unwrap();
2583
2584 ConfigWithInclude {
2585 config: config_file,
2586 inc1: inc1_file,
2587 inc2: inc2_file,
2588 inc3: inc3_file,
2589 inc4: inc4_file,
2590 }
2591 }
2592}