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