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