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