diff options
author | Thomas Wolf <thomas.wolf@paranor.ch> | 2017-06-10 14:26:32 +0200 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2017-08-26 01:44:36 +0200 |
commit | c758a8cd37b71851bd71a5e558abc218c8082164 (patch) | |
tree | 486c3231fdb17a21bf8f294d88e74ff2033fa135 | |
parent | 9d2447063de3bdad6f68aa912d31f3934f1cebc5 (diff) | |
download | jgit-c758a8cd37b71851bd71a5e558abc218c8082164.tar.gz jgit-c758a8cd37b71851bd71a5e558abc218c8082164.zip |
Do most %-token substitutions in OpenSshConfig
Except for %p and %r and partially %C, we can do token substitutions
as defined by OpenSSH inside the config file parser. %p and %r can
be replaced only if specified in the config; if not, it would be the
caller's responsibility to replace them with values obtained from the
URI to connect to.
Jsch doesn't know about token substitutions at all. By doing the
replacements as good as we can in the config file parser, we can
make Jsch support most of these tokens.
%i is not handled at all as Java has no concept of a "user ID".
Includes unit tests.
Bug: 496170
Change-Id: If9d324090707de5d50c740b0d4455aefa8db46ee
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
-rw-r--r-- | org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java | 43 | ||||
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java | 134 |
2 files changed, 171 insertions, 6 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java index 5eccededf8..3eb049758e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java @@ -61,6 +61,7 @@ import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.OpenSshConfig.Host; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.SystemReader; import org.junit.Before; import org.junit.Test; @@ -84,7 +85,7 @@ public class OpenSshConfigTest extends RepositoryTestCase { configFile = new File(new File(home, ".ssh"), Constants.CONFIG); FileUtils.mkdir(configFile.getParentFile()); - System.setProperty("user.name", "jex_junit"); + mockSystemReader.setProperty(Constants.OS_USER_NAME_KEY, "jex_junit"); osc = new OpenSshConfig(home, configFile); } @@ -444,4 +445,44 @@ public class OpenSshConfigTest extends RepositoryTestCase { assertNull(h.getIdentityFile()); assertNull(h.getConfig().getValue("ForwardX11")); } + + @Test + public void testHomeDirUserReplacement() throws Exception { + config("Host=orcz\n\tIdentityFile %d/.ssh/%u_id_dsa"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals(new File(new File(home, ".ssh"), "jex_junit_id_dsa"), + h.getIdentityFile()); + } + + @Test + public void testHostnameReplacement() throws Exception { + config("Host=orcz\nHost *.*\n\tHostname %h\nHost *\n\tHostname %h.example.org"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals("orcz.example.org", h.getHostName()); + } + + @Test + public void testRemoteUserReplacement() throws Exception { + config("Host=orcz\n\tUser foo\n" + "Host *.*\n\tHostname %h\n" + + "Host *\n\tHostname %h.ex%%20ample.org\n\tIdentityFile ~/.ssh/%h_%r_id_dsa"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals( + new File(new File(home, ".ssh"), + "orcz.ex%20ample.org_foo_id_dsa"), + h.getIdentityFile()); + } + + @Test + public void testLocalhostFQDNReplacement() throws Exception { + String localhost = SystemReader.getInstance().getHostname(); + config("Host=orcz\n\tIdentityFile ~/.ssh/%l_id_dsa"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals( + new File(new File(home, ".ssh"), localhost + "_id_dsa"), + h.getIdentityFile()); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java index ad79f3ebea..2c547afea6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java @@ -65,6 +65,7 @@ import org.eclipse.jgit.fnmatch.FileNameMatcher; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.SystemReader; import com.jcraft.jsch.ConfigRepository; @@ -94,15 +95,38 @@ import com.jcraft.jsch.ConfigRepository; * </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> + * <p> * Note that OpenSSH's readconf.c is a validating parser; Jsch's * ConfigRepository OTOH treats all option values as plain strings, so any * validation must happen in Jsch outside of the parser. Thus this parser does * not validate option values, except for a few options when constructing a * {@link Host} object. + * </p> + * <p> + * This config does %-substitutions for the following tokens: + * </p> + * <ul> + * <li>%% - single % + * <li>%C - short-hand for %l%h%p%r. See %p and %r below; the replacement may be + * done partially only and may leave %p or %r or both unreplaced. + * <li>%d - home directory path + * <li>%h - remote host name + * <li>%L - local host name without domain + * <li>%l - FQDN of the local host + * <li>%n - host name as specified in {@link #lookup(String)} + * <li>%p - port number; replaced only if set in the config + * <li>%r - remote user name; replaced only if set in the config + * <li>%u - local user name + * </ul> + * <p> + * If the config doesn't set the port or the remote user name, %p and %r remain + * un-substituted. It's the caller's responsibility to replace them with values + * obtained from the connection URI. %i is not handled; Java has no concept of a + * "user ID". + * </p> */ public class OpenSshConfig implements ConfigRepository { @@ -185,6 +209,7 @@ public class OpenSshConfig implements ConfigRepository { fullConfig.merge(e.getValue()); } } + fullConfig.substitute(hostName, home); h = new Host(fullConfig, hostName, home); cache.hosts.put(hostName, h); return h; @@ -336,7 +361,8 @@ public class OpenSshConfig implements ConfigRepository { return AccessController.doPrivileged(new PrivilegedAction<String>() { @Override public String run() { - return System.getProperty("user.name"); //$NON-NLS-1$ + return SystemReader.getInstance() + .getProperty(Constants.OS_USER_NAME_KEY); } }); } @@ -562,12 +588,12 @@ public class OpenSshConfig implements ConfigRepository { continue; } if (argument.charAt(start) == '"') { - int stop = argument.indexOf('"', start + 1); - if (stop <= start) { + int stop = argument.indexOf('"', ++start); + if (stop < start) { // No closing double quote: skip break; } - result.add(argument.substring(start + 1, stop)); + result.add(argument.substring(start, stop)); start = stop + 1; } else { int stop = start + 1; @@ -626,6 +652,104 @@ public class OpenSshConfig implements ConfigRepository { } } } + + 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; + } + + 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$ + 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$ + multiOptions.put("CERTIFICATEFILE", 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$ + 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. + } } /** |