import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.io.File;
c.getValues("UserKnownHostsFile"));
}
- @Test
- public void testRepeatedLookups() throws Exception {
- config("Host orcz\n" + "\tConnectionAttempts 5\n");
- final Host h1 = osc.lookup("orcz");
- final Host h2 = osc.lookup("orcz");
- assertNotNull(h1);
- assertSame(h1, h2);
- assertEquals(5, h1.getConnectionAttempts());
- assertEquals(h1.getConnectionAttempts(), h2.getConnectionAttempts());
- final ConfigRepository.Config c = osc.getConfig("orcz");
- assertNotNull(c);
- assertSame(c, h1.getConfig());
- assertSame(c, h2.getConfig());
- }
-
@Test
public void testRepeatedLookupsWithModification() throws Exception {
config("Host orcz\n" + "\tConnectionAttempts -1\n");
org.eclipse.jgit.internal.storage.reftree;version="5.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
org.eclipse.jgit.internal.submodule;version="5.2.0";x-internal:=true,
org.eclipse.jgit.internal.transport.parser;version="5.2.0";x-friends:="org.eclipse.jgit.test",
+ org.eclipse.jgit.internal.transport.ssh;version="5.2.0";x-internal:=true,
org.eclipse.jgit.lib;version="5.2.0";
uses:="org.eclipse.jgit.revwalk,
org.eclipse.jgit.treewalk.filter,
--- /dev/null
+/*
+ * Copyright (C) 2008, 2017, Google Inc.
+ * Copyright (C) 2017, 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * 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.internal.transport.ssh;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.errors.InvalidPatternException;
+import org.eclipse.jgit.fnmatch.FileNameMatcher;
+import org.eclipse.jgit.transport.SshConstants;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.SystemReader;
+
+/**
+ * Fairly complete configuration parser for the openssh ~/.ssh/config file.
+ * <p>
+ * Both JSch 0.1.54 and Apache MINA sshd 2.1.0 have parsers for this, but both
+ * are buggy. Therefore we implement our own parser to read an openssh
+ * configuration file.
+ * </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.
+ * </ul>
+ * <p>
+ * Note that openssh's readconf.c is a validating parser; this parser does not
+ * validate entries.
+ * </p>
+ * <p>
+ * This config does %-substitutions for the following tokens:
+ * </p>
+ * <ul>
+ * <li>%% - single %
+ * <li>%C - short-hand for %l%h%p%r.
+ * <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, int, String)}
+ * <li>%p - port number; if not given in {@link #lookup(String, int, String)}
+ * replaced only if set in the config
+ * <li>%r - remote user name; if not given in
+ * {@link #lookup(String, int, String)} replaced only if set in the config
+ * <li>%u - local user name
+ * </ul>
+ * <p>
+ * %i is not handled; Java has no concept of a "user ID". %T is always replaced
+ * by NONE.
+ * </p>
+ *
+ * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
+ * ssh-config</a>
+ */
+public class OpenSshConfigFile {
+
+ /**
+ * "Host" name of the HostEntry for the default options before the first
+ * host block in a config file.
+ */
+ private static final String DEFAULT_NAME = ""; //$NON-NLS-1$
+
+ /** 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;
+
+ /** User name of the user on the host OS. */
+ private final String localUserName;
+
+ /** Modification time of {@link #configFile} when it was last loaded. */
+ private long lastModified;
+
+ /**
+ * Encapsulates entries read out of the configuration file, and a cache of
+ * fully resolved entries created from that.
+ */
+ private static class State {
+ // Keyed by pattern; if a "Host" line has multiple patterns, we generate
+ // duplicate HostEntry objects
+ Map<String, HostEntry> entries = new LinkedHashMap<>();
+
+ // Keyed by user@hostname:port
+ Map<String, HostEntry> hosts = new HashMap<>();
+
+ @Override
+ @SuppressWarnings("nls")
+ public String toString() {
+ return "State [entries=" + entries + ", hosts=" + hosts + "]";
+ }
+ }
+
+ /** State read from the config file, plus the cache. */
+ private State state;
+
+ /**
+ * Creates a new {@link OpenSshConfigFile} that will read the config from
+ * file {@code config} use the given file {@code home} as "home" directory.
+ *
+ * @param home
+ * user's home directory for the purpose of ~ replacement
+ * @param config
+ * file to load.
+ * @param localUserName
+ * user name of the current user on the local host OS
+ */
+ public OpenSshConfigFile(@NonNull File home, @NonNull File config,
+ @NonNull String localUserName) {
+ this.home = home;
+ this.configFile = config;
+ this.localUserName = localUserName;
+ 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.
+ * @param port
+ * the user supplied; <= 0 if none
+ * @param userName
+ * the user supplied, may be {@code null} or empty if none given
+ * @return r configuration for the requested name.
+ */
+ @NonNull
+ public HostEntry lookup(@NonNull String hostName, int port,
+ String userName) {
+ final State cache = refresh();
+ String cacheKey = toCacheKey(hostName, port, userName);
+ HostEntry h = cache.hosts.get(cacheKey);
+ 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(DEFAULT_NAME));
+ for (Map.Entry<String, HostEntry> e : cache.entries.entrySet()) {
+ String pattern = e.getKey();
+ if (isHostMatch(pattern, hostName)) {
+ fullConfig.merge(e.getValue());
+ }
+ }
+ fullConfig.substitute(hostName, port, userName, localUserName, home);
+ cache.hosts.put(cacheKey, fullConfig);
+ return fullConfig;
+ }
+
+ @NonNull
+ private String toCacheKey(@NonNull String hostName, int port,
+ String userName) {
+ String key = hostName;
+ if (port > 0) {
+ key = key + ':' + Integer.toString(port);
+ }
+ if (userName != null && !userName.isEmpty()) {
+ key = userName + '@' + key;
+ }
+ return key;
+ }
+
+ private synchronized State refresh() {
+ final long mtime = configFile.lastModified();
+ if (mtime != lastModified) {
+ State newState = new State();
+ try (BufferedReader br = Files
+ .newBufferedReader(configFile.toPath(), UTF_8)) {
+ newState.entries = parse(br);
+ } catch (IOException | RuntimeException none) {
+ // Ignore -- we'll set and return an empty state
+ }
+ lastModified = mtime;
+ state = newState;
+ }
+ return state;
+ }
+
+ private Map<String, HostEntry> parse(BufferedReader reader)
+ throws IOException {
+ final Map<String, HostEntry> entries = new LinkedHashMap<>();
+ 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);
+ entries.put(DEFAULT_NAME, defaults);
+
+ while ((line = reader.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(SshConstants.HOST, keyword)) {
+ current.clear();
+ for (String name : parseList(argValue)) {
+ if (name == null || name.isEmpty()) {
+ // null should not occur, but better be safe than sorry.
+ continue;
+ }
+ HostEntry c = entries.get(name);
+ if (c == null) {
+ c = new HostEntry();
+ entries.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 = validate(keyword, parseList(argValue));
+ for (HostEntry entry : current) {
+ entry.setValue(keyword, args);
+ }
+ } else if (!argValue.isEmpty()) {
+ argValue = validate(keyword, dequote(argValue));
+ for (HostEntry entry : current) {
+ entry.setValue(keyword, argValue);
+ }
+ }
+ }
+
+ return entries;
+ }
+
+ /**
+ * 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, but not containing {@code null}
+ */
+ private 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;
+ }
+
+ /**
+ * Hook to perform validation on a single value, or to sanitize it. If this
+ * throws an (unchecked) exception, parsing of the file is abandoned.
+ *
+ * @param key
+ * of the entry
+ * @param value
+ * as read from the config file
+ * @return the validated and possibly sanitized value
+ */
+ protected String validate(String key, String value) {
+ if (String.CASE_INSENSITIVE_ORDER.compare(key,
+ SshConstants.PREFERRED_AUTHENTICATIONS) == 0) {
+ return stripWhitespace(value);
+ }
+ return value;
+ }
+
+ /**
+ * Hook to perform validation on values, or to sanitize them. If this throws
+ * an (unchecked) exception, parsing of the file is abandoned.
+ *
+ * @param key
+ * of the entry
+ * @param value
+ * list of arguments as read from the config file
+ * @return a {@link List} of values, possibly empty and possibly containing
+ * empty elements, but not containing {@code null}
+ */
+ protected List<String> validate(String key, List<String> value) {
+ return value;
+ }
+
+ private static boolean isHostMatch(String pattern, String name) {
+ if (pattern.startsWith("!")) { //$NON-NLS-1$
+ return !patternMatchesHost(pattern.substring(1), name);
+ } else {
+ return patternMatchesHost(pattern, name);
+ }
+ }
+
+ private static boolean patternMatchesHost(String pattern, 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(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 stripWhitespace(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 File toFile(String path, File home) {
+ if (path.startsWith("~/") || path.startsWith("~" + File.separator)) { //$NON-NLS-1$ //$NON-NLS-2$
+ return new File(home, path.substring(2));
+ }
+ File ret = new File(path);
+ if (ret.isAbsolute()) {
+ return ret;
+ }
+ return new File(home, path);
+ }
+
+ /**
+ * Converts a positive value into an {@code int}.
+ *
+ * @param value
+ * to convert
+ * @return the value, or -1 if it wasn't a positive integral value
+ */
+ public static int positive(String value) {
+ if (value != null) {
+ try {
+ return Integer.parseUnsignedInt(value);
+ } catch (NumberFormatException e) {
+ // Ignore
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Converts a ssh config flag value (yes/true/on - no/false/off) into an
+ * {@code boolean}.
+ *
+ * @param value
+ * to convert
+ * @return {@code true} if {@code value}Â is "yes", "on", or "true";
+ * {@code false} otherwise
+ */
+ public static boolean flag(String value) {
+ if (value == null) {
+ return false;
+ }
+ return SshConstants.YES.equals(value) || SshConstants.ON.equals(value)
+ || SshConstants.TRUE.equals(value);
+ }
+
+ /**
+ * Retrieves the local user name as given in the constructor.
+ *
+ * @return the user name
+ */
+ public String getLocalUserName() {
+ return localUserName;
+ }
+
+ /**
+ * A host entry from the ssh config file. Any merging of global values and
+ * of several matching host entries, %-substitutions, and ~ replacement have
+ * all been done.
+ */
+ public static class HostEntry {
+
+ /**
+ * 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 TreeSet<>(
+ String.CASE_INSENSITIVE_ORDER);
+
+ static {
+ MULTI_KEYS.add(SshConstants.CERTIFICATE_FILE);
+ MULTI_KEYS.add(SshConstants.IDENTITY_FILE);
+ MULTI_KEYS.add(SshConstants.LOCAL_FORWARD);
+ MULTI_KEYS.add(SshConstants.REMOTE_FORWARD);
+ MULTI_KEYS.add(SshConstants.SEND_ENV);
+ }
+
+ /**
+ * 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 TreeSet<>(
+ String.CASE_INSENSITIVE_ORDER);
+
+ static {
+ LIST_KEYS.add(SshConstants.CANONICAL_DOMAINS);
+ LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE);
+ LIST_KEYS.add(SshConstants.SEND_ENV);
+ LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
+ }
+
+ private Map<String, String> options;
+
+ private Map<String, List<String>> multiOptions;
+
+ private Map<String, List<String>> listOptions;
+
+ /**
+ * Retrieves the value of a single-valued key, or the first is the key
+ * has multiple values. Keys are case-insensitive, so
+ * {@code getValue("HostName") == getValue("HOSTNAME")}.
+ *
+ * @param key
+ * to get the value of
+ * @return the value, or {@code null} if none
+ */
+ public String getValue(String key) {
+ String result = options != null ? options.get(key) : null;
+ if (result == null) {
+ // Let's be lenient and return at least the first value from
+ // a list-valued or multi-valued key.
+ List<String> values = listOptions != null ? listOptions.get(key)
+ : null;
+ if (values == null) {
+ values = multiOptions != null ? multiOptions.get(key)
+ : null;
+ }
+ if (values != null && !values.isEmpty()) {
+ result = values.get(0);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Retrieves the values of a multi or list-valued key. Keys are
+ * case-insensitive, so
+ * {@code getValue("HostName") == getValue("HOSTNAME")}.
+ *
+ * @param key
+ * to get the values of
+ * @return a possibly empty list of values
+ */
+ public List<String> getValues(String key) {
+ List<String> values = listOptions != null ? listOptions.get(key)
+ : null;
+ if (values == null) {
+ values = multiOptions != null ? multiOptions.get(key) : null;
+ }
+ if (values == null || values.isEmpty()) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(values);
+ }
+
+ /**
+ * Sets the value of a single-valued key if it not set yet, or adds a
+ * value to a multi-valued key. If the value is {@code null}, the key is
+ * removed altogether, whether it is single-, list-, or multi-valued.
+ *
+ * @param key
+ * to modify
+ * @param value
+ * to set or add
+ */
+ public void setValue(String key, String value) {
+ if (value == null) {
+ if (multiOptions != null) {
+ multiOptions.remove(key);
+ }
+ if (listOptions != null) {
+ listOptions.remove(key);
+ }
+ if (options != null) {
+ options.remove(key);
+ }
+ return;
+ }
+ if (MULTI_KEYS.contains(key)) {
+ if (multiOptions == null) {
+ multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ List<String> values = multiOptions.get(key);
+ if (values == null) {
+ values = new ArrayList<>(4);
+ multiOptions.put(key, values);
+ }
+ values.add(value);
+ } else {
+ if (options == null) {
+ options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ if (!options.containsKey(key)) {
+ options.put(key, value);
+ }
+ }
+ }
+
+ /**
+ * Sets the values of a multi- or list-valued key.
+ *
+ * @param key
+ * to set
+ * @param values
+ * a non-empty list of values
+ */
+ public void setValue(String key, List<String> values) {
+ if (values.isEmpty()) {
+ return;
+ }
+ // 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(key)) {
+ if (multiOptions == null) {
+ multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ List<String> items = multiOptions.get(key);
+ if (items == null) {
+ items = new ArrayList<>(values);
+ multiOptions.put(key, items);
+ } else {
+ items.addAll(values);
+ }
+ } else {
+ if (listOptions == null) {
+ listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ if (!listOptions.containsKey(key)) {
+ listOptions.put(key, values);
+ }
+ }
+ }
+
+ /**
+ * Does the key take a whitespace-separated list of values?
+ *
+ * @param key
+ * to check
+ * @return {@code true} if the key is a list-valued key.
+ */
+ public static boolean isListKey(String key) {
+ return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT));
+ }
+
+ 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 TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ 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 TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ 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 TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ 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 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;
+ }
+
+ void substitute(String originalHostName, int port, String userName,
+ String localUserName, File home) {
+ int p = port >= 0 ? port : positive(getValue(SshConstants.PORT));
+ if (p < 0) {
+ p = SshConstants.SSH_DEFAULT_PORT;
+ }
+ String u = userName != null && !userName.isEmpty() ? userName
+ : getValue(SshConstants.USER);
+ if (u == null || u.isEmpty()) {
+ u = localUserName;
+ }
+ Replacer r = new Replacer(originalHostName, p, u, localUserName,
+ home);
+ if (options != null) {
+ // HOSTNAME first
+ String hostName = options.get(SshConstants.HOST_NAME);
+ if (hostName == null || hostName.isEmpty()) {
+ options.put(SshConstants.HOST_NAME, originalHostName);
+ } else {
+ hostName = r.substitute(hostName, "h"); //$NON-NLS-1$
+ options.put(SshConstants.HOST_NAME, hostName);
+ r.update('h', hostName);
+ }
+ }
+ if (multiOptions != null) {
+ List<String> values = multiOptions
+ .get(SshConstants.IDENTITY_FILE);
+ if (values != null) {
+ values = substitute(values, "dhlru", r); //$NON-NLS-1$
+ values = replaceTilde(values, home);
+ multiOptions.put(SshConstants.IDENTITY_FILE, values);
+ }
+ values = multiOptions.get(SshConstants.CERTIFICATE_FILE);
+ if (values != null) {
+ values = substitute(values, "dhlru", r); //$NON-NLS-1$
+ values = replaceTilde(values, home);
+ multiOptions.put(SshConstants.CERTIFICATE_FILE, values);
+ }
+ }
+ if (listOptions != null) {
+ List<String> values = listOptions
+ .get(SshConstants.USER_KNOWN_HOSTS_FILE);
+ if (values != null) {
+ values = replaceTilde(values, home);
+ listOptions.put(SshConstants.USER_KNOWN_HOSTS_FILE, values);
+ }
+ }
+ if (options != null) {
+ // HOSTNAME already done above
+ String value = options.get(SshConstants.IDENTITY_AGENT);
+ if (value != null) {
+ value = r.substitute(value, "dhlru"); //$NON-NLS-1$
+ value = toFile(value, home).getPath();
+ options.put(SshConstants.IDENTITY_AGENT, value);
+ }
+ value = options.get(SshConstants.CONTROL_PATH);
+ if (value != null) {
+ value = r.substitute(value, "ChLlnpru"); //$NON-NLS-1$
+ value = toFile(value, home).getPath();
+ options.put(SshConstants.CONTROL_PATH, value);
+ }
+ value = options.get(SshConstants.LOCAL_COMMAND);
+ if (value != null) {
+ value = r.substitute(value, "CdhlnprTu"); //$NON-NLS-1$
+ options.put(SshConstants.LOCAL_COMMAND, value);
+ }
+ value = options.get(SshConstants.REMOTE_COMMAND);
+ if (value != null) {
+ value = r.substitute(value, "Cdhlnpru"); //$NON-NLS-1$
+ options.put(SshConstants.REMOTE_COMMAND, value);
+ }
+ value = options.get(SshConstants.PROXY_COMMAND);
+ if (value != null) {
+ value = r.substitute(value, "hpr"); //$NON-NLS-1$
+ options.put(SshConstants.PROXY_COMMAND, value);
+ }
+ }
+ // Match is not implemented and would need to be done elsewhere
+ // anyway.
+ }
+
+ /**
+ * Retrieves an unmodifiable map of all single-valued options, with
+ * case-insensitive lookup by keys.
+ *
+ * @return all single-valued options
+ */
+ @NonNull
+ public Map<String, String> getOptions() {
+ if (options == null) {
+ return Collections.emptyMap();
+ }
+ return Collections.unmodifiableMap(options);
+ }
+
+ /**
+ * Retrieves an unmodifiable map of all multi-valued options, with
+ * case-insensitive lookup by keys.
+ *
+ * @return all multi-valued options
+ */
+ @NonNull
+ public Map<String, List<String>> getMultiValuedOptions() {
+ if (listOptions == null && multiOptions == null) {
+ return Collections.emptyMap();
+ }
+ Map<String, List<String>> allValues = new TreeMap<>(
+ String.CASE_INSENSITIVE_ORDER);
+ if (multiOptions != null) {
+ allValues.putAll(multiOptions);
+ }
+ if (listOptions != null) {
+ allValues.putAll(listOptions);
+ }
+ return Collections.unmodifiableMap(allValues);
+ }
+
+ @Override
+ @SuppressWarnings("nls")
+ public String toString() {
+ return "HostEntry [options=" + options + ", multiOptions="
+ + multiOptions + ", listOptions=" + listOptions + "]";
+ }
+ }
+
+ private static class Replacer {
+ private final Map<Character, String> replacements = new HashMap<>();
+
+ public Replacer(String host, int port, String user,
+ String localUserName, File home) {
+ replacements.put(Character.valueOf('%'), "%"); //$NON-NLS-1$
+ replacements.put(Character.valueOf('d'), home.getPath());
+ 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'), host);
+ replacements.put(Character.valueOf('p'), Integer.toString(port));
+ replacements.put(Character.valueOf('r'), user == null ? "" : user); //$NON-NLS-1$
+ replacements.put(Character.valueOf('u'), localUserName);
+ replacements.put(Character.valueOf('C'),
+ substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
+ replacements.put(Character.valueOf('T'), "NONE"); //$NON-NLS-1$
+ }
+
+ public void update(char key, String value) {
+ replacements.put(Character.valueOf(key), value);
+ if ("lhpr".indexOf(key) >= 0) { //$NON-NLS-1$
+ 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();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @SuppressWarnings("nls")
+ public String toString() {
+ return "OpenSshConfig [home=" + home + ", configFile=" + configFile
+ + ", lastModified=" + lastModified + ", state=" + state + "]";
+ }
+}
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
-import static org.eclipse.jgit.transport.OpenSshConfig.SSH_PORT;
import java.io.File;
import java.io.FileInputStream;
}
private static String hostName(Session s) {
- if (s.getPort() == SSH_PORT) {
+ if (s.getPort() == SshConstants.SSH_DEFAULT_PORT) {
return s.getHost();
}
return String.format("[%s]:%d", s.getHost(), //$NON-NLS-1$
/*
- * Copyright (C) 2008, 2017, Google Inc.
+ * Copyright (C) 2008, 2018, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
package org.eclipse.jgit.transport;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
-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.TreeMap;
-import org.eclipse.jgit.errors.InvalidPatternException;
-import org.eclipse.jgit.fnmatch.FileNameMatcher;
-import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry;
import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.StringUtils;
-import org.eclipse.jgit.util.SystemReader;
import com.jcraft.jsch.ConfigRepository;
* <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
+ * This parser makes the critical options available to
* {@link org.eclipse.jgit.transport.SshSessionFactory} via
* {@link org.eclipse.jgit.transport.OpenSshConfig.Host} objects returned by
* {@link #lookup(String)}, and implements a fully conforming
* {@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 org.eclipse.jgit.transport.OpenSshConfig.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>
+ *
+ * @see OpenSshConfigFile
*/
public class OpenSshConfig implements ConfigRepository {
- /** IANA assigned port number for SSH. */
- static final int SSH_PORT = 22;
-
/**
* Obtain the user's configuration data.
* <p>
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 + "]";
- }
+ final File config = new File(new File(home, SshConstants.SSH_DIR),
+ SshConstants.CONFIG);
+ return new OpenSshConfig(home, config);
}
- /** State read from the config file, plus {@link Host}s created from it. */
- private State state;
+ /** The base file. */
+ private OpenSshConfigFile configFile;
OpenSshConfig(File h, File cfg) {
- home = h;
- configFile = cfg;
- state = new State();
+ configFile = new OpenSshConfigFile(h, cfg,
+ SshSessionFactory.getLocalUserName());
}
/**
* @return r configuration for the requested name. Never null.
*/
public Host lookup(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 (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(InputStream in)
- throws IOException {
- final Map<String, HostEntry> m = new LinkedHashMap<>();
- final BufferedReader br = new BufferedReader(
- new InputStreamReader(in, UTF_8));
- 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(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(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(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(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[0]);
- }
-
- 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 + "]";
- }
+ HostEntry entry = configFile.lookup(hostName, -1, null);
+ return new Host(entry, hostName, configFile.getLocalUserName());
}
/**
int connectionAttempts;
+ private HostEntry entry;
+
private Config config;
+ // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
+ // to ssh-config keys.
+ private static final Map<String, String> KEY_MAP = new TreeMap<>(
+ String.CASE_INSENSITIVE_ORDER);
+
+ static {
+ KEY_MAP.put("kex", SshConstants.KEX_ALGORITHMS); //$NON-NLS-1$
+ KEY_MAP.put("server_host_key", SshConstants.HOST_KEY_ALGORITHMS); //$NON-NLS-1$
+ KEY_MAP.put("cipher.c2s", SshConstants.CIPHERS); //$NON-NLS-1$
+ KEY_MAP.put("cipher.s2c", SshConstants.CIPHERS); //$NON-NLS-1$
+ KEY_MAP.put("mac.c2s", SshConstants.MACS); //$NON-NLS-1$
+ KEY_MAP.put("mac.s2c", SshConstants.MACS); //$NON-NLS-1$
+ KEY_MAP.put("compression.s2c", SshConstants.COMPRESSION); //$NON-NLS-1$
+ KEY_MAP.put("compression.c2s", SshConstants.COMPRESSION); //$NON-NLS-1$
+ KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$
+ KEY_MAP.put("MaxAuthTries", //$NON-NLS-1$
+ SshConstants.NUMBER_OF_PASSWORD_PROMPTS);
+ }
+
+ private static String mapKey(String key) {
+ String k = KEY_MAP.get(key);
+ return k != null ? k : key;
+ }
+
/**
* Creates a new uninitialized {@link Host}.
*/
// For API backwards compatibility with pre-4.9 JGit
}
- Host(Config config, String hostName, File homeDir) {
- this.config = config;
- complete(hostName, homeDir);
+ Host(HostEntry entry, String hostName, String localUserName) {
+ this.entry = entry;
+ complete(hostName, localUserName);
}
/**
}
- private void complete(String initialHostName, File homeDir) {
+ private void complete(String initialHostName, String localUserName) {
// Try to set values from the options.
- hostName = config.getHostname();
- user = config.getUser();
- port = config.getPort();
+ hostName = entry.getValue(SshConstants.HOST_NAME);
+ user = entry.getValue(SshConstants.USER);
+ port = positive(entry.getValue(SshConstants.PORT));
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);
- }
+ entry.getValue(SshConstants.CONNECTION_ATTEMPTS));
+ strictHostKeyChecking = entry
+ .getValue(SshConstants.STRICT_HOST_KEY_CHECKING);
+ batchMode = Boolean.valueOf(OpenSshConfigFile
+ .flag(entry.getValue(SshConstants.BATCH_MODE)));
+ preferredAuthentications = entry
+ .getValue(SshConstants.PREFERRED_AUTHENTICATIONS);
// Fill in defaults if still not set
- if (hostName == null) {
+ if (hostName == null || hostName.isEmpty()) {
hostName = initialHostName;
}
- if (user == null) {
- user = OpenSshConfig.userName();
+ if (user == null || user.isEmpty()) {
+ user = localUserName;
}
if (port <= 0) {
- port = OpenSshConfig.SSH_PORT;
+ port = SshConstants.SSH_DEFAULT_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);
+ List<String> identityFiles = entry
+ .getValues(SshConstants.IDENTITY_FILE);
+ if (identityFiles != null && !identityFiles.isEmpty()) {
+ identityFile = new File(identityFiles.get(0));
}
}
Config getConfig() {
+ if (config == null) {
+ config = new Config() {
+
+ @Override
+ public String getHostname() {
+ return Host.this.getHostName();
+ }
+
+ @Override
+ public String getUser() {
+ return Host.this.getUser();
+ }
+
+ @Override
+ public int getPort() {
+ return Host.this.getPort();
+ }
+
+ @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$
+ if (!OpenSshConfigFile.flag(
+ Host.this.entry.getValue(mapKey(key)))) {
+ return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$
+ }
+ return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$
+ }
+ return Host.this.entry.getValue(mapKey(key));
+ }
+
+ @Override
+ public String[] getValues(String key) {
+ List<String> values = Host.this.entry
+ .getValues(mapKey(key));
+ if (values == null) {
+ return new String[0];
+ }
+ return values.toArray(new String[0]);
+ }
+ };
+ }
return config;
}
+ ", preferredAuthentications=" + preferredAuthentications
+ ", batchMode=" + batchMode + ", strictHostKeyChecking="
+ strictHostKeyChecking + ", connectionAttempts="
- + connectionAttempts + ", config=" + config + "]";
+ + connectionAttempts + ", entry=" + entry + "]";
}
}
/** {@inheritDoc} */
@Override
- @SuppressWarnings("nls")
public String toString() {
- return "OpenSshConfig [home=" + home + ", configFile=" + configFile
- + ", lastModified=" + lastModified + ", state=" + state + "]";
+ return "OpenSshConfig [configFile=" + configFile + ']'; //$NON-NLS-1$
}
}
--- /dev/null
+/*
+ * Copyright (C) 2018 Thomas Wolf <thomas.wolf@paranor.ch>
+ * 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 org.eclipse.jgit.lib.Constants;
+
+/**
+ * Constants relating to ssh.
+ *
+ * @since 5.2
+ */
+@SuppressWarnings("nls")
+public final class SshConstants {
+
+ private SshConstants() {
+ // No instances, please.
+ }
+
+ /** IANA assigned port number for ssh. */
+ public static final int SSH_DEFAULT_PORT = 22;
+
+ /** URI scheme for ssh. */
+ public static final String SSH_SCHEME = "ssh";
+
+ /** URI scheme for sftp. */
+ public static final String SFTP_SCHEME = "sftp";
+
+ /** Default name for a ssh directory. */
+ public static final String SSH_DIR = ".ssh";
+
+ /** Name of the ssh config file. */
+ public static final String CONFIG = Constants.CONFIG;
+
+ /** Default name of the user "known hosts" file. */
+ public static final String KNOWN_HOSTS = "known_hosts";
+
+ // Config file keys
+
+ /** Key in an ssh config file. */
+ public static final String BATCH_MODE = "BatchMode";
+
+ /** Key in an ssh config file. */
+ public static final String CANONICAL_DOMAINS = "CanonicalDomains";
+
+ /** Key in an ssh config file. */
+ public static final String CERTIFICATE_FILE = "CertificateFile";
+
+ /** Key in an ssh config file. */
+ public static final String CIPHERS = "Ciphers";
+
+ /** Key in an ssh config file. */
+ public static final String COMPRESSION = "Compression";
+
+ /** Key in an ssh config file. */
+ public static final String CONNECTION_ATTEMPTS = "ConnectionAttempts";
+
+ /** Key in an ssh config file. */
+ public static final String CONTROL_PATH = "ControlPath";
+
+ /** Key in an ssh config file. */
+ public static final String GLOBAL_KNOWN_HOSTS_FILE = "GlobalKnownHostsFile";
+
+ /** Key in an ssh config file. */
+ public static final String HOST = "Host";
+
+ /** Key in an ssh config file. */
+ public static final String HOST_KEY_ALGORITHMS = "HostKeyAlgorithms";
+
+ /** Key in an ssh config file. */
+ public static final String HOST_NAME = "HostName";
+
+ /** Key in an ssh config file. */
+ public static final String IDENTITIES_ONLY = "IdentitiesOnly";
+
+ /** Key in an ssh config file. */
+ public static final String IDENTITY_AGENT = "IdentityAgent";
+
+ /** Key in an ssh config file. */
+ public static final String IDENTITY_FILE = "IdentityFile";
+
+ /** Key in an ssh config file. */
+ public static final String KEX_ALGORITHMS = "KexAlgorithms";
+
+ /** Key in an ssh config file. */
+ public static final String LOCAL_COMMAND = "LocalCommand";
+
+ /** Key in an ssh config file. */
+ public static final String LOCAL_FORWARD = "LocalForward";
+
+ /** Key in an ssh config file. */
+ public static final String MACS = "MACs";
+
+ /** Key in an ssh config file. */
+ public static final String NUMBER_OF_PASSWORD_PROMPTS = "NumberOfPasswordPrompts";
+
+ /** Key in an ssh config file. */
+ public static final String PORT = "Port";
+
+ /** Key in an ssh config file. */
+ public static final String PREFERRED_AUTHENTICATIONS = "PreferredAuthentications";
+
+ /** Key in an ssh config file. */
+ public static final String PROXY_COMMAND = "ProxyCommand";
+
+ /** Key in an ssh config file. */
+ public static final String REMOTE_COMMAND = "RemoteCommand";
+
+ /** Key in an ssh config file. */
+ public static final String REMOTE_FORWARD = "RemoteForward";
+
+ /** Key in an ssh config file. */
+ public static final String SEND_ENV = "SendEnv";
+
+ /** Key in an ssh config file. */
+ public static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
+
+ /** Key in an ssh config file. */
+ public static final String USER = "User";
+
+ /** Key in an ssh config file. */
+ public static final String USER_KNOWN_HOSTS_FILE = "UserKnownHostsFile";
+
+ // Values
+
+ /** Flag value. */
+ public static final String YES = "yes";
+
+ /** Flag value. */
+ public static final String ON = "on";
+
+ /** Flag value. */
+ public static final String TRUE = "true";
+
+ /** Flag value. */
+ public static final String NO = "no";
+
+ /** Flag value. */
+ public static final String OFF = "off";
+
+ /** Flag value. */
+ public static final String FALSE = "false";
+
+ // Default identity file names
+
+ /** Name of the default RSA private identity file. */
+ public static final String ID_RSA = "id_rsa";
+
+ /** Name of the default DSA private identity file. */
+ public static final String ID_DSA = "id_dsa";
+
+ /** Name of the default ECDSA private identity file. */
+ public static final String ID_ECDSA = "id_ecdsa";
+
+ /** Name of the default ECDSA private identity file. */
+ public static final String ID_ED25519 = "id_ed25519";
+
+ /** All known default identity file names. */
+ public static final String[] DEFAULT_IDENTITIES = { //
+ ID_RSA, ID_DSA, ID_ECDSA // , ID_ED25519 // not yet...
+ };
+}
package org.eclipse.jgit.transport;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
/**
* Creates and destroys SSH connections to a remote system.
INSTANCE = new DefaultSshSessionFactory();
}
+ /**
+ * Retrieves the local user name as defined by the system property
+ * "user.name".
+ *
+ * @return the user name
+ * @since 5.2
+ */
+ public static String getLocalUserName() {
+ return AccessController.doPrivileged(new PrivilegedAction<String>() {
+ @Override
+ public String run() {
+ return SystemReader.getInstance()
+ .getProperty(Constants.OS_USER_NAME_KEY);
+ }
+ });
+ }
+
/**
* Open (or reuse) a session to a host.
* <p>
* A reasonable UserInfo that can interact with the end-user (if necessary)
* is installed on the returned session by this method.
* <p>
- * The caller must connect the session by invoking <code>connect()</code>
- * if it has not already been connected.
+ * The caller must connect the session by invoking <code>connect()</code> if
+ * it has not already been connected.
*
* @param uri
* URI information about the remote host
* @param credentialsProvider
* provider to support authentication, may be null.
* @param fs
- * the file system abstraction which will be necessary to
- * perform certain file system operations.
+ * the file system abstraction which will be necessary to perform
+ * certain file system operations.
* @param tms
* Timeout value, in milliseconds.
* @return a session that can contact the remote host.