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 package org.eclipse.jgit.transport;
45
46 import java.io.BufferedReader;
47 import java.io.File;
48 import java.io.FileInputStream;
49 import java.io.IOException;
50 import java.io.InputStream;
51 import java.io.InputStreamReader;
52 import java.security.AccessController;
53 import java.security.PrivilegedAction;
54 import java.util.ArrayList;
55 import java.util.HashMap;
56 import java.util.HashSet;
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
63 import org.eclipse.jgit.errors.InvalidPatternException;
64 import org.eclipse.jgit.fnmatch.FileNameMatcher;
65 import org.eclipse.jgit.lib.Constants;
66 import org.eclipse.jgit.util.FS;
67 import org.eclipse.jgit.util.StringUtils;
68 import org.eclipse.jgit.util.SystemReader;
69
70 import com.jcraft.jsch.ConfigRepository;
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133 public class OpenSshConfig implements ConfigRepository {
134
135
136 static final int SSH_PORT = 22;
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151 public static OpenSshConfig get(FS fs) {
152 File home = fs.userHome();
153 if (home == null)
154 home = new File(".").getAbsoluteFile();
155
156 final File config = new File(new File(home, ".ssh"), Constants.CONFIG);
157 final OpenSshConfig osc = new OpenSshConfig(home, config);
158 osc.refresh();
159 return osc;
160 }
161
162
163 private final File home;
164
165
166 private final File configFile;
167
168
169 private long lastModified;
170
171
172
173
174
175 private static class State {
176 Map<String, HostEntry> entries = new LinkedHashMap<>();
177 Map<String, Host> hosts = new HashMap<>();
178
179 @Override
180 @SuppressWarnings("nls")
181 public String toString() {
182 return "State [entries=" + entries + ", hosts=" + hosts + "]";
183 }
184 }
185
186
187 private State state;
188
189 OpenSshConfig(File h, File cfg) {
190 home = h;
191 configFile = cfg;
192 state = new State();
193 }
194
195
196
197
198
199
200
201
202
203
204 public Host lookup(String hostName) {
205 final State cache = refresh();
206 Host h = cache.hosts.get(hostName);
207 if (h != null) {
208 return h;
209 }
210 HostEntry fullConfig = new HostEntry();
211
212
213 fullConfig.merge(cache.entries.get(HostEntry.DEFAULT_NAME));
214 for (Map.Entry<String, HostEntry> e : cache.entries.entrySet()) {
215 String key = e.getKey();
216 if (isHostMatch(key, hostName)) {
217 fullConfig.merge(e.getValue());
218 }
219 }
220 fullConfig.substitute(hostName, home);
221 h = new Host(fullConfig, hostName, home);
222 cache.hosts.put(hostName, h);
223 return h;
224 }
225
226 private synchronized State refresh() {
227 final long mtime = configFile.lastModified();
228 if (mtime != lastModified) {
229 State newState = new State();
230 try (FileInputStream in = new FileInputStream(configFile)) {
231 newState.entries = parse(in);
232 } catch (IOException none) {
233
234 }
235 lastModified = mtime;
236 state = newState;
237 }
238 return state;
239 }
240
241 private Map<String, HostEntry> parse(InputStream in)
242 throws IOException {
243 final Map<String, HostEntry> m = new LinkedHashMap<>();
244 final BufferedReader br = new BufferedReader(new InputStreamReader(in));
245 final List<HostEntry> current = new ArrayList<>(4);
246 String line;
247
248
249
250
251
252 HostEntry defaults = new HostEntry();
253 current.add(defaults);
254 m.put(HostEntry.DEFAULT_NAME, defaults);
255
256 while ((line = br.readLine()) != null) {
257 line = line.trim();
258 if (line.isEmpty() || line.startsWith("#")) {
259 continue;
260 }
261 String[] parts = line.split("[ \t]*[= \t]", 2);
262
263
264 String keyword = dequote(parts[0].trim());
265
266
267
268 String argValue = parts.length > 1 ? parts[1].trim() : "";
269
270 if (StringUtils.equalsIgnoreCase("Host", keyword)) {
271 current.clear();
272 for (String name : HostEntry.parseList(argValue)) {
273 if (name == null || name.isEmpty()) {
274
275 continue;
276 }
277 HostEntry c = m.get(name);
278 if (c == null) {
279 c = new HostEntry();
280 m.put(name, c);
281 }
282 current.add(c);
283 }
284 continue;
285 }
286
287 if (current.isEmpty()) {
288
289
290 continue;
291 }
292
293 if (HostEntry.isListKey(keyword)) {
294 List<String> args = HostEntry.parseList(argValue);
295 for (HostEntry entry : current) {
296 entry.setValue(keyword, args);
297 }
298 } else if (!argValue.isEmpty()) {
299 argValue = dequote(argValue);
300 for (HostEntry entry : current) {
301 entry.setValue(keyword, argValue);
302 }
303 }
304 }
305
306 return m;
307 }
308
309 private static boolean isHostMatch(final String pattern,
310 final String name) {
311 if (pattern.startsWith("!")) {
312 return !patternMatchesHost(pattern.substring(1), name);
313 } else {
314 return patternMatchesHost(pattern, name);
315 }
316 }
317
318 private static boolean patternMatchesHost(final String pattern,
319 final String name) {
320 if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
321 final FileNameMatcher fn;
322 try {
323 fn = new FileNameMatcher(pattern, null);
324 } catch (InvalidPatternException e) {
325 return false;
326 }
327 fn.append(name);
328 return fn.isMatch();
329 } else {
330
331 return pattern.equals(name);
332 }
333 }
334
335 private static String dequote(String value) {
336 if (value.startsWith("\"") && value.endsWith("\"")
337 && value.length() > 1)
338 return value.substring(1, value.length() - 1);
339 return value;
340 }
341
342 private static String nows(String value) {
343 final StringBuilder b = new StringBuilder();
344 for (int i = 0; i < value.length(); i++) {
345 if (!Character.isSpaceChar(value.charAt(i)))
346 b.append(value.charAt(i));
347 }
348 return b.toString();
349 }
350
351 private static Boolean yesno(String value) {
352 if (StringUtils.equalsIgnoreCase("yes", value))
353 return Boolean.TRUE;
354 return Boolean.FALSE;
355 }
356
357 private static File toFile(String path, File home) {
358 if (path.startsWith("~/")) {
359 return new File(home, path.substring(2));
360 }
361 File ret = new File(path);
362 if (ret.isAbsolute()) {
363 return ret;
364 }
365 return new File(home, path);
366 }
367
368 private static int positive(String value) {
369 if (value != null) {
370 try {
371 return Integer.parseUnsignedInt(value);
372 } catch (NumberFormatException e) {
373
374 }
375 }
376 return -1;
377 }
378
379 static String userName() {
380 return AccessController.doPrivileged(new PrivilegedAction<String>() {
381 @Override
382 public String run() {
383 return SystemReader.getInstance()
384 .getProperty(Constants.OS_USER_NAME_KEY);
385 }
386 });
387 }
388
389 private static class HostEntry implements ConfigRepository.Config {
390
391
392
393
394
395 public static final String DEFAULT_NAME = "";
396
397
398
399 private static final Map<String, String> KEY_MAP = new HashMap<>();
400
401 static {
402 KEY_MAP.put("kex", "KexAlgorithms");
403 KEY_MAP.put("server_host_key", "HostKeyAlgorithms");
404 KEY_MAP.put("cipher.c2s", "Ciphers");
405 KEY_MAP.put("cipher.s2c", "Ciphers");
406 KEY_MAP.put("mac.c2s", "Macs");
407 KEY_MAP.put("mac.s2c", "Macs");
408 KEY_MAP.put("compression.s2c", "Compression");
409 KEY_MAP.put("compression.c2s", "Compression");
410 KEY_MAP.put("compression_level", "CompressionLevel");
411 KEY_MAP.put("MaxAuthTries", "NumberOfPasswordPrompts");
412 }
413
414
415
416
417
418
419 private static final Set<String> MULTI_KEYS = new HashSet<>();
420
421 static {
422 MULTI_KEYS.add("CERTIFICATEFILE");
423 MULTI_KEYS.add("IDENTITYFILE");
424 MULTI_KEYS.add("LOCALFORWARD");
425 MULTI_KEYS.add("REMOTEFORWARD");
426 MULTI_KEYS.add("SENDENV");
427 }
428
429
430
431
432
433
434
435
436
437 private static final Set<String> LIST_KEYS = new HashSet<>();
438
439 static {
440 LIST_KEYS.add("CANONICALDOMAINS");
441 LIST_KEYS.add("GLOBALKNOWNHOSTSFILE");
442 LIST_KEYS.add("SENDENV");
443 LIST_KEYS.add("USERKNOWNHOSTSFILE");
444 }
445
446 private Map<String, String> options;
447
448 private Map<String, List<String>> multiOptions;
449
450 private Map<String, List<String>> listOptions;
451
452 @Override
453 public String getHostname() {
454 return getValue("HOSTNAME");
455 }
456
457 @Override
458 public String getUser() {
459 return getValue("USER");
460 }
461
462 @Override
463 public int getPort() {
464 return positive(getValue("PORT"));
465 }
466
467 private static String mapKey(String key) {
468 String k = KEY_MAP.get(key);
469 if (k == null) {
470 k = key;
471 }
472 return k.toUpperCase(Locale.ROOT);
473 }
474
475 private String findValue(String key) {
476 String k = mapKey(key);
477 String result = options != null ? options.get(k) : null;
478 if (result == null) {
479
480
481
482
483
484
485
486
487
488
489 List<String> values = listOptions != null ? listOptions.get(k)
490 : null;
491 if (values == null) {
492 values = multiOptions != null ? multiOptions.get(k) : null;
493 }
494 if (values != null && !values.isEmpty()) {
495 result = values.get(0);
496 }
497 }
498 return result;
499 }
500
501 @Override
502 public String getValue(String key) {
503
504
505 if (key.equals("compression.s2c")
506 || key.equals("compression.c2s")) {
507 String foo = findValue(key);
508 if (foo == null || foo.equals("no")) {
509 return "none,zlib@openssh.com,zlib";
510 }
511 return "zlib@openssh.com,zlib,none";
512 }
513 return findValue(key);
514 }
515
516 @Override
517 public String[] getValues(String key) {
518 String k = mapKey(key);
519 List<String> values = listOptions != null ? listOptions.get(k)
520 : null;
521 if (values == null) {
522 values = multiOptions != null ? multiOptions.get(k) : null;
523 }
524 if (values == null || values.isEmpty()) {
525 return new String[0];
526 }
527 return values.toArray(new String[values.size()]);
528 }
529
530 public void setValue(String key, String value) {
531 String k = key.toUpperCase(Locale.ROOT);
532 if (MULTI_KEYS.contains(k)) {
533 if (multiOptions == null) {
534 multiOptions = new HashMap<>();
535 }
536 List<String> values = multiOptions.get(k);
537 if (values == null) {
538 values = new ArrayList<>(4);
539 multiOptions.put(k, values);
540 }
541 values.add(value);
542 } else {
543 if (options == null) {
544 options = new HashMap<>();
545 }
546 if (!options.containsKey(k)) {
547 options.put(k, value);
548 }
549 }
550 }
551
552 public void setValue(String key, List<String> values) {
553 if (values.isEmpty()) {
554
555 return;
556 }
557 String k = key.toUpperCase(Locale.ROOT);
558
559
560
561
562
563
564 if (MULTI_KEYS.contains(k)) {
565 if (multiOptions == null) {
566 multiOptions = new HashMap<>(2 * MULTI_KEYS.size());
567 }
568 List<String> items = multiOptions.get(k);
569 if (items == null) {
570 items = new ArrayList<>(values);
571 multiOptions.put(k, items);
572 } else {
573 items.addAll(values);
574 }
575 } else {
576 if (listOptions == null) {
577 listOptions = new HashMap<>(2 * LIST_KEYS.size());
578 }
579 if (!listOptions.containsKey(k)) {
580 listOptions.put(k, values);
581 }
582 }
583 }
584
585 public static boolean isListKey(String key) {
586 return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT));
587 }
588
589
590
591
592
593
594
595
596
597
598
599 public static List<String> parseList(String argument) {
600 List<String> result = new ArrayList<>(4);
601 int start = 0;
602 int length = argument.length();
603 while (start < length) {
604
605 if (Character.isSpaceChar(argument.charAt(start))) {
606 start++;
607 continue;
608 }
609 if (argument.charAt(start) == '"') {
610 int stop = argument.indexOf('"', ++start);
611 if (stop < start) {
612
613 break;
614 }
615 result.add(argument.substring(start, stop));
616 start = stop + 1;
617 } else {
618 int stop = start + 1;
619 while (stop < length
620 && !Character.isSpaceChar(argument.charAt(stop))) {
621 stop++;
622 }
623 result.add(argument.substring(start, stop));
624 start = stop + 1;
625 }
626 }
627 return result;
628 }
629
630 protected void merge(HostEntry entry) {
631 if (entry == null) {
632
633 return;
634 }
635 if (entry.options != null) {
636 if (options == null) {
637 options = new HashMap<>();
638 }
639 for (Map.Entry<String, String> item : entry.options
640 .entrySet()) {
641 if (!options.containsKey(item.getKey())) {
642 options.put(item.getKey(), item.getValue());
643 }
644 }
645 }
646 if (entry.listOptions != null) {
647 if (listOptions == null) {
648 listOptions = new HashMap<>(2 * LIST_KEYS.size());
649 }
650 for (Map.Entry<String, List<String>> item : entry.listOptions
651 .entrySet()) {
652 if (!listOptions.containsKey(item.getKey())) {
653 listOptions.put(item.getKey(), item.getValue());
654 }
655 }
656
657 }
658 if (entry.multiOptions != null) {
659 if (multiOptions == null) {
660 multiOptions = new HashMap<>(2 * MULTI_KEYS.size());
661 }
662 for (Map.Entry<String, List<String>> item : entry.multiOptions
663 .entrySet()) {
664 List<String> values = multiOptions.get(item.getKey());
665 if (values == null) {
666 values = new ArrayList<>(item.getValue());
667 multiOptions.put(item.getKey(), values);
668 } else {
669 values.addAll(item.getValue());
670 }
671 }
672 }
673 }
674
675 private class Replacer {
676 private final Map<Character, String> replacements = new HashMap<>();
677
678 public Replacer(String originalHostName, File home) {
679 replacements.put(Character.valueOf('%'), "%");
680 replacements.put(Character.valueOf('d'), home.getPath());
681
682 String host = getValue("HOSTNAME");
683 replacements.put(Character.valueOf('h'), originalHostName);
684 if (host != null && host.indexOf('%') >= 0) {
685 host = substitute(host, "h");
686 options.put("HOSTNAME", host);
687 }
688 if (host != null) {
689 replacements.put(Character.valueOf('h'), host);
690 }
691 String localhost = SystemReader.getInstance().getHostname();
692 replacements.put(Character.valueOf('l'), localhost);
693 int period = localhost.indexOf('.');
694 if (period > 0) {
695 localhost = localhost.substring(0, period);
696 }
697 replacements.put(Character.valueOf('L'), localhost);
698 replacements.put(Character.valueOf('n'), originalHostName);
699 replacements.put(Character.valueOf('p'), getValue("PORT"));
700 replacements.put(Character.valueOf('r'), getValue("USER"));
701 replacements.put(Character.valueOf('u'), userName());
702 replacements.put(Character.valueOf('C'),
703 substitute("%l%h%p%r", "hlpr"));
704 }
705
706 public String substitute(String input, String allowed) {
707 if (input == null || input.length() <= 1
708 || input.indexOf('%') < 0) {
709 return input;
710 }
711 StringBuilder builder = new StringBuilder();
712 int start = 0;
713 int length = input.length();
714 while (start < length) {
715 int percent = input.indexOf('%', start);
716 if (percent < 0 || percent + 1 >= length) {
717 builder.append(input.substring(start));
718 break;
719 }
720 String replacement = null;
721 char ch = input.charAt(percent + 1);
722 if (ch == '%' || allowed.indexOf(ch) >= 0) {
723 replacement = replacements.get(Character.valueOf(ch));
724 }
725 if (replacement == null) {
726 builder.append(input.substring(start, percent + 2));
727 } else {
728 builder.append(input.substring(start, percent))
729 .append(replacement);
730 }
731 start = percent + 2;
732 }
733 return builder.toString();
734 }
735 }
736
737 private List<String> substitute(List<String> values, String allowed,
738 Replacer r) {
739 List<String> result = new ArrayList<>(values.size());
740 for (String value : values) {
741 result.add(r.substitute(value, allowed));
742 }
743 return result;
744 }
745
746 private List<String> replaceTilde(List<String> values, File home) {
747 List<String> result = new ArrayList<>(values.size());
748 for (String value : values) {
749 result.add(toFile(value, home).getPath());
750 }
751 return result;
752 }
753
754 protected void substitute(String originalHostName, File home) {
755 Replacer r = new Replacer(originalHostName, home);
756 if (multiOptions != null) {
757 List<String> values = multiOptions.get("IDENTITYFILE");
758 if (values != null) {
759 values = substitute(values, "dhlru", r);
760 values = replaceTilde(values, home);
761 multiOptions.put("IDENTITYFILE", values);
762 }
763 values = multiOptions.get("CERTIFICATEFILE");
764 if (values != null) {
765 values = substitute(values, "dhlru", r);
766 values = replaceTilde(values, home);
767 multiOptions.put("CERTIFICATEFILE", values);
768 }
769 }
770 if (listOptions != null) {
771 List<String> values = listOptions.get("GLOBALKNOWNHOSTSFILE");
772 if (values != null) {
773 values = replaceTilde(values, home);
774 listOptions.put("GLOBALKNOWNHOSTSFILE", values);
775 }
776 values = listOptions.get("USERKNOWNHOSTSFILE");
777 if (values != null) {
778 values = replaceTilde(values, home);
779 listOptions.put("USERKNOWNHOSTSFILE", values);
780 }
781 }
782 if (options != null) {
783
784 String value = options.get("IDENTITYAGENT");
785 if (value != null) {
786 value = r.substitute(value, "dhlru");
787 value = toFile(value, home).getPath();
788 options.put("IDENTITYAGENT", value);
789 }
790 }
791
792
793
794 }
795
796 @Override
797 @SuppressWarnings("nls")
798 public String toString() {
799 return "HostEntry [options=" + options + ", multiOptions="
800 + multiOptions + ", listOptions=" + listOptions + "]";
801 }
802 }
803
804
805
806
807
808
809
810
811
812
813
814
815 public static class Host {
816 String hostName;
817
818 int port;
819
820 File identityFile;
821
822 String user;
823
824 String preferredAuthentications;
825
826 Boolean batchMode;
827
828 String strictHostKeyChecking;
829
830 int connectionAttempts;
831
832 private Config config;
833
834
835
836
837 public Host() {
838
839 }
840
841 Host(Config config, String hostName, File homeDir) {
842 this.config = config;
843 complete(hostName, homeDir);
844 }
845
846
847
848
849
850
851
852 public String getStrictHostKeyChecking() {
853 return strictHostKeyChecking;
854 }
855
856
857
858
859 public String getHostName() {
860 return hostName;
861 }
862
863
864
865
866 public int getPort() {
867 return port;
868 }
869
870
871
872
873
874 public File getIdentityFile() {
875 return identityFile;
876 }
877
878
879
880
881 public String getUser() {
882 return user;
883 }
884
885
886
887
888
889 public String getPreferredAuthentications() {
890 return preferredAuthentications;
891 }
892
893
894
895
896
897 public boolean isBatchMode() {
898 return batchMode != null && batchMode.booleanValue();
899 }
900
901
902
903
904
905
906
907
908 public int getConnectionAttempts() {
909 return connectionAttempts;
910 }
911
912
913 private void complete(String initialHostName, File homeDir) {
914
915 hostName = config.getHostname();
916 user = config.getUser();
917 port = config.getPort();
918 connectionAttempts = positive(
919 config.getValue("ConnectionAttempts"));
920 strictHostKeyChecking = config.getValue("StrictHostKeyChecking");
921 String value = config.getValue("BatchMode");
922 if (value != null) {
923 batchMode = yesno(value);
924 }
925 value = config.getValue("PreferredAuthentications");
926 if (value != null) {
927 preferredAuthentications = nows(value);
928 }
929
930 if (hostName == null) {
931 hostName = initialHostName;
932 }
933 if (user == null) {
934 user = OpenSshConfig.userName();
935 }
936 if (port <= 0) {
937 port = OpenSshConfig.SSH_PORT;
938 }
939 if (connectionAttempts <= 0) {
940 connectionAttempts = 1;
941 }
942 String[] identityFiles = config.getValues("IdentityFile");
943 if (identityFiles != null && identityFiles.length > 0) {
944 identityFile = toFile(identityFiles[0], homeDir);
945 }
946 }
947
948 Config getConfig() {
949 return config;
950 }
951
952 @Override
953 @SuppressWarnings("nls")
954 public String toString() {
955 return "Host [hostName=" + hostName + ", port=" + port
956 + ", identityFile=" + identityFile + ", user=" + user
957 + ", preferredAuthentications=" + preferredAuthentications
958 + ", batchMode=" + batchMode + ", strictHostKeyChecking="
959 + strictHostKeyChecking + ", connectionAttempts="
960 + connectionAttempts + ", config=" + config + "]";
961 }
962 }
963
964
965
966
967
968
969
970
971
972 @Override
973 public Config getConfig(String hostName) {
974 Host host = lookup(hostName);
975 return host.getConfig();
976 }
977
978
979 @Override
980 @SuppressWarnings("nls")
981 public String toString() {
982 return "OpenSshConfig [home=" + home + ", configFile=" + configFile
983 + ", lastModified=" + lastModified + ", state=" + state + "]";
984 }
985 }