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 public String getLocalUserName() {
510 return localUserName;
511 }
512
513
514
515
516
517
518 public static class HostEntry implements SshConfigStore.HostConfig {
519
520
521
522
523
524
525 private static final Set<String> MULTI_KEYS = new TreeSet<>(
526 String.CASE_INSENSITIVE_ORDER);
527
528 static {
529 MULTI_KEYS.add(SshConstants.CERTIFICATE_FILE);
530 MULTI_KEYS.add(SshConstants.IDENTITY_FILE);
531 MULTI_KEYS.add(SshConstants.LOCAL_FORWARD);
532 MULTI_KEYS.add(SshConstants.REMOTE_FORWARD);
533 MULTI_KEYS.add(SshConstants.SEND_ENV);
534 }
535
536
537
538
539
540
541
542
543
544 private static final Set<String> LIST_KEYS = new TreeSet<>(
545 String.CASE_INSENSITIVE_ORDER);
546
547 static {
548 LIST_KEYS.add(SshConstants.CANONICAL_DOMAINS);
549 LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE);
550 LIST_KEYS.add(SshConstants.SEND_ENV);
551 LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
552 }
553
554
555
556
557
558 private static final Map<String, String> ALIASES = new TreeMap<>(
559 String.CASE_INSENSITIVE_ORDER);
560
561 static {
562
563 ALIASES.put("PubkeyAcceptedKeyTypes",
564 SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
565 }
566
567 private Map<String, String> options;
568
569 private Map<String, List<String>> multiOptions;
570
571 private Map<String, List<String>> listOptions;
572
573 private final List<String> patterns;
574
575
576
577
578 public HostEntry() {
579 this.patterns = Collections.emptyList();
580 }
581
582
583
584
585
586 public HostEntry(List<String> patterns) {
587 this.patterns = patterns;
588 }
589
590 boolean matches(String hostName) {
591 boolean doesMatch = false;
592 for (String pattern : patterns) {
593 if (pattern.startsWith("!")) {
594 if (patternMatchesHost(pattern.substring(1), hostName)) {
595 return false;
596 }
597 } else if (!doesMatch
598 && patternMatchesHost(pattern, hostName)) {
599 doesMatch = true;
600 }
601 }
602 return doesMatch;
603 }
604
605 private static String toKey(String key) {
606 String k = ALIASES.get(key);
607 return k != null ? k : key;
608 }
609
610
611
612
613
614
615
616
617
618
619 @Override
620 public String getValue(String key) {
621 String k = toKey(key);
622 String result = options != null ? options.get(k) : null;
623 if (result == null) {
624
625
626 List<String> values = listOptions != null ? listOptions.get(k)
627 : null;
628 if (values == null) {
629 values = multiOptions != null ? multiOptions.get(k) : null;
630 }
631 if (values != null && !values.isEmpty()) {
632 result = values.get(0);
633 }
634 }
635 return result;
636 }
637
638
639
640
641
642
643
644
645
646
647 @Override
648 public List<String> getValues(String key) {
649 String k = toKey(key);
650 List<String> values = listOptions != null ? listOptions.get(k)
651 : null;
652 if (values == null) {
653 values = multiOptions != null ? multiOptions.get(k) : null;
654 }
655 if (values == null || values.isEmpty()) {
656 return new ArrayList<>();
657 }
658 return new ArrayList<>(values);
659 }
660
661
662
663
664
665
666
667
668
669
670
671 public void setValue(String key, String value) {
672 String k = toKey(key);
673 if (value == null) {
674 if (multiOptions != null) {
675 multiOptions.remove(k);
676 }
677 if (listOptions != null) {
678 listOptions.remove(k);
679 }
680 if (options != null) {
681 options.remove(k);
682 }
683 return;
684 }
685 if (MULTI_KEYS.contains(k)) {
686 if (multiOptions == null) {
687 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
688 }
689 List<String> values = multiOptions.get(k);
690 if (values == null) {
691 values = new ArrayList<>(4);
692 multiOptions.put(k, values);
693 }
694 values.add(value);
695 } else {
696 if (options == null) {
697 options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
698 }
699 if (!options.containsKey(k)) {
700 options.put(k, value);
701 }
702 }
703 }
704
705
706
707
708
709
710
711
712
713 public void setValue(String key, List<String> values) {
714 if (values.isEmpty()) {
715 return;
716 }
717 String k = toKey(key);
718
719
720
721
722
723
724 if (MULTI_KEYS.contains(k)) {
725 if (multiOptions == null) {
726 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
727 }
728 List<String> items = multiOptions.get(k);
729 if (items == null) {
730 items = new ArrayList<>(values);
731 multiOptions.put(k, items);
732 } else {
733 items.addAll(values);
734 }
735 } else {
736 if (listOptions == null) {
737 listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
738 }
739 if (!listOptions.containsKey(k)) {
740 listOptions.put(k, values);
741 }
742 }
743 }
744
745
746
747
748
749
750
751
752 public static boolean isListKey(String key) {
753 return LIST_KEYS.contains(toKey(key));
754 }
755
756 void merge(HostEntry entry) {
757 if (entry == null) {
758
759 return;
760 }
761 if (entry.options != null) {
762 if (options == null) {
763 options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
764 }
765 for (Map.Entry<String, String> item : entry.options
766 .entrySet()) {
767 if (!options.containsKey(item.getKey())) {
768 options.put(item.getKey(), item.getValue());
769 }
770 }
771 }
772 if (entry.listOptions != null) {
773 if (listOptions == null) {
774 listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
775 }
776 for (Map.Entry<String, List<String>> item : entry.listOptions
777 .entrySet()) {
778 if (!listOptions.containsKey(item.getKey())) {
779 listOptions.put(item.getKey(), item.getValue());
780 }
781 }
782
783 }
784 if (entry.multiOptions != null) {
785 if (multiOptions == null) {
786 multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
787 }
788 for (Map.Entry<String, List<String>> item : entry.multiOptions
789 .entrySet()) {
790 List<String> values = multiOptions.get(item.getKey());
791 if (values == null) {
792 values = new ArrayList<>(item.getValue());
793 multiOptions.put(item.getKey(), values);
794 } else {
795 values.addAll(item.getValue());
796 }
797 }
798 }
799 }
800
801 private List<String> substitute(List<String> values, String allowed,
802 Replacer r, boolean withEnv) {
803 List<String> result = new ArrayList<>(values.size());
804 for (String value : values) {
805 result.add(r.substitute(value, allowed, withEnv));
806 }
807 return result;
808 }
809
810 private List<String> replaceTilde(List<String> values, File home) {
811 List<String> result = new ArrayList<>(values.size());
812 for (String value : values) {
813 result.add(toFile(value, home).getPath());
814 }
815 return result;
816 }
817
818 void substitute(String originalHostName, int port, String userName,
819 String localUserName, File home, boolean fillDefaults) {
820 int p = port > 0 ? port : positive(getValue(SshConstants.PORT));
821 if (p <= 0) {
822 p = SshConstants.SSH_DEFAULT_PORT;
823 }
824 String u = !StringUtils.isEmptyOrNull(userName) ? userName
825 : getValue(SshConstants.USER);
826 if (u == null || u.isEmpty()) {
827 u = localUserName;
828 }
829 Replacer r = new Replacer(originalHostName, p, u, localUserName,
830 home);
831 if (options != null) {
832
833 String hostName = options.get(SshConstants.HOST_NAME);
834 if (hostName == null || hostName.isEmpty()) {
835 options.put(SshConstants.HOST_NAME, originalHostName);
836 } else {
837 hostName = r.substitute(hostName, "h", false);
838 options.put(SshConstants.HOST_NAME, hostName);
839 r.update('h', hostName);
840 }
841 } else if (fillDefaults) {
842 setValue(SshConstants.HOST_NAME, originalHostName);
843 }
844 if (multiOptions != null) {
845 List<String> values = multiOptions
846 .get(SshConstants.IDENTITY_FILE);
847 if (values != null) {
848 values = substitute(values, Replacer.DEFAULT_TOKENS, r,
849 true);
850 values = replaceTilde(values, home);
851 multiOptions.put(SshConstants.IDENTITY_FILE, values);
852 }
853 values = multiOptions.get(SshConstants.CERTIFICATE_FILE);
854 if (values != null) {
855 values = substitute(values, Replacer.DEFAULT_TOKENS, r,
856 true);
857 values = replaceTilde(values, home);
858 multiOptions.put(SshConstants.CERTIFICATE_FILE, values);
859 }
860 }
861 if (listOptions != null) {
862 List<String> values = listOptions
863 .get(SshConstants.USER_KNOWN_HOSTS_FILE);
864 if (values != null) {
865 values = substitute(values, Replacer.DEFAULT_TOKENS, r,
866 true);
867 values = replaceTilde(values, home);
868 listOptions.put(SshConstants.USER_KNOWN_HOSTS_FILE, values);
869 }
870 }
871 if (options != null) {
872
873 String value = options.get(SshConstants.IDENTITY_AGENT);
874 if (value != null) {
875 value = r.substitute(value, Replacer.DEFAULT_TOKENS, true);
876 value = toFile(value, home).getPath();
877 options.put(SshConstants.IDENTITY_AGENT, value);
878 }
879 value = options.get(SshConstants.CONTROL_PATH);
880 if (value != null) {
881 value = r.substitute(value, Replacer.DEFAULT_TOKENS, true);
882 value = toFile(value, home).getPath();
883 options.put(SshConstants.CONTROL_PATH, value);
884 }
885 value = options.get(SshConstants.LOCAL_COMMAND);
886 if (value != null) {
887 value = r.substitute(value, "CdhLlnprTu", false);
888 options.put(SshConstants.LOCAL_COMMAND, value);
889 }
890 value = options.get(SshConstants.REMOTE_COMMAND);
891 if (value != null) {
892 value = r.substitute(value, Replacer.DEFAULT_TOKENS, false);
893 options.put(SshConstants.REMOTE_COMMAND, value);
894 }
895 value = options.get(SshConstants.PROXY_COMMAND);
896 if (value != null) {
897 value = r.substitute(value, "hnpr", false);
898 options.put(SshConstants.PROXY_COMMAND, value);
899 }
900 }
901
902
903 if (fillDefaults) {
904 String s = options.get(SshConstants.USER);
905 if (StringUtils.isEmptyOrNull(s)) {
906 options.put(SshConstants.USER, u);
907 }
908 if (positive(options.get(SshConstants.PORT)) <= 0) {
909 options.put(SshConstants.PORT, Integer.toString(p));
910 }
911 if (positive(
912 options.get(SshConstants.CONNECTION_ATTEMPTS)) <= 0) {
913 options.put(SshConstants.CONNECTION_ATTEMPTS, "1");
914 }
915 }
916 }
917
918
919
920
921
922
923
924 @Override
925 @NonNull
926 public Map<String, String> getOptions() {
927 if (options == null) {
928 return Collections.emptyMap();
929 }
930 return Collections.unmodifiableMap(options);
931 }
932
933
934
935
936
937
938
939 @Override
940 @NonNull
941 public Map<String, List<String>> getMultiValuedOptions() {
942 if (listOptions == null && multiOptions == null) {
943 return Collections.emptyMap();
944 }
945 Map<String, List<String>> allValues = new TreeMap<>(
946 String.CASE_INSENSITIVE_ORDER);
947 if (multiOptions != null) {
948 allValues.putAll(multiOptions);
949 }
950 if (listOptions != null) {
951 allValues.putAll(listOptions);
952 }
953 return Collections.unmodifiableMap(allValues);
954 }
955
956 @Override
957 @SuppressWarnings("nls")
958 public String toString() {
959 return "HostEntry [options=" + options + ", multiOptions="
960 + multiOptions + ", listOptions=" + listOptions + "]";
961 }
962 }
963
964 private static class Replacer {
965
966
967
968
969
970
971
972 public static final String DEFAULT_TOKENS = "CdhLlnpru";
973
974 private final Map<Character, String> replacements = new HashMap<>();
975
976 public Replacer(String host, int port, String user,
977 String localUserName, File home) {
978 replacements.put(Character.valueOf('%'), "%");
979 replacements.put(Character.valueOf('d'), home.getPath());
980 replacements.put(Character.valueOf('h'), host);
981 String localhost = SystemReader.getInstance().getHostname();
982 replacements.put(Character.valueOf('l'), localhost);
983 int period = localhost.indexOf('.');
984 if (period > 0) {
985 localhost = localhost.substring(0, period);
986 }
987 replacements.put(Character.valueOf('L'), localhost);
988 replacements.put(Character.valueOf('n'), host);
989 replacements.put(Character.valueOf('p'), Integer.toString(port));
990 replacements.put(Character.valueOf('r'), user == null ? "" : user);
991 replacements.put(Character.valueOf('u'), localUserName);
992 replacements.put(Character.valueOf('C'),
993 substitute("%l%h%p%r", "hlpr", false));
994 replacements.put(Character.valueOf('T'), "NONE");
995 }
996
997 public void update(char key, String value) {
998 replacements.put(Character.valueOf(key), value);
999 if ("lhpr".indexOf(key) >= 0) {
1000 replacements.put(Character.valueOf('C'),
1001 substitute("%l%h%p%r", "hlpr", false));
1002 }
1003 }
1004
1005 public String substitute(String input, String allowed,
1006 boolean withEnv) {
1007 if (input == null || input.length() <= 1
1008 || (input.indexOf('%') < 0
1009 && (!withEnv || input.indexOf("${") < 0))) {
1010 return input;
1011 }
1012 StringBuilder builder = new StringBuilder();
1013 int start = 0;
1014 int length = input.length();
1015 while (start < length) {
1016 char ch = input.charAt(start);
1017 switch (ch) {
1018 case '%':
1019 if (start + 1 >= length) {
1020 break;
1021 }
1022 String replacement = null;
1023 ch = input.charAt(start + 1);
1024 if (ch == '%' || allowed.indexOf(ch) >= 0) {
1025 replacement = replacements.get(Character.valueOf(ch));
1026 }
1027 if (replacement == null) {
1028 builder.append('%').append(ch);
1029 } else {
1030 builder.append(replacement);
1031 }
1032 start += 2;
1033 continue;
1034 case '$':
1035 if (!withEnv || start + 2 >= length) {
1036 break;
1037 }
1038 ch = input.charAt(start + 1);
1039 if (ch == '{') {
1040 int close = input.indexOf('}', start + 2);
1041 if (close > start + 2) {
1042 String variable = SystemReader.getInstance()
1043 .getenv(input.substring(start + 2, close));
1044 if (!StringUtils.isEmptyOrNull(variable)) {
1045 builder.append(variable);
1046 }
1047 start = close + 1;
1048 continue;
1049 }
1050 }
1051 ch = '$';
1052 break;
1053 default:
1054 break;
1055 }
1056 builder.append(ch);
1057 start++;
1058 }
1059 return builder.toString();
1060 }
1061 }
1062
1063
1064 @Override
1065 @SuppressWarnings("nls")
1066 public String toString() {
1067 return "OpenSshConfig [home=" + home + ", configFile=" + configFile
1068 + ", lastModified=" + lastModified + ", state=" + state + "]";
1069 }
1070 }