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
89   * {@link org.eclipse.jgit.transport.SshSessionFactory} via
90   * {@link org.eclipse.jgit.transport.OpenSshConfig.Host} objects returned by
91   * {@link #lookup(String)}, and implements a fully conforming
92   * {@link com.jcraft.jsch.ConfigRepository} providing
93   * {@link com.jcraft.jsch.ConfigRepository.Config}s via
94   * {@link #getConfig(String)}.
95   * </p>
96   * <p>
97   * Limitations compared to the full OpenSSH 7.5 parser:
98   * </p>
99   * <ul>
100  * <li>This parser does not handle Match or Include keywords.
101  * <li>This parser does not do host name canonicalization (Jsch ignores it
102  * anyway).
103  * </ul>
104  * <p>
105  * Note that OpenSSH's readconf.c is a validating parser; Jsch's
106  * ConfigRepository OTOH treats all option values as plain strings, so any
107  * validation must happen in Jsch outside of the parser. Thus this parser does
108  * not validate option values, except for a few options when constructing a
109  * {@link org.eclipse.jgit.transport.OpenSshConfig.Host} object.
110  * </p>
111  * <p>
112  * This config does %-substitutions for the following tokens:
113  * </p>
114  * <ul>
115  * <li>%% - single %
116  * <li>%C - short-hand for %l%h%p%r. See %p and %r below; the replacement may be
117  * done partially only and may leave %p or %r or both unreplaced.
118  * <li>%d - home directory path
119  * <li>%h - remote host name
120  * <li>%L - local host name without domain
121  * <li>%l - FQDN of the local host
122  * <li>%n - host name as specified in {@link #lookup(String)}
123  * <li>%p - port number; replaced only if set in the config
124  * <li>%r - remote user name; replaced only if set in the config
125  * <li>%u - local user name
126  * </ul>
127  * <p>
128  * If the config doesn't set the port or the remote user name, %p and %r remain
129  * un-substituted. It's the caller's responsibility to replace them with values
130  * obtained from the connection URI. %i is not handled; Java has no concept of a
131  * "user ID".
132  * </p>
133  */
134 public class OpenSshConfig implements ConfigRepository {
135 
136 	/** IANA assigned port number for SSH. */
137 	static final int SSH_PORT = 22;
138 
139 	/**
140 	 * Obtain the user's configuration data.
141 	 * <p>
142 	 * The configuration file is always returned to the caller, even if no file
143 	 * exists in the user's home directory at the time the call was made. Lookup
144 	 * requests are cached and are automatically updated if the user modifies
145 	 * the configuration file since the last time it was cached.
146 	 *
147 	 * @param fs
148 	 *            the file system abstraction which will be necessary to
149 	 *            perform certain file system operations.
150 	 * @return a caching reader of the user's configuration file.
151 	 */
152 	public static OpenSshConfig get(FS fs) {
153 		File home = fs.userHome();
154 		if (home == null)
155 			home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
156 
157 		final File config = new File(new File(home, ".ssh"), Constants.CONFIG); //$NON-NLS-1$
158 		final OpenSshConfig osc = new OpenSshConfig(home, config);
159 		osc.refresh();
160 		return osc;
161 	}
162 
163 	/** The user's home directory, as key files may be relative to here. */
164 	private final File home;
165 
166 	/** The .ssh/config file we read and monitor for updates. */
167 	private final File configFile;
168 
169 	/** Modification time of {@link #configFile} when it was last loaded. */
170 	private long lastModified;
171 
172 	/**
173 	 * Encapsulates entries read out of the configuration file, and
174 	 * {@link Host}s created from that.
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 	/** State read from the config file, plus {@link Host}s created from it. */
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 	 * Locate the configuration for a specific host request.
198 	 *
199 	 * @param hostName
200 	 *            the name the user has supplied to the SSH tool. This may be a
201 	 *            real host name, or it may just be a "Host" block in the
202 	 *            configuration file.
203 	 * @return r configuration for the requested name. Never null.
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 		// Initialize with default entries at the top of the file, before the
213 		// first Host block.
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 				// Ignore -- we'll set and return an empty state
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 		// The man page doesn't say so, but the OpenSSH parser (readconf.c)
250 		// starts out in active mode and thus always applies any lines that
251 		// occur before the first host block. We gather those options in a
252 		// HostEntry for DEFAULT_NAME.
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("#")) { //$NON-NLS-1$
260 				continue;
261 			}
262 			String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
263 			// Although the ssh-config man page doesn't say so, the OpenSSH
264 			// parser does allow quoted keywords.
265 			String keyword = dequote(parts[0].trim());
266 			// man 5 ssh-config says lines had the format "keyword arguments",
267 			// with no indication that arguments were optional. However, let's
268 			// not crap out on missing arguments. See bug 444319.
269 			String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$
270 
271 			if (StringUtils.equalsIgnoreCase("Host", keyword)) { //$NON-NLS-1$
272 				current.clear();
273 				for (String name : HostEntry.parseList(argValue)) {
274 					if (name == null || name.isEmpty()) {
275 						// null should not occur, but better be safe than sorry.
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 				// We received an option outside of a Host block. We
290 				// don't know who this should match against, so skip.
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("!")) { //$NON-NLS-1$
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 			// Not a pattern but a full host name
332 			return pattern.equals(name);
333 		}
334 	}
335 
336 	private static String dequote(final String value) {
337 		if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$
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)) //$NON-NLS-1$
354 			return Boolean.TRUE;
355 		return Boolean.FALSE;
356 	}
357 
358 	private static File toFile(String path, File home) {
359 		if (path.startsWith("~/")) { //$NON-NLS-1$
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 				// Ignore
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 		 * "Host name" of the HostEntry for the default options before the first
394 		 * host block in a config file.
395 		 */
396 		public static final String DEFAULT_NAME = ""; //$NON-NLS-1$
397 
398 		// See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
399 		// to ssh-config keys.
400 		private static final Map<String, String> KEY_MAP = new HashMap<>();
401 
402 		static {
403 			KEY_MAP.put("kex", "KexAlgorithms"); //$NON-NLS-1$//$NON-NLS-2$
404 			KEY_MAP.put("server_host_key", "HostKeyAlgorithms"); //$NON-NLS-1$ //$NON-NLS-2$
405 			KEY_MAP.put("cipher.c2s", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
406 			KEY_MAP.put("cipher.s2c", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
407 			KEY_MAP.put("mac.c2s", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
408 			KEY_MAP.put("mac.s2c", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
409 			KEY_MAP.put("compression.s2c", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
410 			KEY_MAP.put("compression.c2s", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
411 			KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$
412 			KEY_MAP.put("MaxAuthTries", "NumberOfPasswordPrompts"); //$NON-NLS-1$ //$NON-NLS-2$
413 		}
414 
415 		/**
416 		 * Keys that can be specified multiple times, building up a list. (I.e.,
417 		 * those are the keys that do not follow the general rule of "first
418 		 * occurrence wins".)
419 		 */
420 		private static final Set<String> MULTI_KEYS = new HashSet<>();
421 
422 		static {
423 			MULTI_KEYS.add("CERTIFICATEFILE"); //$NON-NLS-1$
424 			MULTI_KEYS.add("IDENTITYFILE"); //$NON-NLS-1$
425 			MULTI_KEYS.add("LOCALFORWARD"); //$NON-NLS-1$
426 			MULTI_KEYS.add("REMOTEFORWARD"); //$NON-NLS-1$
427 			MULTI_KEYS.add("SENDENV"); //$NON-NLS-1$
428 		}
429 
430 		/**
431 		 * Keys that take a whitespace-separated list of elements as argument.
432 		 * Because the dequote-handling is different, we must handle those in
433 		 * the parser. There are a few other keys that take comma-separated
434 		 * lists as arguments, but for the parser those are single arguments
435 		 * that must be quoted if they contain whitespace, and taking them apart
436 		 * is the responsibility of the user of those keys.
437 		 */
438 		private static final Set<String> LIST_KEYS = new HashSet<>();
439 
440 		static {
441 			LIST_KEYS.add("CANONICALDOMAINS"); //$NON-NLS-1$
442 			LIST_KEYS.add("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
443 			LIST_KEYS.add("SENDENV"); //$NON-NLS-1$
444 			LIST_KEYS.add("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
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"); //$NON-NLS-1$
456 		}
457 
458 		@Override
459 		public String getUser() {
460 			return getValue("USER"); //$NON-NLS-1$
461 		}
462 
463 		@Override
464 		public int getPort() {
465 			return positive(getValue("PORT")); //$NON-NLS-1$
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 				// Also check the list and multi options. Modern OpenSSH treats
481 				// UserKnownHostsFile and GlobalKnownHostsFile as list-valued,
482 				// and so does this parser. Jsch 0.1.54 in general doesn't know
483 				// about list-valued options (it _does_ know multi-valued
484 				// options, though), and will ask for a single value for such
485 				// options.
486 				//
487 				// Let's be lenient and return at least the first value from
488 				// a list-valued or multi-valued key for which Jsch asks for a
489 				// single value.
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 			// See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() for this
505 			// special case.
506 			if (key.equals("compression.s2c") //$NON-NLS-1$
507 					|| key.equals("compression.c2s")) { //$NON-NLS-1$
508 				String foo = findValue(key);
509 				if (foo == null || foo.equals("no")) { //$NON-NLS-1$
510 					return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$
511 				}
512 				return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$
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 				// Can occur only on a missing argument: ignore.
556 				return;
557 			}
558 			String k = key.toUpperCase(Locale.ROOT);
559 			// Check multi-valued keys first; because of the replacement
560 			// strategy, they must take precedence over list-valued keys
561 			// which always follow the "first occurrence wins" strategy.
562 			//
563 			// Note that SendEnv is a multi-valued list-valued key. (It's
564 			// rather immaterial for JGit, though.)
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 		 * Splits the argument into a list of whitespace-separated elements.
592 		 * Elements containing whitespace must be quoted and will be de-quoted.
593 		 *
594 		 * @param argument
595 		 *            argument part of the configuration line as read from the
596 		 *            config file
597 		 * @return a {@link List} of elements, possibly empty and possibly
598 		 *         containing empty elements
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 				// Skip whitespace
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 						// No closing double quote: skip
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 				// Can occur if we could not read the config file
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('%'), "%"); //$NON-NLS-1$
681 				replacements.put(Character.valueOf('d'), home.getPath());
682 				// Needs special treatment...
683 				String host = getValue("HOSTNAME"); //$NON-NLS-1$
684 				replacements.put(Character.valueOf('h'), originalHostName);
685 				if (host != null && host.indexOf('%') >= 0) {
686 					host = substitute(host, "h"); //$NON-NLS-1$
687 					options.put("HOSTNAME", host); //$NON-NLS-1$
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")); //$NON-NLS-1$
701 				replacements.put(Character.valueOf('r'), getValue("USER")); //$NON-NLS-1$
702 				replacements.put(Character.valueOf('u'), userName());
703 				replacements.put(Character.valueOf('C'),
704 						substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
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"); //$NON-NLS-1$
759 				if (values != null) {
760 					values = substitute(values, "dhlru", r); //$NON-NLS-1$
761 					values = replaceTilde(values, home);
762 					multiOptions.put("IDENTITYFILE", values); //$NON-NLS-1$
763 				}
764 				values = multiOptions.get("CERTIFICATEFILE"); //$NON-NLS-1$
765 				if (values != null) {
766 					values = substitute(values, "dhlru", r); //$NON-NLS-1$
767 					values = replaceTilde(values, home);
768 					multiOptions.put("CERTIFICATEFILE", values); //$NON-NLS-1$
769 				}
770 			}
771 			if (listOptions != null) {
772 				List<String> values = listOptions.get("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
773 				if (values != null) {
774 					values = replaceTilde(values, home);
775 					listOptions.put("GLOBALKNOWNHOSTSFILE", values); //$NON-NLS-1$
776 				}
777 				values = listOptions.get("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
778 				if (values != null) {
779 					values = replaceTilde(values, home);
780 					listOptions.put("USERKNOWNHOSTSFILE", values); //$NON-NLS-1$
781 				}
782 			}
783 			if (options != null) {
784 				// HOSTNAME already done in Replacer constructor
785 				String value = options.get("IDENTITYAGENT"); //$NON-NLS-1$
786 				if (value != null) {
787 					value = r.substitute(value, "dhlru"); //$NON-NLS-1$
788 					value = toFile(value, home).getPath();
789 					options.put("IDENTITYAGENT", value); //$NON-NLS-1$
790 				}
791 			}
792 			// Match is not implemented and would need to be done elsewhere
793 			// anyway. ControlPath, LocalCommand, ProxyCommand, and
794 			// RemoteCommand are not used by Jsch.
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 	 * Configuration of one "Host" block in the configuration file.
807 	 * <p>
808 	 * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
809 	 * properties may not be populated. The properties which are not populated
810 	 * should be defaulted by the caller.
811 	 * <p>
812 	 * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
813 	 * entries which appear later in the configuration file will have been
814 	 * already merged into this block.
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 		 * Creates a new uninitialized {@link Host}.
837 		 */
838 		public Host() {
839 			// For API backwards compatibility with pre-4.9 JGit
840 		}
841 
842 		Host(Config config, String hostName, File homeDir) {
843 			this.config = config;
844 			complete(hostName, homeDir);
845 		}
846 
847 		/**
848 		 * @return the value StrictHostKeyChecking property, the valid values
849 		 *         are "yes" (unknown hosts are not accepted), "no" (unknown
850 		 *         hosts are always accepted), and "ask" (user should be asked
851 		 *         before accepting the host)
852 		 */
853 		public String getStrictHostKeyChecking() {
854 			return strictHostKeyChecking;
855 		}
856 
857 		/**
858 		 * @return the real IP address or host name to connect to; never null.
859 		 */
860 		public String getHostName() {
861 			return hostName;
862 		}
863 
864 		/**
865 		 * @return the real port number to connect to; never 0.
866 		 */
867 		public int getPort() {
868 			return port;
869 		}
870 
871 		/**
872 		 * @return path of the private key file to use for authentication; null
873 		 *         if the caller should use default authentication strategies.
874 		 */
875 		public File getIdentityFile() {
876 			return identityFile;
877 		}
878 
879 		/**
880 		 * @return the real user name to connect as; never null.
881 		 */
882 		public String getUser() {
883 			return user;
884 		}
885 
886 		/**
887 		 * @return the preferred authentication methods, separated by commas if
888 		 *         more than one authentication method is preferred.
889 		 */
890 		public String getPreferredAuthentications() {
891 			return preferredAuthentications;
892 		}
893 
894 		/**
895 		 * @return true if batch (non-interactive) mode is preferred for this
896 		 *         host connection.
897 		 */
898 		public boolean isBatchMode() {
899 			return batchMode != null && batchMode.booleanValue();
900 		}
901 
902 		/**
903 		 * @return the number of tries (one per second) to connect before
904 		 *         exiting. The argument must be an integer. This may be useful
905 		 *         in scripts if the connection sometimes fails. The default is
906 		 *         1.
907 		 * @since 3.4
908 		 */
909 		public int getConnectionAttempts() {
910 			return connectionAttempts;
911 		}
912 
913 
914 		private void complete(String initialHostName, File homeDir) {
915 			// Try to set values from the options.
916 			hostName = config.getHostname();
917 			user = config.getUser();
918 			port = config.getPort();
919 			connectionAttempts = positive(
920 					config.getValue("ConnectionAttempts")); //$NON-NLS-1$
921 			strictHostKeyChecking = config.getValue("StrictHostKeyChecking"); //$NON-NLS-1$
922 			String value = config.getValue("BatchMode"); //$NON-NLS-1$
923 			if (value != null) {
924 				batchMode = yesno(value);
925 			}
926 			value = config.getValue("PreferredAuthentications"); //$NON-NLS-1$
927 			if (value != null) {
928 				preferredAuthentications = nows(value);
929 			}
930 			// Fill in defaults if still not set
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"); //$NON-NLS-1$
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 	 * {@inheritDoc}
967 	 * <p>
968 	 * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config}
969 	 * for the given host name. Should be called only by Jsch and tests.
970 	 *
971 	 * @since 4.9
972 	 */
973 	@Override
974 	public Config getConfig(String hostName) {
975 		Host host = lookup(hostName);
976 		return new JschBugFixingConfig(host.getConfig());
977 	}
978 
979 	/** {@inheritDoc} */
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 	 * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms some
989 	 * values from the config file into the format Jsch 0.1.54 expects. This is
990 	 * a work-around for bugs in Jsch.
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) //$NON-NLS-1$
1021 						|| "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$
1022 					// These values are in seconds. Jsch 0.1.54 passes them on
1023 					// as is to java.net.Socket.setSoTimeout(), which expects
1024 					// milliseconds. So convert here to milliseconds...
1025 					try {
1026 						int timeout = Integer.parseInt(result);
1027 						result = Long
1028 								.toString(TimeUnit.SECONDS.toMillis(timeout));
1029 					} catch (NumberFormatException e) {
1030 						// Ignore
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 }