View Javadoc
1   /*
2    * Copyright (C) 2008, 2017, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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   * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file.
75   * <p>
76   * JSch does have its own config file parser
77   * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a
78   * number of problems:
79   * <ul>
80   * <li>it splits lines of the format "keyword = value" wrongly: you'd end up
81   * with the value "= value".
82   * <li>its "Host" keyword is not case insensitive.
83   * <li>it doesn't handle quoted values.
84   * <li>JSch's OpenSSHConfig doesn't monitor for config file changes.
85   * </ul>
86   * <p>
87   * Therefore implement our own parser to read an OpenSSH configuration file. It
88   * makes the critical options available to {@link SshSessionFactory} via
89   * {@link Host} objects returned by {@link #lookup(String)}, and implements a
90   * fully conforming {@link ConfigRepository} providing
91   * {@link com.jcraft.jsch.ConfigRepository.Config}s via
92   * {@link #getConfig(String)}.
93   * </p>
94   * <p>
95   * Limitations compared to the full OpenSSH 7.5 parser:
96   * </p>
97   * <ul>
98   * <li>This parser does not handle Match or Include keywords.
99   * <li>This parser does not do host name canonicalization (Jsch ignores it
100  * anyway).
101  * </ul>
102  * <p>
103  * Note that OpenSSH's readconf.c is a validating parser; Jsch's
104  * ConfigRepository OTOH treats all option values as plain strings, so any
105  * validation must happen in Jsch outside of the parser. Thus this parser does
106  * not validate option values, except for a few options when constructing a
107  * {@link Host} object.
108  * </p>
109  * <p>
110  * This config does %-substitutions for the following tokens:
111  * </p>
112  * <ul>
113  * <li>%% - single %
114  * <li>%C - short-hand for %l%h%p%r. See %p and %r below; the replacement may be
115  * done partially only and may leave %p or %r or both unreplaced.
116  * <li>%d - home directory path
117  * <li>%h - remote host name
118  * <li>%L - local host name without domain
119  * <li>%l - FQDN of the local host
120  * <li>%n - host name as specified in {@link #lookup(String)}
121  * <li>%p - port number; replaced only if set in the config
122  * <li>%r - remote user name; replaced only if set in the config
123  * <li>%u - local user name
124  * </ul>
125  * <p>
126  * If the config doesn't set the port or the remote user name, %p and %r remain
127  * un-substituted. It's the caller's responsibility to replace them with values
128  * obtained from the connection URI. %i is not handled; Java has no concept of a
129  * "user ID".
130  * </p>
131  */
132 public class OpenSshConfig implements ConfigRepository {
133 
134 	/** IANA assigned port number for SSH. */
135 	static final int SSH_PORT = 22;
136 
137 	/**
138 	 * Obtain the user's configuration data.
139 	 * <p>
140 	 * The configuration file is always returned to the caller, even if no file
141 	 * exists in the user's home directory at the time the call was made. Lookup
142 	 * requests are cached and are automatically updated if the user modifies
143 	 * the configuration file since the last time it was cached.
144 	 *
145 	 * @param fs
146 	 *            the file system abstraction which will be necessary to
147 	 *            perform certain file system operations.
148 	 * @return a caching reader of the user's configuration file.
149 	 */
150 	public static OpenSshConfig get(FS fs) {
151 		File home = fs.userHome();
152 		if (home == null)
153 			home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
154 
155 		final File config = new File(new File(home, ".ssh"), Constants.CONFIG); //$NON-NLS-1$
156 		final OpenSshConfig osc = new OpenSshConfig(home, config);
157 		osc.refresh();
158 		return osc;
159 	}
160 
161 	/** The user's home directory, as key files may be relative to here. */
162 	private final File home;
163 
164 	/** The .ssh/config file we read and monitor for updates. */
165 	private final File configFile;
166 
167 	/** Modification time of {@link #configFile} when it was last loaded. */
168 	private long lastModified;
169 
170 	/**
171 	 * Encapsulates entries read out of the configuration file, and
172 	 * {@link Host}s created from that.
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 	/** State read from the config file, plus {@link Host}s created from it. */
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 	 * Locate the configuration for a specific host request.
196 	 *
197 	 * @param hostName
198 	 *            the name the user has supplied to the SSH tool. This may be a
199 	 *            real host name, or it may just be a "Host" block in the
200 	 *            configuration file.
201 	 * @return r configuration for the requested name. Never null.
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 		// Initialize with default entries at the top of the file, before the
211 		// first Host block.
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 				// Ignore -- we'll set and return an empty state
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 		// The man page doesn't say so, but the OpenSSH parser (readconf.c)
248 		// starts out in active mode and thus always applies any lines that
249 		// occur before the first host block. We gather those options in a
250 		// HostEntry for DEFAULT_NAME.
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("#")) { //$NON-NLS-1$
258 				continue;
259 			}
260 			String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
261 			// Although the ssh-config man page doesn't say so, the OpenSSH
262 			// parser does allow quoted keywords.
263 			String keyword = dequote(parts[0].trim());
264 			// man 5 ssh-config says lines had the format "keyword arguments",
265 			// with no indication that arguments were optional. However, let's
266 			// not crap out on missing arguments. See bug 444319.
267 			String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$
268 
269 			if (StringUtils.equalsIgnoreCase("Host", keyword)) { //$NON-NLS-1$
270 				current.clear();
271 				for (String name : HostEntry.parseList(argValue)) {
272 					if (name == null || name.isEmpty()) {
273 						// null should not occur, but better be safe than sorry.
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 				// We received an option outside of a Host block. We
288 				// don't know who this should match against, so skip.
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("!")) { //$NON-NLS-1$
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 			// Not a pattern but a full host name
330 			return pattern.equals(name);
331 		}
332 	}
333 
334 	private static String dequote(final String value) {
335 		if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$
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)) //$NON-NLS-1$
352 			return Boolean.TRUE;
353 		return Boolean.FALSE;
354 	}
355 
356 	private static File toFile(String path, File home) {
357 		if (path.startsWith("~/")) { //$NON-NLS-1$
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 				// Ignore
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 		 * "Host name" of the HostEntry for the default options before the first
392 		 * host block in a config file.
393 		 */
394 		public static final String DEFAULT_NAME = ""; //$NON-NLS-1$
395 
396 		// See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
397 		// to ssh-config keys.
398 		private static final Map<String, String> KEY_MAP = new HashMap<>();
399 
400 		static {
401 			KEY_MAP.put("kex", "KexAlgorithms"); //$NON-NLS-1$//$NON-NLS-2$
402 			KEY_MAP.put("server_host_key", "HostKeyAlgorithms"); //$NON-NLS-1$ //$NON-NLS-2$
403 			KEY_MAP.put("cipher.c2s", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
404 			KEY_MAP.put("cipher.s2c", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
405 			KEY_MAP.put("mac.c2s", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
406 			KEY_MAP.put("mac.s2c", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
407 			KEY_MAP.put("compression.s2c", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
408 			KEY_MAP.put("compression.c2s", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
409 			KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$
410 			KEY_MAP.put("MaxAuthTries", "NumberOfPasswordPrompts"); //$NON-NLS-1$ //$NON-NLS-2$
411 		}
412 
413 		/**
414 		 * Keys that can be specified multiple times, building up a list. (I.e.,
415 		 * those are the keys that do not follow the general rule of "first
416 		 * occurrence wins".)
417 		 */
418 		private static final Set<String> MULTI_KEYS = new HashSet<>();
419 
420 		static {
421 			MULTI_KEYS.add("CERTIFICATEFILE"); //$NON-NLS-1$
422 			MULTI_KEYS.add("IDENTITYFILE"); //$NON-NLS-1$
423 			MULTI_KEYS.add("LOCALFORWARD"); //$NON-NLS-1$
424 			MULTI_KEYS.add("REMOTEFORWARD"); //$NON-NLS-1$
425 			MULTI_KEYS.add("SENDENV"); //$NON-NLS-1$
426 		}
427 
428 		/**
429 		 * Keys that take a whitespace-separated list of elements as argument.
430 		 * Because the dequote-handling is different, we must handle those in
431 		 * the parser. There are a few other keys that take comma-separated
432 		 * lists as arguments, but for the parser those are single arguments
433 		 * that must be quoted if they contain whitespace, and taking them apart
434 		 * is the responsibility of the user of those keys.
435 		 */
436 		private static final Set<String> LIST_KEYS = new HashSet<>();
437 
438 		static {
439 			LIST_KEYS.add("CANONICALDOMAINS"); //$NON-NLS-1$
440 			LIST_KEYS.add("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
441 			LIST_KEYS.add("SENDENV"); //$NON-NLS-1$
442 			LIST_KEYS.add("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
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"); //$NON-NLS-1$
454 		}
455 
456 		@Override
457 		public String getUser() {
458 			return getValue("USER"); //$NON-NLS-1$
459 		}
460 
461 		@Override
462 		public int getPort() {
463 			return positive(getValue("PORT")); //$NON-NLS-1$
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 				// Also check the list and multi options. Modern OpenSSH treats
479 				// UserKnownHostsFile and GlobalKnownHostsFile as list-valued,
480 				// and so does this parser. Jsch 0.1.54 in general doesn't know
481 				// about list-valued options (it _does_ know multi-valued
482 				// options, though), and will ask for a single value for such
483 				// options.
484 				//
485 				// Let's be lenient and return at least the first value from
486 				// a list-valued or multi-valued key for which Jsch asks for a
487 				// single value.
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 			// See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() for this
503 			// special case.
504 			if (key.equals("compression.s2c") //$NON-NLS-1$
505 					|| key.equals("compression.c2s")) { //$NON-NLS-1$
506 				String foo = findValue(key);
507 				if (foo == null || foo.equals("no")) { //$NON-NLS-1$
508 					return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$
509 				}
510 				return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$
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 				// Can occur only on a missing argument: ignore.
554 				return;
555 			}
556 			String k = key.toUpperCase(Locale.ROOT);
557 			// Check multi-valued keys first; because of the replacement
558 			// strategy, they must take precedence over list-valued keys
559 			// which always follow the "first occurrence wins" strategy.
560 			//
561 			// Note that SendEnv is a multi-valued list-valued key. (It's
562 			// rather immaterial for JGit, though.)
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 		 * Splits the argument into a list of whitespace-separated elements.
590 		 * Elements containing whitespace must be quoted and will be de-quoted.
591 		 *
592 		 * @param argument
593 		 *            argument part of the configuration line as read from the
594 		 *            config file
595 		 * @return a {@link List} of elements, possibly empty and possibly
596 		 *         containing empty elements
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 				// Skip whitespace
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 						// No closing double quote: skip
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 				// Can occur if we could not read the config file
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('%'), "%"); //$NON-NLS-1$
679 				replacements.put(Character.valueOf('d'), home.getPath());
680 				// Needs special treatment...
681 				String host = getValue("HOSTNAME"); //$NON-NLS-1$
682 				replacements.put(Character.valueOf('h'), originalHostName);
683 				if (host != null && host.indexOf('%') >= 0) {
684 					host = substitute(host, "h"); //$NON-NLS-1$
685 					options.put("HOSTNAME", host); //$NON-NLS-1$
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")); //$NON-NLS-1$
699 				replacements.put(Character.valueOf('r'), getValue("USER")); //$NON-NLS-1$
700 				replacements.put(Character.valueOf('u'), userName());
701 				replacements.put(Character.valueOf('C'),
702 						substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
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"); //$NON-NLS-1$
757 				if (values != null) {
758 					values = substitute(values, "dhlru", r); //$NON-NLS-1$
759 					values = replaceTilde(values, home);
760 					multiOptions.put("IDENTITYFILE", values); //$NON-NLS-1$
761 				}
762 				values = multiOptions.get("CERTIFICATEFILE"); //$NON-NLS-1$
763 				if (values != null) {
764 					values = substitute(values, "dhlru", r); //$NON-NLS-1$
765 					values = replaceTilde(values, home);
766 					multiOptions.put("CERTIFICATEFILE", values); //$NON-NLS-1$
767 				}
768 			}
769 			if (listOptions != null) {
770 				List<String> values = listOptions.get("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
771 				if (values != null) {
772 					values = replaceTilde(values, home);
773 					listOptions.put("GLOBALKNOWNHOSTSFILE", values); //$NON-NLS-1$
774 				}
775 				values = listOptions.get("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
776 				if (values != null) {
777 					values = replaceTilde(values, home);
778 					listOptions.put("USERKNOWNHOSTSFILE", values); //$NON-NLS-1$
779 				}
780 			}
781 			if (options != null) {
782 				// HOSTNAME already done in Replacer constructor
783 				String value = options.get("IDENTITYAGENT"); //$NON-NLS-1$
784 				if (value != null) {
785 					value = r.substitute(value, "dhlru"); //$NON-NLS-1$
786 					value = toFile(value, home).getPath();
787 					options.put("IDENTITYAGENT", value); //$NON-NLS-1$
788 				}
789 			}
790 			// Match is not implemented and would need to be done elsewhere
791 			// anyway. ControlPath, LocalCommand, ProxyCommand, and
792 			// RemoteCommand are not used by Jsch.
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 	 * Configuration of one "Host" block in the configuration file.
805 	 * <p>
806 	 * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
807 	 * properties may not be populated. The properties which are not populated
808 	 * should be defaulted by the caller.
809 	 * <p>
810 	 * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
811 	 * entries which appear later in the configuration file will have been
812 	 * already merged into this block.
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 		 * Creates a new uninitialized {@link Host}.
835 		 */
836 		public Host() {
837 			// For API backwards compatibility with pre-4.9 JGit
838 		}
839 
840 		Host(Config config, String hostName, File homeDir) {
841 			this.config = config;
842 			complete(hostName, homeDir);
843 		}
844 
845 		/**
846 		 * @return the value StrictHostKeyChecking property, the valid values
847 		 *         are "yes" (unknown hosts are not accepted), "no" (unknown
848 		 *         hosts are always accepted), and "ask" (user should be asked
849 		 *         before accepting the host)
850 		 */
851 		public String getStrictHostKeyChecking() {
852 			return strictHostKeyChecking;
853 		}
854 
855 		/**
856 		 * @return the real IP address or host name to connect to; never null.
857 		 */
858 		public String getHostName() {
859 			return hostName;
860 		}
861 
862 		/**
863 		 * @return the real port number to connect to; never 0.
864 		 */
865 		public int getPort() {
866 			return port;
867 		}
868 
869 		/**
870 		 * @return path of the private key file to use for authentication; null
871 		 *         if the caller should use default authentication strategies.
872 		 */
873 		public File getIdentityFile() {
874 			return identityFile;
875 		}
876 
877 		/**
878 		 * @return the real user name to connect as; never null.
879 		 */
880 		public String getUser() {
881 			return user;
882 		}
883 
884 		/**
885 		 * @return the preferred authentication methods, separated by commas if
886 		 *         more than one authentication method is preferred.
887 		 */
888 		public String getPreferredAuthentications() {
889 			return preferredAuthentications;
890 		}
891 
892 		/**
893 		 * @return true if batch (non-interactive) mode is preferred for this
894 		 *         host connection.
895 		 */
896 		public boolean isBatchMode() {
897 			return batchMode != null && batchMode.booleanValue();
898 		}
899 
900 		/**
901 		 * @return the number of tries (one per second) to connect before
902 		 *         exiting. The argument must be an integer. This may be useful
903 		 *         in scripts if the connection sometimes fails. The default is
904 		 *         1.
905 		 * @since 3.4
906 		 */
907 		public int getConnectionAttempts() {
908 			return connectionAttempts;
909 		}
910 
911 
912 		private void complete(String initialHostName, File homeDir) {
913 			// Try to set values from the options.
914 			hostName = config.getHostname();
915 			user = config.getUser();
916 			port = config.getPort();
917 			connectionAttempts = positive(
918 					config.getValue("ConnectionAttempts")); //$NON-NLS-1$
919 			strictHostKeyChecking = config.getValue("StrictHostKeyChecking"); //$NON-NLS-1$
920 			String value = config.getValue("BatchMode"); //$NON-NLS-1$
921 			if (value != null) {
922 				batchMode = yesno(value);
923 			}
924 			value = config.getValue("PreferredAuthentications"); //$NON-NLS-1$
925 			if (value != null) {
926 				preferredAuthentications = nows(value);
927 			}
928 			// Fill in defaults if still not set
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"); //$NON-NLS-1$
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 	 * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config}
965 	 * for the given host name. Should be called only by Jsch and tests.
966 	 *
967 	 * @param hostName
968 	 *            to get the config for
969 	 * @return the configuration for the host
970 	 * @since 4.9
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 	 * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms some
987 	 * values from the config file into the format Jsch 0.1.54 expects. This is
988 	 * a work-around for bugs in Jsch.
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) //$NON-NLS-1$
1019 						|| "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$
1020 					// These values are in seconds. Jsch 0.1.54 passes them on
1021 					// as is to java.net.Socket.setSoTimeout(), which expects
1022 					// milliseconds. So convert here to milliseconds...
1023 					try {
1024 						int timeout = Integer.parseInt(result);
1025 						result = Long
1026 								.toString(TimeUnit.SECONDS.toMillis(timeout));
1027 					} catch (NumberFormatException e) {
1028 						// Ignore
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 }