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