summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Wolf <thomas.wolf@paranor.ch>2017-06-10 14:26:32 +0200
committerMatthias Sohn <matthias.sohn@sap.com>2017-08-26 01:44:36 +0200
commitc758a8cd37b71851bd71a5e558abc218c8082164 (patch)
tree486c3231fdb17a21bf8f294d88e74ff2033fa135
parent9d2447063de3bdad6f68aa912d31f3934f1cebc5 (diff)
downloadjgit-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.java43
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java134
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.
+ }
}
/**