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