123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041 |
- /*
- * Copyright (C) 2008, 2017, Google Inc.
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials provided
- * with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- * names of its contributors may be used to endorse or promote
- * products derived from this software without specific prior
- * written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
- package org.eclipse.jgit.transport;
-
- import java.io.BufferedReader;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.security.AccessController;
- import java.security.PrivilegedAction;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.TimeUnit;
-
- import org.eclipse.jgit.errors.InvalidPatternException;
- import org.eclipse.jgit.fnmatch.FileNameMatcher;
- import org.eclipse.jgit.lib.Constants;
- import org.eclipse.jgit.util.FS;
- import org.eclipse.jgit.util.StringUtils;
- import org.eclipse.jgit.util.SystemReader;
-
- import com.jcraft.jsch.ConfigRepository;
-
- /**
- * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file.
- * <p>
- * JSch does have its own config file parser
- * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a
- * number of problems:
- * <ul>
- * <li>it splits lines of the format "keyword = value" wrongly: you'd end up
- * with the value "= value".
- * <li>its "Host" keyword is not case insensitive.
- * <li>it doesn't handle quoted values.
- * <li>JSch's OpenSSHConfig doesn't monitor for config file changes.
- * </ul>
- * <p>
- * Therefore implement our own parser to read an OpenSSH configuration file. It
- * makes the critical options available to {@link SshSessionFactory} via
- * {@link Host} objects returned by {@link #lookup(String)}, and implements a
- * fully conforming {@link ConfigRepository} providing
- * {@link com.jcraft.jsch.ConfigRepository.Config}s via
- * {@link #getConfig(String)}.
- * </p>
- * <p>
- * Limitations compared to the full OpenSSH 7.5 parser:
- * </p>
- * <ul>
- * <li>This parser does not handle Match or Include keywords.
- * <li>This parser does not do host name canonicalization (Jsch ignores it
- * anyway).
- * </ul>
- * <p>
- * Note that OpenSSH's readconf.c is a validating parser; Jsch's
- * ConfigRepository OTOH treats all option values as plain strings, so any
- * validation must happen in Jsch outside of the parser. Thus this parser does
- * not validate option values, except for a few options when constructing a
- * {@link Host} object.
- * </p>
- * <p>
- * This config does %-substitutions for the following tokens:
- * </p>
- * <ul>
- * <li>%% - single %
- * <li>%C - short-hand for %l%h%p%r. See %p and %r below; the replacement may be
- * done partially only and may leave %p or %r or both unreplaced.
- * <li>%d - home directory path
- * <li>%h - remote host name
- * <li>%L - local host name without domain
- * <li>%l - FQDN of the local host
- * <li>%n - host name as specified in {@link #lookup(String)}
- * <li>%p - port number; replaced only if set in the config
- * <li>%r - remote user name; replaced only if set in the config
- * <li>%u - local user name
- * </ul>
- * <p>
- * If the config doesn't set the port or the remote user name, %p and %r remain
- * un-substituted. It's the caller's responsibility to replace them with values
- * obtained from the connection URI. %i is not handled; Java has no concept of a
- * "user ID".
- * </p>
- */
- public class OpenSshConfig implements ConfigRepository {
-
- /** IANA assigned port number for SSH. */
- static final int SSH_PORT = 22;
-
- /**
- * Obtain the user's configuration data.
- * <p>
- * The configuration file is always returned to the caller, even if no file
- * exists in the user's home directory at the time the call was made. Lookup
- * requests are cached and are automatically updated if the user modifies
- * the configuration file since the last time it was cached.
- *
- * @param fs
- * the file system abstraction which will be necessary to
- * perform certain file system operations.
- * @return a caching reader of the user's configuration file.
- */
- public static OpenSshConfig get(FS fs) {
- File home = fs.userHome();
- if (home == null)
- home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
-
- final File config = new File(new File(home, ".ssh"), Constants.CONFIG); //$NON-NLS-1$
- final OpenSshConfig osc = new OpenSshConfig(home, config);
- osc.refresh();
- return osc;
- }
-
- /** The user's home directory, as key files may be relative to here. */
- private final File home;
-
- /** The .ssh/config file we read and monitor for updates. */
- private final File configFile;
-
- /** Modification time of {@link #configFile} when it was last loaded. */
- private long lastModified;
-
- /**
- * Encapsulates entries read out of the configuration file, and
- * {@link Host}s created from that.
- */
- private static class State {
- Map<String, HostEntry> entries = new LinkedHashMap<>();
- Map<String, Host> hosts = new HashMap<>();
-
- @Override
- @SuppressWarnings("nls")
- public String toString() {
- return "State [entries=" + entries + ", hosts=" + hosts + "]";
- }
- }
-
- /** State read from the config file, plus {@link Host}s created from it. */
- private State state;
-
- OpenSshConfig(final File h, final File cfg) {
- home = h;
- configFile = cfg;
- state = new State();
- }
-
- /**
- * Locate the configuration for a specific host request.
- *
- * @param hostName
- * the name the user has supplied to the SSH tool. This may be a
- * real host name, or it may just be a "Host" block in the
- * configuration file.
- * @return r configuration for the requested name. Never null.
- */
- public Host lookup(final String hostName) {
- final State cache = refresh();
- Host h = cache.hosts.get(hostName);
- if (h != null) {
- return h;
- }
- HostEntry fullConfig = new HostEntry();
- // Initialize with default entries at the top of the file, before the
- // first Host block.
- fullConfig.merge(cache.entries.get(HostEntry.DEFAULT_NAME));
- for (final Map.Entry<String, HostEntry> e : cache.entries.entrySet()) {
- String key = e.getKey();
- if (isHostMatch(key, hostName)) {
- fullConfig.merge(e.getValue());
- }
- }
- fullConfig.substitute(hostName, home);
- h = new Host(fullConfig, hostName, home);
- cache.hosts.put(hostName, h);
- return h;
- }
-
- private synchronized State refresh() {
- final long mtime = configFile.lastModified();
- if (mtime != lastModified) {
- State newState = new State();
- try (FileInputStream in = new FileInputStream(configFile)) {
- newState.entries = parse(in);
- } catch (IOException none) {
- // Ignore -- we'll set and return an empty state
- }
- lastModified = mtime;
- state = newState;
- }
- return state;
- }
-
- private Map<String, HostEntry> parse(final InputStream in)
- throws IOException {
- final Map<String, HostEntry> m = new LinkedHashMap<>();
- final BufferedReader br = new BufferedReader(new InputStreamReader(in));
- final List<HostEntry> current = new ArrayList<>(4);
- String line;
-
- // The man page doesn't say so, but the OpenSSH parser (readconf.c)
- // starts out in active mode and thus always applies any lines that
- // occur before the first host block. We gather those options in a
- // HostEntry for DEFAULT_NAME.
- HostEntry defaults = new HostEntry();
- current.add(defaults);
- m.put(HostEntry.DEFAULT_NAME, defaults);
-
- while ((line = br.readLine()) != null) {
- line = line.trim();
- if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$
- continue;
- }
- String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
- // Although the ssh-config man page doesn't say so, the OpenSSH
- // parser does allow quoted keywords.
- String keyword = dequote(parts[0].trim());
- // man 5 ssh-config says lines had the format "keyword arguments",
- // with no indication that arguments were optional. However, let's
- // not crap out on missing arguments. See bug 444319.
- String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$
-
- if (StringUtils.equalsIgnoreCase("Host", keyword)) { //$NON-NLS-1$
- current.clear();
- for (String name : HostEntry.parseList(argValue)) {
- if (name == null || name.isEmpty()) {
- // null should not occur, but better be safe than sorry.
- continue;
- }
- HostEntry c = m.get(name);
- if (c == null) {
- c = new HostEntry();
- m.put(name, c);
- }
- current.add(c);
- }
- continue;
- }
-
- if (current.isEmpty()) {
- // We received an option outside of a Host block. We
- // don't know who this should match against, so skip.
- continue;
- }
-
- if (HostEntry.isListKey(keyword)) {
- List<String> args = HostEntry.parseList(argValue);
- for (HostEntry entry : current) {
- entry.setValue(keyword, args);
- }
- } else if (!argValue.isEmpty()) {
- argValue = dequote(argValue);
- for (HostEntry entry : current) {
- entry.setValue(keyword, argValue);
- }
- }
- }
-
- return m;
- }
-
- private static boolean isHostMatch(final String pattern,
- final String name) {
- if (pattern.startsWith("!")) { //$NON-NLS-1$
- return !patternMatchesHost(pattern.substring(1), name);
- } else {
- return patternMatchesHost(pattern, name);
- }
- }
-
- private static boolean patternMatchesHost(final String pattern,
- final String name) {
- if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
- final FileNameMatcher fn;
- try {
- fn = new FileNameMatcher(pattern, null);
- } catch (InvalidPatternException e) {
- return false;
- }
- fn.append(name);
- return fn.isMatch();
- } else {
- // Not a pattern but a full host name
- return pattern.equals(name);
- }
- }
-
- private static String dequote(final String value) {
- if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$
- && value.length() > 1)
- return value.substring(1, value.length() - 1);
- return value;
- }
-
- private static String nows(final String value) {
- final StringBuilder b = new StringBuilder();
- for (int i = 0; i < value.length(); i++) {
- if (!Character.isSpaceChar(value.charAt(i)))
- b.append(value.charAt(i));
- }
- return b.toString();
- }
-
- private static Boolean yesno(final String value) {
- if (StringUtils.equalsIgnoreCase("yes", value)) //$NON-NLS-1$
- return Boolean.TRUE;
- return Boolean.FALSE;
- }
-
- private static File toFile(String path, File home) {
- if (path.startsWith("~/")) { //$NON-NLS-1$
- return new File(home, path.substring(2));
- }
- File ret = new File(path);
- if (ret.isAbsolute()) {
- return ret;
- }
- return new File(home, path);
- }
-
- private static int positive(final String value) {
- if (value != null) {
- try {
- return Integer.parseUnsignedInt(value);
- } catch (NumberFormatException e) {
- // Ignore
- }
- }
- return -1;
- }
-
- static String userName() {
- return AccessController.doPrivileged(new PrivilegedAction<String>() {
- @Override
- public String run() {
- return SystemReader.getInstance()
- .getProperty(Constants.OS_USER_NAME_KEY);
- }
- });
- }
-
- private static class HostEntry implements ConfigRepository.Config {
-
- /**
- * "Host name" of the HostEntry for the default options before the first
- * host block in a config file.
- */
- public static final String DEFAULT_NAME = ""; //$NON-NLS-1$
-
- // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
- // to ssh-config keys.
- private static final Map<String, String> KEY_MAP = new HashMap<>();
-
- static {
- KEY_MAP.put("kex", "KexAlgorithms"); //$NON-NLS-1$//$NON-NLS-2$
- KEY_MAP.put("server_host_key", "HostKeyAlgorithms"); //$NON-NLS-1$ //$NON-NLS-2$
- KEY_MAP.put("cipher.c2s", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
- KEY_MAP.put("cipher.s2c", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
- KEY_MAP.put("mac.c2s", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
- KEY_MAP.put("mac.s2c", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
- KEY_MAP.put("compression.s2c", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
- KEY_MAP.put("compression.c2s", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
- KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$
- KEY_MAP.put("MaxAuthTries", "NumberOfPasswordPrompts"); //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- /**
- * Keys that can be specified multiple times, building up a list. (I.e.,
- * those are the keys that do not follow the general rule of "first
- * occurrence wins".)
- */
- private static final Set<String> MULTI_KEYS = new HashSet<>();
-
- static {
- MULTI_KEYS.add("CERTIFICATEFILE"); //$NON-NLS-1$
- MULTI_KEYS.add("IDENTITYFILE"); //$NON-NLS-1$
- MULTI_KEYS.add("LOCALFORWARD"); //$NON-NLS-1$
- MULTI_KEYS.add("REMOTEFORWARD"); //$NON-NLS-1$
- MULTI_KEYS.add("SENDENV"); //$NON-NLS-1$
- }
-
- /**
- * Keys that take a whitespace-separated list of elements as argument.
- * Because the dequote-handling is different, we must handle those in
- * the parser. There are a few other keys that take comma-separated
- * lists as arguments, but for the parser those are single arguments
- * that must be quoted if they contain whitespace, and taking them apart
- * is the responsibility of the user of those keys.
- */
- private static final Set<String> LIST_KEYS = new HashSet<>();
-
- static {
- LIST_KEYS.add("CANONICALDOMAINS"); //$NON-NLS-1$
- LIST_KEYS.add("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
- LIST_KEYS.add("SENDENV"); //$NON-NLS-1$
- LIST_KEYS.add("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
- }
-
- private Map<String, String> options;
-
- private Map<String, List<String>> multiOptions;
-
- private Map<String, List<String>> listOptions;
-
- @Override
- public String getHostname() {
- return getValue("HOSTNAME"); //$NON-NLS-1$
- }
-
- @Override
- public String getUser() {
- return getValue("USER"); //$NON-NLS-1$
- }
-
- @Override
- public int getPort() {
- return positive(getValue("PORT")); //$NON-NLS-1$
- }
-
- private static String mapKey(String key) {
- String k = KEY_MAP.get(key);
- if (k == null) {
- k = key;
- }
- return k.toUpperCase(Locale.ROOT);
- }
-
- private String findValue(String key) {
- String k = mapKey(key);
- String result = options != null ? options.get(k) : null;
- if (result == null) {
- // Also check the list and multi options. Modern OpenSSH treats
- // UserKnownHostsFile and GlobalKnownHostsFile as list-valued,
- // and so does this parser. Jsch 0.1.54 in general doesn't know
- // about list-valued options (it _does_ know multi-valued
- // options, though), and will ask for a single value for such
- // options.
- //
- // Let's be lenient and return at least the first value from
- // a list-valued or multi-valued key for which Jsch asks for a
- // single value.
- List<String> values = listOptions != null ? listOptions.get(k)
- : null;
- if (values == null) {
- values = multiOptions != null ? multiOptions.get(k) : null;
- }
- if (values != null && !values.isEmpty()) {
- result = values.get(0);
- }
- }
- return result;
- }
-
- @Override
- public String getValue(String key) {
- // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() for this
- // special case.
- if (key.equals("compression.s2c") //$NON-NLS-1$
- || key.equals("compression.c2s")) { //$NON-NLS-1$
- String foo = findValue(key);
- if (foo == null || foo.equals("no")) { //$NON-NLS-1$
- return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$
- }
- return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$
- }
- return findValue(key);
- }
-
- @Override
- public String[] getValues(String key) {
- String k = mapKey(key);
- List<String> values = listOptions != null ? listOptions.get(k)
- : null;
- if (values == null) {
- values = multiOptions != null ? multiOptions.get(k) : null;
- }
- if (values == null || values.isEmpty()) {
- return new String[0];
- }
- return values.toArray(new String[values.size()]);
- }
-
- public void setValue(String key, String value) {
- String k = key.toUpperCase(Locale.ROOT);
- if (MULTI_KEYS.contains(k)) {
- if (multiOptions == null) {
- multiOptions = new HashMap<>();
- }
- List<String> values = multiOptions.get(k);
- if (values == null) {
- values = new ArrayList<>(4);
- multiOptions.put(k, values);
- }
- values.add(value);
- } else {
- if (options == null) {
- options = new HashMap<>();
- }
- if (!options.containsKey(k)) {
- options.put(k, value);
- }
- }
- }
-
- public void setValue(String key, List<String> values) {
- if (values.isEmpty()) {
- // Can occur only on a missing argument: ignore.
- return;
- }
- String k = key.toUpperCase(Locale.ROOT);
- // Check multi-valued keys first; because of the replacement
- // strategy, they must take precedence over list-valued keys
- // which always follow the "first occurrence wins" strategy.
- //
- // Note that SendEnv is a multi-valued list-valued key. (It's
- // rather immaterial for JGit, though.)
- if (MULTI_KEYS.contains(k)) {
- if (multiOptions == null) {
- multiOptions = new HashMap<>(2 * MULTI_KEYS.size());
- }
- List<String> items = multiOptions.get(k);
- if (items == null) {
- items = new ArrayList<>(values);
- multiOptions.put(k, items);
- } else {
- items.addAll(values);
- }
- } else {
- if (listOptions == null) {
- listOptions = new HashMap<>(2 * LIST_KEYS.size());
- }
- if (!listOptions.containsKey(k)) {
- listOptions.put(k, values);
- }
- }
- }
-
- public static boolean isListKey(String key) {
- return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT));
- }
-
- /**
- * Splits the argument into a list of whitespace-separated elements.
- * Elements containing whitespace must be quoted and will be de-quoted.
- *
- * @param argument
- * argument part of the configuration line as read from the
- * config file
- * @return a {@link List} of elements, possibly empty and possibly
- * containing empty elements
- */
- public static List<String> parseList(String argument) {
- List<String> result = new ArrayList<>(4);
- int start = 0;
- int length = argument.length();
- while (start < length) {
- // Skip whitespace
- if (Character.isSpaceChar(argument.charAt(start))) {
- start++;
- continue;
- }
- if (argument.charAt(start) == '"') {
- int stop = argument.indexOf('"', ++start);
- if (stop < start) {
- // No closing double quote: skip
- break;
- }
- result.add(argument.substring(start, stop));
- start = stop + 1;
- } else {
- int stop = start + 1;
- while (stop < length
- && !Character.isSpaceChar(argument.charAt(stop))) {
- stop++;
- }
- result.add(argument.substring(start, stop));
- start = stop + 1;
- }
- }
- return result;
- }
-
- protected void merge(HostEntry entry) {
- if (entry == null) {
- // Can occur if we could not read the config file
- return;
- }
- if (entry.options != null) {
- if (options == null) {
- options = new HashMap<>();
- }
- for (Map.Entry<String, String> item : entry.options
- .entrySet()) {
- if (!options.containsKey(item.getKey())) {
- options.put(item.getKey(), item.getValue());
- }
- }
- }
- if (entry.listOptions != null) {
- if (listOptions == null) {
- listOptions = new HashMap<>(2 * LIST_KEYS.size());
- }
- for (Map.Entry<String, List<String>> item : entry.listOptions
- .entrySet()) {
- if (!listOptions.containsKey(item.getKey())) {
- listOptions.put(item.getKey(), item.getValue());
- }
- }
-
- }
- if (entry.multiOptions != null) {
- if (multiOptions == null) {
- multiOptions = new HashMap<>(2 * MULTI_KEYS.size());
- }
- for (Map.Entry<String, List<String>> item : entry.multiOptions
- .entrySet()) {
- List<String> values = multiOptions.get(item.getKey());
- if (values == null) {
- values = new ArrayList<>(item.getValue());
- multiOptions.put(item.getKey(), values);
- } else {
- values.addAll(item.getValue());
- }
- }
- }
- }
-
- private class Replacer {
- private final Map<Character, String> replacements = new HashMap<>();
-
- public Replacer(String originalHostName, File home) {
- replacements.put(Character.valueOf('%'), "%"); //$NON-NLS-1$
- replacements.put(Character.valueOf('d'), home.getPath());
- // Needs special treatment...
- String host = getValue("HOSTNAME"); //$NON-NLS-1$
- replacements.put(Character.valueOf('h'), originalHostName);
- if (host != null && host.indexOf('%') >= 0) {
- host = substitute(host, "h"); //$NON-NLS-1$
- options.put("HOSTNAME", host); //$NON-NLS-1$
- }
- if (host != null) {
- replacements.put(Character.valueOf('h'), host);
- }
- String localhost = SystemReader.getInstance().getHostname();
- replacements.put(Character.valueOf('l'), localhost);
- int period = localhost.indexOf('.');
- if (period > 0) {
- localhost = localhost.substring(0, period);
- }
- replacements.put(Character.valueOf('L'), localhost);
- replacements.put(Character.valueOf('n'), originalHostName);
- replacements.put(Character.valueOf('p'), getValue("PORT")); //$NON-NLS-1$
- replacements.put(Character.valueOf('r'), getValue("USER")); //$NON-NLS-1$
- replacements.put(Character.valueOf('u'), userName());
- replacements.put(Character.valueOf('C'),
- substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- public String substitute(String input, String allowed) {
- if (input == null || input.length() <= 1
- || input.indexOf('%') < 0) {
- return input;
- }
- StringBuilder builder = new StringBuilder();
- int start = 0;
- int length = input.length();
- while (start < length) {
- int percent = input.indexOf('%', start);
- if (percent < 0 || percent + 1 >= length) {
- builder.append(input.substring(start));
- break;
- }
- String replacement = null;
- char ch = input.charAt(percent + 1);
- if (ch == '%' || allowed.indexOf(ch) >= 0) {
- replacement = replacements.get(Character.valueOf(ch));
- }
- if (replacement == null) {
- builder.append(input.substring(start, percent + 2));
- } else {
- builder.append(input.substring(start, percent))
- .append(replacement);
- }
- start = percent + 2;
- }
- return builder.toString();
- }
- }
-
- private List<String> substitute(List<String> values, String allowed,
- Replacer r) {
- List<String> result = new ArrayList<>(values.size());
- for (String value : values) {
- result.add(r.substitute(value, allowed));
- }
- return result;
- }
-
- private List<String> replaceTilde(List<String> values, File home) {
- List<String> result = new ArrayList<>(values.size());
- for (String value : values) {
- result.add(toFile(value, home).getPath());
- }
- return result;
- }
-
- protected void substitute(String originalHostName, File home) {
- Replacer r = new Replacer(originalHostName, home);
- if (multiOptions != null) {
- List<String> values = multiOptions.get("IDENTITYFILE"); //$NON-NLS-1$
- if (values != null) {
- values = substitute(values, "dhlru", r); //$NON-NLS-1$
- values = replaceTilde(values, home);
- multiOptions.put("IDENTITYFILE", values); //$NON-NLS-1$
- }
- values = multiOptions.get("CERTIFICATEFILE"); //$NON-NLS-1$
- if (values != null) {
- values = substitute(values, "dhlru", r); //$NON-NLS-1$
- values = replaceTilde(values, home);
- multiOptions.put("CERTIFICATEFILE", values); //$NON-NLS-1$
- }
- }
- if (listOptions != null) {
- List<String> values = listOptions.get("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
- if (values != null) {
- values = replaceTilde(values, home);
- listOptions.put("GLOBALKNOWNHOSTSFILE", values); //$NON-NLS-1$
- }
- values = listOptions.get("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
- if (values != null) {
- values = replaceTilde(values, home);
- listOptions.put("USERKNOWNHOSTSFILE", values); //$NON-NLS-1$
- }
- }
- if (options != null) {
- // HOSTNAME already done in Replacer constructor
- String value = options.get("IDENTITYAGENT"); //$NON-NLS-1$
- if (value != null) {
- value = r.substitute(value, "dhlru"); //$NON-NLS-1$
- value = toFile(value, home).getPath();
- options.put("IDENTITYAGENT", value); //$NON-NLS-1$
- }
- }
- // Match is not implemented and would need to be done elsewhere
- // anyway. ControlPath, LocalCommand, ProxyCommand, and
- // RemoteCommand are not used by Jsch.
- }
-
- @Override
- @SuppressWarnings("nls")
- public String toString() {
- return "HostEntry [options=" + options + ", multiOptions="
- + multiOptions + ", listOptions=" + listOptions + "]";
- }
- }
-
- /**
- * Configuration of one "Host" block in the configuration file.
- * <p>
- * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
- * properties may not be populated. The properties which are not populated
- * should be defaulted by the caller.
- * <p>
- * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
- * entries which appear later in the configuration file will have been
- * already merged into this block.
- */
- public static class Host {
- String hostName;
-
- int port;
-
- File identityFile;
-
- String user;
-
- String preferredAuthentications;
-
- Boolean batchMode;
-
- String strictHostKeyChecking;
-
- int connectionAttempts;
-
- private Config config;
-
- /**
- * Creates a new uninitialized {@link Host}.
- */
- public Host() {
- // For API backwards compatibility with pre-4.9 JGit
- }
-
- Host(Config config, String hostName, File homeDir) {
- this.config = config;
- complete(hostName, homeDir);
- }
-
- /**
- * @return the value StrictHostKeyChecking property, the valid values
- * are "yes" (unknown hosts are not accepted), "no" (unknown
- * hosts are always accepted), and "ask" (user should be asked
- * before accepting the host)
- */
- public String getStrictHostKeyChecking() {
- return strictHostKeyChecking;
- }
-
- /**
- * @return the real IP address or host name to connect to; never null.
- */
- public String getHostName() {
- return hostName;
- }
-
- /**
- * @return the real port number to connect to; never 0.
- */
- public int getPort() {
- return port;
- }
-
- /**
- * @return path of the private key file to use for authentication; null
- * if the caller should use default authentication strategies.
- */
- public File getIdentityFile() {
- return identityFile;
- }
-
- /**
- * @return the real user name to connect as; never null.
- */
- public String getUser() {
- return user;
- }
-
- /**
- * @return the preferred authentication methods, separated by commas if
- * more than one authentication method is preferred.
- */
- public String getPreferredAuthentications() {
- return preferredAuthentications;
- }
-
- /**
- * @return true if batch (non-interactive) mode is preferred for this
- * host connection.
- */
- public boolean isBatchMode() {
- return batchMode != null && batchMode.booleanValue();
- }
-
- /**
- * @return the number of tries (one per second) to connect before
- * exiting. The argument must be an integer. This may be useful
- * in scripts if the connection sometimes fails. The default is
- * 1.
- * @since 3.4
- */
- public int getConnectionAttempts() {
- return connectionAttempts;
- }
-
-
- private void complete(String initialHostName, File homeDir) {
- // Try to set values from the options.
- hostName = config.getHostname();
- user = config.getUser();
- port = config.getPort();
- connectionAttempts = positive(
- config.getValue("ConnectionAttempts")); //$NON-NLS-1$
- strictHostKeyChecking = config.getValue("StrictHostKeyChecking"); //$NON-NLS-1$
- String value = config.getValue("BatchMode"); //$NON-NLS-1$
- if (value != null) {
- batchMode = yesno(value);
- }
- value = config.getValue("PreferredAuthentications"); //$NON-NLS-1$
- if (value != null) {
- preferredAuthentications = nows(value);
- }
- // Fill in defaults if still not set
- if (hostName == null) {
- hostName = initialHostName;
- }
- if (user == null) {
- user = OpenSshConfig.userName();
- }
- if (port <= 0) {
- port = OpenSshConfig.SSH_PORT;
- }
- if (connectionAttempts <= 0) {
- connectionAttempts = 1;
- }
- String[] identityFiles = config.getValues("IdentityFile"); //$NON-NLS-1$
- if (identityFiles != null && identityFiles.length > 0) {
- identityFile = toFile(identityFiles[0], homeDir);
- }
- }
-
- Config getConfig() {
- return config;
- }
-
- @Override
- @SuppressWarnings("nls")
- public String toString() {
- return "Host [hostName=" + hostName + ", port=" + port
- + ", identityFile=" + identityFile + ", user=" + user
- + ", preferredAuthentications=" + preferredAuthentications
- + ", batchMode=" + batchMode + ", strictHostKeyChecking="
- + strictHostKeyChecking + ", connectionAttempts="
- + connectionAttempts + ", config=" + config + "]";
- }
- }
-
- /**
- * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config}
- * for the given host name. Should be called only by Jsch and tests.
- *
- * @param hostName
- * to get the config for
- * @return the configuration for the host
- * @since 4.9
- */
- @Override
- public Config getConfig(String hostName) {
- Host host = lookup(hostName);
- return new JschBugFixingConfig(host.getConfig());
- }
-
- @Override
- @SuppressWarnings("nls")
- public String toString() {
- return "OpenSshConfig [home=" + home + ", configFile=" + configFile
- + ", lastModified=" + lastModified + ", state=" + state + "]";
- }
-
- /**
- * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms some
- * values from the config file into the format Jsch 0.1.54 expects. This is
- * a work-around for bugs in Jsch.
- */
- private static class JschBugFixingConfig implements Config {
-
- private final Config real;
-
- public JschBugFixingConfig(Config delegate) {
- real = delegate;
- }
-
- @Override
- public String getHostname() {
- return real.getHostname();
- }
-
- @Override
- public String getUser() {
- return real.getUser();
- }
-
- @Override
- public int getPort() {
- return real.getPort();
- }
-
- @Override
- public String getValue(String key) {
- String result = real.getValue(key);
- if (result != null) {
- String k = key.toUpperCase(Locale.ROOT);
- if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$
- || "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$
- // These values are in seconds. Jsch 0.1.54 passes them on
- // as is to java.net.Socket.setSoTimeout(), which expects
- // milliseconds. So convert here to milliseconds...
- try {
- int timeout = Integer.parseInt(result);
- result = Long
- .toString(TimeUnit.SECONDS.toMillis(timeout));
- } catch (NumberFormatException e) {
- // Ignore
- }
- }
- }
- return result;
- }
-
- @Override
- public String[] getValues(String key) {
- return real.getValues(key);
- }
-
- }
- }
|