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