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  
63  import org.eclipse.jgit.errors.InvalidPatternException;
64  import org.eclipse.jgit.fnmatch.FileNameMatcher;
65  import org.eclipse.jgit.lib.Constants;
66  import org.eclipse.jgit.util.FS;
67  import org.eclipse.jgit.util.StringUtils;
68  import org.eclipse.jgit.util.SystemReader;
69  
70  import com.jcraft.jsch.ConfigRepository;
71  
72  /**
73   * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file.
74   * <p>
75   * JSch does have its own config file parser
76   * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a
77   * number of problems:
78   * <ul>
79   * <li>it splits lines of the format "keyword = value" wrongly: you'd end up
80   * with the value "= value".
81   * <li>its "Host" keyword is not case insensitive.
82   * <li>it doesn't handle quoted values.
83   * <li>JSch's OpenSSHConfig doesn't monitor for config file changes.
84   * </ul>
85   * <p>
86   * Therefore implement our own parser to read an OpenSSH configuration file. It
87   * makes the critical options available to
88   * {@link org.eclipse.jgit.transport.SshSessionFactory} via
89   * {@link org.eclipse.jgit.transport.OpenSshConfig.Host} objects returned by
90   * {@link #lookup(String)}, and implements a fully conforming
91   * {@link com.jcraft.jsch.ConfigRepository} providing
92   * {@link com.jcraft.jsch.ConfigRepository.Config}s via
93   * {@link #getConfig(String)}.
94   * </p>
95   * <p>
96   * Limitations compared to the full OpenSSH 7.5 parser:
97   * </p>
98   * <ul>
99   * <li>This parser does not handle Match or Include keywords.
100  * <li>This parser does not do host name canonicalization (Jsch ignores it
101  * anyway).
102  * </ul>
103  * <p>
104  * Note that OpenSSH's readconf.c is a validating parser; Jsch's
105  * ConfigRepository OTOH treats all option values as plain strings, so any
106  * validation must happen in Jsch outside of the parser. Thus this parser does
107  * not validate option values, except for a few options when constructing a
108  * {@link org.eclipse.jgit.transport.OpenSshConfig.Host} object.
109  * </p>
110  * <p>
111  * This config does %-substitutions for the following tokens:
112  * </p>
113  * <ul>
114  * <li>%% - single %
115  * <li>%C - short-hand for %l%h%p%r. See %p and %r below; the replacement may be
116  * done partially only and may leave %p or %r or both unreplaced.
117  * <li>%d - home directory path
118  * <li>%h - remote host name
119  * <li>%L - local host name without domain
120  * <li>%l - FQDN of the local host
121  * <li>%n - host name as specified in {@link #lookup(String)}
122  * <li>%p - port number; replaced only if set in the config
123  * <li>%r - remote user name; replaced only if set in the config
124  * <li>%u - local user name
125  * </ul>
126  * <p>
127  * If the config doesn't set the port or the remote user name, %p and %r remain
128  * un-substituted. It's the caller's responsibility to replace them with values
129  * obtained from the connection URI. %i is not handled; Java has no concept of a
130  * "user ID".
131  * </p>
132  */
133 public class OpenSshConfig implements ConfigRepository {
134 
135 	/** IANA assigned port number for SSH. */
136 	static final int SSH_PORT = 22;
137 
138 	/**
139 	 * Obtain the user's configuration data.
140 	 * <p>
141 	 * The configuration file is always returned to the caller, even if no file
142 	 * exists in the user's home directory at the time the call was made. Lookup
143 	 * requests are cached and are automatically updated if the user modifies
144 	 * the configuration file since the last time it was cached.
145 	 *
146 	 * @param fs
147 	 *            the file system abstraction which will be necessary to
148 	 *            perform certain file system operations.
149 	 * @return a caching reader of the user's configuration file.
150 	 */
151 	public static OpenSshConfig get(FS fs) {
152 		File home = fs.userHome();
153 		if (home == null)
154 			home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
155 
156 		final File config = new File(new File(home, ".ssh"), Constants.CONFIG); //$NON-NLS-1$
157 		final OpenSshConfig osc = new OpenSshConfig(home, config);
158 		osc.refresh();
159 		return osc;
160 	}
161 
162 	/** The user's home directory, as key files may be relative to here. */
163 	private final File home;
164 
165 	/** The .ssh/config file we read and monitor for updates. */
166 	private final File configFile;
167 
168 	/** Modification time of {@link #configFile} when it was last loaded. */
169 	private long lastModified;
170 
171 	/**
172 	 * Encapsulates entries read out of the configuration file, and
173 	 * {@link Host}s created from that.
174 	 */
175 	private static class State {
176 		Map<String, HostEntry> entries = new LinkedHashMap<>();
177 		Map<String, Host> hosts = new HashMap<>();
178 
179 		@Override
180 		@SuppressWarnings("nls")
181 		public String toString() {
182 			return "State [entries=" + entries + ", hosts=" + hosts + "]";
183 		}
184 	}
185 
186 	/** State read from the config file, plus {@link Host}s created from it. */
187 	private State state;
188 
189 	OpenSshConfig(File h, File cfg) {
190 		home = h;
191 		configFile = cfg;
192 		state = new State();
193 	}
194 
195 	/**
196 	 * Locate the configuration for a specific host request.
197 	 *
198 	 * @param hostName
199 	 *            the name the user has supplied to the SSH tool. This may be a
200 	 *            real host name, or it may just be a "Host" block in the
201 	 *            configuration file.
202 	 * @return r configuration for the requested name. Never null.
203 	 */
204 	public Host lookup(String hostName) {
205 		final State cache = refresh();
206 		Host h = cache.hosts.get(hostName);
207 		if (h != null) {
208 			return h;
209 		}
210 		HostEntry fullConfig = new HostEntry();
211 		// Initialize with default entries at the top of the file, before the
212 		// first Host block.
213 		fullConfig.merge(cache.entries.get(HostEntry.DEFAULT_NAME));
214 		for (Map.Entry<String, HostEntry> e : cache.entries.entrySet()) {
215 			String key = e.getKey();
216 			if (isHostMatch(key, hostName)) {
217 				fullConfig.merge(e.getValue());
218 			}
219 		}
220 		fullConfig.substitute(hostName, home);
221 		h = new Host(fullConfig, hostName, home);
222 		cache.hosts.put(hostName, h);
223 		return h;
224 	}
225 
226 	private synchronized State refresh() {
227 		final long mtime = configFile.lastModified();
228 		if (mtime != lastModified) {
229 			State newState = new State();
230 			try (FileInputStream in = new FileInputStream(configFile)) {
231 				newState.entries = parse(in);
232 			} catch (IOException none) {
233 				// Ignore -- we'll set and return an empty state
234 			}
235 			lastModified = mtime;
236 			state = newState;
237 		}
238 		return state;
239 	}
240 
241 	private Map<String, HostEntry> parse(InputStream in)
242 			throws IOException {
243 		final Map<String, HostEntry> m = new LinkedHashMap<>();
244 		final BufferedReader br = new BufferedReader(new InputStreamReader(in));
245 		final List<HostEntry> current = new ArrayList<>(4);
246 		String line;
247 
248 		// The man page doesn't say so, but the OpenSSH parser (readconf.c)
249 		// starts out in active mode and thus always applies any lines that
250 		// occur before the first host block. We gather those options in a
251 		// HostEntry for DEFAULT_NAME.
252 		HostEntry defaults = new HostEntry();
253 		current.add(defaults);
254 		m.put(HostEntry.DEFAULT_NAME, defaults);
255 
256 		while ((line = br.readLine()) != null) {
257 			line = line.trim();
258 			if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$
259 				continue;
260 			}
261 			String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
262 			// Although the ssh-config man page doesn't say so, the OpenSSH
263 			// parser does allow quoted keywords.
264 			String keyword = dequote(parts[0].trim());
265 			// man 5 ssh-config says lines had the format "keyword arguments",
266 			// with no indication that arguments were optional. However, let's
267 			// not crap out on missing arguments. See bug 444319.
268 			String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$
269 
270 			if (StringUtils.equalsIgnoreCase("Host", keyword)) { //$NON-NLS-1$
271 				current.clear();
272 				for (String name : HostEntry.parseList(argValue)) {
273 					if (name == null || name.isEmpty()) {
274 						// null should not occur, but better be safe than sorry.
275 						continue;
276 					}
277 					HostEntry c = m.get(name);
278 					if (c == null) {
279 						c = new HostEntry();
280 						m.put(name, c);
281 					}
282 					current.add(c);
283 				}
284 				continue;
285 			}
286 
287 			if (current.isEmpty()) {
288 				// We received an option outside of a Host block. We
289 				// don't know who this should match against, so skip.
290 				continue;
291 			}
292 
293 			if (HostEntry.isListKey(keyword)) {
294 				List<String> args = HostEntry.parseList(argValue);
295 				for (HostEntry entry : current) {
296 					entry.setValue(keyword, args);
297 				}
298 			} else if (!argValue.isEmpty()) {
299 				argValue = dequote(argValue);
300 				for (HostEntry entry : current) {
301 					entry.setValue(keyword, argValue);
302 				}
303 			}
304 		}
305 
306 		return m;
307 	}
308 
309 	private static boolean isHostMatch(final String pattern,
310 			final String name) {
311 		if (pattern.startsWith("!")) { //$NON-NLS-1$
312 			return !patternMatchesHost(pattern.substring(1), name);
313 		} else {
314 			return patternMatchesHost(pattern, name);
315 		}
316 	}
317 
318 	private static boolean patternMatchesHost(final String pattern,
319 			final String name) {
320 		if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
321 			final FileNameMatcher fn;
322 			try {
323 				fn = new FileNameMatcher(pattern, null);
324 			} catch (InvalidPatternException e) {
325 				return false;
326 			}
327 			fn.append(name);
328 			return fn.isMatch();
329 		} else {
330 			// Not a pattern but a full host name
331 			return pattern.equals(name);
332 		}
333 	}
334 
335 	private static String dequote(String value) {
336 		if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$
337 				&& value.length() > 1)
338 			return value.substring(1, value.length() - 1);
339 		return value;
340 	}
341 
342 	private static String nows(String value) {
343 		final StringBuilder b = new StringBuilder();
344 		for (int i = 0; i < value.length(); i++) {
345 			if (!Character.isSpaceChar(value.charAt(i)))
346 				b.append(value.charAt(i));
347 		}
348 		return b.toString();
349 	}
350 
351 	private static Boolean yesno(String value) {
352 		if (StringUtils.equalsIgnoreCase("yes", value)) //$NON-NLS-1$
353 			return Boolean.TRUE;
354 		return Boolean.FALSE;
355 	}
356 
357 	private static File toFile(String path, File home) {
358 		if (path.startsWith("~/")) { //$NON-NLS-1$
359 			return new File(home, path.substring(2));
360 		}
361 		File ret = new File(path);
362 		if (ret.isAbsolute()) {
363 			return ret;
364 		}
365 		return new File(home, path);
366 	}
367 
368 	private static int positive(String value) {
369 		if (value != null) {
370 			try {
371 				return Integer.parseUnsignedInt(value);
372 			} catch (NumberFormatException e) {
373 				// Ignore
374 			}
375 		}
376 		return -1;
377 	}
378 
379 	static String userName() {
380 		return AccessController.doPrivileged(new PrivilegedAction<String>() {
381 			@Override
382 			public String run() {
383 				return SystemReader.getInstance()
384 						.getProperty(Constants.OS_USER_NAME_KEY);
385 			}
386 		});
387 	}
388 
389 	private static class HostEntry implements ConfigRepository.Config {
390 
391 		/**
392 		 * "Host name" of the HostEntry for the default options before the first
393 		 * host block in a config file.
394 		 */
395 		public static final String DEFAULT_NAME = ""; //$NON-NLS-1$
396 
397 		// See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
398 		// to ssh-config keys.
399 		private static final Map<String, String> KEY_MAP = new HashMap<>();
400 
401 		static {
402 			KEY_MAP.put("kex", "KexAlgorithms"); //$NON-NLS-1$//$NON-NLS-2$
403 			KEY_MAP.put("server_host_key", "HostKeyAlgorithms"); //$NON-NLS-1$ //$NON-NLS-2$
404 			KEY_MAP.put("cipher.c2s", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
405 			KEY_MAP.put("cipher.s2c", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
406 			KEY_MAP.put("mac.c2s", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
407 			KEY_MAP.put("mac.s2c", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
408 			KEY_MAP.put("compression.s2c", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
409 			KEY_MAP.put("compression.c2s", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
410 			KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$
411 			KEY_MAP.put("MaxAuthTries", "NumberOfPasswordPrompts"); //$NON-NLS-1$ //$NON-NLS-2$
412 		}
413 
414 		/**
415 		 * Keys that can be specified multiple times, building up a list. (I.e.,
416 		 * those are the keys that do not follow the general rule of "first
417 		 * occurrence wins".)
418 		 */
419 		private static final Set<String> MULTI_KEYS = new HashSet<>();
420 
421 		static {
422 			MULTI_KEYS.add("CERTIFICATEFILE"); //$NON-NLS-1$
423 			MULTI_KEYS.add("IDENTITYFILE"); //$NON-NLS-1$
424 			MULTI_KEYS.add("LOCALFORWARD"); //$NON-NLS-1$
425 			MULTI_KEYS.add("REMOTEFORWARD"); //$NON-NLS-1$
426 			MULTI_KEYS.add("SENDENV"); //$NON-NLS-1$
427 		}
428 
429 		/**
430 		 * Keys that take a whitespace-separated list of elements as argument.
431 		 * Because the dequote-handling is different, we must handle those in
432 		 * the parser. There are a few other keys that take comma-separated
433 		 * lists as arguments, but for the parser those are single arguments
434 		 * that must be quoted if they contain whitespace, and taking them apart
435 		 * is the responsibility of the user of those keys.
436 		 */
437 		private static final Set<String> LIST_KEYS = new HashSet<>();
438 
439 		static {
440 			LIST_KEYS.add("CANONICALDOMAINS"); //$NON-NLS-1$
441 			LIST_KEYS.add("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
442 			LIST_KEYS.add("SENDENV"); //$NON-NLS-1$
443 			LIST_KEYS.add("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
444 		}
445 
446 		private Map<String, String> options;
447 
448 		private Map<String, List<String>> multiOptions;
449 
450 		private Map<String, List<String>> listOptions;
451 
452 		@Override
453 		public String getHostname() {
454 			return getValue("HOSTNAME"); //$NON-NLS-1$
455 		}
456 
457 		@Override
458 		public String getUser() {
459 			return getValue("USER"); //$NON-NLS-1$
460 		}
461 
462 		@Override
463 		public int getPort() {
464 			return positive(getValue("PORT")); //$NON-NLS-1$
465 		}
466 
467 		private static String mapKey(String key) {
468 			String k = KEY_MAP.get(key);
469 			if (k == null) {
470 				k = key;
471 			}
472 			return k.toUpperCase(Locale.ROOT);
473 		}
474 
475 		private String findValue(String key) {
476 			String k = mapKey(key);
477 			String result = options != null ? options.get(k) : null;
478 			if (result == null) {
479 				// Also check the list and multi options. Modern OpenSSH treats
480 				// UserKnownHostsFile and GlobalKnownHostsFile as list-valued,
481 				// and so does this parser. Jsch 0.1.54 in general doesn't know
482 				// about list-valued options (it _does_ know multi-valued
483 				// options, though), and will ask for a single value for such
484 				// options.
485 				//
486 				// Let's be lenient and return at least the first value from
487 				// a list-valued or multi-valued key for which Jsch asks for a
488 				// single value.
489 				List<String> values = listOptions != null ? listOptions.get(k)
490 						: null;
491 				if (values == null) {
492 					values = multiOptions != null ? multiOptions.get(k) : null;
493 				}
494 				if (values != null && !values.isEmpty()) {
495 					result = values.get(0);
496 				}
497 			}
498 			return result;
499 		}
500 
501 		@Override
502 		public String getValue(String key) {
503 			// See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() for this
504 			// special case.
505 			if (key.equals("compression.s2c") //$NON-NLS-1$
506 					|| key.equals("compression.c2s")) { //$NON-NLS-1$
507 				String foo = findValue(key);
508 				if (foo == null || foo.equals("no")) { //$NON-NLS-1$
509 					return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$
510 				}
511 				return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$
512 			}
513 			return findValue(key);
514 		}
515 
516 		@Override
517 		public String[] getValues(String key) {
518 			String k = mapKey(key);
519 			List<String> values = listOptions != null ? listOptions.get(k)
520 					: null;
521 			if (values == null) {
522 				values = multiOptions != null ? multiOptions.get(k) : null;
523 			}
524 			if (values == null || values.isEmpty()) {
525 				return new String[0];
526 			}
527 			return values.toArray(new String[values.size()]);
528 		}
529 
530 		public void setValue(String key, String value) {
531 			String k = key.toUpperCase(Locale.ROOT);
532 			if (MULTI_KEYS.contains(k)) {
533 				if (multiOptions == null) {
534 					multiOptions = new HashMap<>();
535 				}
536 				List<String> values = multiOptions.get(k);
537 				if (values == null) {
538 					values = new ArrayList<>(4);
539 					multiOptions.put(k, values);
540 				}
541 				values.add(value);
542 			} else {
543 				if (options == null) {
544 					options = new HashMap<>();
545 				}
546 				if (!options.containsKey(k)) {
547 					options.put(k, value);
548 				}
549 			}
550 		}
551 
552 		public void setValue(String key, List<String> values) {
553 			if (values.isEmpty()) {
554 				// Can occur only on a missing argument: ignore.
555 				return;
556 			}
557 			String k = key.toUpperCase(Locale.ROOT);
558 			// Check multi-valued keys first; because of the replacement
559 			// strategy, they must take precedence over list-valued keys
560 			// which always follow the "first occurrence wins" strategy.
561 			//
562 			// Note that SendEnv is a multi-valued list-valued key. (It's
563 			// rather immaterial for JGit, though.)
564 			if (MULTI_KEYS.contains(k)) {
565 				if (multiOptions == null) {
566 					multiOptions = new HashMap<>(2 * MULTI_KEYS.size());
567 				}
568 				List<String> items = multiOptions.get(k);
569 				if (items == null) {
570 					items = new ArrayList<>(values);
571 					multiOptions.put(k, items);
572 				} else {
573 					items.addAll(values);
574 				}
575 			} else {
576 				if (listOptions == null) {
577 					listOptions = new HashMap<>(2 * LIST_KEYS.size());
578 				}
579 				if (!listOptions.containsKey(k)) {
580 					listOptions.put(k, values);
581 				}
582 			}
583 		}
584 
585 		public static boolean isListKey(String key) {
586 			return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT));
587 		}
588 
589 		/**
590 		 * Splits the argument into a list of whitespace-separated elements.
591 		 * Elements containing whitespace must be quoted and will be de-quoted.
592 		 *
593 		 * @param argument
594 		 *            argument part of the configuration line as read from the
595 		 *            config file
596 		 * @return a {@link List} of elements, possibly empty and possibly
597 		 *         containing empty elements
598 		 */
599 		public static List<String> parseList(String argument) {
600 			List<String> result = new ArrayList<>(4);
601 			int start = 0;
602 			int length = argument.length();
603 			while (start < length) {
604 				// Skip whitespace
605 				if (Character.isSpaceChar(argument.charAt(start))) {
606 					start++;
607 					continue;
608 				}
609 				if (argument.charAt(start) == '"') {
610 					int stop = argument.indexOf('"', ++start);
611 					if (stop < start) {
612 						// No closing double quote: skip
613 						break;
614 					}
615 					result.add(argument.substring(start, stop));
616 					start = stop + 1;
617 				} else {
618 					int stop = start + 1;
619 					while (stop < length
620 							&& !Character.isSpaceChar(argument.charAt(stop))) {
621 						stop++;
622 					}
623 					result.add(argument.substring(start, stop));
624 					start = stop + 1;
625 				}
626 			}
627 			return result;
628 		}
629 
630 		protected void merge(HostEntry entry) {
631 			if (entry == null) {
632 				// Can occur if we could not read the config file
633 				return;
634 			}
635 			if (entry.options != null) {
636 				if (options == null) {
637 					options = new HashMap<>();
638 				}
639 				for (Map.Entry<String, String> item : entry.options
640 						.entrySet()) {
641 					if (!options.containsKey(item.getKey())) {
642 						options.put(item.getKey(), item.getValue());
643 					}
644 				}
645 			}
646 			if (entry.listOptions != null) {
647 				if (listOptions == null) {
648 					listOptions = new HashMap<>(2 * LIST_KEYS.size());
649 				}
650 				for (Map.Entry<String, List<String>> item : entry.listOptions
651 						.entrySet()) {
652 					if (!listOptions.containsKey(item.getKey())) {
653 						listOptions.put(item.getKey(), item.getValue());
654 					}
655 				}
656 
657 			}
658 			if (entry.multiOptions != null) {
659 				if (multiOptions == null) {
660 					multiOptions = new HashMap<>(2 * MULTI_KEYS.size());
661 				}
662 				for (Map.Entry<String, List<String>> item : entry.multiOptions
663 						.entrySet()) {
664 					List<String> values = multiOptions.get(item.getKey());
665 					if (values == null) {
666 						values = new ArrayList<>(item.getValue());
667 						multiOptions.put(item.getKey(), values);
668 					} else {
669 						values.addAll(item.getValue());
670 					}
671 				}
672 			}
673 		}
674 
675 		private class Replacer {
676 			private final Map<Character, String> replacements = new HashMap<>();
677 
678 			public Replacer(String originalHostName, File home) {
679 				replacements.put(Character.valueOf('%'), "%"); //$NON-NLS-1$
680 				replacements.put(Character.valueOf('d'), home.getPath());
681 				// Needs special treatment...
682 				String host = getValue("HOSTNAME"); //$NON-NLS-1$
683 				replacements.put(Character.valueOf('h'), originalHostName);
684 				if (host != null && host.indexOf('%') >= 0) {
685 					host = substitute(host, "h"); //$NON-NLS-1$
686 					options.put("HOSTNAME", host); //$NON-NLS-1$
687 				}
688 				if (host != null) {
689 					replacements.put(Character.valueOf('h'), host);
690 				}
691 				String localhost = SystemReader.getInstance().getHostname();
692 				replacements.put(Character.valueOf('l'), localhost);
693 				int period = localhost.indexOf('.');
694 				if (period > 0) {
695 					localhost = localhost.substring(0, period);
696 				}
697 				replacements.put(Character.valueOf('L'), localhost);
698 				replacements.put(Character.valueOf('n'), originalHostName);
699 				replacements.put(Character.valueOf('p'), getValue("PORT")); //$NON-NLS-1$
700 				replacements.put(Character.valueOf('r'), getValue("USER")); //$NON-NLS-1$
701 				replacements.put(Character.valueOf('u'), userName());
702 				replacements.put(Character.valueOf('C'),
703 						substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
704 			}
705 
706 			public String substitute(String input, String allowed) {
707 				if (input == null || input.length() <= 1
708 						|| input.indexOf('%') < 0) {
709 					return input;
710 				}
711 				StringBuilder builder = new StringBuilder();
712 				int start = 0;
713 				int length = input.length();
714 				while (start < length) {
715 					int percent = input.indexOf('%', start);
716 					if (percent < 0 || percent + 1 >= length) {
717 						builder.append(input.substring(start));
718 						break;
719 					}
720 					String replacement = null;
721 					char ch = input.charAt(percent + 1);
722 					if (ch == '%' || allowed.indexOf(ch) >= 0) {
723 						replacement = replacements.get(Character.valueOf(ch));
724 					}
725 					if (replacement == null) {
726 						builder.append(input.substring(start, percent + 2));
727 					} else {
728 						builder.append(input.substring(start, percent))
729 								.append(replacement);
730 					}
731 					start = percent + 2;
732 				}
733 				return builder.toString();
734 			}
735 		}
736 
737 		private List<String> substitute(List<String> values, String allowed,
738 				Replacer r) {
739 			List<String> result = new ArrayList<>(values.size());
740 			for (String value : values) {
741 				result.add(r.substitute(value, allowed));
742 			}
743 			return result;
744 		}
745 
746 		private List<String> replaceTilde(List<String> values, File home) {
747 			List<String> result = new ArrayList<>(values.size());
748 			for (String value : values) {
749 				result.add(toFile(value, home).getPath());
750 			}
751 			return result;
752 		}
753 
754 		protected void substitute(String originalHostName, File home) {
755 			Replacer r = new Replacer(originalHostName, home);
756 			if (multiOptions != null) {
757 				List<String> values = multiOptions.get("IDENTITYFILE"); //$NON-NLS-1$
758 				if (values != null) {
759 					values = substitute(values, "dhlru", r); //$NON-NLS-1$
760 					values = replaceTilde(values, home);
761 					multiOptions.put("IDENTITYFILE", values); //$NON-NLS-1$
762 				}
763 				values = multiOptions.get("CERTIFICATEFILE"); //$NON-NLS-1$
764 				if (values != null) {
765 					values = substitute(values, "dhlru", r); //$NON-NLS-1$
766 					values = replaceTilde(values, home);
767 					multiOptions.put("CERTIFICATEFILE", values); //$NON-NLS-1$
768 				}
769 			}
770 			if (listOptions != null) {
771 				List<String> values = listOptions.get("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
772 				if (values != null) {
773 					values = replaceTilde(values, home);
774 					listOptions.put("GLOBALKNOWNHOSTSFILE", values); //$NON-NLS-1$
775 				}
776 				values = listOptions.get("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
777 				if (values != null) {
778 					values = replaceTilde(values, home);
779 					listOptions.put("USERKNOWNHOSTSFILE", values); //$NON-NLS-1$
780 				}
781 			}
782 			if (options != null) {
783 				// HOSTNAME already done in Replacer constructor
784 				String value = options.get("IDENTITYAGENT"); //$NON-NLS-1$
785 				if (value != null) {
786 					value = r.substitute(value, "dhlru"); //$NON-NLS-1$
787 					value = toFile(value, home).getPath();
788 					options.put("IDENTITYAGENT", value); //$NON-NLS-1$
789 				}
790 			}
791 			// Match is not implemented and would need to be done elsewhere
792 			// anyway. ControlPath, LocalCommand, ProxyCommand, and
793 			// RemoteCommand are not used by Jsch.
794 		}
795 
796 		@Override
797 		@SuppressWarnings("nls")
798 		public String toString() {
799 			return "HostEntry [options=" + options + ", multiOptions="
800 					+ multiOptions + ", listOptions=" + listOptions + "]";
801 		}
802 	}
803 
804 	/**
805 	 * Configuration of one "Host" block in the configuration file.
806 	 * <p>
807 	 * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
808 	 * properties may not be populated. The properties which are not populated
809 	 * should be defaulted by the caller.
810 	 * <p>
811 	 * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
812 	 * entries which appear later in the configuration file will have been
813 	 * already merged into this block.
814 	 */
815 	public static class Host {
816 		String hostName;
817 
818 		int port;
819 
820 		File identityFile;
821 
822 		String user;
823 
824 		String preferredAuthentications;
825 
826 		Boolean batchMode;
827 
828 		String strictHostKeyChecking;
829 
830 		int connectionAttempts;
831 
832 		private Config config;
833 
834 		/**
835 		 * Creates a new uninitialized {@link Host}.
836 		 */
837 		public Host() {
838 			// For API backwards compatibility with pre-4.9 JGit
839 		}
840 
841 		Host(Config config, String hostName, File homeDir) {
842 			this.config = config;
843 			complete(hostName, homeDir);
844 		}
845 
846 		/**
847 		 * @return the value StrictHostKeyChecking property, the valid values
848 		 *         are "yes" (unknown hosts are not accepted), "no" (unknown
849 		 *         hosts are always accepted), and "ask" (user should be asked
850 		 *         before accepting the host)
851 		 */
852 		public String getStrictHostKeyChecking() {
853 			return strictHostKeyChecking;
854 		}
855 
856 		/**
857 		 * @return the real IP address or host name to connect to; never null.
858 		 */
859 		public String getHostName() {
860 			return hostName;
861 		}
862 
863 		/**
864 		 * @return the real port number to connect to; never 0.
865 		 */
866 		public int getPort() {
867 			return port;
868 		}
869 
870 		/**
871 		 * @return path of the private key file to use for authentication; null
872 		 *         if the caller should use default authentication strategies.
873 		 */
874 		public File getIdentityFile() {
875 			return identityFile;
876 		}
877 
878 		/**
879 		 * @return the real user name to connect as; never null.
880 		 */
881 		public String getUser() {
882 			return user;
883 		}
884 
885 		/**
886 		 * @return the preferred authentication methods, separated by commas if
887 		 *         more than one authentication method is preferred.
888 		 */
889 		public String getPreferredAuthentications() {
890 			return preferredAuthentications;
891 		}
892 
893 		/**
894 		 * @return true if batch (non-interactive) mode is preferred for this
895 		 *         host connection.
896 		 */
897 		public boolean isBatchMode() {
898 			return batchMode != null && batchMode.booleanValue();
899 		}
900 
901 		/**
902 		 * @return the number of tries (one per second) to connect before
903 		 *         exiting. The argument must be an integer. This may be useful
904 		 *         in scripts if the connection sometimes fails. The default is
905 		 *         1.
906 		 * @since 3.4
907 		 */
908 		public int getConnectionAttempts() {
909 			return connectionAttempts;
910 		}
911 
912 
913 		private void complete(String initialHostName, File homeDir) {
914 			// Try to set values from the options.
915 			hostName = config.getHostname();
916 			user = config.getUser();
917 			port = config.getPort();
918 			connectionAttempts = positive(
919 					config.getValue("ConnectionAttempts")); //$NON-NLS-1$
920 			strictHostKeyChecking = config.getValue("StrictHostKeyChecking"); //$NON-NLS-1$
921 			String value = config.getValue("BatchMode"); //$NON-NLS-1$
922 			if (value != null) {
923 				batchMode = yesno(value);
924 			}
925 			value = config.getValue("PreferredAuthentications"); //$NON-NLS-1$
926 			if (value != null) {
927 				preferredAuthentications = nows(value);
928 			}
929 			// Fill in defaults if still not set
930 			if (hostName == null) {
931 				hostName = initialHostName;
932 			}
933 			if (user == null) {
934 				user = OpenSshConfig.userName();
935 			}
936 			if (port <= 0) {
937 				port = OpenSshConfig.SSH_PORT;
938 			}
939 			if (connectionAttempts <= 0) {
940 				connectionAttempts = 1;
941 			}
942 			String[] identityFiles = config.getValues("IdentityFile"); //$NON-NLS-1$
943 			if (identityFiles != null && identityFiles.length > 0) {
944 				identityFile = toFile(identityFiles[0], homeDir);
945 			}
946 		}
947 
948 		Config getConfig() {
949 			return config;
950 		}
951 
952 		@Override
953 		@SuppressWarnings("nls")
954 		public String toString() {
955 			return "Host [hostName=" + hostName + ", port=" + port
956 					+ ", identityFile=" + identityFile + ", user=" + user
957 					+ ", preferredAuthentications=" + preferredAuthentications
958 					+ ", batchMode=" + batchMode + ", strictHostKeyChecking="
959 					+ strictHostKeyChecking + ", connectionAttempts="
960 					+ connectionAttempts + ", config=" + config + "]";
961 		}
962 	}
963 
964 	/**
965 	 * {@inheritDoc}
966 	 * <p>
967 	 * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config}
968 	 * for the given host name. Should be called only by Jsch and tests.
969 	 *
970 	 * @since 4.9
971 	 */
972 	@Override
973 	public Config getConfig(String hostName) {
974 		Host host = lookup(hostName);
975 		return host.getConfig();
976 	}
977 
978 	/** {@inheritDoc} */
979 	@Override
980 	@SuppressWarnings("nls")
981 	public String toString() {
982 		return "OpenSshConfig [home=" + home + ", configFile=" + configFile
983 				+ ", lastModified=" + lastModified + ", state=" + state + "]";
984 	}
985 }