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