/*
- * Copyright (C) 2008, 2014 Google Inc.
+ * 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
package org.eclipse.jgit.transport;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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;
import org.junit.Before;
import org.junit.Test;
+import com.jcraft.jsch.ConfigRepository;
+
public class OpenSshConfigTest extends RepositoryTestCase {
private File home;
}
private void config(final String data) throws IOException {
- final OutputStreamWriter fw = new OutputStreamWriter(
- new FileOutputStream(configFile), "UTF-8");
- fw.write(data);
- fw.close();
+ long lastMtime = configFile.lastModified();
+ do {
+ try (final OutputStreamWriter fw = new OutputStreamWriter(
+ new FileOutputStream(configFile), "UTF-8")) {
+ fw.write(data);
+ }
+ } while (lastMtime == configFile.lastModified());
}
@Test
@Test
public void testAlias_DoesNotMatch() throws Exception {
- config("Host orcz\n" + "\tHostName repo.or.cz\n");
+ config("Host orcz\n" + "Port 29418\n" + "\tHostName repo.or.cz\n");
final Host h = osc.lookup("repo.or.cz");
assertNotNull(h);
assertEquals("repo.or.cz", h.getHostName());
assertEquals("jex_junit", h.getUser());
assertEquals(22, h.getPort());
assertNull(h.getIdentityFile());
+ final Host h2 = osc.lookup("orcz");
+ assertEquals("repo.or.cz", h.getHostName());
+ assertEquals("jex_junit", h.getUser());
+ assertEquals(29418, h2.getPort());
+ assertNull(h.getIdentityFile());
}
@Test
assertNotNull(h);
assertEquals(1, h.getConnectionAttempts());
}
+
+ @Test
+ public void testDefaultBlock() throws Exception {
+ config("ConnectionAttempts 5\n\nHost orcz\nConnectionAttempts 3\n");
+ final Host h = osc.lookup("orcz");
+ assertNotNull(h);
+ assertEquals(5, h.getConnectionAttempts());
+ }
+
+ @Test
+ public void testHostCaseInsensitive() throws Exception {
+ config("hOsT orcz\nConnectionAttempts 3\n");
+ final Host h = osc.lookup("orcz");
+ assertNotNull(h);
+ assertEquals(3, h.getConnectionAttempts());
+ }
+
+ @Test
+ public void testListValueSingle() throws Exception {
+ config("Host orcz\nUserKnownHostsFile /foo/bar\n");
+ final ConfigRepository.Config c = osc.getConfig("orcz");
+ assertNotNull(c);
+ assertEquals("/foo/bar", c.getValue("UserKnownHostsFile"));
+ }
+
+ @Test
+ public void testListValueMultiple() throws Exception {
+ // Tilde expansion doesn't occur within the parser
+ config("Host orcz\nUserKnownHostsFile \"~/foo/ba z\" /foo/bar \n");
+ final ConfigRepository.Config c = osc.getConfig("orcz");
+ assertNotNull(c);
+ assertArrayEquals(new Object[] { "~/foo/ba z", "/foo/bar" },
+ 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");
+ final Host h1 = osc.lookup("orcz");
+ assertNotNull(h1);
+ assertEquals(1, h1.getConnectionAttempts());
+ config("Host orcz\n" + "\tConnectionAttempts 5\n");
+ final Host h2 = osc.lookup("orcz");
+ assertNotNull(h2);
+ assertNotSame(h1, h2);
+ assertEquals(5, h2.getConnectionAttempts());
+ assertEquals(1, h1.getConnectionAttempts());
+ assertNotSame(h1.getConfig(), h2.getConfig());
+ }
+
+ @Test
+ public void testIdentityFile() throws Exception {
+ config("Host orcz\nIdentityFile \"~/foo/ba z\"\nIdentityFile /foo/bar");
+ final Host h = osc.lookup("orcz");
+ assertNotNull(h);
+ File f = h.getIdentityFile();
+ assertNotNull(f);
+ // Host does tilde replacement
+ assertEquals(new File(home, "foo/ba z"), f);
+ final ConfigRepository.Config c = h.getConfig();
+ // Config doesn't
+ assertArrayEquals(new Object[] { "~/foo/ba z", "/foo/bar" },
+ c.getValues("IdentityFile"));
+ }
+
+ @Test
+ public void testMultiIdentityFile() throws Exception {
+ config("IdentityFile \"~/foo/ba z\"\nHost orcz\nIdentityFile /foo/bar\nHOST *\nIdentityFile /foo/baz");
+ final Host h = osc.lookup("orcz");
+ assertNotNull(h);
+ File f = h.getIdentityFile();
+ assertNotNull(f);
+ // Host does tilde replacement
+ assertEquals(new File(home, "foo/ba z"), f);
+ final ConfigRepository.Config c = h.getConfig();
+ // Config doesn't
+ assertArrayEquals(new Object[] { "~/foo/ba z", "/foo/bar", "/foo/baz" },
+ c.getValues("IdentityFile"));
+ }
+
+ @Test
+ public void testNegatedPattern() throws Exception {
+ config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST !*.or.cz\nIdentityFile /foo/baz");
+ final Host h = osc.lookup("repo.or.cz");
+ assertNotNull(h);
+ assertEquals(new File(home, "foo/bar"), h.getIdentityFile());
+ assertArrayEquals(new Object[] { "~/foo/bar" },
+ h.getConfig().getValues("IdentityFile"));
+ }
+
+ @Test
+ public void testPattern() throws Exception {
+ config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile /foo/baz");
+ final Host h = osc.lookup("repo.or.cz");
+ assertNotNull(h);
+ assertEquals(new File(home, "foo/bar"), h.getIdentityFile());
+ assertArrayEquals(new Object[] { "~/foo/bar", "/foo/baz" },
+ h.getConfig().getValues("IdentityFile"));
+ }
+
+ @Test
+ public void testMultiHost() throws Exception {
+ config("Host orcz *.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile /foo/baz");
+ final Host h1 = osc.lookup("repo.or.cz");
+ assertNotNull(h1);
+ assertEquals(new File(home, "foo/bar"), h1.getIdentityFile());
+ assertArrayEquals(new Object[] { "~/foo/bar", "/foo/baz" },
+ h1.getConfig().getValues("IdentityFile"));
+ final Host h2 = osc.lookup("orcz");
+ assertNotNull(h2);
+ assertEquals(new File(home, "foo/bar"), h2.getIdentityFile());
+ assertArrayEquals(new Object[] { "~/foo/bar" },
+ h2.getConfig().getValues("IdentityFile"));
+ }
+
+ @Test
+ public void testEqualsSign() throws Exception {
+ config("Host=orcz\n\tConnectionAttempts = 5\n\tUser=\t foobar\t\n");
+ final Host h = osc.lookup("orcz");
+ assertNotNull(h);
+ assertEquals(5, h.getConnectionAttempts());
+ assertEquals("foobar", h.getUser());
+ }
+
+ @Test
+ public void testMissingArgument() throws Exception {
+ config("Host=orcz\n\tSendEnv\nIdentityFile\t\nForwardX11\n\tUser=\t foobar\t\n");
+ final Host h = osc.lookup("orcz");
+ assertNotNull(h);
+ assertEquals("foobar", h.getUser());
+ assertArrayEquals(new String[0], h.getConfig().getValues("SendEnv"));
+ assertNull(h.getIdentityFile());
+ assertNull(h.getConfig().getValue("ForwardX11"));
+ }
}
/*
- * Copyright (C) 2008, 2014, Google Inc.
+ * 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
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
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.Collections;
+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 org.eclipse.jgit.errors.InvalidPatternException;
import org.eclipse.jgit.fnmatch.FileNameMatcher;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.StringUtils;
+import com.jcraft.jsch.ConfigRepository;
+
/**
- * Simple configuration parser for the OpenSSH ~/.ssh/config file.
+ * 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>
- * Since JSch does not (currently) have the ability to parse an OpenSSH
- * configuration file this is a simple parser to read that file and make the
- * critical options available to {@link SshSessionFactory}.
+ * 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 %-substitutions.
+ * <li>This parser does not do host name canonicalization (Jsch ignores it
+ * anyway).
+ * </ul>
+ * 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.
*/
-public class OpenSshConfig {
+public class OpenSshConfig implements ConfigRepository {
+
/** IANA assigned port number for SSH. */
static final int SSH_PORT = 22;
/** The .ssh/config file we read and monitor for updates. */
private final File configFile;
- /** Modification time of {@link #configFile} when {@link #hosts} loaded. */
+ /** Modification time of {@link #configFile} when it was last loaded. */
private long lastModified;
- /** Cached entries read out of the configuration file. */
- private Map<String, Host> hosts;
+ /**
+ * 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<>();
+ }
+
+ /** 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;
- hosts = Collections.emptyMap();
+ state = new State();
}
/**
* @return r configuration for the requested name. Never null.
*/
public Host lookup(final String hostName) {
- final Map<String, Host> cache = refresh();
- Host h = cache.get(hostName);
- if (h == null)
- h = new Host();
- if (h.patternsApplied)
+ final State cache = refresh();
+ Host h = cache.hosts.get(hostName);
+ if (h != null) {
return h;
-
- for (final Map.Entry<String, Host> e : cache.entrySet()) {
- if (!isHostPattern(e.getKey()))
- continue;
- if (!isHostMatch(e.getKey(), hostName))
- continue;
- h.copyFrom(e.getValue());
- }
-
- if (h.hostName == null)
- h.hostName = hostName;
- if (h.user == null)
- h.user = OpenSshConfig.userName();
- if (h.port == 0)
- h.port = OpenSshConfig.SSH_PORT;
- if (h.connectionAttempts == 0)
- h.connectionAttempts = 1;
- h.patternsApplied = true;
+ }
+ 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());
+ }
+ }
+ h = new Host(fullConfig, hostName, home);
+ cache.hosts.put(hostName, h);
return h;
}
- private synchronized Map<String, Host> refresh() {
+ private synchronized State refresh() {
final long mtime = configFile.lastModified();
if (mtime != lastModified) {
- try {
- final FileInputStream in = new FileInputStream(configFile);
- try {
- hosts = parse(in);
- } finally {
- in.close();
- }
- } catch (FileNotFoundException none) {
- hosts = Collections.emptyMap();
- } catch (IOException err) {
- hosts = Collections.emptyMap();
+ 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 hosts;
+ return state;
}
- private Map<String, Host> parse(final InputStream in) throws IOException {
- final Map<String, Host> m = new LinkedHashMap<>();
+ 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<Host> current = new ArrayList<>(4);
+ 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.length() == 0 || line.startsWith("#")) //$NON-NLS-1$
+ if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$
continue;
-
- final String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
- final String keyword = parts[0].trim();
- final String argValue = parts[1].trim();
+ }
+ 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 (final String pattern : argValue.split("[ \t]")) { //$NON-NLS-1$
- final String name = dequote(pattern);
- Host c = m.get(name);
+ 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 Host();
+ c = new HostEntry();
m.put(name, c);
}
current.add(c);
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 (StringUtils.equalsIgnoreCase("HostName", keyword)) { //$NON-NLS-1$
- for (final Host c : current)
- if (c.hostName == null)
- c.hostName = dequote(argValue);
- } else if (StringUtils.equalsIgnoreCase("User", keyword)) { //$NON-NLS-1$
- for (final Host c : current)
- if (c.user == null)
- c.user = dequote(argValue);
- } else if (StringUtils.equalsIgnoreCase("Port", keyword)) { //$NON-NLS-1$
- try {
- final int port = Integer.parseInt(dequote(argValue));
- for (final Host c : current)
- if (c.port == 0)
- c.port = port;
- } catch (NumberFormatException nfe) {
- // Bad port number. Don't set it.
+ if (HostEntry.isListKey(keyword)) {
+ List<String> args = HostEntry.parseList(argValue);
+ for (HostEntry entry : current) {
+ entry.setValue(keyword, args);
}
- } else if (StringUtils.equalsIgnoreCase("IdentityFile", keyword)) { //$NON-NLS-1$
- for (final Host c : current)
- if (c.identityFile == null)
- c.identityFile = toFile(dequote(argValue));
- } else if (StringUtils.equalsIgnoreCase(
- "PreferredAuthentications", keyword)) { //$NON-NLS-1$
- for (final Host c : current)
- if (c.preferredAuthentications == null)
- c.preferredAuthentications = nows(dequote(argValue));
- } else if (StringUtils.equalsIgnoreCase("BatchMode", keyword)) { //$NON-NLS-1$
- for (final Host c : current)
- if (c.batchMode == null)
- c.batchMode = yesno(dequote(argValue));
- } else if (StringUtils.equalsIgnoreCase(
- "StrictHostKeyChecking", keyword)) { //$NON-NLS-1$
- String value = dequote(argValue);
- for (final Host c : current)
- if (c.strictHostKeyChecking == null)
- c.strictHostKeyChecking = value;
- } else if (StringUtils.equalsIgnoreCase(
- "ConnectionAttempts", keyword)) { //$NON-NLS-1$
- try {
- final int connectionAttempts = Integer.parseInt(dequote(argValue));
- if (connectionAttempts > 0) {
- for (final Host c : current)
- if (c.connectionAttempts == 0)
- c.connectionAttempts = connectionAttempts;
- }
- } catch (NumberFormatException nfe) {
- // ignore bad values
+ } else if (!argValue.isEmpty()) {
+ argValue = dequote(argValue);
+ for (HostEntry entry : current) {
+ entry.setValue(keyword, argValue);
}
}
}
return m;
}
- private static boolean isHostPattern(final String s) {
- return s.indexOf('*') >= 0 || s.indexOf('?') >= 0;
+ 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 isHostMatch(final String pattern, final String name) {
- final FileNameMatcher fn;
- try {
- fn = new FileNameMatcher(pattern, null);
- } catch (InvalidPatternException e) {
- return false;
+ 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);
}
- fn.append(name);
- return fn.isMatch();
}
private static String dequote(final String value) {
- if (value.startsWith("\"") && value.endsWith("\"")) //$NON-NLS-1$ //$NON-NLS-2$
+ if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$
+ && value.length() > 1)
return value.substring(1, value.length() - 1);
return value;
}
return Boolean.FALSE;
}
- private File toFile(final String path) {
- 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() {
});
}
+ 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 + 1);
+ if (stop <= start) {
+ // No closing double quote: skip
+ break;
+ }
+ result.add(argument.substring(start + 1, 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());
+ }
+ }
+ }
+ }
+ }
+
/**
* Configuration of one "Host" block in the configuration file.
* <p>
* already merged into this block.
*/
public static class Host {
- boolean patternsApplied;
-
String hostName;
int port;
int connectionAttempts;
- void copyFrom(final Host src) {
- if (hostName == null)
- hostName = src.hostName;
- if (port == 0)
- port = src.port;
- if (identityFile == null)
- identityFile = src.identityFile;
- if (user == null)
- user = src.user;
- if (preferredAuthentications == null)
- preferredAuthentications = src.preferredAuthentications;
- if (batchMode == null)
- batchMode = src.batchMode;
- if (strictHostKeyChecking == null)
- strictHostKeyChecking = src.strictHostKeyChecking;
- if (connectionAttempts == 0)
- connectionAttempts = src.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);
}
/**
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);
+ }
+ }
+
+ private 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);
+ }
+
+ Config getConfig() {
+ return config;
+ }
+ }
+
+ /**
+ * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config}
+ * for the given host name.
+ *
+ * @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 host.getConfig();
}
}