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