1use regex::{Captures, Regex};
3use std::collections::BTreeMap;
4use std::path::Path;
5
6pub type ConfigMap = BTreeMap<String, String>;
7
8#[derive(Debug, PartialEq, Eq, Clone)]
10struct Pattern {
11 negated: bool,
12 pattern: String,
13}
14
15fn wildcard_to_pattern(s: &str) -> String {
17 let mut pattern = String::new();
18 pattern.push('^');
19 for c in s.chars() {
20 if c == '*' {
21 pattern.push_str(".*");
22 } else if c == '?' {
23 pattern.push('.');
24 } else {
25 let s = regex::escape(&c.to_string());
26 pattern.push_str(&s);
27 }
28 }
29 pattern.push('$');
30 pattern
31}
32
33impl Pattern {
34 fn match_text(&self, hostname: &str) -> bool {
36 if let Ok(re) = Regex::new(&self.pattern) {
37 re.is_match(hostname)
38 } else {
39 false
40 }
41 }
42
43 fn new(text: &str, negated: bool) -> Self {
44 Self {
45 pattern: wildcard_to_pattern(text),
46 negated,
47 }
48 }
49
50 fn match_group(hostname: &str, patterns: &[Self]) -> bool {
53 for pat in patterns {
54 if pat.match_text(hostname) {
55 return !pat.negated;
60 }
61 }
62 false
63 }
64}
65
66#[derive(Clone, Eq, PartialEq, Debug)]
67enum Criteria {
68 Host(Vec<Pattern>),
69 Exec(String),
70 OriginalHost(Vec<Pattern>),
71 User(Vec<Pattern>),
72 LocalUser(Vec<Pattern>),
73 All,
74}
75
76#[derive(Copy, Clone, Eq, PartialEq, Debug)]
77enum Context {
78 FirstPass,
79 Canonical,
80 Final,
81}
82
83#[derive(Debug, PartialEq, Eq, Clone)]
86struct MatchGroup {
87 criteria: Vec<Criteria>,
88 context: Context,
89 options: ConfigMap,
90}
91
92impl MatchGroup {
93 fn is_match(&self, hostname: &str, user: &str, local_user: &str, context: Context) -> bool {
94 if self.context != context {
95 return false;
96 }
97 for c in &self.criteria {
98 match c {
99 Criteria::Host(patterns) => {
100 if !Pattern::match_group(hostname, patterns) {
101 return false;
102 }
103 }
104 Criteria::Exec(_) => {
105 log::warn!("Match Exec is not implemented");
106 }
107 Criteria::OriginalHost(patterns) => {
108 if !Pattern::match_group(hostname, patterns) {
109 return false;
110 }
111 }
112 Criteria::User(patterns) => {
113 if !Pattern::match_group(user, patterns) {
114 return false;
115 }
116 }
117 Criteria::LocalUser(patterns) => {
118 if !Pattern::match_group(local_user, patterns) {
119 return false;
120 }
121 }
122 Criteria::All => {
123 }
125 }
126 }
127 true
128 }
129}
130
131#[derive(Debug, PartialEq, Eq, Clone)]
135struct ParsedConfigFile {
136 options: ConfigMap,
138 groups: Vec<MatchGroup>,
140}
141
142impl ParsedConfigFile {
143 fn parse(s: &str, cwd: Option<&Path>) -> Self {
144 let mut options = ConfigMap::new();
145 let mut groups = vec![];
146
147 Self::parse_impl(s, cwd, &mut options, &mut groups);
148
149 Self { options, groups }
150 }
151
152 fn do_include(
153 pattern: &str,
154 cwd: Option<&Path>,
155 options: &mut ConfigMap,
156 groups: &mut Vec<MatchGroup>,
157 ) {
158 match filenamegen::Glob::new(&pattern) {
159 Ok(g) => {
160 match cwd
161 .as_ref()
162 .map(|p| p.to_path_buf())
163 .or_else(|| std::env::current_dir().ok())
164 {
165 Some(cwd) => {
166 for path in g.walk(&cwd) {
167 let path = if path.is_absolute() {
168 path
169 } else {
170 cwd.join(path)
171 };
172 match std::fs::read_to_string(&path) {
173 Ok(data) => {
174 Self::parse_impl(&data, Some(&cwd), options, groups);
175 }
176 Err(err) => {
177 log::error!(
178 "error expanding `Include {}`: unable to open {}: {:#}",
179 pattern,
180 path.display(),
181 err
182 );
183 }
184 }
185 }
186 }
187 None => {
188 log::error!(
189 "error expanding `Include {}`: unable to determine cwd",
190 pattern
191 );
192 }
193 }
194 }
195 Err(err) => {
196 log::error!("error expanding `Include {}`: {:#}", pattern, err);
197 }
198 }
199 }
200
201 fn parse_impl(
202 s: &str,
203 cwd: Option<&Path>,
204 options: &mut ConfigMap,
205 groups: &mut Vec<MatchGroup>,
206 ) {
207 for line in s.lines() {
208 let line = line.trim();
209 if line.is_empty() || line.starts_with('#') {
210 continue;
211 }
212
213 if let Some(sep) = line
214 .find('=')
215 .or_else(|| line.find(|c: char| c.is_whitespace()))
216 {
217 let (k, v) = line.split_at(sep);
218 let k = k.trim().to_lowercase();
219 let v = v[1..].trim();
220
221 let v = if v.starts_with('"') && v.ends_with('"') {
222 &v[1..v.len() - 1]
223 } else {
224 v
225 };
226
227 fn parse_pattern_list(v: &str) -> Vec<Pattern> {
228 let mut patterns = vec![];
229 for p in v.split(',') {
230 let p = p.trim();
231 if p.starts_with('!') {
232 patterns.push(Pattern::new(&p[1..], true));
233 } else {
234 patterns.push(Pattern::new(p, false));
235 }
236 }
237 patterns
238 }
239 fn parse_whitespace_pattern_list(v: &str) -> Vec<Pattern> {
240 let mut patterns = vec![];
241 for p in v.split_ascii_whitespace() {
242 let p = p.trim();
243 if p.starts_with('!') {
244 patterns.push(Pattern::new(&p[1..], true));
245 } else {
246 patterns.push(Pattern::new(p, false));
247 }
248 }
249 patterns
250 }
251
252 if k == "include" {
253 Self::do_include(v, cwd, options, groups);
254 continue;
255 }
256
257 if k == "host" {
258 let patterns = parse_whitespace_pattern_list(v);
259 groups.push(MatchGroup {
260 criteria: vec![Criteria::Host(patterns)],
261 options: ConfigMap::new(),
262 context: Context::FirstPass,
263 });
264 continue;
265 }
266
267 if k == "match" {
268 let mut criteria = vec![];
269 let mut context = Context::FirstPass;
270
271 let mut tokens = v.split_ascii_whitespace();
272
273 while let Some(cname) = tokens.next() {
274 match cname.to_lowercase().as_str() {
275 "all" => {
276 criteria.push(Criteria::All);
277 }
278 "canonical" => {
279 context = Context::Canonical;
280 }
281 "final" => {
282 context = Context::Final;
283 }
284 "exec" => {
285 criteria.push(Criteria::Exec(
286 tokens.next().unwrap_or("false").to_string(),
287 ));
288 }
289 "host" => {
290 criteria.push(Criteria::Host(parse_pattern_list(
291 tokens.next().unwrap_or(""),
292 )));
293 }
294 "originalhost" => {
295 criteria.push(Criteria::OriginalHost(parse_pattern_list(
296 tokens.next().unwrap_or(""),
297 )));
298 }
299 "user" => {
300 criteria.push(Criteria::User(parse_pattern_list(
301 tokens.next().unwrap_or(""),
302 )));
303 }
304 "localuser" => {
305 criteria.push(Criteria::LocalUser(parse_pattern_list(
306 tokens.next().unwrap_or(""),
307 )));
308 }
309 _ => break,
310 }
311 }
312
313 groups.push(MatchGroup {
314 criteria,
315 options: ConfigMap::new(),
316 context,
317 });
318 continue;
319 }
320
321 fn add_option(options: &mut ConfigMap, k: String, v: &str) {
322 let is_identity_file = k == "identityfile";
325 options
326 .entry(k)
327 .and_modify(|e| {
328 if is_identity_file {
329 e.push(' ');
330 e.push_str(v);
331 }
332 })
333 .or_insert_with(|| v.to_string());
334 }
335
336 if let Some(group) = groups.last_mut() {
337 add_option(&mut group.options, k, v);
338 } else {
339 add_option(options, k, v);
340 }
341 }
342 }
343 }
344
345 fn apply_matches(
349 &self,
350 hostname: &str,
351 user: &str,
352 local_user: &str,
353 context: Context,
354 target: &mut ConfigMap,
355 ) -> bool {
356 let mut needs_reparse = false;
357
358 for (k, v) in &self.options {
359 target.entry(k.to_string()).or_insert_with(|| v.to_string());
360 }
361 for group in &self.groups {
362 if group.context != Context::FirstPass {
363 needs_reparse = true;
364 }
365 if group.is_match(hostname, user, local_user, context) {
366 for (k, v) in &group.options {
367 target.entry(k.to_string()).or_insert_with(|| v.to_string());
368 }
369 }
370 }
371
372 needs_reparse
373 }
374}
375
376#[derive(Debug, Clone)]
380pub struct Config {
381 config_files: Vec<ParsedConfigFile>,
382 options: ConfigMap,
383 tokens: ConfigMap,
384 environment: Option<ConfigMap>,
385}
386
387impl Config {
388 pub fn new() -> Self {
390 Self {
391 config_files: vec![],
392 options: ConfigMap::new(),
393 tokens: ConfigMap::new(),
394 environment: None,
395 }
396 }
397
398 pub fn assign_environment(&mut self, env: ConfigMap) {
402 self.environment.replace(env);
403 }
404
405 pub fn assign_tokens(&mut self, tokens: ConfigMap) {
409 self.tokens = tokens;
410 }
411
412 pub fn set_option<K: AsRef<str>, V: AsRef<str>>(&mut self, key: K, value: V) {
417 self.options
418 .insert(key.as_ref().to_lowercase(), value.as_ref().to_string());
419 }
420
421 pub fn add_config_string(&mut self, config_string: &str) {
424 self.config_files
425 .push(ParsedConfigFile::parse(config_string, None));
426 }
427
428 pub fn add_config_file<P: AsRef<Path>>(&mut self, path: P) {
431 if let Ok(data) = std::fs::read_to_string(path.as_ref()) {
432 self.config_files
433 .push(ParsedConfigFile::parse(&data, path.as_ref().parent()));
434 }
435 }
436
437 pub fn add_default_config_files(&mut self) {
440 if let Some(home) = dirs_next::home_dir() {
441 self.add_config_file(home.join(".ssh").join("config"));
442 }
443 self.add_config_file("/etc/ssh/ssh_config");
444 if let Ok(sysdrive) = std::env::var("SystemDrive") {
445 self.add_config_file(format!("{}/ProgramData/ssh/ssh_config", sysdrive));
446 }
447 }
448
449 fn resolve_local_user(&self) -> String {
450 for user in &["USER", "USERNAME"] {
451 if let Some(user) = self.resolve_env(user) {
452 return user;
453 }
454 }
455 "unknown-user".to_string()
456 }
457
458 pub fn for_host<H: AsRef<str>>(&self, host: H) -> ConfigMap {
467 let host = host.as_ref();
468 let local_user = self.resolve_local_user();
469 let target_user = &local_user;
470
471 let mut result = self.options.clone();
472 let mut needs_reparse = false;
473
474 for config in &self.config_files {
475 if config.apply_matches(
476 host,
477 target_user,
478 &local_user,
479 Context::FirstPass,
480 &mut result,
481 ) {
482 needs_reparse = true;
483 }
484 }
485
486 if needs_reparse {
487 log::warn!(
488 "ssh configuration uses options that require two-phase \
489 parsing, which isn't supported"
490 );
491 }
492
493 for (k, v) in &mut result {
494 if let Some(tokens) = self.should_expand_tokens(k) {
495 self.expand_tokens(v, tokens);
496 }
497
498 if self.should_expand_environment(k) {
499 self.expand_environment(v);
500 }
501 }
502
503 result
504 .entry("hostname".to_string())
505 .or_insert_with(|| host.to_string());
506
507 result
508 .entry("port".to_string())
509 .or_insert_with(|| "22".to_string());
510
511 result
512 .entry("user".to_string())
513 .or_insert_with(|| target_user.clone());
514
515 if !result.contains_key("userknownhostsfile") {
516 if let Some(home) = self.resolve_home() {
517 result.insert(
518 "userknownhostsfile".to_string(),
519 format!("{}/.ssh/known_hosts {}/.ssh/known_hosts2", home, home,),
520 );
521 }
522 }
523
524 if !result.contains_key("identityfile") {
525 if let Some(home) = self.resolve_home() {
526 result.insert(
527 "identityfile".to_string(),
528 format!(
529 "{}/.ssh/id_dsa {}/.ssh/id_ecdsa {}/.ssh/id_ed25519 {}/.ssh/id_rsa",
530 home, home, home, home
531 ),
532 );
533 }
534 }
535
536 if !result.contains_key("identityagent") {
537 if let Some(sock_path) = self.resolve_env("SSH_AUTH_SOCK") {
538 result.insert("identityagent".to_string(), sock_path);
539 }
540 }
541
542 result
543 }
544
545 fn should_expand_environment(&self, key: &str) -> bool {
548 match key {
549 "certificatefile" | "controlpath" | "identityagent" | "identityfile"
550 | "userknownhostsfile" | "localforward" | "remoteforward" => true,
551 _ => false,
552 }
553 }
554
555 fn should_expand_tokens(&self, key: &str) -> Option<&[&str]> {
557 match key {
558 "certificatefile" | "controlpath" | "identityagent" | "identityfile"
559 | "localforward" | "remotecommand" | "remoteforward" | "userknownkostsfile" => {
560 Some(&["%C", "%d", "%h", "%i", "%L", "%l", "%n", "%p", "%r", "%u"])
561 }
562 "hostname" => Some(&["%h"]),
563 "localcommand" => Some(&[
564 "%C", "%d", "%h", "%i", "%k", "%L", "%l", "%n", "%p", "%r", "%T", "%u",
565 ]),
566 "proxycommand" => Some(&["%h", "%n", "%p", "%r"]),
567 _ => None,
568 }
569 }
570
571 fn resolve_home(&self) -> Option<String> {
575 if let Some(env) = self.environment.as_ref() {
576 if let Some(home) = env.get("HOME") {
577 return Some(home.to_string());
578 }
579 }
580 if let Some(home) = dirs_next::home_dir() {
581 if let Some(home) = home.to_str() {
582 return Some(home.to_string());
583 }
584 }
585 None
586 }
587
588 fn expand_tokens(&self, value: &mut String, tokens: &[&str]) {
590 for &t in tokens {
591 if let Some(v) = self.tokens.get(t) {
592 *value = value.replace(t, v);
593 } else if t == "%u" {
594 *value = value.replace(t, &self.resolve_local_user());
595 } else if t == "%d" {
596 if let Some(home) = self.resolve_home() {
597 let mut items = value
598 .split_whitespace()
599 .map(|s| s.to_string())
600 .collect::<Vec<String>>();
601 for item in &mut items {
602 if item.starts_with("~/") {
603 item.replace_range(0..1, &home);
604 } else {
605 *item = item.replace(t, &home);
606 }
607 }
608 *value = items.join(" ");
609 }
610 }
611 }
612
613 *value = value.replace("%%", "%");
614 }
615
616 fn resolve_env(&self, name: &str) -> Option<String> {
619 if let Some(env) = self.environment.as_ref() {
620 env.get(name).cloned()
621 } else {
622 std::env::var(name).ok()
623 }
624 }
625
626 fn expand_environment(&self, value: &mut String) {
629 let re = Regex::new(r#"\$\{([a-zA-Z_][a-zA-Z_0-9]+)\}"#).unwrap();
630 *value = re
631 .replace_all(value, |caps: &Captures| -> String {
632 if let Some(rep) = self.resolve_env(&caps[1]) {
633 rep
634 } else {
635 caps[0].to_string()
636 }
637 })
638 .to_string();
639 }
640}
641
642#[cfg(test)]
643mod test {
644 use super::*;
645 use k9::snapshot;
646
647 #[test]
648 fn parse_user() {
649 let mut config = Config::new();
650
651 let mut fake_env = ConfigMap::new();
652 fake_env.insert("HOME".to_string(), "/home/me".to_string());
653 fake_env.insert("USER".to_string(), "me".to_string());
654 config.assign_environment(fake_env);
655
656 config.add_config_string(
657 r#"
658 Host foo
659 HostName 10.0.0.1
660 User foo
661 IdentityFile "%d/.ssh/id_pub.dsa"
662 "#,
663 );
664
665 snapshot!(
666 &config,
667 r#"
668Config {
669 config_files: [
670 ParsedConfigFile {
671 options: {},
672 groups: [
673 MatchGroup {
674 criteria: [
675 Host(
676 [
677 Pattern {
678 negated: false,
679 pattern: "^foo$",
680 },
681 ],
682 ),
683 ],
684 context: FirstPass,
685 options: {
686 "hostname": "10.0.0.1",
687 "identityfile": "%d/.ssh/id_pub.dsa",
688 "user": "foo",
689 },
690 },
691 ],
692 },
693 ],
694 options: {},
695 tokens: {},
696 environment: Some(
697 {
698 "HOME": "/home/me",
699 "USER": "me",
700 },
701 ),
702}
703"#
704 );
705
706 let opts = config.for_host("foo");
707 snapshot!(
708 opts,
709 r#"
710{
711 "hostname": "10.0.0.1",
712 "identityfile": "/home/me/.ssh/id_pub.dsa",
713 "port": "22",
714 "user": "foo",
715 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
716}
717"#
718 );
719 }
720
721 #[test]
722 fn multiple_identityfile() {
723 let mut config = Config::new();
724
725 let mut fake_env = ConfigMap::new();
726 fake_env.insert("HOME".to_string(), "/home/me".to_string());
727 fake_env.insert("USER".to_string(), "me".to_string());
728 config.assign_environment(fake_env);
729
730 config.add_config_string(
731 r#"
732 Host foo
733 HostName 10.0.0.1
734 User foo
735 IdentityFile "~/.ssh/id_pub.dsa"
736 IdentityFile "~/.ssh/id_pub.rsa"
737 "#,
738 );
739
740 let opts = config.for_host("foo");
741 snapshot!(
742 opts,
743 r#"
744{
745 "hostname": "10.0.0.1",
746 "identityfile": "/home/me/.ssh/id_pub.dsa /home/me/.ssh/id_pub.rsa",
747 "port": "22",
748 "user": "foo",
749 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
750}
751"#
752 );
753 }
754
755 #[test]
756 fn sub_tilde() {
757 let mut config = Config::new();
758
759 let mut fake_env = ConfigMap::new();
760 fake_env.insert("HOME".to_string(), "/home/me".to_string());
761 fake_env.insert("USER".to_string(), "me".to_string());
762 config.assign_environment(fake_env);
763
764 config.add_config_string(
765 r#"
766 Host foo
767 HostName 10.0.0.1
768 User foo
769 IdentityFile "~/.ssh/id_pub.dsa"
770 "#,
771 );
772
773 let opts = config.for_host("foo");
774 snapshot!(
775 opts,
776 r#"
777{
778 "hostname": "10.0.0.1",
779 "identityfile": "/home/me/.ssh/id_pub.dsa",
780 "port": "22",
781 "user": "foo",
782 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
783}
784"#
785 );
786 }
787
788 #[test]
789 fn parse_match() {
790 let mut config = Config::new();
791
792 let mut fake_env = ConfigMap::new();
793 fake_env.insert("HOME".to_string(), "/home/me".to_string());
794 fake_env.insert("USER".to_string(), "me".to_string());
795 config.assign_environment(fake_env);
796
797 config.add_config_string(
798 r#"
799 # I am a comment
800 Something first
801 # the prior Something takes precedence
802 Something ignored
803 Match Host 192.168.1.8,wopr
804 FowardAgent yes
805 IdentityFile "%d/.ssh/id_pub.dsa"
806
807 Match Host !a.b,*.b User fred
808 ForwardAgent no
809 IdentityAgent "${HOME}/.ssh/agent"
810
811 Match Host !a.b,*.b User me
812 ForwardAgent no
813 IdentityAgent "${HOME}/.ssh/agent-me"
814
815 Host *
816 Something else
817 "#,
818 );
819
820 snapshot!(
821 &config,
822 r#"
823Config {
824 config_files: [
825 ParsedConfigFile {
826 options: {
827 "something": "first",
828 },
829 groups: [
830 MatchGroup {
831 criteria: [
832 Host(
833 [
834 Pattern {
835 negated: false,
836 pattern: "^192\\.168\\.1\\.8$",
837 },
838 Pattern {
839 negated: false,
840 pattern: "^wopr$",
841 },
842 ],
843 ),
844 ],
845 context: FirstPass,
846 options: {
847 "fowardagent": "yes",
848 "identityfile": "%d/.ssh/id_pub.dsa",
849 },
850 },
851 MatchGroup {
852 criteria: [
853 Host(
854 [
855 Pattern {
856 negated: true,
857 pattern: "^a\\.b$",
858 },
859 Pattern {
860 negated: false,
861 pattern: "^.*\\.b$",
862 },
863 ],
864 ),
865 User(
866 [
867 Pattern {
868 negated: false,
869 pattern: "^fred$",
870 },
871 ],
872 ),
873 ],
874 context: FirstPass,
875 options: {
876 "forwardagent": "no",
877 "identityagent": "${HOME}/.ssh/agent",
878 },
879 },
880 MatchGroup {
881 criteria: [
882 Host(
883 [
884 Pattern {
885 negated: true,
886 pattern: "^a\\.b$",
887 },
888 Pattern {
889 negated: false,
890 pattern: "^.*\\.b$",
891 },
892 ],
893 ),
894 User(
895 [
896 Pattern {
897 negated: false,
898 pattern: "^me$",
899 },
900 ],
901 ),
902 ],
903 context: FirstPass,
904 options: {
905 "forwardagent": "no",
906 "identityagent": "${HOME}/.ssh/agent-me",
907 },
908 },
909 MatchGroup {
910 criteria: [
911 Host(
912 [
913 Pattern {
914 negated: false,
915 pattern: "^.*$",
916 },
917 ],
918 ),
919 ],
920 context: FirstPass,
921 options: {
922 "something": "else",
923 },
924 },
925 ],
926 },
927 ],
928 options: {},
929 tokens: {},
930 environment: Some(
931 {
932 "HOME": "/home/me",
933 "USER": "me",
934 },
935 ),
936}
937"#
938 );
939
940 let opts = config.for_host("random");
941 snapshot!(
942 opts,
943 r#"
944{
945 "hostname": "random",
946 "identityfile": "/home/me/.ssh/id_dsa /home/me/.ssh/id_ecdsa /home/me/.ssh/id_ed25519 /home/me/.ssh/id_rsa",
947 "port": "22",
948 "something": "first",
949 "user": "me",
950 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
951}
952"#
953 );
954
955 let opts = config.for_host("192.168.1.8");
956 snapshot!(
957 opts,
958 r#"
959{
960 "fowardagent": "yes",
961 "hostname": "192.168.1.8",
962 "identityfile": "/home/me/.ssh/id_pub.dsa",
963 "port": "22",
964 "something": "first",
965 "user": "me",
966 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
967}
968"#
969 );
970
971 let opts = config.for_host("a.b");
972 snapshot!(
973 opts,
974 r#"
975{
976 "hostname": "a.b",
977 "identityfile": "/home/me/.ssh/id_dsa /home/me/.ssh/id_ecdsa /home/me/.ssh/id_ed25519 /home/me/.ssh/id_rsa",
978 "port": "22",
979 "something": "first",
980 "user": "me",
981 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
982}
983"#
984 );
985
986 let opts = config.for_host("b.b");
987 snapshot!(
988 opts,
989 r#"
990{
991 "forwardagent": "no",
992 "hostname": "b.b",
993 "identityagent": "/home/me/.ssh/agent-me",
994 "identityfile": "/home/me/.ssh/id_dsa /home/me/.ssh/id_ecdsa /home/me/.ssh/id_ed25519 /home/me/.ssh/id_rsa",
995 "port": "22",
996 "something": "first",
997 "user": "me",
998 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
999}
1000"#
1001 );
1002
1003 let mut fake_env = ConfigMap::new();
1004 fake_env.insert("HOME".to_string(), "/home/fred".to_string());
1005 fake_env.insert("USER".to_string(), "fred".to_string());
1006 config.assign_environment(fake_env);
1007
1008 let opts = config.for_host("b.b");
1009 snapshot!(
1010 opts,
1011 r#"
1012{
1013 "forwardagent": "no",
1014 "hostname": "b.b",
1015 "identityagent": "/home/fred/.ssh/agent",
1016 "identityfile": "/home/fred/.ssh/id_dsa /home/fred/.ssh/id_ecdsa /home/fred/.ssh/id_ed25519 /home/fred/.ssh/id_rsa",
1017 "port": "22",
1018 "something": "first",
1019 "user": "fred",
1020 "userknownhostsfile": "/home/fred/.ssh/known_hosts /home/fred/.ssh/known_hosts2",
1021}
1022"#
1023 );
1024 }
1025
1026 #[test]
1027 fn parse_simple() {
1028 let mut config = Config::new();
1029
1030 let mut fake_env = ConfigMap::new();
1031 fake_env.insert("HOME".to_string(), "/home/me".to_string());
1032 fake_env.insert("USER".to_string(), "me".to_string());
1033 config.assign_environment(fake_env);
1034
1035 config.add_config_string(
1036 r#"
1037 # I am a comment
1038 Something first
1039 # the prior Something takes precedence
1040 Something ignored
1041 Host 192.168.1.8 wopr
1042 FowardAgent yes
1043 IdentityFile "%d/.ssh/id_pub.dsa"
1044
1045 Host !a.b *.b
1046 ForwardAgent no
1047 IdentityAgent "${HOME}/.ssh/agent"
1048
1049 Host *
1050 Something else
1051 "#,
1052 );
1053
1054 snapshot!(
1055 &config,
1056 r#"
1057Config {
1058 config_files: [
1059 ParsedConfigFile {
1060 options: {
1061 "something": "first",
1062 },
1063 groups: [
1064 MatchGroup {
1065 criteria: [
1066 Host(
1067 [
1068 Pattern {
1069 negated: false,
1070 pattern: "^192\\.168\\.1\\.8$",
1071 },
1072 Pattern {
1073 negated: false,
1074 pattern: "^wopr$",
1075 },
1076 ],
1077 ),
1078 ],
1079 context: FirstPass,
1080 options: {
1081 "fowardagent": "yes",
1082 "identityfile": "%d/.ssh/id_pub.dsa",
1083 },
1084 },
1085 MatchGroup {
1086 criteria: [
1087 Host(
1088 [
1089 Pattern {
1090 negated: true,
1091 pattern: "^a\\.b$",
1092 },
1093 Pattern {
1094 negated: false,
1095 pattern: "^.*\\.b$",
1096 },
1097 ],
1098 ),
1099 ],
1100 context: FirstPass,
1101 options: {
1102 "forwardagent": "no",
1103 "identityagent": "${HOME}/.ssh/agent",
1104 },
1105 },
1106 MatchGroup {
1107 criteria: [
1108 Host(
1109 [
1110 Pattern {
1111 negated: false,
1112 pattern: "^.*$",
1113 },
1114 ],
1115 ),
1116 ],
1117 context: FirstPass,
1118 options: {
1119 "something": "else",
1120 },
1121 },
1122 ],
1123 },
1124 ],
1125 options: {},
1126 tokens: {},
1127 environment: Some(
1128 {
1129 "HOME": "/home/me",
1130 "USER": "me",
1131 },
1132 ),
1133}
1134"#
1135 );
1136
1137 let opts = config.for_host("random");
1138 snapshot!(
1139 opts,
1140 r#"
1141{
1142 "hostname": "random",
1143 "identityfile": "/home/me/.ssh/id_dsa /home/me/.ssh/id_ecdsa /home/me/.ssh/id_ed25519 /home/me/.ssh/id_rsa",
1144 "port": "22",
1145 "something": "first",
1146 "user": "me",
1147 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
1148}
1149"#
1150 );
1151
1152 let opts = config.for_host("192.168.1.8");
1153 snapshot!(
1154 opts,
1155 r#"
1156{
1157 "fowardagent": "yes",
1158 "hostname": "192.168.1.8",
1159 "identityfile": "/home/me/.ssh/id_pub.dsa",
1160 "port": "22",
1161 "something": "first",
1162 "user": "me",
1163 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
1164}
1165"#
1166 );
1167
1168 let opts = config.for_host("a.b");
1169 snapshot!(
1170 opts,
1171 r#"
1172{
1173 "hostname": "a.b",
1174 "identityfile": "/home/me/.ssh/id_dsa /home/me/.ssh/id_ecdsa /home/me/.ssh/id_ed25519 /home/me/.ssh/id_rsa",
1175 "port": "22",
1176 "something": "first",
1177 "user": "me",
1178 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
1179}
1180"#
1181 );
1182
1183 let opts = config.for_host("b.b");
1184 snapshot!(
1185 opts,
1186 r#"
1187{
1188 "forwardagent": "no",
1189 "hostname": "b.b",
1190 "identityagent": "/home/me/.ssh/agent",
1191 "identityfile": "/home/me/.ssh/id_dsa /home/me/.ssh/id_ecdsa /home/me/.ssh/id_ed25519 /home/me/.ssh/id_rsa",
1192 "port": "22",
1193 "something": "first",
1194 "user": "me",
1195 "userknownhostsfile": "/home/me/.ssh/known_hosts /home/me/.ssh/known_hosts2",
1196}
1197"#
1198 );
1199 }
1200}