1
2
3
4
5
6
7
8
9
10
11
12 package org.eclipse.jgit.internal.transport.ssh;
13
14 import static java.nio.charset.StandardCharsets.UTF_8;
15
16 import java.io.BufferedReader;
17 import java.io.File;
18 import java.io.IOException;
19 import java.nio.file.Files;
20 import java.time.Instant;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.LinkedList;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.TreeMap;
30 import java.util.TreeSet;
31
32 import org.eclipse.jgit.annotations.NonNull;
33 import org.eclipse.jgit.errors.InvalidPatternException;
34 import org.eclipse.jgit.fnmatch.FileNameMatcher;
35 import org.eclipse.jgit.transport.SshConfigStore;
36 import org.eclipse.jgit.transport.SshConstants;
37 import org.eclipse.jgit.util.FS;
38 import org.eclipse.jgit.util.StringUtils;
39 import org.eclipse.jgit.util.SystemReader;
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 public class OpenSshConfigFile implements SshConfigStore {
85
86
87 private final File home;
88
89
90 private final File configFile;
91
92
93 private final String localUserName;
94
95
96 private Instant lastModified;
97
98
99
100
101
102 private static class State {
103 List<HostEntry> entries = new LinkedList<>();
104
105
106 Map<String, HostEntry> hosts = new HashMap<>();
107
108 @Override
109 @SuppressWarnings("nls")
110 public String toString() {
111 return "State [entries=" + entries + ", hosts=" + hosts + "]";
112 }
113 }
114
115
116 private State state;
117
118
119
120
121
122
123
124
125
126
127
128
129 public OpenSshConfigFile(@NonNull File home, @NonNull File config,
130 @NonNull String localUserName) {
131 this.home = home;
132 this.configFile = config;
133 this.localUserName = localUserName;
134 state = new State();
135 }
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150 @Override
151 @NonNull
152 public HostEntry lookup(@NonNull String hostName, int port,
153 String userName) {
154 final State cache = refresh();
155 String cacheKey = toCacheKey(hostName, port, userName);
156 HostEntry h = cache.hosts.get(cacheKey);
157 if (h != null) {
158 return h;
159 }
160 HostEntry fullConfig = new HostEntry();
161 Iterator<HostEntry> entries = cache.entries.iterator();
162 if (entries.hasNext()) {
163
164
165 fullConfig.merge(entries.next());
166 entries.forEachRemaining(entry -> {
167 if (entry.matches(hostName)) {
168 fullConfig.merge(entry);
169 }
170 });
171 }
172 fullConfig.substitute(hostName, port, userName, localUserName, home);
173 cache.hosts.put(cacheKey, fullConfig);
174 return fullConfig;
175 }
176
177 @NonNull
178 private String toCacheKey(@NonNull String hostName, int port,
179 String userName) {
180 String key = hostName;
181 if (port > 0) {
182 key = key + ':' + Integer.toString(port);
183 }
184 if (userName != null && !userName.isEmpty()) {
185 key = userName + '@' + key;
186 }
187 return key;
188 }
189
190 private synchronized State refresh() {
191 final Instant mtime = FS.DETECTED.lastModifiedInstant(configFile);
192 if (!mtime.equals(lastModified)) {
193 State newState = new State();
194 try (BufferedReader br = Files
195 .newBufferedReader(configFile.toPath(), UTF_8)) {
196 newState.entries = parse(br);
197 } catch (IOException | RuntimeException none) {
198
199 }
200 lastModified = mtime;
201 state = newState;
202 }
203 return state;
204 }
205
206 private List<HostEntry> parse(BufferedReader reader)
207 throws IOException {
208 final List<HostEntry> entries = new LinkedList<>();
209
210
211
212
213
214 HostEntry defaults = new HostEntry();
215 HostEntry current = defaults;
216 entries.add(defaults);
217
218 String line;
219 while ((line = reader.readLine()) != null) {
220
221
222
223
224
225 int i = line.indexOf('#');
226 if (i >= 0) {
227 line = line.substring(0, i);
228 }
229 line = line.trim();
230 if (line.isEmpty()) {
231 continue;
232 }
233 String[] parts = line.split("[ \t]*[= \t]", 2);
234
235
236 String keyword = dequote(parts[0].trim());
237
238
239
240 String argValue = parts.length > 1 ? parts[1].trim() : "";
241
242 if (StringUtils.equalsIgnoreCase(SshConstants.HOST, keyword)) {
243 current = new HostEntry(parseList(argValue));
244 entries.add(current);
245 continue;
246 }
247
248 if (HostEntry.isListKey(keyword)) {
249 List<String> args = validate(keyword, parseList(argValue));
250 current.setValue(keyword, args);
251 } else if (!argValue.isEmpty()) {
252 argValue = validate(keyword, dequote(argValue));
253 current.setValue(keyword, argValue);
254 }
255 }
256
257 return entries;
258 }
259
260
261
262
263
264
265
266
267
268
269
270 private List<String> parseList(String argument) {
271 List<String> result = new ArrayList<>(4);
272 int start = 0;
273 int length = argument.length();
274 while (start < length) {
275
276 if (Character.isWhitespace(argument.charAt(start))) {
277 start++;
278 continue;
279 }
280 if (argument.charAt(start) == '"') {
281 int stop = argument.indexOf('"', ++start);
282 if (stop < start) {
283
284 break;
285 }
286 result.add(argument.substring(start, stop));
287 start = stop + 1;
288 } else {
289 int stop = start + 1;
290 while (stop < length
291 && !Character.isWhitespace(argument.charAt(stop))) {
292 stop++;
293 }
294 result.add(argument.substring(start, stop));
295 start = stop + 1;
296 }
297 }
298 return result;
299 }
300
301
302
303
304
305
306
307
308
309
310
311 protected String validate(String key, String value) {
312 if (String.CASE_INSENSITIVE_ORDER.compare(key,
313 SshConstants.PREFERRED_AUTHENTICATIONS) == 0) {
314 return stripWhitespace(value);
315 }
316 return value;
317 }
318
319
320
321
322
323
324
325
326
327
328
329
330 protected List<String> validate(String key, List<String> value) {
331 return value;
332 }
333
334 private static boolean patternMatchesHost(String pattern, String name) {
335 if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
336 final FileNameMatcher fn;
337 try {
338 fn = new FileNameMatcher(pattern, null);
339 } catch (InvalidPatternException e) {
340 return false;
341 }
342 fn.append(name);
343 return fn.isMatch();
344 }
345
346 return pattern.equals(name);
347 }
348
349 private static String dequote(String value) {
350 if (value.startsWith("\"") && value.endsWith("\"")
351 && value.length() > 1)
352 return value.substring(1, value.length() - 1);
353 return value;
354 }
355
356 private static String stripWhitespace(String value) {
357 final StringBuilder b = new StringBuilder();
358 int length = value.length();
359 for (int i = 0; i < length; i++) {
360 char ch = value.charAt(i);
361 if (!Character.isWhitespace(ch)) {
362 b.append(ch);
363 }
364 }
365 return b.toString();
366 }
367
368 private static File toFile(String path, File home) {
369 if (path.startsWith("~/") || path.startsWith("~" + File.separator)) {
370 return new File(home, path.substring(2));
371 }
372 File ret = new File(path);
373 if (ret.isAbsolute()) {
374 return ret;
375 }
376 return new File(home, path);
377 }
378
379
380
381
382
383
384
385
386 public static int positive(String value) {
387 if (value != null) {
388 try {
389 return Integer.parseUnsignedInt(value);
390 } catch (NumberFormatException e) {
391
392 }
393 }
394 return -1;
395 }
396
397
398
399
400
401
402
403
404
405
406 public static boolean flag(String value) {
407 if (value == null) {
408 return false;
409 }
410 return SshConstants.YES.equals(value) || SshConstants.ON.equals(value)
411 || SshConstants.TRUE.equals(value);
412 }
413
414
415
416
417
418
419 public String getLocalUserName() {
420 return localUserName;
421 }
422
423
424
425
426
427
428 public static class HostEntry implements SshConfigStore.HostConfig {
429
430
431
432
433
434
435 private static final Set<String> MULTI_KEYS = new TreeSet<>(
436 String.CASE_INSENSITIVE_ORDER);
437
438 static {
439 MULTI_KEYS.add(SshConstants.CERTIFICATE_FILE);
440 MULTI_KEYS.add(SshConstants.IDENTITY_FILE);
441 MULTI_KEYS.add(SshConstants.LOCAL_FORWARD);
442 MULTI_KEYS.add(SshConstants.REMOTE_FORWARD);
443 MULTI_KEYS.add(SshConstants.SEND_ENV);
444 }
445
446
447
448
449
450
451
452
453
454 private static final Set<String> LIST_KEYS = new TreeSet<>(
455 String.CASE_INSENSITIVE_ORDER);
456
457 static {
458 LIST_KEYS.add(SshConstants.CANONICAL_DOMAINS);
459 LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE);
460 LIST_KEYS.add(SshConstants.SEND_ENV);
461 LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
462 }
463
464
465
466
467
468 private static final Map<String, String> ALIASES = new TreeMap<>(
469 String.CASE_INSENSITIVE_ORDER);
470
471 static {
472
473 ALIASES.put("PubkeyAcceptedKeyTypes",
474 SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
475 }
476
477 private Map<String, String> options;
478
479 private Map<String, List<String>> multiOptions;
480
481 private Map<String, List<String>> listOptions;
482
483 private final List<String> patterns;
484
485
486
487
488 public HostEntry() {
489 this.patterns = Collections.emptyList();
490 }
491
492
493
494
495
496 public HostEntry(List<String> patterns) {
497 this.patterns = patterns;
498 }
499
500 boolean matches(String hostName) {
501 boolean doesMatch = false;
502 for (String pattern : patterns) {
503 if (pattern.startsWith("!")) {
504 if (patternMatchesHost(pattern.substring(1), hostName)) {
505 return false;
506 }
507 } else if (!doesMatch
508 && patternMatchesHost(pattern, hostName)) {
509 doesMatch = true;
510 }
511 }
512 return doesMatch;
513 }
514
515 private static String toKey(String key) {
516 String k = ALIASES.get(key);
517 return k != null ? k : key;
518 }
519
520
521
522
523
524
525
526
527
528
529 @Override
530 public String getValue(String key) {
531 String k = toKey(key);
532 String result = options != null ? options.get(k) : null;
533 if (result == null) {
534
535
536 List<String> values = listOptions != null ? listOptions.get(k)
537 : null;
538 if (values == null) {
539 values = multiOptions != null ? multiOptions.get(k) : null;
540 }
541 if (values != null && !values.isEmpty()) {
542 result = values.get(0);
543 }
544 }
545 return result;
546 }
547
548
549
550
551
552
553
554
555
556
557 @Override
558 public List<String> getValues(String key) {
559 String k = toKey(key);
560 List<String> values = listOptions != null ? listOptions.get(k)
561 : null;
562 if (values == null) {
563 values = multiOptions != null ? multiOptions.get(k) : null;
564 }
565 if (values == null || values.isEmpty()) {
566 return new ArrayList<>();
567 }
568 return new ArrayList<>(values);
569 }
570
571
572
573
574
575
576
577
578
579
580
581 public void setValue(String key, String value) {
582 String k = toKey(key);
583 if (value == null) {
584 if (multiOptions != null) {
585 multiOptions.remove(k);
586 }
587 if (listOptions != null) {
588 listOptions.remove(k);
589 }
590 if (options != null) {
591 options.remove(k);
592 }
593 return;
594 }
595 if (MULTI_KEYS.contains(k)) {
596 if (multiOptions == null) {
597 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
598 }
599 List<String> values = multiOptions.get(k);
600 if (values == null) {
601 values = new ArrayList<>(4);
602 multiOptions.put(k, values);
603 }
604 values.add(value);
605 } else {
606 if (options == null) {
607 options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
608 }
609 if (!options.containsKey(k)) {
610 options.put(k, value);
611 }
612 }
613 }
614
615
616
617
618
619
620
621
622
623 public void setValue(String key, List<String> values) {
624 if (values.isEmpty()) {
625 return;
626 }
627 String k = toKey(key);
628
629
630
631
632
633
634 if (MULTI_KEYS.contains(k)) {
635 if (multiOptions == null) {
636 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
637 }
638 List<String> items = multiOptions.get(k);
639 if (items == null) {
640 items = new ArrayList<>(values);
641 multiOptions.put(k, items);
642 } else {
643 items.addAll(values);
644 }
645 } else {
646 if (listOptions == null) {
647 listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
648 }
649 if (!listOptions.containsKey(k)) {
650 listOptions.put(k, values);
651 }
652 }
653 }
654
655
656
657
658
659
660
661
662 public static boolean isListKey(String key) {
663 return LIST_KEYS.contains(toKey(key));
664 }
665
666 void merge(HostEntry entry) {
667 if (entry == null) {
668
669 return;
670 }
671 if (entry.options != null) {
672 if (options == null) {
673 options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
674 }
675 for (Map.Entry<String, String> item : entry.options
676 .entrySet()) {
677 if (!options.containsKey(item.getKey())) {
678 options.put(item.getKey(), item.getValue());
679 }
680 }
681 }
682 if (entry.listOptions != null) {
683 if (listOptions == null) {
684 listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
685 }
686 for (Map.Entry<String, List<String>> item : entry.listOptions
687 .entrySet()) {
688 if (!listOptions.containsKey(item.getKey())) {
689 listOptions.put(item.getKey(), item.getValue());
690 }
691 }
692
693 }
694 if (entry.multiOptions != null) {
695 if (multiOptions == null) {
696 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
697 }
698 for (Map.Entry<String, List<String>> item : entry.multiOptions
699 .entrySet()) {
700 List<String> values = multiOptions.get(item.getKey());
701 if (values == null) {
702 values = new ArrayList<>(item.getValue());
703 multiOptions.put(item.getKey(), values);
704 } else {
705 values.addAll(item.getValue());
706 }
707 }
708 }
709 }
710
711 private List<String> substitute(List<String> values, String allowed,
712 Replacer r, boolean withEnv) {
713 List<String> result = new ArrayList<>(values.size());
714 for (String value : values) {
715 result.add(r.substitute(value, allowed, withEnv));
716 }
717 return result;
718 }
719
720 private List<String> replaceTilde(List<String> values, File home) {
721 List<String> result = new ArrayList<>(values.size());
722 for (String value : values) {
723 result.add(toFile(value, home).getPath());
724 }
725 return result;
726 }
727
728 void substitute(String originalHostName, int port, String userName,
729 String localUserName, File home) {
730 int p = port >= 0 ? port : positive(getValue(SshConstants.PORT));
731 if (p < 0) {
732 p = SshConstants.SSH_DEFAULT_PORT;
733 }
734 String u = userName != null && !userName.isEmpty() ? userName
735 : getValue(SshConstants.USER);
736 if (u == null || u.isEmpty()) {
737 u = localUserName;
738 }
739 Replacer r = new Replacer(originalHostName, p, u, localUserName,
740 home);
741 if (options != null) {
742
743 String hostName = options.get(SshConstants.HOST_NAME);
744 if (hostName == null || hostName.isEmpty()) {
745 options.put(SshConstants.HOST_NAME, originalHostName);
746 } else {
747 hostName = r.substitute(hostName, "h", false);
748 options.put(SshConstants.HOST_NAME, hostName);
749 r.update('h', hostName);
750 }
751 }
752 if (multiOptions != null) {
753 List<String> values = multiOptions
754 .get(SshConstants.IDENTITY_FILE);
755 if (values != null) {
756 values = substitute(values, "dhlru", r, true);
757 values = replaceTilde(values, home);
758 multiOptions.put(SshConstants.IDENTITY_FILE, values);
759 }
760 values = multiOptions.get(SshConstants.CERTIFICATE_FILE);
761 if (values != null) {
762 values = substitute(values, "dhlru", r, true);
763 values = replaceTilde(values, home);
764 multiOptions.put(SshConstants.CERTIFICATE_FILE, values);
765 }
766 }
767 if (listOptions != null) {
768 List<String> values = listOptions
769 .get(SshConstants.USER_KNOWN_HOSTS_FILE);
770 if (values != null) {
771 values = replaceTilde(values, home);
772 listOptions.put(SshConstants.USER_KNOWN_HOSTS_FILE, values);
773 }
774 }
775 if (options != null) {
776
777 String value = options.get(SshConstants.IDENTITY_AGENT);
778 if (value != null) {
779 value = r.substitute(value, "dhlru", true);
780 value = toFile(value, home).getPath();
781 options.put(SshConstants.IDENTITY_AGENT, value);
782 }
783 value = options.get(SshConstants.CONTROL_PATH);
784 if (value != null) {
785 value = r.substitute(value, "ChLlnpru", true);
786 value = toFile(value, home).getPath();
787 options.put(SshConstants.CONTROL_PATH, value);
788 }
789 value = options.get(SshConstants.LOCAL_COMMAND);
790 if (value != null) {
791 value = r.substitute(value, "CdhlnprTu", false);
792 options.put(SshConstants.LOCAL_COMMAND, value);
793 }
794 value = options.get(SshConstants.REMOTE_COMMAND);
795 if (value != null) {
796 value = r.substitute(value, "Cdhlnpru", false);
797 options.put(SshConstants.REMOTE_COMMAND, value);
798 }
799 value = options.get(SshConstants.PROXY_COMMAND);
800 if (value != null) {
801 value = r.substitute(value, "hpr", false);
802 options.put(SshConstants.PROXY_COMMAND, value);
803 }
804 }
805
806
807 }
808
809
810
811
812
813
814
815 @Override
816 @NonNull
817 public Map<String, String> getOptions() {
818 if (options == null) {
819 return Collections.emptyMap();
820 }
821 return Collections.unmodifiableMap(options);
822 }
823
824
825
826
827
828
829
830 @Override
831 @NonNull
832 public Map<String, List<String>> getMultiValuedOptions() {
833 if (listOptions == null && multiOptions == null) {
834 return Collections.emptyMap();
835 }
836 Map<String, List<String>> allValues = new TreeMap<>(
837 String.CASE_INSENSITIVE_ORDER);
838 if (multiOptions != null) {
839 allValues.putAll(multiOptions);
840 }
841 if (listOptions != null) {
842 allValues.putAll(listOptions);
843 }
844 return Collections.unmodifiableMap(allValues);
845 }
846
847 @Override
848 @SuppressWarnings("nls")
849 public String toString() {
850 return "HostEntry [options=" + options + ", multiOptions="
851 + multiOptions + ", listOptions=" + listOptions + "]";
852 }
853 }
854
855 private static class Replacer {
856 private final Map<Character, String> replacements = new HashMap<>();
857
858 public Replacer(String host, int port, String user,
859 String localUserName, File home) {
860 replacements.put(Character.valueOf('%'), "%");
861 replacements.put(Character.valueOf('d'), home.getPath());
862 replacements.put(Character.valueOf('h'), host);
863 String localhost = SystemReader.getInstance().getHostname();
864 replacements.put(Character.valueOf('l'), localhost);
865 int period = localhost.indexOf('.');
866 if (period > 0) {
867 localhost = localhost.substring(0, period);
868 }
869 replacements.put(Character.valueOf('L'), localhost);
870 replacements.put(Character.valueOf('n'), host);
871 replacements.put(Character.valueOf('p'), Integer.toString(port));
872 replacements.put(Character.valueOf('r'), user == null ? "" : user);
873 replacements.put(Character.valueOf('u'), localUserName);
874 replacements.put(Character.valueOf('C'),
875 substitute("%l%h%p%r", "hlpr", false));
876 replacements.put(Character.valueOf('T'), "NONE");
877 }
878
879 public void update(char key, String value) {
880 replacements.put(Character.valueOf(key), value);
881 if ("lhpr".indexOf(key) >= 0) {
882 replacements.put(Character.valueOf('C'),
883 substitute("%l%h%p%r", "hlpr", false));
884 }
885 }
886
887 public String substitute(String input, String allowed,
888 boolean withEnv) {
889 if (input == null || input.length() <= 1
890 || (input.indexOf('%') < 0
891 && (!withEnv || input.indexOf("${") < 0))) {
892 return input;
893 }
894 StringBuilder builder = new StringBuilder();
895 int start = 0;
896 int length = input.length();
897 while (start < length) {
898 char ch = input.charAt(start);
899 switch (ch) {
900 case '%':
901 if (start + 1 >= length) {
902 break;
903 }
904 String replacement = null;
905 ch = input.charAt(start + 1);
906 if (ch == '%' || allowed.indexOf(ch) >= 0) {
907 replacement = replacements.get(Character.valueOf(ch));
908 }
909 if (replacement == null) {
910 builder.append('%').append(ch);
911 } else {
912 builder.append(replacement);
913 }
914 start += 2;
915 continue;
916 case '$':
917 if (!withEnv || start + 2 >= length) {
918 break;
919 }
920 ch = input.charAt(start + 1);
921 if (ch == '{') {
922 int close = input.indexOf('}', start + 2);
923 if (close > start + 2) {
924 String variable = SystemReader.getInstance()
925 .getenv(input.substring(start + 2, close));
926 if (!StringUtils.isEmptyOrNull(variable)) {
927 builder.append(variable);
928 }
929 start = close + 1;
930 continue;
931 }
932 }
933 ch = '$';
934 break;
935 default:
936 break;
937 }
938 builder.append(ch);
939 start++;
940 }
941 return builder.toString();
942 }
943 }
944
945
946 @Override
947 @SuppressWarnings("nls")
948 public String toString() {
949 return "OpenSshConfig [home=" + home + ", configFile=" + configFile
950 + ", lastModified=" + lastModified + ", state=" + state + "]";
951 }
952 }