From c758a8cd37b71851bd71a5e558abc218c8082164 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Sat, 10 Jun 2017 14:26:32 +0200 Subject: [PATCH] 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 --- .../jgit/transport/OpenSshConfigTest.java | 43 +++++- .../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 5eccededf..3eb049758 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.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 void setUp() throws Exception { 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 void testMissingArgument() throws Exception { 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 ad79f3ebe..2c547afea 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.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 @@ *

* + *

* 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. + *

+ *

+ * This config does %-substitutions for the following tokens: + *

+ * + *

+ * 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". + *

*/ public class OpenSshConfig implements ConfigRepository { @@ -185,6 +209,7 @@ public Host lookup(final String hostName) { 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 @@ static String userName() { return AccessController.doPrivileged(new PrivilegedAction() { @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 static List parseList(String argument) { 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 @@ protected void merge(HostEntry entry) { } } } + + private class Replacer { + private final Map 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 substitute(List values, String allowed, + Replacer r) { + List 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 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. + } } /**