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, HostScope, SshConfig};
16use crate::params::AlgorithmsRule;
17use crate::{DefaultAlgorithms, RemoteForward, RemoteForwardDestination, RemoteForwardListen};
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, HostScope)>),
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("invalid RemoteForward arguments: {0:?}")]
55 InvalidRemoteForward(Vec<String>),
56 #[error("missing argument")]
57 MissingArgument,
58 #[error("pattern error: {0}")]
59 PatternError(#[from] glob::PatternError),
60 #[error("unknown field: {0}")]
61 UnknownField(String, Vec<String>),
62 #[error("unknown field: {0}")]
63 UnsupportedField(String, Vec<String>),
64}
65
66bitflags! {
67 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69 pub struct ParseRule: u8 {
70 const STRICT = 0b00000000;
72 const ALLOW_UNKNOWN_FIELDS = 0b00000001;
74 const ALLOW_UNSUPPORTED_FIELDS = 0b00000010;
76 }
77}
78
79pub(crate) struct SshConfigParser;
83
84impl SshConfigParser {
85 pub(crate) fn parse(
87 config: &mut SshConfig,
88 reader: &mut impl BufRead,
89 rules: ParseRule,
90 ignore_unknown: Option<Vec<String>>,
91 ) -> SshParserResult<()> {
92 let mut default_params = HostParams::new(&config.default_algorithms);
98 default_params.ignore_unknown = ignore_unknown;
99 let mut current_host_index = config.append_host(
100 Host::new(
101 vec![HostClause::new(String::from("*"), false)],
102 default_params,
103 ),
104 Vec::new(),
105 );
106
107 let mut lines = reader.lines();
108 loop {
110 let line = match lines.next() {
111 None => break,
112 Some(Err(err)) => return Err(SshParserError::Io(err)),
113 Some(Ok(line)) => Self::strip_comments(line.trim()),
114 };
115 if line.is_empty() {
116 continue;
117 }
118 let (field, args) = match Self::tokenize_line(&line) {
120 Ok((field, args)) => (field, args),
121 Err(SshParserError::UnknownField(field, args))
122 if rules.intersects(ParseRule::ALLOW_UNKNOWN_FIELDS)
123 || config.hosts[current_host_index].params.ignored(&field) =>
124 {
125 config.hosts[current_host_index]
126 .params
127 .ignored_fields
128 .insert(field, args);
129 continue;
130 }
131 Err(SshParserError::UnknownField(field, args)) => {
132 return Err(SshParserError::UnknownField(field, args));
133 }
134 Err(err) => return Err(err),
135 };
136 if field == Field::Host {
138 let mut params = HostParams::new(&config.default_algorithms);
140 params.ignore_unknown = config.hosts[0].params.ignore_unknown.clone();
141 let pattern = Self::parse_host(args)?;
142 trace!("Adding new host: {pattern:?}",);
143
144 current_host_index = config.append_host(Host::new(pattern, params), Vec::new());
147 } else if field == Field::Include {
148 let (pattern, inherited_scope, ignore_unknown) = {
149 let host = &config.hosts[current_host_index];
150 (
151 host.pattern.clone(),
152 config
153 .host_scopes
154 .get(current_host_index)
155 .cloned()
156 .unwrap_or_default(),
157 host.params.ignore_unknown.clone(),
158 )
159 };
160 let new_hosts = Self::include_files(
161 args,
162 &pattern,
163 &inherited_scope,
164 rules,
165 &config.default_algorithms,
166 ignore_unknown.clone(),
167 )?;
168 for (host, scope) in new_hosts {
169 config.append_host(host, scope);
170 }
171
172 let mut params = HostParams::new(&config.default_algorithms);
173 params.ignore_unknown = ignore_unknown;
174 current_host_index =
175 config.append_host(Host::new(pattern, params), inherited_scope);
176 } else {
177 match Self::update_host(
179 field,
180 args,
181 &mut config.hosts[current_host_index],
182 rules,
183 &config.default_algorithms,
184 ) {
185 Ok(UpdateHost::UpdateHost) => Ok(()),
186 Ok(UpdateHost::NewHosts(new_hosts)) => {
187 trace!("Adding new hosts from 'UpdateHost::NewHosts': {new_hosts:?}",);
188 for (host, scope) in new_hosts {
189 config.append_host(host, scope);
190 }
191 Ok(())
192 }
193 Err(SshParserError::UnsupportedField(field, args))
195 if rules.intersects(ParseRule::ALLOW_UNSUPPORTED_FIELDS) =>
196 {
197 config.hosts[current_host_index]
198 .params
199 .unsupported_fields
200 .insert(field, args);
201 Ok(())
202 }
203 Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
207 Err(e) => Err(e),
208 }?;
209 }
210 }
211
212 Ok(())
213 }
214
215 fn strip_comments(s: &str) -> String {
217 let mut in_quotes = false;
218 let mut result = String::new();
219
220 for c in s.chars() {
221 match c {
222 '"' => {
223 in_quotes = !in_quotes;
224 result.push(c);
225 }
226 '#' if !in_quotes => {
227 break;
229 }
230 _ => {
231 result.push(c);
232 }
233 }
234 }
235
236 result
237 }
238
239 fn split_args_respecting_quotes(s: &str) -> Vec<String> {
244 let mut result = Vec::new();
245 let mut current = String::new();
246 let mut has_token = false;
247 let mut in_quotes = false;
248 let mut chars = s.chars().peekable();
249 while let Some(c) = chars.next() {
250 if in_quotes {
251 current.push(c);
252 if c == '\\' {
253 if let Some(&nc) = chars.peek() {
254 current.push(nc);
255 chars.next();
256 }
257 } else if c == '"' {
258 in_quotes = false;
259 }
260 } else if c.is_whitespace() {
261 if has_token {
262 result.push(std::mem::take(&mut current));
263 has_token = false;
264 }
265 } else if c == '"' {
266 current.push(c);
267 in_quotes = true;
268 has_token = true;
269 } else {
270 current.push(c);
271 has_token = true;
272 }
273 }
274 if has_token {
275 result.push(current);
276 }
277 result
278 }
279
280 fn count_unescaped_quotes(s: &str) -> usize {
283 let mut count = 0;
284 let chars: Vec<char> = s.chars().collect();
285 let mut i = 0;
286 while i < chars.len() {
287 if chars[i] == '\\' && i + 1 < chars.len() {
288 i += 2;
290 } else if chars[i] == '"' {
291 count += 1;
292 i += 1;
293 } else {
294 i += 1;
295 }
296 }
297 count
298 }
299
300 fn ends_with_unescaped_quote(s: &str) -> bool {
302 if !s.ends_with('"') {
303 return false;
304 }
305 let chars: Vec<char> = s.chars().collect();
307 let mut backslash_count = 0;
308 for i in (0..chars.len() - 1).rev() {
309 if chars[i] == '\\' {
310 backslash_count += 1;
311 } else {
312 break;
313 }
314 }
315 backslash_count % 2 == 0
317 }
318
319 fn unescape_string(s: &str) -> String {
323 let mut result = String::with_capacity(s.len());
324 let chars: Vec<char> = s.chars().collect();
325 let mut i = 0;
326 while i < chars.len() {
327 if chars[i] == '\\' && i + 1 < chars.len() {
328 let next = chars[i + 1];
329 match next {
330 '"' | '\\' | '\'' => {
331 result.push(next);
333 i += 2;
334 }
335 _ => {
336 result.push(chars[i]);
338 i += 1;
339 }
340 }
341 } else {
342 result.push(chars[i]);
343 i += 1;
344 }
345 }
346 result
347 }
348
349 fn update_host(
351 field: Field,
352 args: Vec<String>,
353 host: &mut Host,
354 rules: ParseRule,
355 default_algos: &DefaultAlgorithms,
356 ) -> SshParserResult<UpdateHost> {
357 trace!("parsing field {field:?} with args {args:?}",);
358 let params = &mut host.params;
359 match field {
360 Field::AddKeysToAgent => {
361 let value = Self::parse_boolean(args)?;
362 trace!("add_keys_to_agent: {value}",);
363 params.add_keys_to_agent = Some(value);
364 }
365 Field::BindAddress => {
366 let value = Self::parse_string(args)?;
367 trace!("bind_address: {value}",);
368 params.bind_address = Some(value);
369 }
370 Field::BindInterface => {
371 let value = Self::parse_string(args)?;
372 trace!("bind_interface: {value}",);
373 params.bind_interface = Some(value);
374 }
375 Field::CaSignatureAlgorithms => {
376 let rule = Self::parse_algos(args)?;
377 trace!("ca_signature_algorithms: {rule:?}",);
378 params.ca_signature_algorithms.apply(rule);
379 }
380 Field::CertificateFile => {
381 let value = Self::parse_path(args)?;
382 trace!("certificate_file: {value:?}",);
383 params.certificate_file = Some(value);
384 }
385 Field::Ciphers => {
386 let rule = Self::parse_algos(args)?;
387 trace!("ciphers: {rule:?}",);
388 params.ciphers.apply(rule);
389 }
390 Field::Compression => {
391 let value = Self::parse_boolean(args)?;
392 trace!("compression: {value}",);
393 params.compression = Some(value);
394 }
395 Field::ConnectTimeout => {
396 let value = Self::parse_duration(args)?;
397 trace!("connect_timeout: {value:?}",);
398 params.connect_timeout = Some(value);
399 }
400 Field::ConnectionAttempts => {
401 let value = Self::parse_unsigned(args)?;
402 trace!("connection_attempts: {value}",);
403 params.connection_attempts = Some(value);
404 }
405 Field::ForwardAgent => {
406 let value = Self::parse_boolean(args)?;
407 trace!("forward_agent: {value}",);
408 params.forward_agent = Some(value);
409 }
410 Field::Host => { }
411 Field::HostKeyAlgorithms => {
412 let rule = Self::parse_algos(args)?;
413 trace!("host_key_algorithm: {rule:?}",);
414 params.host_key_algorithms.apply(rule);
415 }
416 Field::HostName => {
417 let value = Self::parse_string(args)?;
418 trace!("host_name: {value}",);
419 params.host_name = Some(value);
420 }
421 Field::Include => {
422 return Self::include_files(
423 args,
424 &host.pattern,
425 &[],
426 rules,
427 default_algos,
428 host.params.ignore_unknown.clone(),
429 )
430 .map(UpdateHost::NewHosts);
431 }
432 Field::IdentityFile => {
433 let value = Self::parse_path_list(args)?;
434 trace!("identity_file: {value:?}",);
435 if let Some(existing) = &mut params.identity_file {
436 existing.extend(value);
437 } else {
438 params.identity_file = Some(value);
439 }
440 }
441 Field::IgnoreUnknown => {
442 let value = Self::parse_comma_separated_list(args)?;
443 trace!("ignore_unknown: {value:?}",);
444 params.ignore_unknown = Some(value);
445 }
446 Field::KexAlgorithms => {
447 let rule = Self::parse_algos(args)?;
448 trace!("kex_algorithms: {rule:?}",);
449 params.kex_algorithms.apply(rule);
450 }
451 Field::Mac => {
452 let rule = Self::parse_algos(args)?;
453 trace!("mac: {rule:?}",);
454 params.mac.apply(rule);
455 }
456 Field::Port => {
457 let value = Self::parse_port(args)?;
458 trace!("port: {value}",);
459 params.port = Some(value);
460 }
461 Field::ProxyJump => {
462 let rule = Self::parse_comma_separated_list(args)?;
463 trace!("proxy_jump: {rule:?}",);
464 params.proxy_jump = Some(rule);
465 }
466 Field::PubkeyAcceptedAlgorithms => {
467 let rule = Self::parse_algos(args)?;
468 trace!("pubkey_accepted_algorithms: {rule:?}",);
469 params.pubkey_accepted_algorithms.apply(rule);
470 }
471 Field::PubkeyAuthentication => {
472 let value = Self::parse_boolean(args)?;
473 trace!("pubkey_authentication: {value}",);
474 params.pubkey_authentication = Some(value);
475 }
476 Field::RemoteForward => {
477 let value = Self::parse_remote_forward(args)?;
478 trace!("remote_forward: {value}",);
479 params.remote_forward.push(value);
480 }
481 Field::ServerAliveInterval => {
482 let value = Self::parse_duration(args)?;
483 trace!("server_alive_interval: {value:?}",);
484 params.server_alive_interval = Some(value);
485 }
486 Field::TcpKeepAlive => {
487 let value = Self::parse_boolean(args)?;
488 trace!("tcp_keep_alive: {value}",);
489 params.tcp_keep_alive = Some(value);
490 }
491 #[cfg(target_os = "macos")]
492 Field::UseKeychain => {
493 let value = Self::parse_boolean(args)?;
494 trace!("use_keychain: {value}",);
495 params.use_keychain = Some(value);
496 }
497 Field::User => {
498 let value = Self::parse_string(args)?;
499 trace!("user: {value}",);
500 params.user = Some(value);
501 }
502 Field::AddressFamily
504 | Field::BatchMode
505 | Field::CanonicalDomains
506 | Field::CanonicalizeFallbackLock
507 | Field::CanonicalizeHostname
508 | Field::CanonicalizeMaxDots
509 | Field::CanonicalizePermittedCNAMEs
510 | Field::CheckHostIP
511 | Field::ClearAllForwardings
512 | Field::ControlMaster
513 | Field::ControlPath
514 | Field::ControlPersist
515 | Field::DynamicForward
516 | Field::EnableSSHKeysign
517 | Field::EscapeChar
518 | Field::ExitOnForwardFailure
519 | Field::FingerprintHash
520 | Field::ForkAfterAuthentication
521 | Field::ForwardX11
522 | Field::ForwardX11Timeout
523 | Field::ForwardX11Trusted
524 | Field::GatewayPorts
525 | Field::GlobalKnownHostsFile
526 | Field::GSSAPIAuthentication
527 | Field::GSSAPIDelegateCredentials
528 | Field::HashKnownHosts
529 | Field::HostbasedAcceptedAlgorithms
530 | Field::HostbasedAuthentication
531 | Field::HostKeyAlias
532 | Field::HostbasedKeyTypes
533 | Field::IdentitiesOnly
534 | Field::IdentityAgent
535 | Field::IPQoS
536 | Field::KbdInteractiveAuthentication
537 | Field::KbdInteractiveDevices
538 | Field::KnownHostsCommand
539 | Field::LocalCommand
540 | Field::LocalForward
541 | Field::LogLevel
542 | Field::LogVerbose
543 | Field::NoHostAuthenticationForLocalhost
544 | Field::NumberOfPasswordPrompts
545 | Field::PasswordAuthentication
546 | Field::PermitLocalCommand
547 | Field::PermitRemoteOpen
548 | Field::PKCS11Provider
549 | Field::PreferredAuthentications
550 | Field::ProxyCommand
551 | Field::ProxyUseFdpass
552 | Field::PubkeyAcceptedKeyTypes
553 | Field::RekeyLimit
554 | Field::RequestTTY
555 | Field::RevokedHostKeys
556 | Field::SecruityKeyProvider
557 | Field::SendEnv
558 | Field::ServerAliveCountMax
559 | Field::SessionType
560 | Field::SetEnv
561 | Field::StdinNull
562 | Field::StreamLocalBindMask
563 | Field::StrictHostKeyChecking
564 | Field::SyslogFacility
565 | Field::UpdateHostKeys
566 | Field::UserKnownHostsFile
567 | Field::VerifyHostKeyDNS
568 | Field::VisualHostKey
569 | Field::XAuthLocation => {
570 return Err(SshParserError::UnsupportedField(field.to_string(), args));
571 }
572 }
573 Ok(UpdateHost::UpdateHost)
574 }
575
576 fn resolve_include_path(path_match: &str) -> String {
581 #[cfg(windows)]
582 const PATH_SEPARATOR: &str = "\\";
583 #[cfg(unix)]
584 const PATH_SEPARATOR: &str = "/";
585
586 if path_match.starts_with(PATH_SEPARATOR) {
588 path_match.to_string()
589 } else {
590 let home_dir = dirs::home_dir().unwrap_or(PathBuf::from(PATH_SEPARATOR));
591 if let Some(stripped) = path_match.strip_prefix("~") {
593 format!("{dir}{PATH_SEPARATOR}{stripped}", dir = home_dir.display())
594 } else {
595 format!(
597 "{dir}{PATH_SEPARATOR}{path_match}",
598 dir = home_dir.join(".ssh").display()
599 )
600 }
601 }
602 }
603
604 fn include_files(
606 args: Vec<String>,
607 pattern: &[HostClause],
608 inherited_scope: &[Vec<HostClause>],
609 rules: ParseRule,
610 default_algos: &DefaultAlgorithms,
611 ignore_unknown: Option<Vec<String>>,
612 ) -> SshParserResult<Vec<(Host, HostScope)>> {
613 if args.is_empty() {
614 return Err(SshParserError::MissingArgument);
615 }
616
617 let mut new_hosts = vec![];
618
619 for arg in args {
620 let path_match = Self::resolve_include_path(&arg);
621
622 trace!("include files: {path_match}",);
623 let files = glob(&path_match)?;
624
625 for file in files {
626 let file = file?;
627 trace!("including file: {}", file.display());
628 let mut reader = BufReader::new(File::open(file)?);
629 let mut sub_config = SshConfig::default().default_algorithms(default_algos.clone());
630 Self::parse(&mut sub_config, &mut reader, rules, ignore_unknown.clone())?;
631
632 let SshConfig {
633 hosts: sub_hosts,
634 host_scopes: sub_scopes,
635 ..
636 } = sub_config;
637 for (index, sub_host) in sub_hosts.into_iter().enumerate() {
638 trace!("adding sub-host: {sub_host:?}",);
639 let mut scope = inherited_scope.to_vec();
640 scope.push(pattern.to_vec());
641 if let Some(sub_scope) = sub_scopes.get(index) {
642 scope.extend(sub_scope.iter().cloned());
643 }
644 new_hosts.push((sub_host, scope));
645 }
646 }
647 }
648
649 Ok(new_hosts)
650 }
651
652 fn tokenize_line(line: &str) -> SshParserResult<(Field, Vec<String>)> {
665 let trimmed_line = line.trim();
667 let (field, other_tokens) = if trimmed_line.find('=').unwrap_or(usize::MAX)
669 < trimmed_line.find(char::is_whitespace).unwrap_or(usize::MAX)
670 {
671 trimmed_line
672 .split_once('=')
673 .ok_or(SshParserError::MissingArgument)?
674 } else {
675 trimmed_line
676 .split_once(char::is_whitespace)
677 .ok_or(SshParserError::MissingArgument)?
678 };
679
680 trace!("tokenized line '{line}' - field '{field}' with args '{other_tokens}'",);
681
682 let other_tokens = other_tokens.trim().trim_start_matches('=').trim();
684 trace!("other tokens trimmed: '{other_tokens}'",);
685
686 let unescaped_quote_count = Self::count_unescaped_quotes(other_tokens);
688 if unescaped_quote_count % 2 != 0 {
689 return Err(SshParserError::InvalidQuotes);
690 }
691
692 let raw_tokens = Self::split_args_respecting_quotes(other_tokens);
694
695 let args = if raw_tokens.len() == 1
697 && raw_tokens[0].starts_with('"')
698 && raw_tokens[0].len() >= 2
699 && Self::ends_with_unescaped_quote(&raw_tokens[0])
700 {
701 trace!("quoted args: '{}'", raw_tokens[0]);
702 let t = &raw_tokens[0];
703 let content = &t[1..t.len() - 1];
704 vec![Self::unescape_string(content)]
705 } else {
706 trace!("split args: {:?}", raw_tokens);
707 raw_tokens
708 };
709
710 match Field::from_str(field) {
711 Ok(field) => Ok((field, args)),
712 Err(_) => Err(SshParserError::UnknownField(field.to_string(), args)),
713 }
714 }
715
716 fn parse_boolean(args: Vec<String>) -> SshParserResult<bool> {
722 let value = args.first().ok_or(SshParserError::MissingArgument)?;
723
724 if value.eq_ignore_ascii_case("yes") || value.eq_ignore_ascii_case("true") {
725 Ok(true)
726 } else if value.eq_ignore_ascii_case("no") || value.eq_ignore_ascii_case("false") {
727 Ok(false)
728 } else {
729 Err(SshParserError::ExpectedBoolean)
730 }
731 }
732
733 fn parse_algos(args: Vec<String>) -> SshParserResult<AlgorithmsRule> {
735 let first = args.first().ok_or(SshParserError::MissingArgument)?;
736
737 AlgorithmsRule::from_str(first)
738 }
739
740 fn parse_comma_separated_list(args: Vec<String>) -> SshParserResult<Vec<String>> {
742 match args
743 .first()
744 .map(|x| x.split(',').map(|x| x.to_string()).collect())
745 {
746 Some(args) => Ok(args),
747 _ => Err(SshParserError::MissingArgument),
748 }
749 }
750
751 fn parse_duration(args: Vec<String>) -> SshParserResult<Duration> {
753 let value = Self::parse_unsigned(args)?;
754 Ok(Duration::from_secs(value as u64))
755 }
756
757 fn parse_host(args: Vec<String>) -> SshParserResult<Vec<HostClause>> {
761 if args.is_empty() {
762 return Err(SshParserError::MissingArgument);
763 }
764 Ok(args
766 .into_iter()
767 .map(|x| {
768 if let Some(pattern) = x.strip_prefix('!') {
769 HostClause::new(pattern.to_string(), true)
770 } else {
771 HostClause::new(x, false)
772 }
773 })
774 .collect())
775 }
776
777 fn parse_path_list(args: Vec<String>) -> SshParserResult<Vec<PathBuf>> {
779 if args.is_empty() {
780 return Err(SshParserError::MissingArgument);
781 }
782 args.iter()
783 .map(|x| Self::parse_path_arg(x.as_str()))
784 .collect()
785 }
786
787 fn parse_path(args: Vec<String>) -> SshParserResult<PathBuf> {
789 if let Some(s) = args.first() {
790 Self::parse_path_arg(s)
791 } else {
792 Err(SshParserError::MissingArgument)
793 }
794 }
795
796 fn parse_path_arg(s: &str) -> SshParserResult<PathBuf> {
798 let s = if s.starts_with('~') {
800 let home_dir = dirs::home_dir()
801 .unwrap_or_else(|| PathBuf::from("~"))
802 .to_string_lossy()
803 .to_string();
804 s.replacen('~', &home_dir, 1)
805 } else {
806 s.to_string()
807 };
808 Ok(PathBuf::from(s))
809 }
810
811 fn parse_port(args: Vec<String>) -> SshParserResult<u16> {
813 match args.first().map(|x| u16::from_str(x)) {
814 Some(Ok(val)) => Ok(val),
815 Some(Err(_)) => Err(SshParserError::ExpectedPort),
816 None => Err(SshParserError::MissingArgument),
817 }
818 }
819
820 fn parse_remote_forward(args: Vec<String>) -> SshParserResult<RemoteForward> {
822 if !(1..=2).contains(&args.len()) {
823 return Err(SshParserError::InvalidRemoteForward(args));
824 }
825
826 let listen_argument = Self::normalize_forward_argument(&args[0]);
827 let listen = if listen_argument.contains('/') {
828 RemoteForwardListen::UnixSocket(PathBuf::from(listen_argument))
829 } else if let Ok(port) = u16::from_str(&listen_argument) {
830 RemoteForwardListen::Port(port)
831 } else if let Some((host, port)) = Self::parse_forward_host(&listen_argument, true) {
832 RemoteForwardListen::Host { host, port }
833 } else {
834 return Err(SshParserError::InvalidRemoteForward(args));
835 };
836
837 let destination = if let Some(argument) = args.get(1) {
838 let argument = Self::normalize_forward_argument(argument);
839 if argument.contains('/') {
840 Some(RemoteForwardDestination::UnixSocket(PathBuf::from(
841 argument,
842 )))
843 } else if let Some((host, port)) = Self::parse_forward_host(&argument, false) {
844 Some(RemoteForwardDestination::Host { host, port })
845 } else {
846 return Err(SshParserError::InvalidRemoteForward(args));
847 }
848 } else {
849 None
850 };
851
852 Ok(RemoteForward::new(listen, destination))
853 }
854
855 fn normalize_forward_argument(argument: &str) -> String {
857 if argument.starts_with('"')
858 && argument.len() >= 2
859 && Self::ends_with_unescaped_quote(argument)
860 {
861 Self::unescape_string(&argument[1..argument.len() - 1])
862 } else {
863 argument.to_string()
864 }
865 }
866
867 fn parse_forward_host(value: &str, allow_empty_host: bool) -> Option<(String, u16)> {
869 let (host, port) = if let Some(value) = value.strip_prefix('[') {
870 let (host, port) = value.split_once("]:")?;
871 if host.is_empty() || host.contains(['[', ']']) {
872 return None;
873 }
874 (host, port)
875 } else {
876 let (host, port) = value.rsplit_once(':')?;
877 if host.contains([':', '[', ']']) {
878 return None;
879 }
880 (host, port)
881 };
882
883 if !allow_empty_host && host.is_empty() {
884 return None;
885 }
886
887 Some((host.to_string(), u16::from_str(port).ok()?))
888 }
889
890 fn parse_string(args: Vec<String>) -> SshParserResult<String> {
892 if let Some(s) = args.into_iter().next() {
893 Ok(s)
894 } else {
895 Err(SshParserError::MissingArgument)
896 }
897 }
898
899 fn parse_unsigned(args: Vec<String>) -> SshParserResult<usize> {
901 match args.first().map(|x| usize::from_str(x)) {
902 Some(Ok(val)) => Ok(val),
903 Some(Err(_)) => Err(SshParserError::ExpectedUnsigned),
904 None => Err(SshParserError::MissingArgument),
905 }
906 }
907}
908
909#[cfg(test)]
910mod tests {
911
912 use std::fs::File;
913 use std::io::{BufReader, Write};
914 use std::path::{Path, PathBuf};
915
916 use pretty_assertions::assert_eq;
917 use tempfile::NamedTempFile;
918
919 use super::*;
920 use crate::{DefaultAlgorithms, RemoteForward, RemoteForwardDestination, RemoteForwardListen};
921
922 #[test]
923 fn should_parse_remote_forward_listeners() -> Result<(), SshParserError> {
924 let cases = [
925 (
926 vec!["8080"],
927 RemoteForward::new(RemoteForwardListen::Port(8080), None),
928 ),
929 (
930 vec!["localhost:8080"],
931 RemoteForward::new(
932 RemoteForwardListen::Host {
933 host: "localhost".to_string(),
934 port: 8080,
935 },
936 None,
937 ),
938 ),
939 (
940 vec!["0.0.0.0:8080"],
941 RemoteForward::new(
942 RemoteForwardListen::Host {
943 host: "0.0.0.0".to_string(),
944 port: 8080,
945 },
946 None,
947 ),
948 ),
949 (
950 vec!["*:8080"],
951 RemoteForward::new(
952 RemoteForwardListen::Host {
953 host: "*".to_string(),
954 port: 8080,
955 },
956 None,
957 ),
958 ),
959 (
960 vec![":8080"],
961 RemoteForward::new(
962 RemoteForwardListen::Host {
963 host: String::new(),
964 port: 8080,
965 },
966 None,
967 ),
968 ),
969 (
970 vec!["[::1]:8080"],
971 RemoteForward::new(
972 RemoteForwardListen::Host {
973 host: "::1".to_string(),
974 port: 8080,
975 },
976 None,
977 ),
978 ),
979 (
980 vec!["/tmp/remote.sock"],
981 RemoteForward::new(
982 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote.sock")),
983 None,
984 ),
985 ),
986 ];
987
988 for (args, expected) in cases {
989 let args = args.into_iter().map(str::to_string).collect();
990 assert_eq!(SshConfigParser::parse_remote_forward(args)?, expected);
991 }
992 Ok(())
993 }
994
995 #[test]
996 fn should_parse_remote_forward_destinations() -> Result<(), SshParserError> {
997 let cases = [
998 (
999 vec!["8080", "localhost:80"],
1000 RemoteForward::new(
1001 RemoteForwardListen::Port(8080),
1002 Some(RemoteForwardDestination::Host {
1003 host: "localhost".to_string(),
1004 port: 80,
1005 }),
1006 ),
1007 ),
1008 (
1009 vec!["/tmp/remote.sock", "/tmp/local.sock"],
1010 RemoteForward::new(
1011 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote.sock")),
1012 Some(RemoteForwardDestination::UnixSocket(PathBuf::from(
1013 "/tmp/local.sock",
1014 ))),
1015 ),
1016 ),
1017 (
1018 vec!["\"/tmp/remote socket\"", "[2001:db8::1]:443"],
1019 RemoteForward::new(
1020 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote socket")),
1021 Some(RemoteForwardDestination::Host {
1022 host: "2001:db8::1".to_string(),
1023 port: 443,
1024 }),
1025 ),
1026 ),
1027 ];
1028
1029 for (args, expected) in cases {
1030 let args = args.into_iter().map(str::to_string).collect();
1031 assert_eq!(SshConfigParser::parse_remote_forward(args)?, expected);
1032 }
1033 Ok(())
1034 }
1035
1036 #[test]
1037 fn should_reject_invalid_remote_forward() {
1038 for args in [
1039 vec![],
1040 vec!["localhost"],
1041 vec!["[::1:8080"],
1042 vec!["70000"],
1043 vec!["localhost:70000"],
1044 vec!["8080", "80"],
1045 vec!["8080", "localhost:80", "unexpected"],
1046 ] {
1047 let args = args.into_iter().map(str::to_string).collect();
1048 assert!(matches!(
1049 SshConfigParser::parse_remote_forward(args),
1050 Err(SshParserError::InvalidRemoteForward(_))
1051 ));
1052 }
1053 }
1054
1055 #[test]
1056 fn should_parse_configuration() -> Result<(), SshParserError> {
1057 crate::test_log();
1058 let temp = create_ssh_config();
1059 let file = File::open(temp.path()).expect("Failed to open tempfile");
1060 let mut reader = BufReader::new(file);
1061 let config = SshConfig::default()
1062 .default_algorithms(DefaultAlgorithms {
1063 ca_signature_algorithms: vec![],
1064 ciphers: vec![],
1065 host_key_algorithms: vec![],
1066 kex_algorithms: vec![],
1067 mac: vec![],
1068 pubkey_accepted_algorithms: vec!["omar-crypt".to_string()],
1069 })
1070 .parse(&mut reader, ParseRule::STRICT)?;
1071
1072 let params = config.query("*");
1075 assert_eq!(
1076 params.ignore_unknown.as_deref().unwrap(),
1077 &["Pippo", "Pluto"]
1078 );
1079 assert_eq!(params.compression.unwrap(), true);
1080 assert_eq!(params.connection_attempts.unwrap(), 10);
1081 assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
1082 assert_eq!(
1083 params.server_alive_interval.unwrap(),
1084 Duration::from_secs(40)
1085 );
1086 assert_eq!(params.tcp_keep_alive.unwrap(), true);
1087 assert_eq!(params.ciphers.algorithms(), &["a-manella", "blowfish"]);
1088 assert_eq!(
1089 params.pubkey_accepted_algorithms.algorithms(),
1090 &["desu", "omar-crypt", "fast-omar-crypt"]
1091 );
1092
1093 assert_eq!(params.ca_signature_algorithms.algorithms(), &["random"]);
1095 assert_eq!(
1096 params.host_key_algorithms.algorithms(),
1097 &["luigi", "mario",]
1098 );
1099 assert_eq!(params.kex_algorithms.algorithms(), &["desu", "gigi",]);
1100 assert_eq!(params.mac.algorithms(), &["concorde"]);
1101 assert!(params.bind_address.is_none());
1102
1103 let params_172_26_104_4 = config.query("172.26.104.4");
1107
1108 assert_eq!(params_172_26_104_4.add_keys_to_agent.unwrap(), true);
1110 assert_eq!(params_172_26_104_4.compression.unwrap(), true);
1111 assert_eq!(params_172_26_104_4.connection_attempts.unwrap(), 10);
1112 assert_eq!(
1113 params_172_26_104_4.connect_timeout.unwrap(),
1114 Duration::from_secs(60)
1115 );
1116 assert_eq!(params_172_26_104_4.tcp_keep_alive.unwrap(), true);
1117
1118 assert_eq!(
1120 params_172_26_104_4.ca_signature_algorithms.algorithms(),
1121 &["random"]
1122 );
1123 assert_eq!(
1124 params_172_26_104_4.ciphers.algorithms(),
1125 &["a-manella", "blowfish",]
1126 );
1127 assert_eq!(params_172_26_104_4.mac.algorithms(), &["spyro", "deoxys"]); assert_eq!(
1129 params_172_26_104_4.proxy_jump.unwrap(),
1130 &["jump.example.com"]
1131 ); assert_eq!(
1133 params_172_26_104_4
1134 .pubkey_accepted_algorithms
1135 .algorithms()
1136 .is_empty(), true
1138 );
1139 assert_eq!(
1140 params_172_26_104_4.bind_address.as_deref().unwrap(),
1141 "10.8.0.10"
1142 );
1143 assert_eq!(
1144 params_172_26_104_4.bind_interface.as_deref().unwrap(),
1145 "tun0"
1146 );
1147 assert_eq!(params_172_26_104_4.port.unwrap(), 2222);
1148 assert_eq!(
1149 params_172_26_104_4.identity_file.as_deref().unwrap(),
1150 vec![
1151 Path::new("/home/root/.ssh/pippo.key"),
1152 Path::new("/home/root/.ssh/pluto.key")
1153 ]
1154 );
1155 assert_eq!(params_172_26_104_4.user.as_deref().unwrap(), "omar");
1156
1157 let params_tostapane = config.query("tostapane");
1159 assert_eq!(params_tostapane.compression.unwrap(), true); assert_eq!(params_tostapane.connection_attempts.unwrap(), 10);
1161 assert_eq!(
1162 params_tostapane.connect_timeout.unwrap(),
1163 Duration::from_secs(60)
1164 );
1165 assert_eq!(params_tostapane.tcp_keep_alive.unwrap(), true);
1166 assert_eq!(
1167 params_tostapane.remote_forward,
1168 vec![RemoteForward::new(RemoteForwardListen::Port(88), None)]
1169 );
1170 assert_eq!(params_tostapane.user.as_deref().unwrap(), "ciro-esposito");
1171
1172 assert_eq!(
1174 params_tostapane.ca_signature_algorithms.algorithms(),
1175 &["random"]
1176 );
1177 assert_eq!(
1178 params_tostapane.ciphers.algorithms(),
1179 &["a-manella", "blowfish",]
1180 );
1181 assert_eq!(
1182 params_tostapane.mac.algorithms(),
1183 vec!["spyro".to_string(), "deoxys".to_string(),]
1184 );
1185 assert_eq!(
1186 params_tostapane.proxy_jump.unwrap(),
1187 vec![
1188 "jump1.example.com".to_string(),
1189 "jump2.example.com".to_string(),
1190 ]
1191 );
1192 assert_eq!(
1193 params_tostapane.pubkey_accepted_algorithms.algorithms(),
1194 &["desu", "omar-crypt", "fast-omar-crypt"]
1195 );
1196
1197 let params_192_168_1_30 = config.query("192.168.1.30");
1199
1200 assert_eq!(params_192_168_1_30.user.as_deref().unwrap(), "nutellaro");
1202 assert_eq!(
1203 params_192_168_1_30.remote_forward,
1204 vec![RemoteForward::new(RemoteForwardListen::Port(123), None)]
1205 );
1206
1207 assert_eq!(params_192_168_1_30.compression.unwrap(), true);
1209 assert_eq!(params_192_168_1_30.connection_attempts.unwrap(), 10);
1210 assert_eq!(
1211 params_192_168_1_30.connect_timeout.unwrap(),
1212 Duration::from_secs(60)
1213 );
1214 assert_eq!(params_192_168_1_30.tcp_keep_alive.unwrap(), true);
1215
1216 assert_eq!(
1218 params_192_168_1_30.ca_signature_algorithms.algorithms(),
1219 &["random"]
1220 );
1221 assert_eq!(
1222 params_192_168_1_30.ciphers.algorithms(),
1223 &["a-manella", "blowfish"]
1224 );
1225 assert_eq!(params_192_168_1_30.mac.algorithms(), &["concorde"]);
1226 assert_eq!(
1227 params_192_168_1_30.pubkey_accepted_algorithms.algorithms(),
1228 &["desu", "omar-crypt", "fast-omar-crypt"]
1229 );
1230
1231 Ok(())
1232 }
1233
1234 #[test]
1235 fn should_allow_unknown_field() -> Result<(), SshParserError> {
1236 crate::test_log();
1237 let temp = create_ssh_config_with_unknown_fields();
1238 let file = File::open(temp.path()).expect("Failed to open tempfile");
1239 let mut reader = BufReader::new(file);
1240 let _config = SshConfig::default()
1241 .default_algorithms(DefaultAlgorithms::empty())
1242 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)?;
1243
1244 Ok(())
1245 }
1246
1247 #[test]
1248 fn should_not_allow_unknown_field() {
1249 crate::test_log();
1250 let temp = create_ssh_config_with_unknown_fields();
1251 let file = File::open(temp.path()).expect("Failed to open tempfile");
1252 let mut reader = BufReader::new(file);
1253 assert!(matches!(
1254 SshConfig::default()
1255 .default_algorithms(DefaultAlgorithms::empty())
1256 .parse(&mut reader, ParseRule::STRICT)
1257 .unwrap_err(),
1258 SshParserError::UnknownField(..)
1259 ));
1260 }
1261
1262 #[test]
1263 fn should_store_unknown_fields() {
1264 crate::test_log();
1265 let temp = create_ssh_config_with_unknown_fields();
1266 let file = File::open(temp.path()).expect("Failed to open tempfile");
1267 let mut reader = BufReader::new(file);
1268 let config = SshConfig::default()
1269 .default_algorithms(DefaultAlgorithms::empty())
1270 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
1271 .unwrap();
1272
1273 let host = config.query("cross-platform");
1274 assert_eq!(
1275 host.ignored_fields.get("Piropero").unwrap(),
1276 &vec![String::from("yes")]
1277 );
1278 }
1279
1280 #[test]
1281 fn should_parse_inversed_ssh_config() {
1282 crate::test_log();
1283 let temp = create_inverted_ssh_config();
1284 let file = File::open(temp.path()).expect("Failed to open tempfile");
1285 let mut reader = BufReader::new(file);
1286 let config = SshConfig::default()
1287 .default_algorithms(DefaultAlgorithms::empty())
1288 .parse(&mut reader, ParseRule::STRICT)
1289 .unwrap();
1290
1291 let home_dir = dirs::home_dir()
1292 .unwrap_or_else(|| PathBuf::from("~"))
1293 .to_string_lossy()
1294 .to_string();
1295
1296 let remote_host = config.query("remote-host");
1297
1298 assert_eq!(
1300 remote_host.identity_file.unwrap()[0].as_path(),
1301 Path::new(format!("{home_dir}/.ssh/id_rsa_good").as_str()) );
1303
1304 assert_eq!(remote_host.host_name.unwrap(), "hostname.com");
1306 assert_eq!(remote_host.user.unwrap(), "user");
1307
1308 assert_eq!(
1310 remote_host.connect_timeout.unwrap(),
1311 Duration::from_secs(15)
1312 );
1313 }
1314
1315 #[test]
1316 fn should_parse_configuration_with_hosts() {
1317 crate::test_log();
1318 let temp = create_ssh_config_with_comments();
1319
1320 let file = File::open(temp.path()).expect("Failed to open tempfile");
1321 let mut reader = BufReader::new(file);
1322 let config = SshConfig::default()
1323 .default_algorithms(DefaultAlgorithms::empty())
1324 .parse(&mut reader, ParseRule::STRICT)
1325 .unwrap();
1326
1327 let hostname = config.query("cross-platform").host_name.unwrap();
1328 assert_eq!(&hostname, "hostname.com");
1329
1330 assert!(config.query("this").host_name.is_none());
1331 }
1332
1333 #[test]
1334 fn should_update_host_bind_address() -> Result<(), SshParserError> {
1335 crate::test_log();
1336 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1337 SshConfigParser::update_host(
1338 Field::BindAddress,
1339 vec![String::from("127.0.0.1")],
1340 &mut host,
1341 ParseRule::ALLOW_UNKNOWN_FIELDS,
1342 &DefaultAlgorithms::empty(),
1343 )?;
1344 assert_eq!(host.params.bind_address.as_deref().unwrap(), "127.0.0.1");
1345 Ok(())
1346 }
1347
1348 #[test]
1349 fn should_update_host_bind_interface() -> Result<(), SshParserError> {
1350 crate::test_log();
1351 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1352 SshConfigParser::update_host(
1353 Field::BindInterface,
1354 vec![String::from("aaa")],
1355 &mut host,
1356 ParseRule::ALLOW_UNKNOWN_FIELDS,
1357 &DefaultAlgorithms::empty(),
1358 )?;
1359 assert_eq!(host.params.bind_interface.as_deref().unwrap(), "aaa");
1360 Ok(())
1361 }
1362
1363 #[test]
1364 fn should_update_host_ca_signature_algos() -> Result<(), SshParserError> {
1365 crate::test_log();
1366 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1367 SshConfigParser::update_host(
1368 Field::CaSignatureAlgorithms,
1369 vec![String::from("a,b,c")],
1370 &mut host,
1371 ParseRule::ALLOW_UNKNOWN_FIELDS,
1372 &DefaultAlgorithms::empty(),
1373 )?;
1374 assert_eq!(
1375 host.params.ca_signature_algorithms.algorithms(),
1376 &["a", "b", "c"]
1377 );
1378 Ok(())
1379 }
1380
1381 #[test]
1382 fn should_update_host_certificate_file() -> Result<(), SshParserError> {
1383 crate::test_log();
1384 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1385 SshConfigParser::update_host(
1386 Field::CertificateFile,
1387 vec![String::from("/tmp/a.crt")],
1388 &mut host,
1389 ParseRule::ALLOW_UNKNOWN_FIELDS,
1390 &DefaultAlgorithms::empty(),
1391 )?;
1392 assert_eq!(
1393 host.params.certificate_file.as_deref().unwrap(),
1394 Path::new("/tmp/a.crt")
1395 );
1396 Ok(())
1397 }
1398
1399 #[test]
1400 fn should_update_host_ciphers() -> Result<(), SshParserError> {
1401 crate::test_log();
1402 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1403 SshConfigParser::update_host(
1404 Field::Ciphers,
1405 vec![String::from("a,b,c")],
1406 &mut host,
1407 ParseRule::ALLOW_UNKNOWN_FIELDS,
1408 &DefaultAlgorithms::empty(),
1409 )?;
1410 assert_eq!(host.params.ciphers.algorithms(), &["a", "b", "c"]);
1411 Ok(())
1412 }
1413
1414 #[test]
1415 fn should_update_host_compression() -> Result<(), SshParserError> {
1416 crate::test_log();
1417 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1418 SshConfigParser::update_host(
1419 Field::Compression,
1420 vec![String::from("yes")],
1421 &mut host,
1422 ParseRule::ALLOW_UNKNOWN_FIELDS,
1423 &DefaultAlgorithms::empty(),
1424 )?;
1425 assert_eq!(host.params.compression.unwrap(), true);
1426 Ok(())
1427 }
1428
1429 #[test]
1430 fn should_update_host_connection_attempts() -> Result<(), SshParserError> {
1431 crate::test_log();
1432 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1433 SshConfigParser::update_host(
1434 Field::ConnectionAttempts,
1435 vec![String::from("4")],
1436 &mut host,
1437 ParseRule::ALLOW_UNKNOWN_FIELDS,
1438 &DefaultAlgorithms::empty(),
1439 )?;
1440 assert_eq!(host.params.connection_attempts.unwrap(), 4);
1441 Ok(())
1442 }
1443
1444 #[test]
1445 fn should_update_host_connection_timeout() -> Result<(), SshParserError> {
1446 crate::test_log();
1447 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1448 SshConfigParser::update_host(
1449 Field::ConnectTimeout,
1450 vec![String::from("10")],
1451 &mut host,
1452 ParseRule::ALLOW_UNKNOWN_FIELDS,
1453 &DefaultAlgorithms::empty(),
1454 )?;
1455 assert_eq!(
1456 host.params.connect_timeout.unwrap(),
1457 Duration::from_secs(10)
1458 );
1459 Ok(())
1460 }
1461
1462 #[test]
1463 fn should_update_host_key_algorithms() -> Result<(), SshParserError> {
1464 crate::test_log();
1465 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1466 SshConfigParser::update_host(
1467 Field::HostKeyAlgorithms,
1468 vec![String::from("a,b,c")],
1469 &mut host,
1470 ParseRule::ALLOW_UNKNOWN_FIELDS,
1471 &DefaultAlgorithms::empty(),
1472 )?;
1473 assert_eq!(
1474 host.params.host_key_algorithms.algorithms(),
1475 &["a", "b", "c"]
1476 );
1477 Ok(())
1478 }
1479
1480 #[test]
1481 fn should_update_host_host_name() -> Result<(), SshParserError> {
1482 crate::test_log();
1483 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1484 SshConfigParser::update_host(
1485 Field::HostName,
1486 vec![String::from("192.168.1.1")],
1487 &mut host,
1488 ParseRule::ALLOW_UNKNOWN_FIELDS,
1489 &DefaultAlgorithms::empty(),
1490 )?;
1491 assert_eq!(host.params.host_name.as_deref().unwrap(), "192.168.1.1");
1492 Ok(())
1493 }
1494
1495 #[test]
1496 fn should_update_host_ignore_unknown() -> Result<(), SshParserError> {
1497 crate::test_log();
1498 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1499 SshConfigParser::update_host(
1500 Field::IgnoreUnknown,
1501 vec![String::from("a,b,c")],
1502 &mut host,
1503 ParseRule::ALLOW_UNKNOWN_FIELDS,
1504 &DefaultAlgorithms::empty(),
1505 )?;
1506 assert_eq!(
1507 host.params.ignore_unknown.as_deref().unwrap(),
1508 &["a", "b", "c"]
1509 );
1510 Ok(())
1511 }
1512
1513 #[test]
1514 fn should_update_kex_algorithms() -> Result<(), SshParserError> {
1515 crate::test_log();
1516 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1517 SshConfigParser::update_host(
1518 Field::KexAlgorithms,
1519 vec![String::from("a,b,c")],
1520 &mut host,
1521 ParseRule::ALLOW_UNKNOWN_FIELDS,
1522 &DefaultAlgorithms::empty(),
1523 )?;
1524 assert_eq!(host.params.kex_algorithms.algorithms(), &["a", "b", "c"]);
1525 Ok(())
1526 }
1527
1528 #[test]
1529 fn should_update_host_mac() -> Result<(), SshParserError> {
1530 crate::test_log();
1531 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1532 SshConfigParser::update_host(
1533 Field::Mac,
1534 vec![String::from("a,b,c")],
1535 &mut host,
1536 ParseRule::ALLOW_UNKNOWN_FIELDS,
1537 &DefaultAlgorithms::empty(),
1538 )?;
1539 assert_eq!(host.params.mac.algorithms(), &["a", "b", "c"]);
1540 Ok(())
1541 }
1542
1543 #[test]
1544 fn should_update_host_port() -> Result<(), SshParserError> {
1545 crate::test_log();
1546 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1547 SshConfigParser::update_host(
1548 Field::Port,
1549 vec![String::from("2222")],
1550 &mut host,
1551 ParseRule::ALLOW_UNKNOWN_FIELDS,
1552 &DefaultAlgorithms::empty(),
1553 )?;
1554 assert_eq!(host.params.port.unwrap(), 2222);
1555 Ok(())
1556 }
1557
1558 #[test]
1559 fn should_update_host_pubkey_accepted_algos() -> Result<(), SshParserError> {
1560 crate::test_log();
1561 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1562 SshConfigParser::update_host(
1563 Field::PubkeyAcceptedAlgorithms,
1564 vec![String::from("a,b,c")],
1565 &mut host,
1566 ParseRule::ALLOW_UNKNOWN_FIELDS,
1567 &DefaultAlgorithms::empty(),
1568 )?;
1569 assert_eq!(
1570 host.params.pubkey_accepted_algorithms.algorithms(),
1571 &["a", "b", "c"]
1572 );
1573 Ok(())
1574 }
1575
1576 #[test]
1577 fn should_update_host_pubkey_authentication() -> Result<(), SshParserError> {
1578 crate::test_log();
1579 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1580 SshConfigParser::update_host(
1581 Field::PubkeyAuthentication,
1582 vec![String::from("yes")],
1583 &mut host,
1584 ParseRule::ALLOW_UNKNOWN_FIELDS,
1585 &DefaultAlgorithms::empty(),
1586 )?;
1587 assert_eq!(host.params.pubkey_authentication.unwrap(), true);
1588 Ok(())
1589 }
1590
1591 #[test]
1592 fn should_update_host_remote_forward() -> Result<(), SshParserError> {
1593 crate::test_log();
1594 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1595 SshConfigParser::update_host(
1596 Field::RemoteForward,
1597 vec![String::from("3005"), String::from("localhost:80")],
1598 &mut host,
1599 ParseRule::ALLOW_UNKNOWN_FIELDS,
1600 &DefaultAlgorithms::empty(),
1601 )?;
1602 SshConfigParser::update_host(
1603 Field::RemoteForward,
1604 vec![String::from("/tmp/remote.sock")],
1605 &mut host,
1606 ParseRule::ALLOW_UNKNOWN_FIELDS,
1607 &DefaultAlgorithms::empty(),
1608 )?;
1609 assert_eq!(
1610 host.params.remote_forward,
1611 vec![
1612 RemoteForward::new(
1613 RemoteForwardListen::Port(3005),
1614 Some(RemoteForwardDestination::Host {
1615 host: "localhost".to_string(),
1616 port: 80,
1617 }),
1618 ),
1619 RemoteForward::new(
1620 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote.sock")),
1621 None,
1622 ),
1623 ]
1624 );
1625 Ok(())
1626 }
1627
1628 #[test]
1629 fn should_accumulate_remote_forwards_from_matching_hosts() -> Result<(), SshParserError> {
1630 let config = r#"
1631Host test
1632 RemoteForward 8080 localhost:80
1633
1634Host *
1635 RemoteForward "/tmp/remote socket" "/tmp/local socket"
1636"#;
1637 let mut reader = BufReader::new(config.as_bytes());
1638 let config = SshConfig::default()
1639 .default_algorithms(DefaultAlgorithms::empty())
1640 .parse(&mut reader, ParseRule::STRICT)?;
1641
1642 assert_eq!(
1643 config.query("test").remote_forward,
1644 vec![
1645 RemoteForward::new(
1646 RemoteForwardListen::Port(8080),
1647 Some(RemoteForwardDestination::Host {
1648 host: "localhost".to_string(),
1649 port: 80,
1650 }),
1651 ),
1652 RemoteForward::new(
1653 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote socket")),
1654 Some(RemoteForwardDestination::UnixSocket(PathBuf::from(
1655 "/tmp/local socket",
1656 ))),
1657 ),
1658 ]
1659 );
1660 Ok(())
1661 }
1662
1663 #[test]
1664 fn should_update_host_server_alive_interval() -> Result<(), SshParserError> {
1665 crate::test_log();
1666 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1667 SshConfigParser::update_host(
1668 Field::ServerAliveInterval,
1669 vec![String::from("40")],
1670 &mut host,
1671 ParseRule::ALLOW_UNKNOWN_FIELDS,
1672 &DefaultAlgorithms::empty(),
1673 )?;
1674 assert_eq!(
1675 host.params.server_alive_interval.unwrap(),
1676 Duration::from_secs(40)
1677 );
1678 Ok(())
1679 }
1680
1681 #[test]
1682 fn should_update_host_tcp_keep_alive() -> Result<(), SshParserError> {
1683 crate::test_log();
1684 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1685 SshConfigParser::update_host(
1686 Field::TcpKeepAlive,
1687 vec![String::from("no")],
1688 &mut host,
1689 ParseRule::ALLOW_UNKNOWN_FIELDS,
1690 &DefaultAlgorithms::empty(),
1691 )?;
1692 assert_eq!(host.params.tcp_keep_alive.unwrap(), false);
1693 Ok(())
1694 }
1695
1696 #[test]
1697 fn should_update_host_user() -> Result<(), SshParserError> {
1698 crate::test_log();
1699 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1700 SshConfigParser::update_host(
1701 Field::User,
1702 vec![String::from("pippo")],
1703 &mut host,
1704 ParseRule::ALLOW_UNKNOWN_FIELDS,
1705 &DefaultAlgorithms::empty(),
1706 )?;
1707 assert_eq!(host.params.user.as_deref().unwrap(), "pippo");
1708 Ok(())
1709 }
1710
1711 #[test]
1712 fn should_not_update_host_if_unknown() -> Result<(), SshParserError> {
1713 crate::test_log();
1714 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1715 let result = SshConfigParser::update_host(
1716 Field::PasswordAuthentication,
1717 vec![String::from("yes")],
1718 &mut host,
1719 ParseRule::ALLOW_UNKNOWN_FIELDS,
1720 &DefaultAlgorithms::empty(),
1721 );
1722
1723 match result {
1724 Ok(_) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
1725 Err(e) => Err(e),
1726 }?;
1727
1728 assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1729 Ok(())
1730 }
1731
1732 #[test]
1733 fn should_update_host_if_unsupported() -> Result<(), SshParserError> {
1734 crate::test_log();
1735 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1736 let result = SshConfigParser::update_host(
1737 Field::PasswordAuthentication,
1738 vec![String::from("yes")],
1739 &mut host,
1740 ParseRule::ALLOW_UNKNOWN_FIELDS,
1741 &DefaultAlgorithms::empty(),
1742 );
1743
1744 match result {
1745 Err(SshParserError::UnsupportedField(field, _)) => {
1746 assert_eq!(field, "passwordauthentication");
1747 Ok(())
1748 }
1749 Ok(_) => Ok(()),
1750 Err(e) => Err(e),
1751 }?;
1752
1753 assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1754 Ok(())
1755 }
1756
1757 #[test]
1758 fn should_tokenize_line() -> Result<(), SshParserError> {
1759 crate::test_log();
1760 assert_eq!(
1761 SshConfigParser::tokenize_line("HostName 192.168.*.* 172.26.*.*")?,
1762 (
1763 Field::HostName,
1764 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1765 )
1766 );
1767 assert_eq!(
1769 SshConfigParser::tokenize_line(
1770 " HostName 192.168.*.* 172.26.*.* "
1771 )?,
1772 (
1773 Field::HostName,
1774 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1775 )
1776 );
1777 Ok(())
1778 }
1779
1780 #[test]
1781 fn should_not_tokenize_line() {
1782 crate::test_log();
1783 assert!(matches!(
1784 SshConfigParser::tokenize_line("Omar yes").unwrap_err(),
1785 SshParserError::UnknownField(..)
1786 ));
1787 }
1788
1789 #[test]
1790 fn should_fail_parsing_field() {
1791 crate::test_log();
1792
1793 assert!(matches!(
1794 SshConfigParser::tokenize_line(" ").unwrap_err(),
1795 SshParserError::MissingArgument
1796 ));
1797 }
1798
1799 #[test]
1800 fn should_fail_on_mismatched_quotes() {
1801 crate::test_log();
1802
1803 assert!(matches!(
1805 SshConfigParser::tokenize_line(r#"Hostname "example.com"#).unwrap_err(),
1806 SshParserError::InvalidQuotes
1807 ));
1808 assert!(matches!(
1810 SshConfigParser::tokenize_line(r#"Hostname example.com""#).unwrap_err(),
1811 SshParserError::InvalidQuotes
1812 ));
1813 assert!(matches!(
1815 SshConfigParser::tokenize_line(r#"Hostname foo "bar"#).unwrap_err(),
1816 SshParserError::InvalidQuotes
1817 ));
1818 }
1819
1820 #[test]
1821 fn should_parse_boolean() -> Result<(), SshParserError> {
1822 crate::test_log();
1823 assert_eq!(
1824 SshConfigParser::parse_boolean(vec![String::from("yes")])?,
1825 true
1826 );
1827 assert_eq!(
1828 SshConfigParser::parse_boolean(vec![String::from("no")])?,
1829 false
1830 );
1831 Ok(())
1832 }
1833
1834 #[test]
1835 fn should_parse_boolean_true_false_case_insensitive() -> Result<(), SshParserError> {
1836 crate::test_log();
1837 for value in ["true", "True", "YES", "Yes"] {
1838 assert!(SshConfigParser::parse_boolean(vec![value.to_string()])?);
1839 }
1840 for value in ["false", "False", "NO", "No"] {
1841 assert!(!SshConfigParser::parse_boolean(vec![value.to_string()])?);
1842 }
1843 Ok(())
1844 }
1845
1846 #[test]
1847 fn should_fail_parsing_boolean() {
1848 crate::test_log();
1849 assert!(matches!(
1850 SshConfigParser::parse_boolean(vec!["boh".to_string()]).unwrap_err(),
1851 SshParserError::ExpectedBoolean
1852 ));
1853 assert!(matches!(
1854 SshConfigParser::parse_boolean(vec![]).unwrap_err(),
1855 SshParserError::MissingArgument
1856 ));
1857 }
1858
1859 #[test]
1860 fn should_parse_algos() -> Result<(), SshParserError> {
1861 crate::test_log();
1862 assert_eq!(
1863 SshConfigParser::parse_algos(vec![String::from("a,b,c,d")])?,
1864 AlgorithmsRule::Set(vec![
1865 "a".to_string(),
1866 "b".to_string(),
1867 "c".to_string(),
1868 "d".to_string(),
1869 ])
1870 );
1871
1872 assert_eq!(
1873 SshConfigParser::parse_algos(vec![String::from("a")])?,
1874 AlgorithmsRule::Set(vec!["a".to_string()])
1875 );
1876
1877 assert_eq!(
1878 SshConfigParser::parse_algos(vec![String::from("+a,b")])?,
1879 AlgorithmsRule::Append(vec!["a".to_string(), "b".to_string()])
1880 );
1881
1882 Ok(())
1883 }
1884
1885 #[test]
1886 fn should_parse_comma_separated_list() -> Result<(), SshParserError> {
1887 crate::test_log();
1888 assert_eq!(
1889 SshConfigParser::parse_comma_separated_list(vec![String::from("a,b,c,d")])?,
1890 vec![
1891 "a".to_string(),
1892 "b".to_string(),
1893 "c".to_string(),
1894 "d".to_string(),
1895 ]
1896 );
1897 assert_eq!(
1898 SshConfigParser::parse_comma_separated_list(vec![String::from("a")])?,
1899 vec!["a".to_string()]
1900 );
1901 Ok(())
1902 }
1903
1904 #[test]
1905 fn should_fail_parsing_comma_separated_list() {
1906 crate::test_log();
1907 assert!(matches!(
1908 SshConfigParser::parse_comma_separated_list(vec![]).unwrap_err(),
1909 SshParserError::MissingArgument
1910 ));
1911 }
1912
1913 #[test]
1914 fn should_parse_duration() -> Result<(), SshParserError> {
1915 crate::test_log();
1916 assert_eq!(
1917 SshConfigParser::parse_duration(vec![String::from("60")])?,
1918 Duration::from_secs(60)
1919 );
1920 Ok(())
1921 }
1922
1923 #[test]
1924 fn should_fail_parsing_duration() {
1925 crate::test_log();
1926 assert!(matches!(
1927 SshConfigParser::parse_duration(vec![String::from("AAA")]).unwrap_err(),
1928 SshParserError::ExpectedUnsigned
1929 ));
1930 assert!(matches!(
1931 SshConfigParser::parse_duration(vec![]).unwrap_err(),
1932 SshParserError::MissingArgument
1933 ));
1934 }
1935
1936 #[test]
1937 fn should_parse_host() -> Result<(), SshParserError> {
1938 crate::test_log();
1939 assert_eq!(
1940 SshConfigParser::parse_host(vec![
1941 String::from("192.168.*.*"),
1942 String::from("!192.168.1.1"),
1943 String::from("172.26.104.*"),
1944 String::from("!172.26.104.10"),
1945 ])?,
1946 vec![
1947 HostClause::new(String::from("192.168.*.*"), false),
1948 HostClause::new(String::from("192.168.1.1"), true),
1949 HostClause::new(String::from("172.26.104.*"), false),
1950 HostClause::new(String::from("172.26.104.10"), true),
1951 ]
1952 );
1953 Ok(())
1954 }
1955
1956 #[test]
1957 fn should_fail_parsing_host() {
1958 crate::test_log();
1959 assert!(matches!(
1960 SshConfigParser::parse_host(vec![]).unwrap_err(),
1961 SshParserError::MissingArgument
1962 ));
1963 }
1964
1965 #[test]
1966 fn should_parse_path() -> Result<(), SshParserError> {
1967 crate::test_log();
1968 assert_eq!(
1969 SshConfigParser::parse_path(vec![String::from("/tmp/a.txt")])?,
1970 PathBuf::from("/tmp/a.txt")
1971 );
1972 Ok(())
1973 }
1974
1975 #[test]
1976 fn should_parse_path_and_resolve_tilde() -> Result<(), SshParserError> {
1977 crate::test_log();
1978 let mut expected = dirs::home_dir().unwrap();
1979 expected.push(".ssh/id_dsa");
1980 assert_eq!(
1981 SshConfigParser::parse_path(vec![String::from("~/.ssh/id_dsa")])?,
1982 expected
1983 );
1984 Ok(())
1985 }
1986
1987 #[test]
1988 fn should_parse_path_list() -> Result<(), SshParserError> {
1989 crate::test_log();
1990 assert_eq!(
1991 SshConfigParser::parse_path_list(vec![
1992 String::from("/tmp/a.txt"),
1993 String::from("/tmp/b.txt")
1994 ])?,
1995 vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")]
1996 );
1997 Ok(())
1998 }
1999
2000 #[test]
2001 fn should_fail_parse_path_list() {
2002 crate::test_log();
2003 assert!(matches!(
2004 SshConfigParser::parse_path_list(vec![]).unwrap_err(),
2005 SshParserError::MissingArgument
2006 ));
2007 }
2008
2009 #[test]
2010 fn should_fail_parsing_path() {
2011 crate::test_log();
2012 assert!(matches!(
2013 SshConfigParser::parse_path(vec![]).unwrap_err(),
2014 SshParserError::MissingArgument
2015 ));
2016 }
2017
2018 #[test]
2019 fn should_parse_port() -> Result<(), SshParserError> {
2020 crate::test_log();
2021 assert_eq!(SshConfigParser::parse_port(vec![String::from("22")])?, 22);
2022 Ok(())
2023 }
2024
2025 #[test]
2026 fn should_fail_parsing_port() {
2027 crate::test_log();
2028 assert!(matches!(
2029 SshConfigParser::parse_port(vec![String::from("1234567")]).unwrap_err(),
2030 SshParserError::ExpectedPort
2031 ));
2032 assert!(matches!(
2033 SshConfigParser::parse_port(vec![]).unwrap_err(),
2034 SshParserError::MissingArgument
2035 ));
2036 }
2037
2038 #[test]
2039 fn should_parse_string() -> Result<(), SshParserError> {
2040 crate::test_log();
2041 assert_eq!(
2042 SshConfigParser::parse_string(vec![String::from("foobar")])?,
2043 String::from("foobar")
2044 );
2045 Ok(())
2046 }
2047
2048 #[test]
2049 fn should_fail_parsing_string() {
2050 crate::test_log();
2051 assert!(matches!(
2052 SshConfigParser::parse_string(vec![]).unwrap_err(),
2053 SshParserError::MissingArgument
2054 ));
2055 }
2056
2057 #[test]
2058 fn should_parse_unsigned() -> Result<(), SshParserError> {
2059 crate::test_log();
2060 assert_eq!(
2061 SshConfigParser::parse_unsigned(vec![String::from("43")])?,
2062 43
2063 );
2064 Ok(())
2065 }
2066
2067 #[test]
2068 fn should_fail_parsing_unsigned() {
2069 crate::test_log();
2070 assert!(matches!(
2071 SshConfigParser::parse_unsigned(vec![String::from("abc")]).unwrap_err(),
2072 SshParserError::ExpectedUnsigned
2073 ));
2074 assert!(matches!(
2075 SshConfigParser::parse_unsigned(vec![]).unwrap_err(),
2076 SshParserError::MissingArgument
2077 ));
2078 }
2079
2080 #[test]
2081 fn should_strip_comments() {
2082 crate::test_log();
2083
2084 assert_eq!(
2085 SshConfigParser::strip_comments("host my_host # this is my fav host").as_str(),
2086 "host my_host "
2087 );
2088 assert_eq!(
2089 SshConfigParser::strip_comments("# this is a comment").as_str(),
2090 ""
2091 );
2092 }
2093
2094 #[test]
2095 fn should_preserve_hash_inside_quoted_strings() {
2096 crate::test_log();
2097
2098 assert_eq!(
2100 SshConfigParser::strip_comments(r#"Ciphers "aes256-ctr # not a comment""#).as_str(),
2101 r#"Ciphers "aes256-ctr # not a comment""#
2102 );
2103 assert_eq!(
2105 SshConfigParser::strip_comments(r#"Ciphers "aes256-ctr" # this is a comment"#).as_str(),
2106 r#"Ciphers "aes256-ctr" "#
2107 );
2108 assert_eq!(
2110 SshConfigParser::strip_comments(r#"ProxyCommand "ssh # hop" -W "dest # host""#)
2111 .as_str(),
2112 r#"ProxyCommand "ssh # hop" -W "dest # host""#
2113 );
2114 assert_eq!(
2116 SshConfigParser::strip_comments(r#"Key "val1" "val2" # comment"#).as_str(),
2117 r#"Key "val1" "val2" "#
2118 );
2119 }
2120
2121 #[test]
2122 fn test_should_parse_config_with_quotes_and_eq() {
2123 crate::test_log();
2124
2125 let config = create_ssh_config_with_quotes_and_eq();
2126 let file = File::open(config.path()).expect("Failed to open tempfile");
2127 let mut reader = BufReader::new(file);
2128
2129 let config = SshConfig::default()
2130 .default_algorithms(DefaultAlgorithms::empty())
2131 .parse(&mut reader, ParseRule::STRICT)
2132 .expect("Failed to parse config");
2133
2134 let params = config.query("foo");
2135
2136 assert_eq!(
2138 params.connect_timeout.expect("unspec connect timeout"),
2139 Duration::from_secs(15)
2140 );
2141 assert_eq!(
2142 params
2143 .ignore_unknown
2144 .as_deref()
2145 .expect("unspec ignore unknown"),
2146 &["Pippo", "Pluto"]
2147 );
2148 assert_eq!(
2149 params
2150 .ciphers
2151 .algorithms()
2152 .iter()
2153 .map(|x| x.as_str())
2154 .collect::<Vec<&str>>(),
2155 &["Pepperoni Pizza", "Margherita Pizza", "Hawaiian Pizza"]
2156 );
2157 assert_eq!(
2158 params
2159 .mac
2160 .algorithms()
2161 .iter()
2162 .map(|x| x.as_str())
2163 .collect::<Vec<&str>>(),
2164 &["Pasta Carbonara", "Pasta con tonno"]
2165 );
2166 }
2167
2168 #[test]
2169 fn test_should_resolve_absolute_include_path() {
2170 crate::test_log();
2171
2172 let expected = PathBuf::from("/tmp/config.local");
2173
2174 let s = "/tmp/config.local";
2175 let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
2176 assert_eq!(resolved, expected);
2177 }
2178
2179 #[test]
2180 fn test_should_resolve_relative_include_path() {
2181 crate::test_log();
2182
2183 let expected = dirs::home_dir()
2184 .unwrap_or_else(|| PathBuf::from("~"))
2185 .join(".ssh")
2186 .join("config.local");
2187
2188 let s = "config.local";
2189 let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
2190 assert_eq!(resolved, expected);
2191 }
2192
2193 #[test]
2194 fn test_should_resolve_include_path_with_tilde() {
2195 let p = "~/.ssh/config.local";
2196 let resolved = SshConfigParser::resolve_include_path(p);
2197 let mut expected = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
2198 expected.push(".ssh");
2199 expected.push("config.local");
2200 assert_eq!(PathBuf::from(resolved), expected);
2201 }
2202
2203 #[test]
2204 fn should_fail_parsing_algos_missing_arg() {
2205 crate::test_log();
2206 assert!(matches!(
2207 SshConfigParser::parse_algos(vec![]).unwrap_err(),
2208 SshParserError::MissingArgument
2209 ));
2210 }
2211
2212 #[test]
2213 fn should_parse_duration_zero() {
2214 crate::test_log();
2215 assert_eq!(
2216 SshConfigParser::parse_duration(vec![String::from("0")]).unwrap(),
2217 Duration::from_secs(0)
2218 );
2219 }
2220
2221 #[test]
2222 fn should_parse_port_boundary() {
2223 crate::test_log();
2224 assert_eq!(
2226 SshConfigParser::parse_port(vec![String::from("1")]).unwrap(),
2227 1
2228 );
2229 assert_eq!(
2231 SshConfigParser::parse_port(vec![String::from("65535")]).unwrap(),
2232 65535
2233 );
2234 }
2235
2236 #[test]
2237 fn should_update_host_add_keys_to_agent() {
2238 crate::test_log();
2239 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2240 SshConfigParser::update_host(
2241 Field::AddKeysToAgent,
2242 vec![String::from("yes")],
2243 &mut host,
2244 ParseRule::STRICT,
2245 &DefaultAlgorithms::empty(),
2246 )
2247 .unwrap();
2248 assert_eq!(host.params.add_keys_to_agent.unwrap(), true);
2249
2250 let mut host2 = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2251 SshConfigParser::update_host(
2252 Field::AddKeysToAgent,
2253 vec![String::from("no")],
2254 &mut host2,
2255 ParseRule::STRICT,
2256 &DefaultAlgorithms::empty(),
2257 )
2258 .unwrap();
2259 assert_eq!(host2.params.add_keys_to_agent.unwrap(), false);
2260 }
2261
2262 #[test]
2263 fn should_update_host_forward_agent() {
2264 crate::test_log();
2265 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2266 SshConfigParser::update_host(
2267 Field::ForwardAgent,
2268 vec![String::from("yes")],
2269 &mut host,
2270 ParseRule::STRICT,
2271 &DefaultAlgorithms::empty(),
2272 )
2273 .unwrap();
2274 assert_eq!(host.params.forward_agent.unwrap(), true);
2275 }
2276
2277 #[test]
2278 fn should_update_host_proxy_jump() {
2279 crate::test_log();
2280 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2281 SshConfigParser::update_host(
2282 Field::ProxyJump,
2283 vec![String::from("jump1,jump2,jump3")],
2284 &mut host,
2285 ParseRule::STRICT,
2286 &DefaultAlgorithms::empty(),
2287 )
2288 .unwrap();
2289 assert_eq!(
2290 host.params.proxy_jump.unwrap(),
2291 vec![
2292 "jump1".to_string(),
2293 "jump2".to_string(),
2294 "jump3".to_string()
2295 ]
2296 );
2297 }
2298
2299 #[test]
2300 fn should_update_host_identity_file() {
2301 crate::test_log();
2302 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2303 SshConfigParser::update_host(
2304 Field::IdentityFile,
2305 vec![String::from("/path/to/key1"), String::from("/path/to/key2")],
2306 &mut host,
2307 ParseRule::STRICT,
2308 &DefaultAlgorithms::empty(),
2309 )
2310 .unwrap();
2311 assert_eq!(
2312 host.params.identity_file.unwrap(),
2313 vec![
2314 PathBuf::from("/path/to/key1"),
2315 PathBuf::from("/path/to/key2")
2316 ]
2317 );
2318 }
2319
2320 #[test]
2321 fn test_should_allow_and_append_multiple_identity_files_directives() {
2322 crate::test_log();
2323 let config = r##"
2324Host test
2325 IdentityFile /path/to/key1 /path/to/key2
2326 IdentityFile /path/to/key3
2327"##;
2328 let mut reader = BufReader::new(config.as_bytes());
2329 let config = SshConfig::default()
2330 .default_algorithms(DefaultAlgorithms::empty())
2331 .parse(&mut reader, ParseRule::STRICT)
2332 .expect("Failed to parse config");
2333
2334 let params = config.query("test");
2335 assert_eq!(
2336 params.identity_file.as_ref().unwrap(),
2337 &vec![
2338 PathBuf::from("/path/to/key1"),
2339 PathBuf::from("/path/to/key2"),
2340 PathBuf::from("/path/to/key3"),
2341 ]
2342 );
2343 }
2344
2345 #[test]
2346 fn test_should_accumulate_identity_files_across_host_blocks() {
2347 crate::test_log();
2348 let config = r##"
2349Host test
2350 IdentityFile /path/to/specific_key
2351
2352Host *
2353 IdentityFile /path/to/default_key
2354"##;
2355 let mut reader = BufReader::new(config.as_bytes());
2356 let config = SshConfig::default()
2357 .default_algorithms(DefaultAlgorithms::empty())
2358 .parse(&mut reader, ParseRule::STRICT)
2359 .expect("Failed to parse config");
2360
2361 let params = config.query("test");
2362 assert_eq!(
2364 params.identity_file.as_ref().unwrap(),
2365 &vec![
2366 PathBuf::from("/path/to/specific_key"),
2367 PathBuf::from("/path/to/default_key"),
2368 ]
2369 );
2370 }
2371
2372 #[test]
2373 fn should_store_unsupported_fields_when_allowed() {
2374 crate::test_log();
2375
2376 let config = r##"
2377Host test
2378 PasswordAuthentication yes
2379"##;
2380 let mut reader = BufReader::new(config.as_bytes());
2381 let config = SshConfig::default()
2382 .default_algorithms(DefaultAlgorithms::empty())
2383 .parse(&mut reader, ParseRule::ALLOW_UNSUPPORTED_FIELDS)
2384 .unwrap();
2385
2386 let params = config.query("test");
2387 assert!(
2388 params
2389 .unsupported_fields
2390 .contains_key("passwordauthentication")
2391 );
2392 }
2393
2394 #[test]
2395 fn should_tokenize_line_with_equals_separator() {
2396 crate::test_log();
2397 let (field, args) = SshConfigParser::tokenize_line("HostName=example.com").unwrap();
2398 assert_eq!(field, Field::HostName);
2399 assert_eq!(args, vec!["example.com".to_string()]);
2400 }
2401
2402 #[test]
2403 fn should_tokenize_line_with_quoted_args() {
2404 crate::test_log();
2405 let (field, args) =
2406 SshConfigParser::tokenize_line("Ciphers \"aes256-ctr,aes128-ctr\"").unwrap();
2407 assert_eq!(field, Field::Ciphers);
2408 assert_eq!(args, vec!["aes256-ctr,aes128-ctr".to_string()]);
2409 }
2410
2411 #[test]
2412 fn should_tokenize_line_with_equals_and_quoted_args() {
2413 crate::test_log();
2414 let (field, args) =
2415 SshConfigParser::tokenize_line("Ciphers=\"aes256-ctr,aes128-ctr\"").unwrap();
2416 assert_eq!(field, Field::Ciphers);
2417 assert_eq!(args, vec!["aes256-ctr,aes128-ctr".to_string()]);
2418 }
2419
2420 #[test]
2421 fn should_unescape_quoted_args() {
2422 crate::test_log();
2423
2424 let (field, args) =
2426 SshConfigParser::tokenize_line(r#"HostName "gateway\"server""#).unwrap();
2427 assert_eq!(field, Field::HostName);
2428 assert_eq!(args, vec![r#"gateway"server"#.to_string()]);
2429
2430 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "path\\to\\host""#).unwrap();
2432 assert_eq!(field, Field::HostName);
2433 assert_eq!(args, vec![r#"path\to\host"#.to_string()]);
2434
2435 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "it\'s a test""#).unwrap();
2437 assert_eq!(field, Field::HostName);
2438 assert_eq!(args, vec!["it's a test".to_string()]);
2439
2440 let (field, args) =
2442 SshConfigParser::tokenize_line(r#"HostName "say \"hello\" and \\go""#).unwrap();
2443 assert_eq!(field, Field::HostName);
2444 assert_eq!(args, vec![r#"say "hello" and \go"#.to_string()]);
2445
2446 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "test\nvalue""#).unwrap();
2448 assert_eq!(field, Field::HostName);
2449 assert_eq!(args, vec![r#"test\nvalue"#.to_string()]);
2450 }
2451
2452 #[test]
2453 fn should_tokenize_line_setenv() -> Result<(), SshParserError> {
2454 crate::test_log();
2455 assert_eq!(
2456 SshConfigParser::tokenize_line(
2457 r#"SetEnv TEST_1=Test1 TEST_2="Test 2" TEST_3="Test \"3\"" TEST_4=Test"4""#
2458 )?,
2459 (
2460 Field::SetEnv,
2461 vec![
2462 r#"TEST_1=Test1"#.to_owned(),
2463 r#"TEST_2="Test 2""#.to_owned(),
2464 r#"TEST_3="Test \"3\"""#.to_owned(),
2465 r#"TEST_4=Test"4""#.to_owned(),
2466 ]
2467 )
2468 );
2469 Ok(())
2470 }
2471
2472 #[test]
2473 fn should_count_unescaped_quotes() {
2474 crate::test_log();
2475
2476 assert_eq!(SshConfigParser::count_unescaped_quotes("hello"), 0);
2478
2479 assert_eq!(SshConfigParser::count_unescaped_quotes(r#""hello""#), 2);
2481
2482 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\"hello\""#), 0);
2484
2485 assert_eq!(
2487 SshConfigParser::count_unescaped_quotes(r#""hello\"world""#),
2488 2
2489 );
2490
2491 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\\""#), 1);
2493
2494 assert_eq!(SshConfigParser::count_unescaped_quotes(""), 0);
2496
2497 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\""#), 0);
2499 }
2500
2501 #[test]
2502 fn should_detect_ends_with_unescaped_quote() {
2503 crate::test_log();
2504
2505 assert!(SshConfigParser::ends_with_unescaped_quote(r#""hello""#));
2507
2508 assert!(!SshConfigParser::ends_with_unescaped_quote(r#""hello\""#));
2510
2511 assert!(SshConfigParser::ends_with_unescaped_quote(r#""hello\\""#));
2513
2514 assert!(!SshConfigParser::ends_with_unescaped_quote(r#""hello\\\""#));
2516
2517 assert!(!SshConfigParser::ends_with_unescaped_quote("hello"));
2519
2520 assert!(SshConfigParser::ends_with_unescaped_quote(r#"""#));
2522
2523 assert!(!SshConfigParser::ends_with_unescaped_quote(r#"\""#));
2525 }
2526
2527 #[test]
2528 fn should_unescape_string() {
2529 crate::test_log();
2530
2531 assert_eq!(
2533 SshConfigParser::unescape_string(r#"hello\"world"#),
2534 r#"hello"world"#
2535 );
2536
2537 assert_eq!(
2539 SshConfigParser::unescape_string(r#"path\\to\\file"#),
2540 r#"path\to\file"#
2541 );
2542
2543 assert_eq!(SshConfigParser::unescape_string(r#"it\'s"#), "it's");
2545
2546 assert_eq!(
2548 SshConfigParser::unescape_string(r#"say \"hi\" and \\go"#),
2549 r#"say "hi" and \go"#
2550 );
2551
2552 assert_eq!(
2554 SshConfigParser::unescape_string(r#"test\nvalue"#),
2555 r#"test\nvalue"#
2556 );
2557
2558 assert_eq!(SshConfigParser::unescape_string("plain text"), "plain text");
2560
2561 assert_eq!(SshConfigParser::unescape_string(""), "");
2563
2564 assert_eq!(SshConfigParser::unescape_string(r#"test\"#), r#"test\"#);
2566
2567 assert_eq!(SshConfigParser::unescape_string(r#"\\\\"#), r#"\\"#);
2569 }
2570
2571 #[test]
2572 fn should_parse_host_with_single_pattern() {
2573 crate::test_log();
2574 let result = SshConfigParser::parse_host(vec![String::from("example.com")]).unwrap();
2575 assert_eq!(result.len(), 1);
2576 assert_eq!(result[0].pattern, "example.com");
2577 assert!(!result[0].negated);
2578 }
2579
2580 #[test]
2581 fn should_parse_host_with_exclamation_in_pattern() {
2582 crate::test_log();
2583
2584 let result = SshConfigParser::parse_host(vec![String::from("host!name")]).unwrap();
2586 assert_eq!(result.len(), 1);
2587 assert_eq!(result[0].pattern, "host!name");
2588 assert!(!result[0].negated);
2589
2590 let result = SshConfigParser::parse_host(vec![String::from("!host!name")]).unwrap();
2592 assert_eq!(result.len(), 1);
2593 assert_eq!(result[0].pattern, "host!name");
2594 assert!(result[0].negated);
2595
2596 let result = SshConfigParser::parse_host(vec![String::from("!a!b!c")]).unwrap();
2598 assert_eq!(result.len(), 1);
2599 assert_eq!(result[0].pattern, "a!b!c");
2600 assert!(result[0].negated);
2601
2602 let result = SshConfigParser::parse_host(vec![String::from("a!b")]).unwrap();
2604 assert_eq!(result.len(), 1);
2605 assert_eq!(result[0].pattern, "a!b");
2606 assert!(!result[0].negated);
2607 }
2608
2609 #[cfg(target_os = "macos")]
2610 #[test]
2611 fn should_update_host_use_keychain() {
2612 crate::test_log();
2613 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2614 SshConfigParser::update_host(
2615 Field::UseKeychain,
2616 vec![String::from("yes")],
2617 &mut host,
2618 ParseRule::STRICT,
2619 &DefaultAlgorithms::empty(),
2620 )
2621 .unwrap();
2622 assert_eq!(host.params.use_keychain.unwrap(), true);
2623 }
2624
2625 fn create_ssh_config_with_quotes_and_eq() -> NamedTempFile {
2626 let mut tmpfile: tempfile::NamedTempFile =
2627 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2628 let config = r##"
2629# ssh config
2630# written by veeso
2631
2632
2633# I put a comment here just to annoy
2634
2635IgnoreUnknown=Pippo,Pluto
2636ConnectTimeout = 15
2637Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza"
2638Macs="Pasta Carbonara,Pasta con tonno"
2639"##;
2640 tmpfile.write_all(config.as_bytes()).unwrap();
2641 tmpfile
2642 }
2643
2644 fn create_ssh_config() -> NamedTempFile {
2645 let mut tmpfile: tempfile::NamedTempFile =
2646 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2647 let config = r##"
2648# ssh config
2649# written by veeso
2650
2651
2652 # I put a comment here just to annoy
2653
2654IgnoreUnknown Pippo,Pluto
2655
2656Compression yes
2657ConnectionAttempts 10
2658ConnectTimeout 60
2659ServerAliveInterval 40
2660TcpKeepAlive yes
2661Ciphers +a-manella,blowfish
2662
2663# Let's start defining some hosts
2664
2665Host 192.168.*.* 172.26.*.* !192.168.1.30
2666 User omar
2667 # ForwardX11 is actually not supported; I just want to see that it wont' fail parsing
2668 ForwardX11 yes
2669 BindAddress 10.8.0.10
2670 BindInterface tun0
2671 AddKeysToAgent yes
2672 Ciphers +coi-piedi,cazdecan,triestin-stretto
2673 IdentityFile /home/root/.ssh/pippo.key /home/root/.ssh/pluto.key
2674 Macs spyro,deoxys
2675 Port 2222
2676 PubkeyAcceptedAlgorithms -omar-crypt
2677 ProxyJump jump.example.com
2678
2679Host tostapane
2680 User ciro-esposito
2681 HostName 192.168.24.32
2682 RemoteForward 88
2683 Compression no
2684 Pippo yes
2685 Pluto 56
2686 ProxyJump jump1.example.com,jump2.example.com
2687 Macs +spyro,deoxys
2688
2689Host 192.168.1.30
2690 User nutellaro
2691 RemoteForward 123
2692
2693Host *
2694 CaSignatureAlgorithms random
2695 HostKeyAlgorithms luigi,mario
2696 KexAlgorithms desu,gigi
2697 Macs concorde
2698 PubkeyAcceptedAlgorithms desu,omar-crypt,fast-omar-crypt
2699"##;
2700 tmpfile.write_all(config.as_bytes()).unwrap();
2701 tmpfile
2702 }
2703
2704 fn create_inverted_ssh_config() -> NamedTempFile {
2705 let mut tmpfile: tempfile::NamedTempFile =
2706 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2707 let config = r##"
2708Host *-host
2709 IdentityFile ~/.ssh/id_rsa_good
2710
2711Host remote-*
2712 HostName hostname.com
2713 User user
2714 IdentityFile ~/.ssh/id_rsa_bad
2715
2716Host *
2717 ConnectTimeout 15
2718 IdentityFile ~/.ssh/id_rsa_ugly
2719 "##;
2720 tmpfile.write_all(config.as_bytes()).unwrap();
2721 tmpfile
2722 }
2723
2724 fn create_ssh_config_with_comments() -> NamedTempFile {
2725 let mut tmpfile: tempfile::NamedTempFile =
2726 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2727 let config = r##"
2728Host cross-platform # this is my fav host
2729 HostName hostname.com
2730 User user
2731 IdentityFile ~/.ssh/id_rsa_good
2732
2733Host *
2734 AddKeysToAgent yes
2735 IdentityFile ~/.ssh/id_rsa_bad
2736 "##;
2737 tmpfile.write_all(config.as_bytes()).unwrap();
2738 tmpfile
2739 }
2740
2741 fn create_ssh_config_with_unknown_fields() -> NamedTempFile {
2742 let mut tmpfile: tempfile::NamedTempFile =
2743 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2744 let config = r##"
2745Host cross-platform # this is my fav host
2746 HostName hostname.com
2747 User user
2748 IdentityFile ~/.ssh/id_rsa_good
2749 Piropero yes
2750
2751Host *
2752 AddKeysToAgent yes
2753 IdentityFile ~/.ssh/id_rsa_bad
2754 "##;
2755 tmpfile.write_all(config.as_bytes()).unwrap();
2756 tmpfile
2757 }
2758
2759 #[test]
2760 fn test_should_parse_config_with_include() {
2761 crate::test_log();
2762
2763 let config = create_include_config();
2764 let file = File::open(config.config.path()).expect("Failed to open tempfile");
2765 let mut reader = BufReader::new(file);
2766
2767 let config = SshConfig::default()
2768 .default_algorithms(DefaultAlgorithms::empty())
2769 .parse(&mut reader, ParseRule::STRICT)
2770 .expect("Failed to parse config");
2771
2772 let default_params = config.query("unknown-host");
2773 assert_eq!(
2775 default_params.connect_timeout.unwrap(),
2776 Duration::from_secs(60) );
2778 assert_eq!(
2779 default_params.server_alive_interval.unwrap(),
2780 Duration::from_secs(40) );
2782 assert_eq!(default_params.tcp_keep_alive.unwrap(), true);
2783 assert_eq!(default_params.ciphers.algorithms().is_empty(), true);
2784 assert_eq!(
2785 default_params.ignore_unknown.as_deref().unwrap(),
2786 &["Pippo", "Pluto"]
2787 );
2788 assert_eq!(default_params.compression.unwrap(), true);
2789 assert_eq!(default_params.connection_attempts.unwrap(), 10);
2790 assert_eq!(default_params.port, Some(345));
2791
2792 let glob_params = config.query("192.168.1.1");
2794 assert_eq!(
2795 glob_params.connect_timeout.unwrap(),
2796 Duration::from_secs(60)
2797 );
2798 assert_eq!(
2799 glob_params.server_alive_interval.unwrap(),
2800 Duration::from_secs(40) );
2802 assert_eq!(glob_params.tcp_keep_alive.unwrap(), true);
2803 assert_eq!(glob_params.ciphers.algorithms().is_empty(), true);
2804
2805 let tostapane_params = config.query("tostapane");
2807 assert_eq!(
2808 tostapane_params.connect_timeout.unwrap(),
2809 Duration::from_secs(60) );
2811 assert_eq!(
2812 tostapane_params.server_alive_interval.unwrap(),
2813 Duration::from_secs(40) );
2815 assert_eq!(tostapane_params.tcp_keep_alive.unwrap(), true);
2816 assert_eq!(
2818 tostapane_params.ciphers.algorithms(),
2819 &[
2820 "a-manella",
2821 "blowfish",
2822 "coi-piedi",
2823 "cazdecan",
2824 "triestin-stretto"
2825 ]
2826 );
2827
2828 let microwave_params = config.query("microwave");
2830 assert_eq!(
2831 microwave_params.connect_timeout.unwrap(),
2832 Duration::from_secs(60) );
2834 assert_eq!(
2835 microwave_params.server_alive_interval.unwrap(),
2836 Duration::from_secs(40) );
2838 assert_eq!(
2839 microwave_params.port.unwrap(),
2840 345 );
2842 assert_eq!(microwave_params.tcp_keep_alive.unwrap(), true);
2843 assert_eq!(microwave_params.ciphers.algorithms().is_empty(), true);
2844 assert_eq!(microwave_params.user.as_deref().unwrap(), "mario-rossi");
2845 assert_eq!(
2846 microwave_params.host_name.as_deref().unwrap(),
2847 "192.168.24.33"
2848 );
2849 assert_eq!(
2850 microwave_params.remote_forward,
2851 vec![RemoteForward::new(RemoteForwardListen::Port(88), None)]
2852 );
2853 assert_eq!(microwave_params.compression.unwrap(), true);
2854
2855 let fridge_params = config.query("fridge");
2857 assert_eq!(
2858 fridge_params.connect_timeout.unwrap(),
2859 Duration::from_secs(60)
2860 ); assert_eq!(
2862 fridge_params.server_alive_interval.unwrap(),
2863 Duration::from_secs(40)
2864 ); assert_eq!(fridge_params.tcp_keep_alive.unwrap(), true);
2866 assert_eq!(fridge_params.ciphers.algorithms().is_empty(), true);
2867 assert_eq!(fridge_params.user.as_deref().unwrap(), "luigi-verdi");
2868 assert_eq!(fridge_params.host_name.as_deref().unwrap(), "192.168.24.34");
2869 }
2870
2871 #[test]
2872 fn should_restore_parent_scope_after_include() -> Result<(), SshParserError> {
2873 let mut included = NamedTempFile::new().expect("Failed to create included file");
2874 included.write_all(b"Host foo\n HostName foo.example\n")?;
2875
2876 let config = format!(
2877 "Include {included}\nUser alice\n",
2878 included = included.path().display(),
2879 );
2880 let mut reader = BufReader::new(config.as_bytes());
2881 let config = SshConfig::default()
2882 .default_algorithms(DefaultAlgorithms::empty())
2883 .parse(&mut reader, ParseRule::STRICT)?;
2884
2885 let default = config.query("github.com");
2886 assert_eq!(default.user.as_deref(), Some("alice"));
2887 assert!(default.host_name.is_none());
2888
2889 let foo = config.query("foo");
2890 assert_eq!(foo.user.as_deref(), Some("alice"));
2891 assert_eq!(foo.host_name.as_deref(), Some("foo.example"));
2892
2893 Ok(())
2894 }
2895
2896 #[test]
2897 fn should_keep_included_values_before_parent_fallbacks() -> Result<(), SshParserError> {
2898 let mut included = NamedTempFile::new().expect("Failed to create included file");
2899 included.write_all(b"Host foo\n User bob\n")?;
2900
2901 let config = format!(
2902 "Include {included}\nUser alice\n",
2903 included = included.path().display(),
2904 );
2905 let mut reader = BufReader::new(config.as_bytes());
2906 let config = SshConfig::default()
2907 .default_algorithms(DefaultAlgorithms::empty())
2908 .parse(&mut reader, ParseRule::STRICT)?;
2909
2910 assert_eq!(config.query("foo").user.as_deref(), Some("bob"));
2911 assert_eq!(config.query("github.com").user.as_deref(), Some("alice"));
2912
2913 Ok(())
2914 }
2915
2916 #[test]
2917 fn should_require_enclosing_and_included_host_scopes() -> Result<(), SshParserError> {
2918 let mut included = NamedTempFile::new().expect("Failed to create included file");
2919 included.write_all(b"Host app*\n HostName included.internal\n")?;
2920
2921 let config = format!(
2922 concat!(
2923 "Host *.example.com !app-blocked.example.com\n",
2924 " Include {included}\n",
2925 " Port 2222\n",
2926 "Host *\n",
2927 " User fallback\n",
2928 ),
2929 included = included.path().display(),
2930 );
2931 let mut reader = BufReader::new(config.as_bytes());
2932 let config = SshConfig::default()
2933 .default_algorithms(DefaultAlgorithms::empty())
2934 .parse(&mut reader, ParseRule::STRICT)?;
2935
2936 let allowed = config.query("app1.example.com");
2937 assert_eq!(allowed.host_name.as_deref(), Some("included.internal"));
2938 assert_eq!(allowed.port, Some(2222));
2939 assert_eq!(allowed.user.as_deref(), Some("fallback"));
2940
2941 let negated = config.query("app-blocked.example.com");
2942 assert!(negated.host_name.is_none());
2943 assert!(negated.port.is_none());
2944 assert_eq!(negated.user.as_deref(), Some("fallback"));
2945
2946 let outside = config.query("app1.example.net");
2947 assert!(outside.host_name.is_none());
2948 assert!(outside.port.is_none());
2949 assert_eq!(outside.user.as_deref(), Some("fallback"));
2950
2951 assert!(
2952 config
2953 .intersecting_hosts("app-blocked.example.com")
2954 .all(|host| host.params.host_name.as_deref() != Some("included.internal"))
2955 );
2956
2957 Ok(())
2958 }
2959
2960 #[test]
2961 fn should_restore_scope_between_consecutive_includes() -> Result<(), SshParserError> {
2962 let mut first = NamedTempFile::new().expect("Failed to create first include");
2963 first.write_all(b"Host bar\n HostName bar.internal\n")?;
2964 let mut second = NamedTempFile::new().expect("Failed to create second include");
2965 second.write_all(b"Port 2222\n")?;
2966
2967 let config = format!(
2968 concat!(
2969 "Host foo\n",
2970 " Include {first}\n",
2971 " Include {second}\n",
2972 " User alice\n",
2973 ),
2974 first = first.path().display(),
2975 second = second.path().display(),
2976 );
2977 let mut reader = BufReader::new(config.as_bytes());
2978 let config = SshConfig::default()
2979 .default_algorithms(DefaultAlgorithms::empty())
2980 .parse(&mut reader, ParseRule::STRICT)?;
2981
2982 let foo = config.query("foo");
2983 assert_eq!(foo.port, Some(2222));
2984 assert_eq!(foo.user.as_deref(), Some("alice"));
2985
2986 let bar = config.query("bar");
2987 assert!(bar.host_name.is_none());
2988 assert!(bar.port.is_none());
2989 assert!(bar.user.is_none());
2990
2991 Ok(())
2992 }
2993
2994 #[test]
2995 fn should_preserve_all_nested_include_scopes() -> Result<(), SshParserError> {
2996 let mut inner = NamedTempFile::new().expect("Failed to create inner include");
2997 inner.write_all(b"User nested\n")?;
2998
2999 let mut outer = NamedTempFile::new().expect("Failed to create outer include");
3000 writeln!(
3001 outer,
3002 "Host app*\n Include {inner}",
3003 inner = inner.path().display(),
3004 )?;
3005
3006 let config = format!(
3007 "Host *.example.com\n Include {outer}\n",
3008 outer = outer.path().display(),
3009 );
3010 let mut reader = BufReader::new(config.as_bytes());
3011 let config = SshConfig::default()
3012 .default_algorithms(DefaultAlgorithms::empty())
3013 .parse(&mut reader, ParseRule::STRICT)?;
3014
3015 assert_eq!(
3016 config.query("app1.example.com").user.as_deref(),
3017 Some("nested")
3018 );
3019 assert!(config.query("app1.example.net").user.is_none());
3020 assert!(config.query("other.example.com").user.is_none());
3021
3022 Ok(())
3023 }
3024
3025 #[test]
3026 fn should_include_every_path_argument() -> Result<(), SshParserError> {
3027 let mut first = NamedTempFile::new().expect("Failed to create first include");
3028 first.write_all(b"Host foo\n HostName foo.internal\n")?;
3029 let mut second = NamedTempFile::new().expect("Failed to create second include");
3030 second.write_all(b"Host bar\n HostName bar.internal\n")?;
3031
3032 let config = format!(
3033 "Include {first} {second}\n",
3034 first = first.path().display(),
3035 second = second.path().display(),
3036 );
3037 let mut reader = BufReader::new(config.as_bytes());
3038 let config = SshConfig::default()
3039 .default_algorithms(DefaultAlgorithms::empty())
3040 .parse(&mut reader, ParseRule::STRICT)?;
3041
3042 assert_eq!(
3043 config.query("foo").host_name.as_deref(),
3044 Some("foo.internal")
3045 );
3046 assert_eq!(
3047 config.query("bar").host_name.as_deref(),
3048 Some("bar.internal")
3049 );
3050
3051 Ok(())
3052 }
3053
3054 #[test]
3055 fn should_take_first_obtained_value_across_include_arguments() -> Result<(), SshParserError> {
3056 let mut first = NamedTempFile::new().expect("Failed to create first include");
3057 first.write_all(b"Host foo\n User first\n")?;
3058 let mut second = NamedTempFile::new().expect("Failed to create second include");
3059 second.write_all(b"Host foo\n User second\n Port 2222\n")?;
3060
3061 let config = format!(
3062 "Include {first} {second}\n",
3063 first = first.path().display(),
3064 second = second.path().display(),
3065 );
3066 let mut reader = BufReader::new(config.as_bytes());
3067 let config = SshConfig::default()
3068 .default_algorithms(DefaultAlgorithms::empty())
3069 .parse(&mut reader, ParseRule::STRICT)?;
3070
3071 assert_eq!(config.query("foo").user.as_deref(), Some("first"));
3072 assert_eq!(config.query("foo").port, Some(2222));
3073
3074 let config = format!(
3075 "Include {second} {first}\n",
3076 first = first.path().display(),
3077 second = second.path().display(),
3078 );
3079 let mut reader = BufReader::new(config.as_bytes());
3080 let config = SshConfig::default()
3081 .default_algorithms(DefaultAlgorithms::empty())
3082 .parse(&mut reader, ParseRule::STRICT)?;
3083
3084 assert_eq!(config.query("foo").user.as_deref(), Some("second"));
3085
3086 Ok(())
3087 }
3088
3089 #[test]
3090 fn should_expand_glob_in_each_include_argument() -> Result<(), SshParserError> {
3091 let conf_dir = tempfile::tempdir()?;
3092 std::fs::write(
3093 conf_dir.path().join("10-a.conf"),
3094 "Host foo\n User glob-a\n",
3095 )?;
3096 std::fs::write(
3097 conf_dir.path().join("20-b.conf"),
3098 "Host foo\n User glob-b\n HostName b.internal\n",
3099 )?;
3100 std::fs::write(
3101 conf_dir.path().join("other.txt"),
3102 "Host foo\n Port 9999\n",
3103 )?;
3104 let extra_dir = tempfile::tempdir()?;
3105 std::fs::write(
3106 extra_dir.path().join("x.conf"),
3107 "Host bar\n HostName bar.internal\n",
3108 )?;
3109
3110 let config = format!(
3111 "Include {conf}/*.conf {extra}/*.conf\n",
3112 conf = conf_dir.path().display(),
3113 extra = extra_dir.path().display(),
3114 );
3115 let mut reader = BufReader::new(config.as_bytes());
3116 let config = SshConfig::default()
3117 .default_algorithms(DefaultAlgorithms::empty())
3118 .parse(&mut reader, ParseRule::STRICT)?;
3119
3120 assert_eq!(config.query("foo").user.as_deref(), Some("glob-a"));
3121 assert_eq!(config.query("foo").host_name.as_deref(), Some("b.internal"));
3122 assert!(config.query("foo").port.is_none());
3123 assert_eq!(
3124 config.query("bar").host_name.as_deref(),
3125 Some("bar.internal")
3126 );
3127
3128 Ok(())
3129 }
3130
3131 #[test]
3132 fn should_skip_unmatched_include_arguments() -> Result<(), SshParserError> {
3133 let missing_dir = tempfile::tempdir()?;
3134 let missing = missing_dir.path().join("absent").join("*.conf");
3135 let mut present = NamedTempFile::new().expect("Failed to create include");
3136 present.write_all(b"Host foo\n HostName foo.internal\n")?;
3137
3138 let config = format!(
3139 "Include {missing} {present} {missing}\n",
3140 missing = missing.display(),
3141 present = present.path().display(),
3142 );
3143 let mut reader = BufReader::new(config.as_bytes());
3144 let config = SshConfig::default()
3145 .default_algorithms(DefaultAlgorithms::empty())
3146 .parse(&mut reader, ParseRule::STRICT)?;
3147
3148 assert_eq!(
3149 config.query("foo").host_name.as_deref(),
3150 Some("foo.internal")
3151 );
3152
3153 Ok(())
3154 }
3155
3156 #[test]
3157 fn should_scope_every_include_argument_to_enclosing_host() -> Result<(), SshParserError> {
3158 let mut first = NamedTempFile::new().expect("Failed to create first include");
3159 first.write_all(b"Host app*\n HostName first.internal\n")?;
3160 let mut second = NamedTempFile::new().expect("Failed to create second include");
3161 second.write_all(b"Port 2222\nHost db*\n HostName second.internal\n")?;
3162
3163 let config = format!(
3164 "Host *.example.com\n Include {first} {second}\n User alice\nHost *\n User fallback\n",
3165 first = first.path().display(),
3166 second = second.path().display(),
3167 );
3168 let mut reader = BufReader::new(config.as_bytes());
3169 let config = SshConfig::default()
3170 .default_algorithms(DefaultAlgorithms::empty())
3171 .parse(&mut reader, ParseRule::STRICT)?;
3172
3173 let app = config.query("app1.example.com");
3174 assert_eq!(app.host_name.as_deref(), Some("first.internal"));
3175 assert_eq!(app.port, Some(2222));
3176 assert_eq!(app.user.as_deref(), Some("alice"));
3177
3178 let db = config.query("db1.example.com");
3179 assert_eq!(db.host_name.as_deref(), Some("second.internal"));
3180 assert_eq!(db.port, Some(2222));
3181 assert_eq!(db.user.as_deref(), Some("alice"));
3182
3183 let outside = config.query("db1.example.net");
3184 assert!(outside.host_name.is_none());
3185 assert!(outside.port.is_none());
3186 assert_eq!(outside.user.as_deref(), Some("fallback"));
3187
3188 Ok(())
3189 }
3190
3191 #[allow(dead_code)]
3192 struct ConfigWithInclude {
3193 config: NamedTempFile,
3194 inc1: NamedTempFile,
3195 inc2: NamedTempFile,
3196 inc3: NamedTempFile,
3197 inc4: NamedTempFile,
3198 }
3199
3200 fn create_include_config() -> ConfigWithInclude {
3201 let mut config_file: tempfile::NamedTempFile =
3202 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3203 let mut inc1_file: tempfile::NamedTempFile =
3204 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3205 let mut inc2_file: tempfile::NamedTempFile =
3206 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3207 let mut inc3_file: tempfile::NamedTempFile =
3208 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3209 let mut inc4_file: tempfile::NamedTempFile =
3210 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3211
3212 let config = format!(
3213 r##"
3214# ssh config
3215# written by veeso
3216
3217
3218 # I put a comment here just to annoy
3219
3220IgnoreUnknown Pippo,Pluto
3221
3222Compression yes
3223ConnectionAttempts 10
3224ConnectTimeout 60
3225ServerAliveInterval 40
3226Include {inc1}
3227
3228# Let's start defining some hosts
3229
3230Host tostapane
3231 User ciro-esposito
3232 HostName 192.168.24.32
3233 RemoteForward 88
3234 Compression no
3235 # Ignore unknown fields should be inherited from the global section
3236 Pippo yes
3237 Pluto 56
3238 Include {inc2}
3239
3240Host *
3241Include {inc3}
3242Include {inc4}
3243"##,
3244 inc1 = inc1_file.path().display(),
3245 inc2 = inc2_file.path().display(),
3246 inc3 = inc3_file.path().display(),
3247 inc4 = inc4_file.path().display(),
3248 );
3249 config_file.write_all(config.as_bytes()).unwrap();
3250
3251 let inc1 = r##"
3253 ConnectTimeout 60
3254 ServerAliveInterval 60
3255 TcpKeepAlive yes
3256 "##;
3257 inc1_file.write_all(inc1.as_bytes()).unwrap();
3258
3259 let inc2 = r##"
3261 ConnectTimeout 180
3262 ServerAliveInterval 180
3263 Ciphers +a-manella,blowfish,coi-piedi,cazdecan,triestin-stretto
3264 "##;
3265 inc2_file.write_all(inc2.as_bytes()).unwrap();
3266
3267 let inc3 = r##"
3269Host microwave
3270 User mario-rossi
3271 HostName 192.168.24.33
3272 RemoteForward 88
3273 Compression no
3274 # Ignore unknown fields should be inherited from the global section
3275 Pippo yes
3276 Pluto 56
3277"##;
3278 inc3_file.write_all(inc3.as_bytes()).unwrap();
3279
3280 let inc4 = r##"
3282 # Update microwave
3283 ServerAliveInterval 30
3284 Port 345
3285
3286# Force microwave update (it won't work)
3287Host microwave
3288 ConnectTimeout 30
3289
3290Host fridge
3291 User luigi-verdi
3292 HostName 192.168.24.34
3293 RemoteForward 88
3294 Compression no
3295"##;
3296 inc4_file.write_all(inc4.as_bytes()).unwrap();
3297
3298 ConfigWithInclude {
3299 config: config_file,
3300 inc1: inc1_file,
3301 inc2: inc2_file,
3302 inc3: inc3_file,
3303 inc4: inc4_file,
3304 }
3305 }
3306}