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 let path_match = Self::resolve_include_path(&Self::parse_string(args)?);
614
615 trace!("include files: {path_match}",);
616 let files = glob(&path_match)?;
617
618 let mut new_hosts = vec![];
619
620 for file in files {
621 let file = file?;
622 trace!("including file: {}", file.display());
623 let mut reader = BufReader::new(File::open(file)?);
624 let mut sub_config = SshConfig::default().default_algorithms(default_algos.clone());
625 Self::parse(&mut sub_config, &mut reader, rules, ignore_unknown.clone())?;
626
627 let SshConfig {
628 hosts: sub_hosts,
629 host_scopes: sub_scopes,
630 ..
631 } = sub_config;
632 for (index, sub_host) in sub_hosts.into_iter().enumerate() {
633 trace!("adding sub-host: {sub_host:?}",);
634 let mut scope = inherited_scope.to_vec();
635 scope.push(pattern.to_vec());
636 if let Some(sub_scope) = sub_scopes.get(index) {
637 scope.extend(sub_scope.iter().cloned());
638 }
639 new_hosts.push((sub_host, scope));
640 }
641 }
642
643 Ok(new_hosts)
644 }
645
646 fn tokenize_line(line: &str) -> SshParserResult<(Field, Vec<String>)> {
659 let trimmed_line = line.trim();
661 let (field, other_tokens) = if trimmed_line.find('=').unwrap_or(usize::MAX)
663 < trimmed_line.find(char::is_whitespace).unwrap_or(usize::MAX)
664 {
665 trimmed_line
666 .split_once('=')
667 .ok_or(SshParserError::MissingArgument)?
668 } else {
669 trimmed_line
670 .split_once(char::is_whitespace)
671 .ok_or(SshParserError::MissingArgument)?
672 };
673
674 trace!("tokenized line '{line}' - field '{field}' with args '{other_tokens}'",);
675
676 let other_tokens = other_tokens.trim().trim_start_matches('=').trim();
678 trace!("other tokens trimmed: '{other_tokens}'",);
679
680 let unescaped_quote_count = Self::count_unescaped_quotes(other_tokens);
682 if unescaped_quote_count % 2 != 0 {
683 return Err(SshParserError::InvalidQuotes);
684 }
685
686 let raw_tokens = Self::split_args_respecting_quotes(other_tokens);
688
689 let args = if raw_tokens.len() == 1
691 && raw_tokens[0].starts_with('"')
692 && raw_tokens[0].len() >= 2
693 && Self::ends_with_unescaped_quote(&raw_tokens[0])
694 {
695 trace!("quoted args: '{}'", raw_tokens[0]);
696 let t = &raw_tokens[0];
697 let content = &t[1..t.len() - 1];
698 vec![Self::unescape_string(content)]
699 } else {
700 trace!("split args: {:?}", raw_tokens);
701 raw_tokens
702 };
703
704 match Field::from_str(field) {
705 Ok(field) => Ok((field, args)),
706 Err(_) => Err(SshParserError::UnknownField(field.to_string(), args)),
707 }
708 }
709
710 fn parse_boolean(args: Vec<String>) -> SshParserResult<bool> {
716 let value = args.first().ok_or(SshParserError::MissingArgument)?;
717
718 if value.eq_ignore_ascii_case("yes") || value.eq_ignore_ascii_case("true") {
719 Ok(true)
720 } else if value.eq_ignore_ascii_case("no") || value.eq_ignore_ascii_case("false") {
721 Ok(false)
722 } else {
723 Err(SshParserError::ExpectedBoolean)
724 }
725 }
726
727 fn parse_algos(args: Vec<String>) -> SshParserResult<AlgorithmsRule> {
729 let first = args.first().ok_or(SshParserError::MissingArgument)?;
730
731 AlgorithmsRule::from_str(first)
732 }
733
734 fn parse_comma_separated_list(args: Vec<String>) -> SshParserResult<Vec<String>> {
736 match args
737 .first()
738 .map(|x| x.split(',').map(|x| x.to_string()).collect())
739 {
740 Some(args) => Ok(args),
741 _ => Err(SshParserError::MissingArgument),
742 }
743 }
744
745 fn parse_duration(args: Vec<String>) -> SshParserResult<Duration> {
747 let value = Self::parse_unsigned(args)?;
748 Ok(Duration::from_secs(value as u64))
749 }
750
751 fn parse_host(args: Vec<String>) -> SshParserResult<Vec<HostClause>> {
755 if args.is_empty() {
756 return Err(SshParserError::MissingArgument);
757 }
758 Ok(args
760 .into_iter()
761 .map(|x| {
762 if let Some(pattern) = x.strip_prefix('!') {
763 HostClause::new(pattern.to_string(), true)
764 } else {
765 HostClause::new(x, false)
766 }
767 })
768 .collect())
769 }
770
771 fn parse_path_list(args: Vec<String>) -> SshParserResult<Vec<PathBuf>> {
773 if args.is_empty() {
774 return Err(SshParserError::MissingArgument);
775 }
776 args.iter()
777 .map(|x| Self::parse_path_arg(x.as_str()))
778 .collect()
779 }
780
781 fn parse_path(args: Vec<String>) -> SshParserResult<PathBuf> {
783 if let Some(s) = args.first() {
784 Self::parse_path_arg(s)
785 } else {
786 Err(SshParserError::MissingArgument)
787 }
788 }
789
790 fn parse_path_arg(s: &str) -> SshParserResult<PathBuf> {
792 let s = if s.starts_with('~') {
794 let home_dir = dirs::home_dir()
795 .unwrap_or_else(|| PathBuf::from("~"))
796 .to_string_lossy()
797 .to_string();
798 s.replacen('~', &home_dir, 1)
799 } else {
800 s.to_string()
801 };
802 Ok(PathBuf::from(s))
803 }
804
805 fn parse_port(args: Vec<String>) -> SshParserResult<u16> {
807 match args.first().map(|x| u16::from_str(x)) {
808 Some(Ok(val)) => Ok(val),
809 Some(Err(_)) => Err(SshParserError::ExpectedPort),
810 None => Err(SshParserError::MissingArgument),
811 }
812 }
813
814 fn parse_remote_forward(args: Vec<String>) -> SshParserResult<RemoteForward> {
816 if !(1..=2).contains(&args.len()) {
817 return Err(SshParserError::InvalidRemoteForward(args));
818 }
819
820 let listen_argument = Self::normalize_forward_argument(&args[0]);
821 let listen = if listen_argument.contains('/') {
822 RemoteForwardListen::UnixSocket(PathBuf::from(listen_argument))
823 } else if let Ok(port) = u16::from_str(&listen_argument) {
824 RemoteForwardListen::Port(port)
825 } else if let Some((host, port)) = Self::parse_forward_host(&listen_argument, true) {
826 RemoteForwardListen::Host { host, port }
827 } else {
828 return Err(SshParserError::InvalidRemoteForward(args));
829 };
830
831 let destination = if let Some(argument) = args.get(1) {
832 let argument = Self::normalize_forward_argument(argument);
833 if argument.contains('/') {
834 Some(RemoteForwardDestination::UnixSocket(PathBuf::from(
835 argument,
836 )))
837 } else if let Some((host, port)) = Self::parse_forward_host(&argument, false) {
838 Some(RemoteForwardDestination::Host { host, port })
839 } else {
840 return Err(SshParserError::InvalidRemoteForward(args));
841 }
842 } else {
843 None
844 };
845
846 Ok(RemoteForward::new(listen, destination))
847 }
848
849 fn normalize_forward_argument(argument: &str) -> String {
851 if argument.starts_with('"')
852 && argument.len() >= 2
853 && Self::ends_with_unescaped_quote(argument)
854 {
855 Self::unescape_string(&argument[1..argument.len() - 1])
856 } else {
857 argument.to_string()
858 }
859 }
860
861 fn parse_forward_host(value: &str, allow_empty_host: bool) -> Option<(String, u16)> {
863 let (host, port) = if let Some(value) = value.strip_prefix('[') {
864 let (host, port) = value.split_once("]:")?;
865 if host.is_empty() || host.contains(['[', ']']) {
866 return None;
867 }
868 (host, port)
869 } else {
870 let (host, port) = value.rsplit_once(':')?;
871 if host.contains([':', '[', ']']) {
872 return None;
873 }
874 (host, port)
875 };
876
877 if !allow_empty_host && host.is_empty() {
878 return None;
879 }
880
881 Some((host.to_string(), u16::from_str(port).ok()?))
882 }
883
884 fn parse_string(args: Vec<String>) -> SshParserResult<String> {
886 if let Some(s) = args.into_iter().next() {
887 Ok(s)
888 } else {
889 Err(SshParserError::MissingArgument)
890 }
891 }
892
893 fn parse_unsigned(args: Vec<String>) -> SshParserResult<usize> {
895 match args.first().map(|x| usize::from_str(x)) {
896 Some(Ok(val)) => Ok(val),
897 Some(Err(_)) => Err(SshParserError::ExpectedUnsigned),
898 None => Err(SshParserError::MissingArgument),
899 }
900 }
901}
902
903#[cfg(test)]
904mod tests {
905
906 use std::fs::File;
907 use std::io::{BufReader, Write};
908 use std::path::{Path, PathBuf};
909
910 use pretty_assertions::assert_eq;
911 use tempfile::NamedTempFile;
912
913 use super::*;
914 use crate::{DefaultAlgorithms, RemoteForward, RemoteForwardDestination, RemoteForwardListen};
915
916 #[test]
917 fn should_parse_remote_forward_listeners() -> Result<(), SshParserError> {
918 let cases = [
919 (
920 vec!["8080"],
921 RemoteForward::new(RemoteForwardListen::Port(8080), None),
922 ),
923 (
924 vec!["localhost:8080"],
925 RemoteForward::new(
926 RemoteForwardListen::Host {
927 host: "localhost".to_string(),
928 port: 8080,
929 },
930 None,
931 ),
932 ),
933 (
934 vec!["0.0.0.0:8080"],
935 RemoteForward::new(
936 RemoteForwardListen::Host {
937 host: "0.0.0.0".to_string(),
938 port: 8080,
939 },
940 None,
941 ),
942 ),
943 (
944 vec!["*:8080"],
945 RemoteForward::new(
946 RemoteForwardListen::Host {
947 host: "*".to_string(),
948 port: 8080,
949 },
950 None,
951 ),
952 ),
953 (
954 vec![":8080"],
955 RemoteForward::new(
956 RemoteForwardListen::Host {
957 host: String::new(),
958 port: 8080,
959 },
960 None,
961 ),
962 ),
963 (
964 vec!["[::1]:8080"],
965 RemoteForward::new(
966 RemoteForwardListen::Host {
967 host: "::1".to_string(),
968 port: 8080,
969 },
970 None,
971 ),
972 ),
973 (
974 vec!["/tmp/remote.sock"],
975 RemoteForward::new(
976 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote.sock")),
977 None,
978 ),
979 ),
980 ];
981
982 for (args, expected) in cases {
983 let args = args.into_iter().map(str::to_string).collect();
984 assert_eq!(SshConfigParser::parse_remote_forward(args)?, expected);
985 }
986 Ok(())
987 }
988
989 #[test]
990 fn should_parse_remote_forward_destinations() -> Result<(), SshParserError> {
991 let cases = [
992 (
993 vec!["8080", "localhost:80"],
994 RemoteForward::new(
995 RemoteForwardListen::Port(8080),
996 Some(RemoteForwardDestination::Host {
997 host: "localhost".to_string(),
998 port: 80,
999 }),
1000 ),
1001 ),
1002 (
1003 vec!["/tmp/remote.sock", "/tmp/local.sock"],
1004 RemoteForward::new(
1005 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote.sock")),
1006 Some(RemoteForwardDestination::UnixSocket(PathBuf::from(
1007 "/tmp/local.sock",
1008 ))),
1009 ),
1010 ),
1011 (
1012 vec!["\"/tmp/remote socket\"", "[2001:db8::1]:443"],
1013 RemoteForward::new(
1014 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote socket")),
1015 Some(RemoteForwardDestination::Host {
1016 host: "2001:db8::1".to_string(),
1017 port: 443,
1018 }),
1019 ),
1020 ),
1021 ];
1022
1023 for (args, expected) in cases {
1024 let args = args.into_iter().map(str::to_string).collect();
1025 assert_eq!(SshConfigParser::parse_remote_forward(args)?, expected);
1026 }
1027 Ok(())
1028 }
1029
1030 #[test]
1031 fn should_reject_invalid_remote_forward() {
1032 for args in [
1033 vec![],
1034 vec!["localhost"],
1035 vec!["[::1:8080"],
1036 vec!["70000"],
1037 vec!["localhost:70000"],
1038 vec!["8080", "80"],
1039 vec!["8080", "localhost:80", "unexpected"],
1040 ] {
1041 let args = args.into_iter().map(str::to_string).collect();
1042 assert!(matches!(
1043 SshConfigParser::parse_remote_forward(args),
1044 Err(SshParserError::InvalidRemoteForward(_))
1045 ));
1046 }
1047 }
1048
1049 #[test]
1050 fn should_parse_configuration() -> Result<(), SshParserError> {
1051 crate::test_log();
1052 let temp = create_ssh_config();
1053 let file = File::open(temp.path()).expect("Failed to open tempfile");
1054 let mut reader = BufReader::new(file);
1055 let config = SshConfig::default()
1056 .default_algorithms(DefaultAlgorithms {
1057 ca_signature_algorithms: vec![],
1058 ciphers: vec![],
1059 host_key_algorithms: vec![],
1060 kex_algorithms: vec![],
1061 mac: vec![],
1062 pubkey_accepted_algorithms: vec!["omar-crypt".to_string()],
1063 })
1064 .parse(&mut reader, ParseRule::STRICT)?;
1065
1066 let params = config.query("*");
1069 assert_eq!(
1070 params.ignore_unknown.as_deref().unwrap(),
1071 &["Pippo", "Pluto"]
1072 );
1073 assert_eq!(params.compression.unwrap(), true);
1074 assert_eq!(params.connection_attempts.unwrap(), 10);
1075 assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
1076 assert_eq!(
1077 params.server_alive_interval.unwrap(),
1078 Duration::from_secs(40)
1079 );
1080 assert_eq!(params.tcp_keep_alive.unwrap(), true);
1081 assert_eq!(params.ciphers.algorithms(), &["a-manella", "blowfish"]);
1082 assert_eq!(
1083 params.pubkey_accepted_algorithms.algorithms(),
1084 &["desu", "omar-crypt", "fast-omar-crypt"]
1085 );
1086
1087 assert_eq!(params.ca_signature_algorithms.algorithms(), &["random"]);
1089 assert_eq!(
1090 params.host_key_algorithms.algorithms(),
1091 &["luigi", "mario",]
1092 );
1093 assert_eq!(params.kex_algorithms.algorithms(), &["desu", "gigi",]);
1094 assert_eq!(params.mac.algorithms(), &["concorde"]);
1095 assert!(params.bind_address.is_none());
1096
1097 let params_172_26_104_4 = config.query("172.26.104.4");
1101
1102 assert_eq!(params_172_26_104_4.add_keys_to_agent.unwrap(), true);
1104 assert_eq!(params_172_26_104_4.compression.unwrap(), true);
1105 assert_eq!(params_172_26_104_4.connection_attempts.unwrap(), 10);
1106 assert_eq!(
1107 params_172_26_104_4.connect_timeout.unwrap(),
1108 Duration::from_secs(60)
1109 );
1110 assert_eq!(params_172_26_104_4.tcp_keep_alive.unwrap(), true);
1111
1112 assert_eq!(
1114 params_172_26_104_4.ca_signature_algorithms.algorithms(),
1115 &["random"]
1116 );
1117 assert_eq!(
1118 params_172_26_104_4.ciphers.algorithms(),
1119 &["a-manella", "blowfish",]
1120 );
1121 assert_eq!(params_172_26_104_4.mac.algorithms(), &["spyro", "deoxys"]); assert_eq!(
1123 params_172_26_104_4.proxy_jump.unwrap(),
1124 &["jump.example.com"]
1125 ); assert_eq!(
1127 params_172_26_104_4
1128 .pubkey_accepted_algorithms
1129 .algorithms()
1130 .is_empty(), true
1132 );
1133 assert_eq!(
1134 params_172_26_104_4.bind_address.as_deref().unwrap(),
1135 "10.8.0.10"
1136 );
1137 assert_eq!(
1138 params_172_26_104_4.bind_interface.as_deref().unwrap(),
1139 "tun0"
1140 );
1141 assert_eq!(params_172_26_104_4.port.unwrap(), 2222);
1142 assert_eq!(
1143 params_172_26_104_4.identity_file.as_deref().unwrap(),
1144 vec![
1145 Path::new("/home/root/.ssh/pippo.key"),
1146 Path::new("/home/root/.ssh/pluto.key")
1147 ]
1148 );
1149 assert_eq!(params_172_26_104_4.user.as_deref().unwrap(), "omar");
1150
1151 let params_tostapane = config.query("tostapane");
1153 assert_eq!(params_tostapane.compression.unwrap(), true); assert_eq!(params_tostapane.connection_attempts.unwrap(), 10);
1155 assert_eq!(
1156 params_tostapane.connect_timeout.unwrap(),
1157 Duration::from_secs(60)
1158 );
1159 assert_eq!(params_tostapane.tcp_keep_alive.unwrap(), true);
1160 assert_eq!(
1161 params_tostapane.remote_forward,
1162 vec![RemoteForward::new(RemoteForwardListen::Port(88), None)]
1163 );
1164 assert_eq!(params_tostapane.user.as_deref().unwrap(), "ciro-esposito");
1165
1166 assert_eq!(
1168 params_tostapane.ca_signature_algorithms.algorithms(),
1169 &["random"]
1170 );
1171 assert_eq!(
1172 params_tostapane.ciphers.algorithms(),
1173 &["a-manella", "blowfish",]
1174 );
1175 assert_eq!(
1176 params_tostapane.mac.algorithms(),
1177 vec!["spyro".to_string(), "deoxys".to_string(),]
1178 );
1179 assert_eq!(
1180 params_tostapane.proxy_jump.unwrap(),
1181 vec![
1182 "jump1.example.com".to_string(),
1183 "jump2.example.com".to_string(),
1184 ]
1185 );
1186 assert_eq!(
1187 params_tostapane.pubkey_accepted_algorithms.algorithms(),
1188 &["desu", "omar-crypt", "fast-omar-crypt"]
1189 );
1190
1191 let params_192_168_1_30 = config.query("192.168.1.30");
1193
1194 assert_eq!(params_192_168_1_30.user.as_deref().unwrap(), "nutellaro");
1196 assert_eq!(
1197 params_192_168_1_30.remote_forward,
1198 vec![RemoteForward::new(RemoteForwardListen::Port(123), None)]
1199 );
1200
1201 assert_eq!(params_192_168_1_30.compression.unwrap(), true);
1203 assert_eq!(params_192_168_1_30.connection_attempts.unwrap(), 10);
1204 assert_eq!(
1205 params_192_168_1_30.connect_timeout.unwrap(),
1206 Duration::from_secs(60)
1207 );
1208 assert_eq!(params_192_168_1_30.tcp_keep_alive.unwrap(), true);
1209
1210 assert_eq!(
1212 params_192_168_1_30.ca_signature_algorithms.algorithms(),
1213 &["random"]
1214 );
1215 assert_eq!(
1216 params_192_168_1_30.ciphers.algorithms(),
1217 &["a-manella", "blowfish"]
1218 );
1219 assert_eq!(params_192_168_1_30.mac.algorithms(), &["concorde"]);
1220 assert_eq!(
1221 params_192_168_1_30.pubkey_accepted_algorithms.algorithms(),
1222 &["desu", "omar-crypt", "fast-omar-crypt"]
1223 );
1224
1225 Ok(())
1226 }
1227
1228 #[test]
1229 fn should_allow_unknown_field() -> Result<(), SshParserError> {
1230 crate::test_log();
1231 let temp = create_ssh_config_with_unknown_fields();
1232 let file = File::open(temp.path()).expect("Failed to open tempfile");
1233 let mut reader = BufReader::new(file);
1234 let _config = SshConfig::default()
1235 .default_algorithms(DefaultAlgorithms::empty())
1236 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)?;
1237
1238 Ok(())
1239 }
1240
1241 #[test]
1242 fn should_not_allow_unknown_field() {
1243 crate::test_log();
1244 let temp = create_ssh_config_with_unknown_fields();
1245 let file = File::open(temp.path()).expect("Failed to open tempfile");
1246 let mut reader = BufReader::new(file);
1247 assert!(matches!(
1248 SshConfig::default()
1249 .default_algorithms(DefaultAlgorithms::empty())
1250 .parse(&mut reader, ParseRule::STRICT)
1251 .unwrap_err(),
1252 SshParserError::UnknownField(..)
1253 ));
1254 }
1255
1256 #[test]
1257 fn should_store_unknown_fields() {
1258 crate::test_log();
1259 let temp = create_ssh_config_with_unknown_fields();
1260 let file = File::open(temp.path()).expect("Failed to open tempfile");
1261 let mut reader = BufReader::new(file);
1262 let config = SshConfig::default()
1263 .default_algorithms(DefaultAlgorithms::empty())
1264 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
1265 .unwrap();
1266
1267 let host = config.query("cross-platform");
1268 assert_eq!(
1269 host.ignored_fields.get("Piropero").unwrap(),
1270 &vec![String::from("yes")]
1271 );
1272 }
1273
1274 #[test]
1275 fn should_parse_inversed_ssh_config() {
1276 crate::test_log();
1277 let temp = create_inverted_ssh_config();
1278 let file = File::open(temp.path()).expect("Failed to open tempfile");
1279 let mut reader = BufReader::new(file);
1280 let config = SshConfig::default()
1281 .default_algorithms(DefaultAlgorithms::empty())
1282 .parse(&mut reader, ParseRule::STRICT)
1283 .unwrap();
1284
1285 let home_dir = dirs::home_dir()
1286 .unwrap_or_else(|| PathBuf::from("~"))
1287 .to_string_lossy()
1288 .to_string();
1289
1290 let remote_host = config.query("remote-host");
1291
1292 assert_eq!(
1294 remote_host.identity_file.unwrap()[0].as_path(),
1295 Path::new(format!("{home_dir}/.ssh/id_rsa_good").as_str()) );
1297
1298 assert_eq!(remote_host.host_name.unwrap(), "hostname.com");
1300 assert_eq!(remote_host.user.unwrap(), "user");
1301
1302 assert_eq!(
1304 remote_host.connect_timeout.unwrap(),
1305 Duration::from_secs(15)
1306 );
1307 }
1308
1309 #[test]
1310 fn should_parse_configuration_with_hosts() {
1311 crate::test_log();
1312 let temp = create_ssh_config_with_comments();
1313
1314 let file = File::open(temp.path()).expect("Failed to open tempfile");
1315 let mut reader = BufReader::new(file);
1316 let config = SshConfig::default()
1317 .default_algorithms(DefaultAlgorithms::empty())
1318 .parse(&mut reader, ParseRule::STRICT)
1319 .unwrap();
1320
1321 let hostname = config.query("cross-platform").host_name.unwrap();
1322 assert_eq!(&hostname, "hostname.com");
1323
1324 assert!(config.query("this").host_name.is_none());
1325 }
1326
1327 #[test]
1328 fn should_update_host_bind_address() -> Result<(), SshParserError> {
1329 crate::test_log();
1330 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1331 SshConfigParser::update_host(
1332 Field::BindAddress,
1333 vec![String::from("127.0.0.1")],
1334 &mut host,
1335 ParseRule::ALLOW_UNKNOWN_FIELDS,
1336 &DefaultAlgorithms::empty(),
1337 )?;
1338 assert_eq!(host.params.bind_address.as_deref().unwrap(), "127.0.0.1");
1339 Ok(())
1340 }
1341
1342 #[test]
1343 fn should_update_host_bind_interface() -> Result<(), SshParserError> {
1344 crate::test_log();
1345 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1346 SshConfigParser::update_host(
1347 Field::BindInterface,
1348 vec![String::from("aaa")],
1349 &mut host,
1350 ParseRule::ALLOW_UNKNOWN_FIELDS,
1351 &DefaultAlgorithms::empty(),
1352 )?;
1353 assert_eq!(host.params.bind_interface.as_deref().unwrap(), "aaa");
1354 Ok(())
1355 }
1356
1357 #[test]
1358 fn should_update_host_ca_signature_algos() -> Result<(), SshParserError> {
1359 crate::test_log();
1360 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1361 SshConfigParser::update_host(
1362 Field::CaSignatureAlgorithms,
1363 vec![String::from("a,b,c")],
1364 &mut host,
1365 ParseRule::ALLOW_UNKNOWN_FIELDS,
1366 &DefaultAlgorithms::empty(),
1367 )?;
1368 assert_eq!(
1369 host.params.ca_signature_algorithms.algorithms(),
1370 &["a", "b", "c"]
1371 );
1372 Ok(())
1373 }
1374
1375 #[test]
1376 fn should_update_host_certificate_file() -> Result<(), SshParserError> {
1377 crate::test_log();
1378 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1379 SshConfigParser::update_host(
1380 Field::CertificateFile,
1381 vec![String::from("/tmp/a.crt")],
1382 &mut host,
1383 ParseRule::ALLOW_UNKNOWN_FIELDS,
1384 &DefaultAlgorithms::empty(),
1385 )?;
1386 assert_eq!(
1387 host.params.certificate_file.as_deref().unwrap(),
1388 Path::new("/tmp/a.crt")
1389 );
1390 Ok(())
1391 }
1392
1393 #[test]
1394 fn should_update_host_ciphers() -> Result<(), SshParserError> {
1395 crate::test_log();
1396 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1397 SshConfigParser::update_host(
1398 Field::Ciphers,
1399 vec![String::from("a,b,c")],
1400 &mut host,
1401 ParseRule::ALLOW_UNKNOWN_FIELDS,
1402 &DefaultAlgorithms::empty(),
1403 )?;
1404 assert_eq!(host.params.ciphers.algorithms(), &["a", "b", "c"]);
1405 Ok(())
1406 }
1407
1408 #[test]
1409 fn should_update_host_compression() -> Result<(), SshParserError> {
1410 crate::test_log();
1411 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1412 SshConfigParser::update_host(
1413 Field::Compression,
1414 vec![String::from("yes")],
1415 &mut host,
1416 ParseRule::ALLOW_UNKNOWN_FIELDS,
1417 &DefaultAlgorithms::empty(),
1418 )?;
1419 assert_eq!(host.params.compression.unwrap(), true);
1420 Ok(())
1421 }
1422
1423 #[test]
1424 fn should_update_host_connection_attempts() -> Result<(), SshParserError> {
1425 crate::test_log();
1426 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1427 SshConfigParser::update_host(
1428 Field::ConnectionAttempts,
1429 vec![String::from("4")],
1430 &mut host,
1431 ParseRule::ALLOW_UNKNOWN_FIELDS,
1432 &DefaultAlgorithms::empty(),
1433 )?;
1434 assert_eq!(host.params.connection_attempts.unwrap(), 4);
1435 Ok(())
1436 }
1437
1438 #[test]
1439 fn should_update_host_connection_timeout() -> Result<(), SshParserError> {
1440 crate::test_log();
1441 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1442 SshConfigParser::update_host(
1443 Field::ConnectTimeout,
1444 vec![String::from("10")],
1445 &mut host,
1446 ParseRule::ALLOW_UNKNOWN_FIELDS,
1447 &DefaultAlgorithms::empty(),
1448 )?;
1449 assert_eq!(
1450 host.params.connect_timeout.unwrap(),
1451 Duration::from_secs(10)
1452 );
1453 Ok(())
1454 }
1455
1456 #[test]
1457 fn should_update_host_key_algorithms() -> Result<(), SshParserError> {
1458 crate::test_log();
1459 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1460 SshConfigParser::update_host(
1461 Field::HostKeyAlgorithms,
1462 vec![String::from("a,b,c")],
1463 &mut host,
1464 ParseRule::ALLOW_UNKNOWN_FIELDS,
1465 &DefaultAlgorithms::empty(),
1466 )?;
1467 assert_eq!(
1468 host.params.host_key_algorithms.algorithms(),
1469 &["a", "b", "c"]
1470 );
1471 Ok(())
1472 }
1473
1474 #[test]
1475 fn should_update_host_host_name() -> Result<(), SshParserError> {
1476 crate::test_log();
1477 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1478 SshConfigParser::update_host(
1479 Field::HostName,
1480 vec![String::from("192.168.1.1")],
1481 &mut host,
1482 ParseRule::ALLOW_UNKNOWN_FIELDS,
1483 &DefaultAlgorithms::empty(),
1484 )?;
1485 assert_eq!(host.params.host_name.as_deref().unwrap(), "192.168.1.1");
1486 Ok(())
1487 }
1488
1489 #[test]
1490 fn should_update_host_ignore_unknown() -> Result<(), SshParserError> {
1491 crate::test_log();
1492 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1493 SshConfigParser::update_host(
1494 Field::IgnoreUnknown,
1495 vec![String::from("a,b,c")],
1496 &mut host,
1497 ParseRule::ALLOW_UNKNOWN_FIELDS,
1498 &DefaultAlgorithms::empty(),
1499 )?;
1500 assert_eq!(
1501 host.params.ignore_unknown.as_deref().unwrap(),
1502 &["a", "b", "c"]
1503 );
1504 Ok(())
1505 }
1506
1507 #[test]
1508 fn should_update_kex_algorithms() -> Result<(), SshParserError> {
1509 crate::test_log();
1510 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1511 SshConfigParser::update_host(
1512 Field::KexAlgorithms,
1513 vec![String::from("a,b,c")],
1514 &mut host,
1515 ParseRule::ALLOW_UNKNOWN_FIELDS,
1516 &DefaultAlgorithms::empty(),
1517 )?;
1518 assert_eq!(host.params.kex_algorithms.algorithms(), &["a", "b", "c"]);
1519 Ok(())
1520 }
1521
1522 #[test]
1523 fn should_update_host_mac() -> Result<(), SshParserError> {
1524 crate::test_log();
1525 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1526 SshConfigParser::update_host(
1527 Field::Mac,
1528 vec![String::from("a,b,c")],
1529 &mut host,
1530 ParseRule::ALLOW_UNKNOWN_FIELDS,
1531 &DefaultAlgorithms::empty(),
1532 )?;
1533 assert_eq!(host.params.mac.algorithms(), &["a", "b", "c"]);
1534 Ok(())
1535 }
1536
1537 #[test]
1538 fn should_update_host_port() -> Result<(), SshParserError> {
1539 crate::test_log();
1540 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1541 SshConfigParser::update_host(
1542 Field::Port,
1543 vec![String::from("2222")],
1544 &mut host,
1545 ParseRule::ALLOW_UNKNOWN_FIELDS,
1546 &DefaultAlgorithms::empty(),
1547 )?;
1548 assert_eq!(host.params.port.unwrap(), 2222);
1549 Ok(())
1550 }
1551
1552 #[test]
1553 fn should_update_host_pubkey_accepted_algos() -> Result<(), SshParserError> {
1554 crate::test_log();
1555 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1556 SshConfigParser::update_host(
1557 Field::PubkeyAcceptedAlgorithms,
1558 vec![String::from("a,b,c")],
1559 &mut host,
1560 ParseRule::ALLOW_UNKNOWN_FIELDS,
1561 &DefaultAlgorithms::empty(),
1562 )?;
1563 assert_eq!(
1564 host.params.pubkey_accepted_algorithms.algorithms(),
1565 &["a", "b", "c"]
1566 );
1567 Ok(())
1568 }
1569
1570 #[test]
1571 fn should_update_host_pubkey_authentication() -> Result<(), SshParserError> {
1572 crate::test_log();
1573 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1574 SshConfigParser::update_host(
1575 Field::PubkeyAuthentication,
1576 vec![String::from("yes")],
1577 &mut host,
1578 ParseRule::ALLOW_UNKNOWN_FIELDS,
1579 &DefaultAlgorithms::empty(),
1580 )?;
1581 assert_eq!(host.params.pubkey_authentication.unwrap(), true);
1582 Ok(())
1583 }
1584
1585 #[test]
1586 fn should_update_host_remote_forward() -> Result<(), SshParserError> {
1587 crate::test_log();
1588 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1589 SshConfigParser::update_host(
1590 Field::RemoteForward,
1591 vec![String::from("3005"), String::from("localhost:80")],
1592 &mut host,
1593 ParseRule::ALLOW_UNKNOWN_FIELDS,
1594 &DefaultAlgorithms::empty(),
1595 )?;
1596 SshConfigParser::update_host(
1597 Field::RemoteForward,
1598 vec![String::from("/tmp/remote.sock")],
1599 &mut host,
1600 ParseRule::ALLOW_UNKNOWN_FIELDS,
1601 &DefaultAlgorithms::empty(),
1602 )?;
1603 assert_eq!(
1604 host.params.remote_forward,
1605 vec![
1606 RemoteForward::new(
1607 RemoteForwardListen::Port(3005),
1608 Some(RemoteForwardDestination::Host {
1609 host: "localhost".to_string(),
1610 port: 80,
1611 }),
1612 ),
1613 RemoteForward::new(
1614 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote.sock")),
1615 None,
1616 ),
1617 ]
1618 );
1619 Ok(())
1620 }
1621
1622 #[test]
1623 fn should_accumulate_remote_forwards_from_matching_hosts() -> Result<(), SshParserError> {
1624 let config = r#"
1625Host test
1626 RemoteForward 8080 localhost:80
1627
1628Host *
1629 RemoteForward "/tmp/remote socket" "/tmp/local socket"
1630"#;
1631 let mut reader = BufReader::new(config.as_bytes());
1632 let config = SshConfig::default()
1633 .default_algorithms(DefaultAlgorithms::empty())
1634 .parse(&mut reader, ParseRule::STRICT)?;
1635
1636 assert_eq!(
1637 config.query("test").remote_forward,
1638 vec![
1639 RemoteForward::new(
1640 RemoteForwardListen::Port(8080),
1641 Some(RemoteForwardDestination::Host {
1642 host: "localhost".to_string(),
1643 port: 80,
1644 }),
1645 ),
1646 RemoteForward::new(
1647 RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote socket")),
1648 Some(RemoteForwardDestination::UnixSocket(PathBuf::from(
1649 "/tmp/local socket",
1650 ))),
1651 ),
1652 ]
1653 );
1654 Ok(())
1655 }
1656
1657 #[test]
1658 fn should_update_host_server_alive_interval() -> Result<(), SshParserError> {
1659 crate::test_log();
1660 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1661 SshConfigParser::update_host(
1662 Field::ServerAliveInterval,
1663 vec![String::from("40")],
1664 &mut host,
1665 ParseRule::ALLOW_UNKNOWN_FIELDS,
1666 &DefaultAlgorithms::empty(),
1667 )?;
1668 assert_eq!(
1669 host.params.server_alive_interval.unwrap(),
1670 Duration::from_secs(40)
1671 );
1672 Ok(())
1673 }
1674
1675 #[test]
1676 fn should_update_host_tcp_keep_alive() -> Result<(), SshParserError> {
1677 crate::test_log();
1678 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1679 SshConfigParser::update_host(
1680 Field::TcpKeepAlive,
1681 vec![String::from("no")],
1682 &mut host,
1683 ParseRule::ALLOW_UNKNOWN_FIELDS,
1684 &DefaultAlgorithms::empty(),
1685 )?;
1686 assert_eq!(host.params.tcp_keep_alive.unwrap(), false);
1687 Ok(())
1688 }
1689
1690 #[test]
1691 fn should_update_host_user() -> Result<(), SshParserError> {
1692 crate::test_log();
1693 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1694 SshConfigParser::update_host(
1695 Field::User,
1696 vec![String::from("pippo")],
1697 &mut host,
1698 ParseRule::ALLOW_UNKNOWN_FIELDS,
1699 &DefaultAlgorithms::empty(),
1700 )?;
1701 assert_eq!(host.params.user.as_deref().unwrap(), "pippo");
1702 Ok(())
1703 }
1704
1705 #[test]
1706 fn should_not_update_host_if_unknown() -> Result<(), SshParserError> {
1707 crate::test_log();
1708 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1709 let result = SshConfigParser::update_host(
1710 Field::PasswordAuthentication,
1711 vec![String::from("yes")],
1712 &mut host,
1713 ParseRule::ALLOW_UNKNOWN_FIELDS,
1714 &DefaultAlgorithms::empty(),
1715 );
1716
1717 match result {
1718 Ok(_) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
1719 Err(e) => Err(e),
1720 }?;
1721
1722 assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1723 Ok(())
1724 }
1725
1726 #[test]
1727 fn should_update_host_if_unsupported() -> Result<(), SshParserError> {
1728 crate::test_log();
1729 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1730 let result = SshConfigParser::update_host(
1731 Field::PasswordAuthentication,
1732 vec![String::from("yes")],
1733 &mut host,
1734 ParseRule::ALLOW_UNKNOWN_FIELDS,
1735 &DefaultAlgorithms::empty(),
1736 );
1737
1738 match result {
1739 Err(SshParserError::UnsupportedField(field, _)) => {
1740 assert_eq!(field, "passwordauthentication");
1741 Ok(())
1742 }
1743 Ok(_) => Ok(()),
1744 Err(e) => Err(e),
1745 }?;
1746
1747 assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1748 Ok(())
1749 }
1750
1751 #[test]
1752 fn should_tokenize_line() -> Result<(), SshParserError> {
1753 crate::test_log();
1754 assert_eq!(
1755 SshConfigParser::tokenize_line("HostName 192.168.*.* 172.26.*.*")?,
1756 (
1757 Field::HostName,
1758 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1759 )
1760 );
1761 assert_eq!(
1763 SshConfigParser::tokenize_line(
1764 " HostName 192.168.*.* 172.26.*.* "
1765 )?,
1766 (
1767 Field::HostName,
1768 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1769 )
1770 );
1771 Ok(())
1772 }
1773
1774 #[test]
1775 fn should_not_tokenize_line() {
1776 crate::test_log();
1777 assert!(matches!(
1778 SshConfigParser::tokenize_line("Omar yes").unwrap_err(),
1779 SshParserError::UnknownField(..)
1780 ));
1781 }
1782
1783 #[test]
1784 fn should_fail_parsing_field() {
1785 crate::test_log();
1786
1787 assert!(matches!(
1788 SshConfigParser::tokenize_line(" ").unwrap_err(),
1789 SshParserError::MissingArgument
1790 ));
1791 }
1792
1793 #[test]
1794 fn should_fail_on_mismatched_quotes() {
1795 crate::test_log();
1796
1797 assert!(matches!(
1799 SshConfigParser::tokenize_line(r#"Hostname "example.com"#).unwrap_err(),
1800 SshParserError::InvalidQuotes
1801 ));
1802 assert!(matches!(
1804 SshConfigParser::tokenize_line(r#"Hostname example.com""#).unwrap_err(),
1805 SshParserError::InvalidQuotes
1806 ));
1807 assert!(matches!(
1809 SshConfigParser::tokenize_line(r#"Hostname foo "bar"#).unwrap_err(),
1810 SshParserError::InvalidQuotes
1811 ));
1812 }
1813
1814 #[test]
1815 fn should_parse_boolean() -> Result<(), SshParserError> {
1816 crate::test_log();
1817 assert_eq!(
1818 SshConfigParser::parse_boolean(vec![String::from("yes")])?,
1819 true
1820 );
1821 assert_eq!(
1822 SshConfigParser::parse_boolean(vec![String::from("no")])?,
1823 false
1824 );
1825 Ok(())
1826 }
1827
1828 #[test]
1829 fn should_parse_boolean_true_false_case_insensitive() -> Result<(), SshParserError> {
1830 crate::test_log();
1831 for value in ["true", "True", "YES", "Yes"] {
1832 assert!(SshConfigParser::parse_boolean(vec![value.to_string()])?);
1833 }
1834 for value in ["false", "False", "NO", "No"] {
1835 assert!(!SshConfigParser::parse_boolean(vec![value.to_string()])?);
1836 }
1837 Ok(())
1838 }
1839
1840 #[test]
1841 fn should_fail_parsing_boolean() {
1842 crate::test_log();
1843 assert!(matches!(
1844 SshConfigParser::parse_boolean(vec!["boh".to_string()]).unwrap_err(),
1845 SshParserError::ExpectedBoolean
1846 ));
1847 assert!(matches!(
1848 SshConfigParser::parse_boolean(vec![]).unwrap_err(),
1849 SshParserError::MissingArgument
1850 ));
1851 }
1852
1853 #[test]
1854 fn should_parse_algos() -> Result<(), SshParserError> {
1855 crate::test_log();
1856 assert_eq!(
1857 SshConfigParser::parse_algos(vec![String::from("a,b,c,d")])?,
1858 AlgorithmsRule::Set(vec![
1859 "a".to_string(),
1860 "b".to_string(),
1861 "c".to_string(),
1862 "d".to_string(),
1863 ])
1864 );
1865
1866 assert_eq!(
1867 SshConfigParser::parse_algos(vec![String::from("a")])?,
1868 AlgorithmsRule::Set(vec!["a".to_string()])
1869 );
1870
1871 assert_eq!(
1872 SshConfigParser::parse_algos(vec![String::from("+a,b")])?,
1873 AlgorithmsRule::Append(vec!["a".to_string(), "b".to_string()])
1874 );
1875
1876 Ok(())
1877 }
1878
1879 #[test]
1880 fn should_parse_comma_separated_list() -> Result<(), SshParserError> {
1881 crate::test_log();
1882 assert_eq!(
1883 SshConfigParser::parse_comma_separated_list(vec![String::from("a,b,c,d")])?,
1884 vec![
1885 "a".to_string(),
1886 "b".to_string(),
1887 "c".to_string(),
1888 "d".to_string(),
1889 ]
1890 );
1891 assert_eq!(
1892 SshConfigParser::parse_comma_separated_list(vec![String::from("a")])?,
1893 vec!["a".to_string()]
1894 );
1895 Ok(())
1896 }
1897
1898 #[test]
1899 fn should_fail_parsing_comma_separated_list() {
1900 crate::test_log();
1901 assert!(matches!(
1902 SshConfigParser::parse_comma_separated_list(vec![]).unwrap_err(),
1903 SshParserError::MissingArgument
1904 ));
1905 }
1906
1907 #[test]
1908 fn should_parse_duration() -> Result<(), SshParserError> {
1909 crate::test_log();
1910 assert_eq!(
1911 SshConfigParser::parse_duration(vec![String::from("60")])?,
1912 Duration::from_secs(60)
1913 );
1914 Ok(())
1915 }
1916
1917 #[test]
1918 fn should_fail_parsing_duration() {
1919 crate::test_log();
1920 assert!(matches!(
1921 SshConfigParser::parse_duration(vec![String::from("AAA")]).unwrap_err(),
1922 SshParserError::ExpectedUnsigned
1923 ));
1924 assert!(matches!(
1925 SshConfigParser::parse_duration(vec![]).unwrap_err(),
1926 SshParserError::MissingArgument
1927 ));
1928 }
1929
1930 #[test]
1931 fn should_parse_host() -> Result<(), SshParserError> {
1932 crate::test_log();
1933 assert_eq!(
1934 SshConfigParser::parse_host(vec![
1935 String::from("192.168.*.*"),
1936 String::from("!192.168.1.1"),
1937 String::from("172.26.104.*"),
1938 String::from("!172.26.104.10"),
1939 ])?,
1940 vec![
1941 HostClause::new(String::from("192.168.*.*"), false),
1942 HostClause::new(String::from("192.168.1.1"), true),
1943 HostClause::new(String::from("172.26.104.*"), false),
1944 HostClause::new(String::from("172.26.104.10"), true),
1945 ]
1946 );
1947 Ok(())
1948 }
1949
1950 #[test]
1951 fn should_fail_parsing_host() {
1952 crate::test_log();
1953 assert!(matches!(
1954 SshConfigParser::parse_host(vec![]).unwrap_err(),
1955 SshParserError::MissingArgument
1956 ));
1957 }
1958
1959 #[test]
1960 fn should_parse_path() -> Result<(), SshParserError> {
1961 crate::test_log();
1962 assert_eq!(
1963 SshConfigParser::parse_path(vec![String::from("/tmp/a.txt")])?,
1964 PathBuf::from("/tmp/a.txt")
1965 );
1966 Ok(())
1967 }
1968
1969 #[test]
1970 fn should_parse_path_and_resolve_tilde() -> Result<(), SshParserError> {
1971 crate::test_log();
1972 let mut expected = dirs::home_dir().unwrap();
1973 expected.push(".ssh/id_dsa");
1974 assert_eq!(
1975 SshConfigParser::parse_path(vec![String::from("~/.ssh/id_dsa")])?,
1976 expected
1977 );
1978 Ok(())
1979 }
1980
1981 #[test]
1982 fn should_parse_path_list() -> Result<(), SshParserError> {
1983 crate::test_log();
1984 assert_eq!(
1985 SshConfigParser::parse_path_list(vec![
1986 String::from("/tmp/a.txt"),
1987 String::from("/tmp/b.txt")
1988 ])?,
1989 vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")]
1990 );
1991 Ok(())
1992 }
1993
1994 #[test]
1995 fn should_fail_parse_path_list() {
1996 crate::test_log();
1997 assert!(matches!(
1998 SshConfigParser::parse_path_list(vec![]).unwrap_err(),
1999 SshParserError::MissingArgument
2000 ));
2001 }
2002
2003 #[test]
2004 fn should_fail_parsing_path() {
2005 crate::test_log();
2006 assert!(matches!(
2007 SshConfigParser::parse_path(vec![]).unwrap_err(),
2008 SshParserError::MissingArgument
2009 ));
2010 }
2011
2012 #[test]
2013 fn should_parse_port() -> Result<(), SshParserError> {
2014 crate::test_log();
2015 assert_eq!(SshConfigParser::parse_port(vec![String::from("22")])?, 22);
2016 Ok(())
2017 }
2018
2019 #[test]
2020 fn should_fail_parsing_port() {
2021 crate::test_log();
2022 assert!(matches!(
2023 SshConfigParser::parse_port(vec![String::from("1234567")]).unwrap_err(),
2024 SshParserError::ExpectedPort
2025 ));
2026 assert!(matches!(
2027 SshConfigParser::parse_port(vec![]).unwrap_err(),
2028 SshParserError::MissingArgument
2029 ));
2030 }
2031
2032 #[test]
2033 fn should_parse_string() -> Result<(), SshParserError> {
2034 crate::test_log();
2035 assert_eq!(
2036 SshConfigParser::parse_string(vec![String::from("foobar")])?,
2037 String::from("foobar")
2038 );
2039 Ok(())
2040 }
2041
2042 #[test]
2043 fn should_fail_parsing_string() {
2044 crate::test_log();
2045 assert!(matches!(
2046 SshConfigParser::parse_string(vec![]).unwrap_err(),
2047 SshParserError::MissingArgument
2048 ));
2049 }
2050
2051 #[test]
2052 fn should_parse_unsigned() -> Result<(), SshParserError> {
2053 crate::test_log();
2054 assert_eq!(
2055 SshConfigParser::parse_unsigned(vec![String::from("43")])?,
2056 43
2057 );
2058 Ok(())
2059 }
2060
2061 #[test]
2062 fn should_fail_parsing_unsigned() {
2063 crate::test_log();
2064 assert!(matches!(
2065 SshConfigParser::parse_unsigned(vec![String::from("abc")]).unwrap_err(),
2066 SshParserError::ExpectedUnsigned
2067 ));
2068 assert!(matches!(
2069 SshConfigParser::parse_unsigned(vec![]).unwrap_err(),
2070 SshParserError::MissingArgument
2071 ));
2072 }
2073
2074 #[test]
2075 fn should_strip_comments() {
2076 crate::test_log();
2077
2078 assert_eq!(
2079 SshConfigParser::strip_comments("host my_host # this is my fav host").as_str(),
2080 "host my_host "
2081 );
2082 assert_eq!(
2083 SshConfigParser::strip_comments("# this is a comment").as_str(),
2084 ""
2085 );
2086 }
2087
2088 #[test]
2089 fn should_preserve_hash_inside_quoted_strings() {
2090 crate::test_log();
2091
2092 assert_eq!(
2094 SshConfigParser::strip_comments(r#"Ciphers "aes256-ctr # not a comment""#).as_str(),
2095 r#"Ciphers "aes256-ctr # not a comment""#
2096 );
2097 assert_eq!(
2099 SshConfigParser::strip_comments(r#"Ciphers "aes256-ctr" # this is a comment"#).as_str(),
2100 r#"Ciphers "aes256-ctr" "#
2101 );
2102 assert_eq!(
2104 SshConfigParser::strip_comments(r#"ProxyCommand "ssh # hop" -W "dest # host""#)
2105 .as_str(),
2106 r#"ProxyCommand "ssh # hop" -W "dest # host""#
2107 );
2108 assert_eq!(
2110 SshConfigParser::strip_comments(r#"Key "val1" "val2" # comment"#).as_str(),
2111 r#"Key "val1" "val2" "#
2112 );
2113 }
2114
2115 #[test]
2116 fn test_should_parse_config_with_quotes_and_eq() {
2117 crate::test_log();
2118
2119 let config = create_ssh_config_with_quotes_and_eq();
2120 let file = File::open(config.path()).expect("Failed to open tempfile");
2121 let mut reader = BufReader::new(file);
2122
2123 let config = SshConfig::default()
2124 .default_algorithms(DefaultAlgorithms::empty())
2125 .parse(&mut reader, ParseRule::STRICT)
2126 .expect("Failed to parse config");
2127
2128 let params = config.query("foo");
2129
2130 assert_eq!(
2132 params.connect_timeout.expect("unspec connect timeout"),
2133 Duration::from_secs(15)
2134 );
2135 assert_eq!(
2136 params
2137 .ignore_unknown
2138 .as_deref()
2139 .expect("unspec ignore unknown"),
2140 &["Pippo", "Pluto"]
2141 );
2142 assert_eq!(
2143 params
2144 .ciphers
2145 .algorithms()
2146 .iter()
2147 .map(|x| x.as_str())
2148 .collect::<Vec<&str>>(),
2149 &["Pepperoni Pizza", "Margherita Pizza", "Hawaiian Pizza"]
2150 );
2151 assert_eq!(
2152 params
2153 .mac
2154 .algorithms()
2155 .iter()
2156 .map(|x| x.as_str())
2157 .collect::<Vec<&str>>(),
2158 &["Pasta Carbonara", "Pasta con tonno"]
2159 );
2160 }
2161
2162 #[test]
2163 fn test_should_resolve_absolute_include_path() {
2164 crate::test_log();
2165
2166 let expected = PathBuf::from("/tmp/config.local");
2167
2168 let s = "/tmp/config.local";
2169 let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
2170 assert_eq!(resolved, expected);
2171 }
2172
2173 #[test]
2174 fn test_should_resolve_relative_include_path() {
2175 crate::test_log();
2176
2177 let expected = dirs::home_dir()
2178 .unwrap_or_else(|| PathBuf::from("~"))
2179 .join(".ssh")
2180 .join("config.local");
2181
2182 let s = "config.local";
2183 let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
2184 assert_eq!(resolved, expected);
2185 }
2186
2187 #[test]
2188 fn test_should_resolve_include_path_with_tilde() {
2189 let p = "~/.ssh/config.local";
2190 let resolved = SshConfigParser::resolve_include_path(p);
2191 let mut expected = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
2192 expected.push(".ssh");
2193 expected.push("config.local");
2194 assert_eq!(PathBuf::from(resolved), expected);
2195 }
2196
2197 #[test]
2198 fn should_fail_parsing_algos_missing_arg() {
2199 crate::test_log();
2200 assert!(matches!(
2201 SshConfigParser::parse_algos(vec![]).unwrap_err(),
2202 SshParserError::MissingArgument
2203 ));
2204 }
2205
2206 #[test]
2207 fn should_parse_duration_zero() {
2208 crate::test_log();
2209 assert_eq!(
2210 SshConfigParser::parse_duration(vec![String::from("0")]).unwrap(),
2211 Duration::from_secs(0)
2212 );
2213 }
2214
2215 #[test]
2216 fn should_parse_port_boundary() {
2217 crate::test_log();
2218 assert_eq!(
2220 SshConfigParser::parse_port(vec![String::from("1")]).unwrap(),
2221 1
2222 );
2223 assert_eq!(
2225 SshConfigParser::parse_port(vec![String::from("65535")]).unwrap(),
2226 65535
2227 );
2228 }
2229
2230 #[test]
2231 fn should_update_host_add_keys_to_agent() {
2232 crate::test_log();
2233 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2234 SshConfigParser::update_host(
2235 Field::AddKeysToAgent,
2236 vec![String::from("yes")],
2237 &mut host,
2238 ParseRule::STRICT,
2239 &DefaultAlgorithms::empty(),
2240 )
2241 .unwrap();
2242 assert_eq!(host.params.add_keys_to_agent.unwrap(), true);
2243
2244 let mut host2 = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2245 SshConfigParser::update_host(
2246 Field::AddKeysToAgent,
2247 vec![String::from("no")],
2248 &mut host2,
2249 ParseRule::STRICT,
2250 &DefaultAlgorithms::empty(),
2251 )
2252 .unwrap();
2253 assert_eq!(host2.params.add_keys_to_agent.unwrap(), false);
2254 }
2255
2256 #[test]
2257 fn should_update_host_forward_agent() {
2258 crate::test_log();
2259 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2260 SshConfigParser::update_host(
2261 Field::ForwardAgent,
2262 vec![String::from("yes")],
2263 &mut host,
2264 ParseRule::STRICT,
2265 &DefaultAlgorithms::empty(),
2266 )
2267 .unwrap();
2268 assert_eq!(host.params.forward_agent.unwrap(), true);
2269 }
2270
2271 #[test]
2272 fn should_update_host_proxy_jump() {
2273 crate::test_log();
2274 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2275 SshConfigParser::update_host(
2276 Field::ProxyJump,
2277 vec![String::from("jump1,jump2,jump3")],
2278 &mut host,
2279 ParseRule::STRICT,
2280 &DefaultAlgorithms::empty(),
2281 )
2282 .unwrap();
2283 assert_eq!(
2284 host.params.proxy_jump.unwrap(),
2285 vec![
2286 "jump1".to_string(),
2287 "jump2".to_string(),
2288 "jump3".to_string()
2289 ]
2290 );
2291 }
2292
2293 #[test]
2294 fn should_update_host_identity_file() {
2295 crate::test_log();
2296 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2297 SshConfigParser::update_host(
2298 Field::IdentityFile,
2299 vec![String::from("/path/to/key1"), String::from("/path/to/key2")],
2300 &mut host,
2301 ParseRule::STRICT,
2302 &DefaultAlgorithms::empty(),
2303 )
2304 .unwrap();
2305 assert_eq!(
2306 host.params.identity_file.unwrap(),
2307 vec![
2308 PathBuf::from("/path/to/key1"),
2309 PathBuf::from("/path/to/key2")
2310 ]
2311 );
2312 }
2313
2314 #[test]
2315 fn test_should_allow_and_append_multiple_identity_files_directives() {
2316 crate::test_log();
2317 let config = r##"
2318Host test
2319 IdentityFile /path/to/key1 /path/to/key2
2320 IdentityFile /path/to/key3
2321"##;
2322 let mut reader = BufReader::new(config.as_bytes());
2323 let config = SshConfig::default()
2324 .default_algorithms(DefaultAlgorithms::empty())
2325 .parse(&mut reader, ParseRule::STRICT)
2326 .expect("Failed to parse config");
2327
2328 let params = config.query("test");
2329 assert_eq!(
2330 params.identity_file.as_ref().unwrap(),
2331 &vec![
2332 PathBuf::from("/path/to/key1"),
2333 PathBuf::from("/path/to/key2"),
2334 PathBuf::from("/path/to/key3"),
2335 ]
2336 );
2337 }
2338
2339 #[test]
2340 fn test_should_accumulate_identity_files_across_host_blocks() {
2341 crate::test_log();
2342 let config = r##"
2343Host test
2344 IdentityFile /path/to/specific_key
2345
2346Host *
2347 IdentityFile /path/to/default_key
2348"##;
2349 let mut reader = BufReader::new(config.as_bytes());
2350 let config = SshConfig::default()
2351 .default_algorithms(DefaultAlgorithms::empty())
2352 .parse(&mut reader, ParseRule::STRICT)
2353 .expect("Failed to parse config");
2354
2355 let params = config.query("test");
2356 assert_eq!(
2358 params.identity_file.as_ref().unwrap(),
2359 &vec![
2360 PathBuf::from("/path/to/specific_key"),
2361 PathBuf::from("/path/to/default_key"),
2362 ]
2363 );
2364 }
2365
2366 #[test]
2367 fn should_store_unsupported_fields_when_allowed() {
2368 crate::test_log();
2369
2370 let config = r##"
2371Host test
2372 PasswordAuthentication yes
2373"##;
2374 let mut reader = BufReader::new(config.as_bytes());
2375 let config = SshConfig::default()
2376 .default_algorithms(DefaultAlgorithms::empty())
2377 .parse(&mut reader, ParseRule::ALLOW_UNSUPPORTED_FIELDS)
2378 .unwrap();
2379
2380 let params = config.query("test");
2381 assert!(
2382 params
2383 .unsupported_fields
2384 .contains_key("passwordauthentication")
2385 );
2386 }
2387
2388 #[test]
2389 fn should_tokenize_line_with_equals_separator() {
2390 crate::test_log();
2391 let (field, args) = SshConfigParser::tokenize_line("HostName=example.com").unwrap();
2392 assert_eq!(field, Field::HostName);
2393 assert_eq!(args, vec!["example.com".to_string()]);
2394 }
2395
2396 #[test]
2397 fn should_tokenize_line_with_quoted_args() {
2398 crate::test_log();
2399 let (field, args) =
2400 SshConfigParser::tokenize_line("Ciphers \"aes256-ctr,aes128-ctr\"").unwrap();
2401 assert_eq!(field, Field::Ciphers);
2402 assert_eq!(args, vec!["aes256-ctr,aes128-ctr".to_string()]);
2403 }
2404
2405 #[test]
2406 fn should_tokenize_line_with_equals_and_quoted_args() {
2407 crate::test_log();
2408 let (field, args) =
2409 SshConfigParser::tokenize_line("Ciphers=\"aes256-ctr,aes128-ctr\"").unwrap();
2410 assert_eq!(field, Field::Ciphers);
2411 assert_eq!(args, vec!["aes256-ctr,aes128-ctr".to_string()]);
2412 }
2413
2414 #[test]
2415 fn should_unescape_quoted_args() {
2416 crate::test_log();
2417
2418 let (field, args) =
2420 SshConfigParser::tokenize_line(r#"HostName "gateway\"server""#).unwrap();
2421 assert_eq!(field, Field::HostName);
2422 assert_eq!(args, vec![r#"gateway"server"#.to_string()]);
2423
2424 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "path\\to\\host""#).unwrap();
2426 assert_eq!(field, Field::HostName);
2427 assert_eq!(args, vec![r#"path\to\host"#.to_string()]);
2428
2429 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "it\'s a test""#).unwrap();
2431 assert_eq!(field, Field::HostName);
2432 assert_eq!(args, vec!["it's a test".to_string()]);
2433
2434 let (field, args) =
2436 SshConfigParser::tokenize_line(r#"HostName "say \"hello\" and \\go""#).unwrap();
2437 assert_eq!(field, Field::HostName);
2438 assert_eq!(args, vec![r#"say "hello" and \go"#.to_string()]);
2439
2440 let (field, args) = SshConfigParser::tokenize_line(r#"HostName "test\nvalue""#).unwrap();
2442 assert_eq!(field, Field::HostName);
2443 assert_eq!(args, vec![r#"test\nvalue"#.to_string()]);
2444 }
2445
2446 #[test]
2447 fn should_tokenize_line_setenv() -> Result<(), SshParserError> {
2448 crate::test_log();
2449 assert_eq!(
2450 SshConfigParser::tokenize_line(
2451 r#"SetEnv TEST_1=Test1 TEST_2="Test 2" TEST_3="Test \"3\"" TEST_4=Test"4""#
2452 )?,
2453 (
2454 Field::SetEnv,
2455 vec![
2456 r#"TEST_1=Test1"#.to_owned(),
2457 r#"TEST_2="Test 2""#.to_owned(),
2458 r#"TEST_3="Test \"3\"""#.to_owned(),
2459 r#"TEST_4=Test"4""#.to_owned(),
2460 ]
2461 )
2462 );
2463 Ok(())
2464 }
2465
2466 #[test]
2467 fn should_count_unescaped_quotes() {
2468 crate::test_log();
2469
2470 assert_eq!(SshConfigParser::count_unescaped_quotes("hello"), 0);
2472
2473 assert_eq!(SshConfigParser::count_unescaped_quotes(r#""hello""#), 2);
2475
2476 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\"hello\""#), 0);
2478
2479 assert_eq!(
2481 SshConfigParser::count_unescaped_quotes(r#""hello\"world""#),
2482 2
2483 );
2484
2485 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\\""#), 1);
2487
2488 assert_eq!(SshConfigParser::count_unescaped_quotes(""), 0);
2490
2491 assert_eq!(SshConfigParser::count_unescaped_quotes(r#"\""#), 0);
2493 }
2494
2495 #[test]
2496 fn should_detect_ends_with_unescaped_quote() {
2497 crate::test_log();
2498
2499 assert!(SshConfigParser::ends_with_unescaped_quote(r#""hello""#));
2501
2502 assert!(!SshConfigParser::ends_with_unescaped_quote(r#""hello\""#));
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("hello"));
2513
2514 assert!(SshConfigParser::ends_with_unescaped_quote(r#"""#));
2516
2517 assert!(!SshConfigParser::ends_with_unescaped_quote(r#"\""#));
2519 }
2520
2521 #[test]
2522 fn should_unescape_string() {
2523 crate::test_log();
2524
2525 assert_eq!(
2527 SshConfigParser::unescape_string(r#"hello\"world"#),
2528 r#"hello"world"#
2529 );
2530
2531 assert_eq!(
2533 SshConfigParser::unescape_string(r#"path\\to\\file"#),
2534 r#"path\to\file"#
2535 );
2536
2537 assert_eq!(SshConfigParser::unescape_string(r#"it\'s"#), "it's");
2539
2540 assert_eq!(
2542 SshConfigParser::unescape_string(r#"say \"hi\" and \\go"#),
2543 r#"say "hi" and \go"#
2544 );
2545
2546 assert_eq!(
2548 SshConfigParser::unescape_string(r#"test\nvalue"#),
2549 r#"test\nvalue"#
2550 );
2551
2552 assert_eq!(SshConfigParser::unescape_string("plain text"), "plain text");
2554
2555 assert_eq!(SshConfigParser::unescape_string(""), "");
2557
2558 assert_eq!(SshConfigParser::unescape_string(r#"test\"#), r#"test\"#);
2560
2561 assert_eq!(SshConfigParser::unescape_string(r#"\\\\"#), r#"\\"#);
2563 }
2564
2565 #[test]
2566 fn should_parse_host_with_single_pattern() {
2567 crate::test_log();
2568 let result = SshConfigParser::parse_host(vec![String::from("example.com")]).unwrap();
2569 assert_eq!(result.len(), 1);
2570 assert_eq!(result[0].pattern, "example.com");
2571 assert!(!result[0].negated);
2572 }
2573
2574 #[test]
2575 fn should_parse_host_with_exclamation_in_pattern() {
2576 crate::test_log();
2577
2578 let result = SshConfigParser::parse_host(vec![String::from("host!name")]).unwrap();
2580 assert_eq!(result.len(), 1);
2581 assert_eq!(result[0].pattern, "host!name");
2582 assert!(!result[0].negated);
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("!a!b!c")]).unwrap();
2592 assert_eq!(result.len(), 1);
2593 assert_eq!(result[0].pattern, "a!b!c");
2594 assert!(result[0].negated);
2595
2596 let result = SshConfigParser::parse_host(vec![String::from("a!b")]).unwrap();
2598 assert_eq!(result.len(), 1);
2599 assert_eq!(result[0].pattern, "a!b");
2600 assert!(!result[0].negated);
2601 }
2602
2603 #[cfg(target_os = "macos")]
2604 #[test]
2605 fn should_update_host_use_keychain() {
2606 crate::test_log();
2607 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
2608 SshConfigParser::update_host(
2609 Field::UseKeychain,
2610 vec![String::from("yes")],
2611 &mut host,
2612 ParseRule::STRICT,
2613 &DefaultAlgorithms::empty(),
2614 )
2615 .unwrap();
2616 assert_eq!(host.params.use_keychain.unwrap(), true);
2617 }
2618
2619 fn create_ssh_config_with_quotes_and_eq() -> NamedTempFile {
2620 let mut tmpfile: tempfile::NamedTempFile =
2621 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2622 let config = r##"
2623# ssh config
2624# written by veeso
2625
2626
2627# I put a comment here just to annoy
2628
2629IgnoreUnknown=Pippo,Pluto
2630ConnectTimeout = 15
2631Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza"
2632Macs="Pasta Carbonara,Pasta con tonno"
2633"##;
2634 tmpfile.write_all(config.as_bytes()).unwrap();
2635 tmpfile
2636 }
2637
2638 fn create_ssh_config() -> NamedTempFile {
2639 let mut tmpfile: tempfile::NamedTempFile =
2640 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2641 let config = r##"
2642# ssh config
2643# written by veeso
2644
2645
2646 # I put a comment here just to annoy
2647
2648IgnoreUnknown Pippo,Pluto
2649
2650Compression yes
2651ConnectionAttempts 10
2652ConnectTimeout 60
2653ServerAliveInterval 40
2654TcpKeepAlive yes
2655Ciphers +a-manella,blowfish
2656
2657# Let's start defining some hosts
2658
2659Host 192.168.*.* 172.26.*.* !192.168.1.30
2660 User omar
2661 # ForwardX11 is actually not supported; I just want to see that it wont' fail parsing
2662 ForwardX11 yes
2663 BindAddress 10.8.0.10
2664 BindInterface tun0
2665 AddKeysToAgent yes
2666 Ciphers +coi-piedi,cazdecan,triestin-stretto
2667 IdentityFile /home/root/.ssh/pippo.key /home/root/.ssh/pluto.key
2668 Macs spyro,deoxys
2669 Port 2222
2670 PubkeyAcceptedAlgorithms -omar-crypt
2671 ProxyJump jump.example.com
2672
2673Host tostapane
2674 User ciro-esposito
2675 HostName 192.168.24.32
2676 RemoteForward 88
2677 Compression no
2678 Pippo yes
2679 Pluto 56
2680 ProxyJump jump1.example.com,jump2.example.com
2681 Macs +spyro,deoxys
2682
2683Host 192.168.1.30
2684 User nutellaro
2685 RemoteForward 123
2686
2687Host *
2688 CaSignatureAlgorithms random
2689 HostKeyAlgorithms luigi,mario
2690 KexAlgorithms desu,gigi
2691 Macs concorde
2692 PubkeyAcceptedAlgorithms desu,omar-crypt,fast-omar-crypt
2693"##;
2694 tmpfile.write_all(config.as_bytes()).unwrap();
2695 tmpfile
2696 }
2697
2698 fn create_inverted_ssh_config() -> NamedTempFile {
2699 let mut tmpfile: tempfile::NamedTempFile =
2700 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2701 let config = r##"
2702Host *-host
2703 IdentityFile ~/.ssh/id_rsa_good
2704
2705Host remote-*
2706 HostName hostname.com
2707 User user
2708 IdentityFile ~/.ssh/id_rsa_bad
2709
2710Host *
2711 ConnectTimeout 15
2712 IdentityFile ~/.ssh/id_rsa_ugly
2713 "##;
2714 tmpfile.write_all(config.as_bytes()).unwrap();
2715 tmpfile
2716 }
2717
2718 fn create_ssh_config_with_comments() -> NamedTempFile {
2719 let mut tmpfile: tempfile::NamedTempFile =
2720 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2721 let config = r##"
2722Host cross-platform # this is my fav host
2723 HostName hostname.com
2724 User user
2725 IdentityFile ~/.ssh/id_rsa_good
2726
2727Host *
2728 AddKeysToAgent yes
2729 IdentityFile ~/.ssh/id_rsa_bad
2730 "##;
2731 tmpfile.write_all(config.as_bytes()).unwrap();
2732 tmpfile
2733 }
2734
2735 fn create_ssh_config_with_unknown_fields() -> NamedTempFile {
2736 let mut tmpfile: tempfile::NamedTempFile =
2737 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
2738 let config = r##"
2739Host cross-platform # this is my fav host
2740 HostName hostname.com
2741 User user
2742 IdentityFile ~/.ssh/id_rsa_good
2743 Piropero yes
2744
2745Host *
2746 AddKeysToAgent yes
2747 IdentityFile ~/.ssh/id_rsa_bad
2748 "##;
2749 tmpfile.write_all(config.as_bytes()).unwrap();
2750 tmpfile
2751 }
2752
2753 #[test]
2754 fn test_should_parse_config_with_include() {
2755 crate::test_log();
2756
2757 let config = create_include_config();
2758 let file = File::open(config.config.path()).expect("Failed to open tempfile");
2759 let mut reader = BufReader::new(file);
2760
2761 let config = SshConfig::default()
2762 .default_algorithms(DefaultAlgorithms::empty())
2763 .parse(&mut reader, ParseRule::STRICT)
2764 .expect("Failed to parse config");
2765
2766 let default_params = config.query("unknown-host");
2767 assert_eq!(
2769 default_params.connect_timeout.unwrap(),
2770 Duration::from_secs(60) );
2772 assert_eq!(
2773 default_params.server_alive_interval.unwrap(),
2774 Duration::from_secs(40) );
2776 assert_eq!(default_params.tcp_keep_alive.unwrap(), true);
2777 assert_eq!(default_params.ciphers.algorithms().is_empty(), true);
2778 assert_eq!(
2779 default_params.ignore_unknown.as_deref().unwrap(),
2780 &["Pippo", "Pluto"]
2781 );
2782 assert_eq!(default_params.compression.unwrap(), true);
2783 assert_eq!(default_params.connection_attempts.unwrap(), 10);
2784 assert_eq!(default_params.port, Some(345));
2785
2786 let glob_params = config.query("192.168.1.1");
2788 assert_eq!(
2789 glob_params.connect_timeout.unwrap(),
2790 Duration::from_secs(60)
2791 );
2792 assert_eq!(
2793 glob_params.server_alive_interval.unwrap(),
2794 Duration::from_secs(40) );
2796 assert_eq!(glob_params.tcp_keep_alive.unwrap(), true);
2797 assert_eq!(glob_params.ciphers.algorithms().is_empty(), true);
2798
2799 let tostapane_params = config.query("tostapane");
2801 assert_eq!(
2802 tostapane_params.connect_timeout.unwrap(),
2803 Duration::from_secs(60) );
2805 assert_eq!(
2806 tostapane_params.server_alive_interval.unwrap(),
2807 Duration::from_secs(40) );
2809 assert_eq!(tostapane_params.tcp_keep_alive.unwrap(), true);
2810 assert_eq!(
2812 tostapane_params.ciphers.algorithms(),
2813 &[
2814 "a-manella",
2815 "blowfish",
2816 "coi-piedi",
2817 "cazdecan",
2818 "triestin-stretto"
2819 ]
2820 );
2821
2822 let microwave_params = config.query("microwave");
2824 assert_eq!(
2825 microwave_params.connect_timeout.unwrap(),
2826 Duration::from_secs(60) );
2828 assert_eq!(
2829 microwave_params.server_alive_interval.unwrap(),
2830 Duration::from_secs(40) );
2832 assert_eq!(
2833 microwave_params.port.unwrap(),
2834 345 );
2836 assert_eq!(microwave_params.tcp_keep_alive.unwrap(), true);
2837 assert_eq!(microwave_params.ciphers.algorithms().is_empty(), true);
2838 assert_eq!(microwave_params.user.as_deref().unwrap(), "mario-rossi");
2839 assert_eq!(
2840 microwave_params.host_name.as_deref().unwrap(),
2841 "192.168.24.33"
2842 );
2843 assert_eq!(
2844 microwave_params.remote_forward,
2845 vec![RemoteForward::new(RemoteForwardListen::Port(88), None)]
2846 );
2847 assert_eq!(microwave_params.compression.unwrap(), true);
2848
2849 let fridge_params = config.query("fridge");
2851 assert_eq!(
2852 fridge_params.connect_timeout.unwrap(),
2853 Duration::from_secs(60)
2854 ); assert_eq!(
2856 fridge_params.server_alive_interval.unwrap(),
2857 Duration::from_secs(40)
2858 ); assert_eq!(fridge_params.tcp_keep_alive.unwrap(), true);
2860 assert_eq!(fridge_params.ciphers.algorithms().is_empty(), true);
2861 assert_eq!(fridge_params.user.as_deref().unwrap(), "luigi-verdi");
2862 assert_eq!(fridge_params.host_name.as_deref().unwrap(), "192.168.24.34");
2863 }
2864
2865 #[test]
2866 fn should_restore_parent_scope_after_include() -> Result<(), SshParserError> {
2867 let mut included = NamedTempFile::new().expect("Failed to create included file");
2868 included.write_all(b"Host foo\n HostName foo.example\n")?;
2869
2870 let config = format!(
2871 "Include {included}\nUser alice\n",
2872 included = included.path().display(),
2873 );
2874 let mut reader = BufReader::new(config.as_bytes());
2875 let config = SshConfig::default()
2876 .default_algorithms(DefaultAlgorithms::empty())
2877 .parse(&mut reader, ParseRule::STRICT)?;
2878
2879 let default = config.query("github.com");
2880 assert_eq!(default.user.as_deref(), Some("alice"));
2881 assert!(default.host_name.is_none());
2882
2883 let foo = config.query("foo");
2884 assert_eq!(foo.user.as_deref(), Some("alice"));
2885 assert_eq!(foo.host_name.as_deref(), Some("foo.example"));
2886
2887 Ok(())
2888 }
2889
2890 #[test]
2891 fn should_keep_included_values_before_parent_fallbacks() -> Result<(), SshParserError> {
2892 let mut included = NamedTempFile::new().expect("Failed to create included file");
2893 included.write_all(b"Host foo\n User bob\n")?;
2894
2895 let config = format!(
2896 "Include {included}\nUser alice\n",
2897 included = included.path().display(),
2898 );
2899 let mut reader = BufReader::new(config.as_bytes());
2900 let config = SshConfig::default()
2901 .default_algorithms(DefaultAlgorithms::empty())
2902 .parse(&mut reader, ParseRule::STRICT)?;
2903
2904 assert_eq!(config.query("foo").user.as_deref(), Some("bob"));
2905 assert_eq!(config.query("github.com").user.as_deref(), Some("alice"));
2906
2907 Ok(())
2908 }
2909
2910 #[test]
2911 fn should_require_enclosing_and_included_host_scopes() -> Result<(), SshParserError> {
2912 let mut included = NamedTempFile::new().expect("Failed to create included file");
2913 included.write_all(b"Host app*\n HostName included.internal\n")?;
2914
2915 let config = format!(
2916 concat!(
2917 "Host *.example.com !app-blocked.example.com\n",
2918 " Include {included}\n",
2919 " Port 2222\n",
2920 "Host *\n",
2921 " User fallback\n",
2922 ),
2923 included = included.path().display(),
2924 );
2925 let mut reader = BufReader::new(config.as_bytes());
2926 let config = SshConfig::default()
2927 .default_algorithms(DefaultAlgorithms::empty())
2928 .parse(&mut reader, ParseRule::STRICT)?;
2929
2930 let allowed = config.query("app1.example.com");
2931 assert_eq!(allowed.host_name.as_deref(), Some("included.internal"));
2932 assert_eq!(allowed.port, Some(2222));
2933 assert_eq!(allowed.user.as_deref(), Some("fallback"));
2934
2935 let negated = config.query("app-blocked.example.com");
2936 assert!(negated.host_name.is_none());
2937 assert!(negated.port.is_none());
2938 assert_eq!(negated.user.as_deref(), Some("fallback"));
2939
2940 let outside = config.query("app1.example.net");
2941 assert!(outside.host_name.is_none());
2942 assert!(outside.port.is_none());
2943 assert_eq!(outside.user.as_deref(), Some("fallback"));
2944
2945 assert!(
2946 config
2947 .intersecting_hosts("app-blocked.example.com")
2948 .all(|host| host.params.host_name.as_deref() != Some("included.internal"))
2949 );
2950
2951 Ok(())
2952 }
2953
2954 #[test]
2955 fn should_restore_scope_between_consecutive_includes() -> Result<(), SshParserError> {
2956 let mut first = NamedTempFile::new().expect("Failed to create first include");
2957 first.write_all(b"Host bar\n HostName bar.internal\n")?;
2958 let mut second = NamedTempFile::new().expect("Failed to create second include");
2959 second.write_all(b"Port 2222\n")?;
2960
2961 let config = format!(
2962 concat!(
2963 "Host foo\n",
2964 " Include {first}\n",
2965 " Include {second}\n",
2966 " User alice\n",
2967 ),
2968 first = first.path().display(),
2969 second = second.path().display(),
2970 );
2971 let mut reader = BufReader::new(config.as_bytes());
2972 let config = SshConfig::default()
2973 .default_algorithms(DefaultAlgorithms::empty())
2974 .parse(&mut reader, ParseRule::STRICT)?;
2975
2976 let foo = config.query("foo");
2977 assert_eq!(foo.port, Some(2222));
2978 assert_eq!(foo.user.as_deref(), Some("alice"));
2979
2980 let bar = config.query("bar");
2981 assert!(bar.host_name.is_none());
2982 assert!(bar.port.is_none());
2983 assert!(bar.user.is_none());
2984
2985 Ok(())
2986 }
2987
2988 #[test]
2989 fn should_preserve_all_nested_include_scopes() -> Result<(), SshParserError> {
2990 let mut inner = NamedTempFile::new().expect("Failed to create inner include");
2991 inner.write_all(b"User nested\n")?;
2992
2993 let mut outer = NamedTempFile::new().expect("Failed to create outer include");
2994 writeln!(
2995 outer,
2996 "Host app*\n Include {inner}",
2997 inner = inner.path().display(),
2998 )?;
2999
3000 let config = format!(
3001 "Host *.example.com\n Include {outer}\n",
3002 outer = outer.path().display(),
3003 );
3004 let mut reader = BufReader::new(config.as_bytes());
3005 let config = SshConfig::default()
3006 .default_algorithms(DefaultAlgorithms::empty())
3007 .parse(&mut reader, ParseRule::STRICT)?;
3008
3009 assert_eq!(
3010 config.query("app1.example.com").user.as_deref(),
3011 Some("nested")
3012 );
3013 assert!(config.query("app1.example.net").user.is_none());
3014 assert!(config.query("other.example.com").user.is_none());
3015
3016 Ok(())
3017 }
3018
3019 #[allow(dead_code)]
3020 struct ConfigWithInclude {
3021 config: NamedTempFile,
3022 inc1: NamedTempFile,
3023 inc2: NamedTempFile,
3024 inc3: NamedTempFile,
3025 inc4: NamedTempFile,
3026 }
3027
3028 fn create_include_config() -> ConfigWithInclude {
3029 let mut config_file: tempfile::NamedTempFile =
3030 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3031 let mut inc1_file: tempfile::NamedTempFile =
3032 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3033 let mut inc2_file: tempfile::NamedTempFile =
3034 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3035 let mut inc3_file: tempfile::NamedTempFile =
3036 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3037 let mut inc4_file: tempfile::NamedTempFile =
3038 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
3039
3040 let config = format!(
3041 r##"
3042# ssh config
3043# written by veeso
3044
3045
3046 # I put a comment here just to annoy
3047
3048IgnoreUnknown Pippo,Pluto
3049
3050Compression yes
3051ConnectionAttempts 10
3052ConnectTimeout 60
3053ServerAliveInterval 40
3054Include {inc1}
3055
3056# Let's start defining some hosts
3057
3058Host tostapane
3059 User ciro-esposito
3060 HostName 192.168.24.32
3061 RemoteForward 88
3062 Compression no
3063 # Ignore unknown fields should be inherited from the global section
3064 Pippo yes
3065 Pluto 56
3066 Include {inc2}
3067
3068Host *
3069Include {inc3}
3070Include {inc4}
3071"##,
3072 inc1 = inc1_file.path().display(),
3073 inc2 = inc2_file.path().display(),
3074 inc3 = inc3_file.path().display(),
3075 inc4 = inc4_file.path().display(),
3076 );
3077 config_file.write_all(config.as_bytes()).unwrap();
3078
3079 let inc1 = r##"
3081 ConnectTimeout 60
3082 ServerAliveInterval 60
3083 TcpKeepAlive yes
3084 "##;
3085 inc1_file.write_all(inc1.as_bytes()).unwrap();
3086
3087 let inc2 = r##"
3089 ConnectTimeout 180
3090 ServerAliveInterval 180
3091 Ciphers +a-manella,blowfish,coi-piedi,cazdecan,triestin-stretto
3092 "##;
3093 inc2_file.write_all(inc2.as_bytes()).unwrap();
3094
3095 let inc3 = r##"
3097Host microwave
3098 User mario-rossi
3099 HostName 192.168.24.33
3100 RemoteForward 88
3101 Compression no
3102 # Ignore unknown fields should be inherited from the global section
3103 Pippo yes
3104 Pluto 56
3105"##;
3106 inc3_file.write_all(inc3.as_bytes()).unwrap();
3107
3108 let inc4 = r##"
3110 # Update microwave
3111 ServerAliveInterval 30
3112 Port 345
3113
3114# Force microwave update (it won't work)
3115Host microwave
3116 ConnectTimeout 30
3117
3118Host fridge
3119 User luigi-verdi
3120 HostName 192.168.24.34
3121 RemoteForward 88
3122 Compression no
3123"##;
3124 inc4_file.write_all(inc4.as_bytes()).unwrap();
3125
3126 ConfigWithInclude {
3127 config: config_file,
3128 inc1: inc1_file,
3129 inc2: inc2_file,
3130 inc3: inc3_file,
3131 inc4: inc4_file,
3132 }
3133 }
3134}