|
|
@@ -1,5 +1,5 @@ |
|
|
|
/* |
|
|
|
* 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 |
|
|
@@ -46,17 +46,19 @@ package org.eclipse.jgit.transport; |
|
|
|
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; |
|
|
@@ -64,14 +66,46 @@ import org.eclipse.jgit.lib.Constants; |
|
|
|
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; |
|
|
|
|
|
|
@@ -105,16 +139,25 @@ public class OpenSshConfig { |
|
|
|
/** 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(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
@@ -127,75 +170,80 @@ public class OpenSshConfig { |
|
|
|
* @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); |
|
|
@@ -206,57 +254,18 @@ public class OpenSshConfig { |
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@@ -264,23 +273,35 @@ public class OpenSshConfig { |
|
|
|
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; |
|
|
|
} |
|
|
@@ -300,13 +321,15 @@ public class OpenSshConfig { |
|
|
|
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() { |
|
|
@@ -318,6 +341,293 @@ public class OpenSshConfig { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
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> |
|
|
@@ -330,8 +640,6 @@ public class OpenSshConfig { |
|
|
|
* already merged into this block. |
|
|
|
*/ |
|
|
|
public static class Host { |
|
|
|
boolean patternsApplied; |
|
|
|
|
|
|
|
String hostName; |
|
|
|
|
|
|
|
int port; |
|
|
@@ -348,23 +656,18 @@ public class OpenSshConfig { |
|
|
|
|
|
|
|
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); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
@@ -432,5 +735,71 @@ public class OpenSshConfig { |
|
|
|
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(); |
|
|
|
} |
|
|
|
} |