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.Iterator;
25 import java.util.LinkedList;
26 import java.util.List;
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.SshConfigStore;
36 import org.eclipse.jgit.transport.SshConstants;
37 import org.eclipse.jgit.util.FS;
38 import org.eclipse.jgit.util.StringUtils;
39 import org.eclipse.jgit.util.SystemReader;
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
84 public class OpenSshConfigFile implements SshConfigStore {
85
86
87 private final File home;
88
89
90 private final File configFile;
91
92
93 private final String localUserName;
94
95
96 private Instant lastModified;
97
98
99
100
101
102 private static class State {
103 List<HostEntry> entries = new LinkedList<>();
104
105
106 Map<String, HostEntry> hosts = new HashMap<>();
107
108 @Override
109 @SuppressWarnings("nls")
110 public String toString() {
111 return "State [entries=" + entries + ", hosts=" + hosts + "]";
112 }
113 }
114
115
116 private State state;
117
118
119
120
121
122
123
124
125
126
127
128
129 public OpenSshConfigFile(@NonNull File home, @NonNull File config,
130 @NonNull String localUserName) {
131 this.home = home;
132 this.configFile = config;
133 this.localUserName = localUserName;
134 state = new State();
135 }
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150 @Override
151 @NonNull
152 public HostEntry lookup(@NonNull String hostName, int port,
153 String userName) {
154 return lookup(hostName, port, userName, false);
155 }
156
157 @Override
158 @NonNull
159 public HostEntry lookupDefault(@NonNull String hostName, int port,
160 String userName) {
161 return lookup(hostName, port, userName, true);
162 }
163
164 private HostEntry lookup(@NonNull String hostName, int port,
165 String userName, boolean fillDefaults) {
166 final State cache = refresh();
167 String cacheKey = toCacheKey(hostName, port, userName);
168 HostEntry h = cache.hosts.get(cacheKey);
169 if (h != null) {
170 return h;
171 }
172 HostEntry fullConfig = new HostEntry();
173 Iterator<HostEntry> entries = cache.entries.iterator();
174 if (entries.hasNext()) {
175
176
177 fullConfig.merge(entries.next());
178 entries.forEachRemaining(entry -> {
179 if (entry.matches(hostName)) {
180 fullConfig.merge(entry);
181 }
182 });
183 }
184 fullConfig.substitute(hostName, port, userName, localUserName, home,
185 fillDefaults);
186 cache.hosts.put(cacheKey, fullConfig);
187 return fullConfig;
188 }
189
190 @NonNull
191 private String toCacheKey(@NonNull String hostName, int port,
192 String userName) {
193 String key = hostName;
194 if (port > 0) {
195 key = key + ':' + Integer.toString(port);
196 }
197 if (userName != null && !userName.isEmpty()) {
198 key = userName + '@' + key;
199 }
200 return key;
201 }
202
203 private synchronized State refresh() {
204 final Instant mtime = FS.DETECTED.lastModifiedInstant(configFile);
205 if (!mtime.equals(lastModified)) {
206 State newState = new State();
207 try (BufferedReader br = Files
208 .newBufferedReader(configFile.toPath(), UTF_8)) {
209 newState.entries = parse(br);
210 } catch (IOException | RuntimeException none) {
211
212 }
213 lastModified = mtime;
214 state = newState;
215 }
216 return state;
217 }
218
219 private List<HostEntry> parse(BufferedReader reader)
220 throws IOException {
221 final List<HostEntry> entries = new LinkedList<>();
222
223
224
225
226
227 HostEntry defaults = new HostEntry();
228 HostEntry current = defaults;
229 entries.add(defaults);
230
231 String line;
232 while ((line = reader.readLine()) != null) {
233 line = line.strip();
234 if (line.isEmpty()) {
235 continue;
236 }
237 String[] parts = line.split("[ \t]*[= \t]", 2);
238 String keyword = parts[0].strip();
239 if (keyword.isEmpty()) {
240 continue;
241 }
242 switch (keyword.charAt(0)) {
243 case '#':
244 continue;
245 case '"':
246
247
248 List<String> dequoted = parseList(keyword);
249 keyword = dequoted.isEmpty() ? "" : dequoted.get(0);
250 break;
251 default:
252
253 int i = keyword.indexOf('#');
254 if (i >= 0) {
255 keyword = keyword.substring(0, i);
256 }
257 break;
258 }
259 if (keyword.isEmpty()) {
260 continue;
261 }
262
263
264
265 String argValue = parts.length > 1 ? parts[1].strip() : "";
266
267 if (StringUtils.equalsIgnoreCase(SshConstants.HOST, keyword)) {
268 current = new HostEntry(parseList(argValue));
269 entries.add(current);
270 continue;
271 }
272
273 if (HostEntry.isListKey(keyword)) {
274 List<String> args = validate(keyword, parseList(argValue));
275 current.setValue(keyword, args);
276 } else if (!argValue.isEmpty()) {
277 List<String> args = parseList(argValue);
278 String arg = args.isEmpty() ? "" : args.get(0);
279 argValue = validate(keyword, arg);
280 current.setValue(keyword, argValue);
281 }
282 }
283
284 return entries;
285 }
286
287
288
289
290
291
292
293
294
295
296
297
298 private static List<String> parseList(String argument) {
299 List<String> result = new ArrayList<>(4);
300 int start = 0;
301 int length = argument.length();
302 while (start < length) {
303
304 char ch = argument.charAt(start);
305 if (Character.isWhitespace(ch)) {
306 start++;
307 } else if (ch == '#') {
308 break;
309 } else {
310
311 start = parseToken(argument, start, length, result);
312 }
313 }
314 return result;
315 }
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334 private static int parseToken(String argument, int from, int to,
335 List<String> result) {
336 StringBuilder b = new StringBuilder();
337 int i = from;
338 char quote = 0;
339 boolean escaped = false;
340 SCAN: while (i < to) {
341 char ch = argument.charAt(i);
342 switch (ch) {
343 case '"':
344 case '\'':
345 if (quote == 0) {
346 if (escaped) {
347 b.append(ch);
348 } else {
349 quote = ch;
350 }
351 } else if (!escaped && quote == ch) {
352 quote = 0;
353 } else {
354 b.append(ch);
355 }
356 escaped = false;
357 break;
358 case '\\':
359 if (escaped) {
360 b.append(ch);
361 }
362 escaped = !escaped;
363 break;
364 case ' ':
365 if (quote == 0) {
366 if (escaped) {
367 b.append(ch);
368 escaped = false;
369 } else {
370 break SCAN;
371 }
372 } else {
373 if (escaped) {
374 b.append('\\');
375 }
376 b.append(ch);
377 escaped = false;
378 }
379 break;
380 default:
381 if (escaped) {
382 b.append('\\');
383 }
384 if (quote == 0 && Character.isWhitespace(ch)) {
385 break SCAN;
386 }
387 b.append(ch);
388 escaped = false;
389 break;
390 }
391 i++;
392 }
393 if (b.length() > 0) {
394 result.add(b.toString());
395 }
396 return i;
397 }
398
399
400
401
402
403
404
405
406
407
408
409 protected String validate(String key, String value) {
410 if (SshConstants.PREFERRED_AUTHENTICATIONS.equalsIgnoreCase(key)) {
411 return stripWhitespace(value);
412 }
413 return value;
414 }
415
416
417
418
419
420
421
422
423
424
425
426
427 protected List<String> validate(String key, List<String> value) {
428 return value;
429 }
430
431 private static boolean patternMatchesHost(String pattern, String name) {
432 if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
433 final FileNameMatcher fn;
434 try {
435 fn = new FileNameMatcher(pattern, null);
436 } catch (InvalidPatternException e) {
437 return false;
438 }
439 fn.append(name);
440 return fn.isMatch();
441 }
442
443 return pattern.equals(name);
444 }
445
446 private static String stripWhitespace(String value) {
447 final StringBuilder b = new StringBuilder();
448 int length = value.length();
449 for (int i = 0; i < length; i++) {
450 char ch = value.charAt(i);
451 if (!Character.isWhitespace(ch)) {
452 b.append(ch);
453 }
454 }
455 return b.toString();
456 }
457
458 private static File toFile(String path, File home) {
459 if (path.startsWith("~/") || path.startsWith("~" + File.separator)) {
460 return new File(home, path.substring(2));
461 }
462 File ret = new File(path);
463 if (ret.isAbsolute()) {
464 return ret;
465 }
466 return new File(home, path);
467 }
468
469
470
471
472
473
474
475
476 public static int positive(String value) {
477 if (value != null) {
478 try {
479 return Integer.parseUnsignedInt(value);
480 } catch (NumberFormatException e) {
481
482 }
483 }
484 return -1;
485 }
486
487
488
489
490
491
492
493
494
495
496 public static boolean flag(String value) {
497 if (value == null) {
498 return false;
499 }
500 return SshConstants.YES.equals(value) || SshConstants.ON.equals(value)
501 || SshConstants.TRUE.equals(value);
502 }
503
504
505
506
507
508
509
510
511
512
513
514
515
516 public static int timeSpec(String value) {
517 if (value == null) {
518 return -1;
519 }
520 try {
521 int length = value.length();
522 int i = 0;
523 int seconds = 0;
524 boolean valueSeen = false;
525 while (i < length) {
526
527 char ch = value.charAt(i);
528 if (Character.isWhitespace(ch)) {
529 i++;
530 continue;
531 }
532 if (ch == '+') {
533
534
535 i++;
536 }
537 int val = 0;
538 int j = i;
539 while (j < length) {
540 ch = value.charAt(j++);
541 if (ch >= '0' && ch <= '9') {
542 val = Math.addExact(Math.multiplyExact(val, 10),
543 ch - '0');
544 } else {
545 j--;
546 break;
547 }
548 }
549 if (i == j) {
550
551 return -1;
552 }
553 i = j;
554 int multiplier = 1;
555 if (i < length) {
556 ch = value.charAt(i++);
557 switch (ch) {
558 case 's':
559 case 'S':
560 break;
561 case 'm':
562 case 'M':
563 multiplier = 60;
564 break;
565 case 'h':
566 case 'H':
567 multiplier = 3600;
568 break;
569 case 'd':
570 case 'D':
571 multiplier = 24 * 3600;
572 break;
573 case 'w':
574 case 'W':
575 multiplier = 7 * 24 * 3600;
576 break;
577 default:
578 if (Character.isWhitespace(ch)) {
579 break;
580 }
581
582 return -1;
583 }
584 }
585 seconds = Math.addExact(seconds,
586 Math.multiplyExact(val, multiplier));
587 valueSeen = true;
588 }
589 return valueSeen ? seconds : -1;
590 } catch (ArithmeticException e) {
591
592 return -1;
593 }
594 }
595
596
597
598
599
600
601 public String getLocalUserName() {
602 return localUserName;
603 }
604
605
606
607
608
609
610 public static class HostEntry implements SshConfigStore.HostConfig {
611
612
613
614
615
616
617 private static final Set<String> MULTI_KEYS = new TreeSet<>(
618 String.CASE_INSENSITIVE_ORDER);
619
620 static {
621 MULTI_KEYS.add(SshConstants.CERTIFICATE_FILE);
622 MULTI_KEYS.add(SshConstants.IDENTITY_FILE);
623 MULTI_KEYS.add(SshConstants.LOCAL_FORWARD);
624 MULTI_KEYS.add(SshConstants.REMOTE_FORWARD);
625 MULTI_KEYS.add(SshConstants.SEND_ENV);
626 }
627
628
629
630
631
632
633
634
635
636 private static final Set<String> LIST_KEYS = new TreeSet<>(
637 String.CASE_INSENSITIVE_ORDER);
638
639 static {
640 LIST_KEYS.add(SshConstants.CANONICAL_DOMAINS);
641 LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE);
642 LIST_KEYS.add(SshConstants.SEND_ENV);
643 LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
644 LIST_KEYS.add(SshConstants.ADD_KEYS_TO_AGENT);
645 }
646
647
648
649
650
651 private static final Map<String, String> ALIASES = new TreeMap<>(
652 String.CASE_INSENSITIVE_ORDER);
653
654 static {
655
656 ALIASES.put("PubkeyAcceptedKeyTypes",
657 SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
658 }
659
660 private Map<String, String> options;
661
662 private Map<String, List<String>> multiOptions;
663
664 private Map<String, List<String>> listOptions;
665
666 private final List<String> patterns;
667
668
669
670
671 public HostEntry() {
672 this.patterns = Collections.emptyList();
673 }
674
675
676
677
678
679 public HostEntry(List<String> patterns) {
680 this.patterns = patterns;
681 }
682
683 boolean matches(String hostName) {
684 boolean doesMatch = false;
685 for (String pattern : patterns) {
686 if (pattern.startsWith("!")) {
687 if (patternMatchesHost(pattern.substring(1), hostName)) {
688 return false;
689 }
690 } else if (!doesMatch
691 && patternMatchesHost(pattern, hostName)) {
692 doesMatch = true;
693 }
694 }
695 return doesMatch;
696 }
697
698 private static String toKey(String key) {
699 String k = ALIASES.get(key);
700 return k != null ? k : key;
701 }
702
703
704
705
706
707
708
709
710
711
712 @Override
713 public String getValue(String key) {
714 String k = toKey(key);
715 String result = options != null ? options.get(k) : null;
716 if (result == null) {
717
718
719 List<String> values = listOptions != null ? listOptions.get(k)
720 : null;
721 if (values == null) {
722 values = multiOptions != null ? multiOptions.get(k) : null;
723 }
724 if (values != null && !values.isEmpty()) {
725 result = values.get(0);
726 }
727 }
728 return result;
729 }
730
731
732
733
734
735
736
737
738
739
740 @Override
741 public List<String> getValues(String key) {
742 String k = toKey(key);
743 List<String> values = listOptions != null ? listOptions.get(k)
744 : null;
745 if (values == null) {
746 values = multiOptions != null ? multiOptions.get(k) : null;
747 }
748 if (values == null || values.isEmpty()) {
749 return new ArrayList<>();
750 }
751 return new ArrayList<>(values);
752 }
753
754
755
756
757
758
759
760
761
762
763
764 public void setValue(String key, String value) {
765 String k = toKey(key);
766 if (value == null) {
767 if (multiOptions != null) {
768 multiOptions.remove(k);
769 }
770 if (listOptions != null) {
771 listOptions.remove(k);
772 }
773 if (options != null) {
774 options.remove(k);
775 }
776 return;
777 }
778 if (MULTI_KEYS.contains(k)) {
779 if (multiOptions == null) {
780 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
781 }
782 List<String> values = multiOptions.get(k);
783 if (values == null) {
784 values = new ArrayList<>(4);
785 multiOptions.put(k, values);
786 }
787 values.add(value);
788 } else {
789 if (options == null) {
790 options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
791 }
792 if (!options.containsKey(k)) {
793 options.put(k, value);
794 }
795 }
796 }
797
798
799
800
801
802
803
804
805
806 public void setValue(String key, List<String> values) {
807 if (values.isEmpty()) {
808 return;
809 }
810 String k = toKey(key);
811
812
813
814
815
816
817 if (MULTI_KEYS.contains(k)) {
818 if (multiOptions == null) {
819 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
820 }
821 List<String> items = multiOptions.get(k);
822 if (items == null) {
823 items = new ArrayList<>(values);
824 multiOptions.put(k, items);
825 } else {
826 items.addAll(values);
827 }
828 } else {
829 if (listOptions == null) {
830 listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
831 }
832 if (!listOptions.containsKey(k)) {
833 listOptions.put(k, values);
834 }
835 }
836 }
837
838
839
840
841
842
843
844
845 public static boolean isListKey(String key) {
846 return LIST_KEYS.contains(toKey(key));
847 }
848
849 void merge(HostEntry entry) {
850 if (entry == null) {
851
852 return;
853 }
854 if (entry.options != null) {
855 if (options == null) {
856 options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
857 }
858 for (Map.Entry<String, String> item : entry.options
859 .entrySet()) {
860 if (!options.containsKey(item.getKey())) {
861 options.put(item.getKey(), item.getValue());
862 }
863 }
864 }
865 if (entry.listOptions != null) {
866 if (listOptions == null) {
867 listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
868 }
869 for (Map.Entry<String, List<String>> item : entry.listOptions
870 .entrySet()) {
871 if (!listOptions.containsKey(item.getKey())) {
872 listOptions.put(item.getKey(), item.getValue());
873 }
874 }
875
876 }
877 if (entry.multiOptions != null) {
878 if (multiOptions == null) {
879 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
880 }
881 for (Map.Entry<String, List<String>> item : entry.multiOptions
882 .entrySet()) {
883 List<String> values = multiOptions.get(item.getKey());
884 if (values == null) {
885 values = new ArrayList<>(item.getValue());
886 multiOptions.put(item.getKey(), values);
887 } else {
888 values.addAll(item.getValue());
889 }
890 }
891 }
892 }
893
894 private List<String> substitute(List<String> values, String allowed,
895 Replacer r, boolean withEnv) {
896 List<String> result = new ArrayList<>(values.size());
897 for (String value : values) {
898 result.add(r.substitute(value, allowed, withEnv));
899 }
900 return result;
901 }
902
903 private List<String> replaceTilde(List<String> values, File home) {
904 List<String> result = new ArrayList<>(values.size());
905 for (String value : values) {
906 result.add(toFile(value, home).getPath());
907 }
908 return result;
909 }
910
911 void substitute(String originalHostName, int port, String userName,
912 String localUserName, File home, boolean fillDefaults) {
913 int p = port > 0 ? port : positive(getValue(SshConstants.PORT));
914 if (p <= 0) {
915 p = SshConstants.SSH_DEFAULT_PORT;
916 }
917 String u = !StringUtils.isEmptyOrNull(userName) ? userName
918 : getValue(SshConstants.USER);
919 if (u == null || u.isEmpty()) {
920 u = localUserName;
921 }
922 Replacer r = new Replacer(originalHostName, p, u, localUserName,
923 home);
924 if (options != null) {
925
926 String hostName = options.get(SshConstants.HOST_NAME);
927 if (hostName == null || hostName.isEmpty()) {
928 options.put(SshConstants.HOST_NAME, originalHostName);
929 } else {
930 hostName = r.substitute(hostName, "h", false);
931 options.put(SshConstants.HOST_NAME, hostName);
932 r.update('h', hostName);
933 }
934 } else if (fillDefaults) {
935 setValue(SshConstants.HOST_NAME, originalHostName);
936 }
937 if (multiOptions != null) {
938 List<String> values = multiOptions
939 .get(SshConstants.IDENTITY_FILE);
940 if (values != null) {
941 values = substitute(values, Replacer.DEFAULT_TOKENS, r,
942 true);
943 values = replaceTilde(values, home);
944 multiOptions.put(SshConstants.IDENTITY_FILE, values);
945 }
946 values = multiOptions.get(SshConstants.CERTIFICATE_FILE);
947 if (values != null) {
948 values = substitute(values, Replacer.DEFAULT_TOKENS, r,
949 true);
950 values = replaceTilde(values, home);
951 multiOptions.put(SshConstants.CERTIFICATE_FILE, values);
952 }
953 }
954 if (listOptions != null) {
955 List<String> values = listOptions
956 .get(SshConstants.USER_KNOWN_HOSTS_FILE);
957 if (values != null) {
958 values = substitute(values, Replacer.DEFAULT_TOKENS, r,
959 true);
960 values = replaceTilde(values, home);
961 listOptions.put(SshConstants.USER_KNOWN_HOSTS_FILE, values);
962 }
963 }
964 if (options != null) {
965
966 String value = options.get(SshConstants.IDENTITY_AGENT);
967 if (value != null && !SshConstants.NONE.equals(value)
968 && !SshConstants.ENV_SSH_AUTH_SOCKET.equals(value)) {
969 value = r.substitute(value, Replacer.DEFAULT_TOKENS, true);
970 value = toFile(value, home).getPath();
971 options.put(SshConstants.IDENTITY_AGENT, value);
972 }
973 value = options.get(SshConstants.CONTROL_PATH);
974 if (value != null) {
975 value = r.substitute(value, Replacer.DEFAULT_TOKENS, true);
976 value = toFile(value, home).getPath();
977 options.put(SshConstants.CONTROL_PATH, value);
978 }
979 value = options.get(SshConstants.LOCAL_COMMAND);
980 if (value != null) {
981 value = r.substitute(value, "CdhLlnprTu", false);
982 options.put(SshConstants.LOCAL_COMMAND, value);
983 }
984 value = options.get(SshConstants.REMOTE_COMMAND);
985 if (value != null) {
986 value = r.substitute(value, Replacer.DEFAULT_TOKENS, false);
987 options.put(SshConstants.REMOTE_COMMAND, value);
988 }
989 value = options.get(SshConstants.PROXY_COMMAND);
990 if (value != null) {
991 value = r.substitute(value, "hnpr", false);
992 options.put(SshConstants.PROXY_COMMAND, value);
993 }
994 }
995
996
997 if (fillDefaults) {
998 String s = options.get(SshConstants.USER);
999 if (StringUtils.isEmptyOrNull(s)) {
1000 options.put(SshConstants.USER, u);
1001 }
1002 if (positive(options.get(SshConstants.PORT)) <= 0) {
1003 options.put(SshConstants.PORT, Integer.toString(p));
1004 }
1005 if (positive(
1006 options.get(SshConstants.CONNECTION_ATTEMPTS)) <= 0) {
1007 options.put(SshConstants.CONNECTION_ATTEMPTS, "1");
1008 }
1009 }
1010 }
1011
1012
1013
1014
1015
1016
1017
1018 @Override
1019 @NonNull
1020 public Map<String, String> getOptions() {
1021 if (options == null) {
1022 return Collections.emptyMap();
1023 }
1024 return Collections.unmodifiableMap(options);
1025 }
1026
1027
1028
1029
1030
1031
1032
1033 @Override
1034 @NonNull
1035 public Map<String, List<String>> getMultiValuedOptions() {
1036 if (listOptions == null && multiOptions == null) {
1037 return Collections.emptyMap();
1038 }
1039 Map<String, List<String>> allValues = new TreeMap<>(
1040 String.CASE_INSENSITIVE_ORDER);
1041 if (multiOptions != null) {
1042 allValues.putAll(multiOptions);
1043 }
1044 if (listOptions != null) {
1045 allValues.putAll(listOptions);
1046 }
1047 return Collections.unmodifiableMap(allValues);
1048 }
1049
1050 @Override
1051 @SuppressWarnings("nls")
1052 public String toString() {
1053 return "HostEntry [options=" + options + ", multiOptions="
1054 + multiOptions + ", listOptions=" + listOptions + "]";
1055 }
1056 }
1057
1058 private static class Replacer {
1059
1060
1061
1062
1063
1064
1065
1066 public static final String DEFAULT_TOKENS = "CdhLlnpru";
1067
1068 private final Map<Character, String> replacements = new HashMap<>();
1069
1070 public Replacer(String host, int port, String user,
1071 String localUserName, File home) {
1072 replacements.put(Character.valueOf('%'), "%");
1073 replacements.put(Character.valueOf('d'), home.getPath());
1074 replacements.put(Character.valueOf('h'), host);
1075 String localhost = SystemReader.getInstance().getHostname();
1076 replacements.put(Character.valueOf('l'), localhost);
1077 int period = localhost.indexOf('.');
1078 if (period > 0) {
1079 localhost = localhost.substring(0, period);
1080 }
1081 replacements.put(Character.valueOf('L'), localhost);
1082 replacements.put(Character.valueOf('n'), host);
1083 replacements.put(Character.valueOf('p'), Integer.toString(port));
1084 replacements.put(Character.valueOf('r'), user == null ? "" : user);
1085 replacements.put(Character.valueOf('u'), localUserName);
1086 replacements.put(Character.valueOf('C'),
1087 substitute("%l%h%p%r", "hlpr", false));
1088 replacements.put(Character.valueOf('T'), "NONE");
1089 }
1090
1091 public void update(char key, String value) {
1092 replacements.put(Character.valueOf(key), value);
1093 if ("lhpr".indexOf(key) >= 0) {
1094 replacements.put(Character.valueOf('C'),
1095 substitute("%l%h%p%r", "hlpr", false));
1096 }
1097 }
1098
1099 public String substitute(String input, String allowed,
1100 boolean withEnv) {
1101 if (input == null || input.length() <= 1
1102 || (input.indexOf('%') < 0
1103 && (!withEnv || input.indexOf("${") < 0))) {
1104 return input;
1105 }
1106 StringBuilder builder = new StringBuilder();
1107 int start = 0;
1108 int length = input.length();
1109 while (start < length) {
1110 char ch = input.charAt(start);
1111 switch (ch) {
1112 case '%':
1113 if (start + 1 >= length) {
1114 break;
1115 }
1116 String replacement = null;
1117 ch = input.charAt(start + 1);
1118 if (ch == '%' || allowed.indexOf(ch) >= 0) {
1119 replacement = replacements.get(Character.valueOf(ch));
1120 }
1121 if (replacement == null) {
1122 builder.append('%').append(ch);
1123 } else {
1124 builder.append(replacement);
1125 }
1126 start += 2;
1127 continue;
1128 case '$':
1129 if (!withEnv || start + 2 >= length) {
1130 break;
1131 }
1132 ch = input.charAt(start + 1);
1133 if (ch == '{') {
1134 int close = input.indexOf('}', start + 2);
1135 if (close > start + 2) {
1136 String variable = SystemReader.getInstance()
1137 .getenv(input.substring(start + 2, close));
1138 if (!StringUtils.isEmptyOrNull(variable)) {
1139 builder.append(variable);
1140 }
1141 start = close + 1;
1142 continue;
1143 }
1144 }
1145 ch = '$';
1146 break;
1147 default:
1148 break;
1149 }
1150 builder.append(ch);
1151 start++;
1152 }
1153 return builder.toString();
1154 }
1155 }
1156
1157
1158 @Override
1159 @SuppressWarnings("nls")
1160 public String toString() {
1161 return "OpenSshConfig [home=" + home + ", configFile=" + configFile
1162 + ", lastModified=" + lastModified + ", state=" + state + "]";
1163 }
1164 }