{"thread":{"id":"15027","subject":"[EGIT PATCH 1/2] Refactor SSH key loading so we don't duplicate keys","startedAt":"2008-08-15T17:35:04Z","lastAt":"2008-08-20T20:38:18Z","messageCount":4,"participants":["Shawn O. Pearce","Robin Rosenberg"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"87327","messageId":"1218821705-2631-1-git-send-email-spearce@spearce.org","threadId":"15027","inReplyTo":null,"subject":"[EGIT PATCH 1/2] Refactor SSH key loading so we don't duplicate keys","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2008-08-15T17:35:04Z","receivedAt":"2008-08-15T17:35:04Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"We only want to read each private key once, so we cache the\nnames of the keys we have processed before, adding keys which\nwe have not yet seen.  This allows us to alter add keys on\nthe fly and avoid duplication.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n .../jgit/transport/DefaultSshSessionFactory.java   |   20 ++++++++++++++------\n 1 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/transport/DefaultSshSessionFactory.java b/org.spearce.jgit/src/org/spearce/jgit/transport/DefaultSshSessionFactory.java\nindex b4578d4..0484fc0 100644\n--- a/org.spearce.jgit/src/org/spearce/jgit/transport/DefaultSshSessionFactory.java\n+++ b/org.spearce.jgit/src/org/spearce/jgit/transport/DefaultSshSessionFactory.java\n@@ -78,6 +78,8 @@\n \t/** IANA assigned port number for SSH. */\n \tprivate static final int SSH_PORT = 22;\n \n+\tprivate Set<String> loadedIdentities;\n+\n \tprivate JSch userJSch;\n \n \t@Override\n@@ -106,10 +108,10 @@ public String run() {\n \n \tprivate JSch getUserJSch() throws JSchException {\n \t\tif (userJSch == null) {\n-\t\t\tfinal JSch sch = new JSch();\n-\t\t\tknownHosts(sch);\n-\t\t\tidentities(sch);\n-\t\t\tuserJSch = sch;\n+\t\t\tloadedIdentities = new HashSet<String>();\n+\t\t\tuserJSch = new JSch();\n+\t\t\tknownHosts(userJSch);\n+\t\t\tidentities();\n \t\t}\n \t\treturn userJSch;\n \t}\n@@ -133,7 +135,7 @@ private void knownHosts(final JSch sch) throws JSchException {\n \t\t}\n \t}\n \n-\tprivate void identities(final JSch sch) throws JSchException {\n+\tprivate void identities() throws JSchException {\n \t\tfinal File home = FS.userHome();\n \t\tif (home == null)\n \t\t\treturn;\n@@ -149,10 +151,16 @@ private void identities(final JSch sch) throws JSchException {\n \t\t\tfinal File k = new File(sshdir, n.substring(0, n.length() - 4));\n \t\t\tif (!k.isFile())\n \t\t\t\tcontinue;\n-\t\t\tsch.addIdentity(k.getAbsolutePath());\n+\t\t\taddIdentity(k);\n \t\t}\n \t}\n \n+\tprivate void addIdentity(final File identityFile) throws JSchException {\n+\t\tfinal String path = identityFile.getAbsolutePath();\n+\t\tif (loadedIdentities.add(path))\n+\t\t\tuserJSch.addIdentity(path);\n+\t}\n+\n \tprivate static class AWT_UserInfo implements UserInfo,\n \t\t\tUIKeyboardInteractive {\n \t\tprivate String passwd;\n-- \n1.6.0.rc3.250.g8dd0\n"},{"id":"87328","messageId":"1218821705-2631-2-git-send-email-spearce@spearce.org","threadId":"15027","inReplyTo":"1218821705-2631-1-git-send-email-spearce@spearce.org","subject":"[EGIT PATCH 2/2] Honor ~/.ssh/config whenever possible during SSH based transport","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2008-08-15T17:35:05Z","receivedAt":"2008-08-15T17:35:05Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"I rely on ~/.ssh/config to setup host aliases, especially for very\ncommon destinations.  For example I have the following on most of\nmy systems:\n\n\tHost orcz\n\t\tHostName repo.or.cz\n\t\tUser spearce\n\t\tIdentityFile .ssh/id_orcz\n\nas not every system I use has my local user name as \"spearce\".  The\nC Git transport honors these settings just fine for short URLs like\n\"orcz:/srv/git/egit.git\" but jgit failed horribly on these as there\nis no local system named \"orcz\" on any of my networks.\n\nBy reading (and caching) the ~/.ssh/config file jgit can now honor\nthe important aspects of the host configuration blocks, allowing it\nto use the same URLs as C Git.\n\nThe JSch API does not seem to allow forcing a specific key identity\nfor a Session so we are forced to load the identity file listed in\nthe configuration into the core JSch object we are running with.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n .../spearce/egit/ui/EclipseSshSessionFactory.java  |   44 ++-\n .../spearce/jgit/transport/OpenSshConfigTest.java  |  131 ++++++++\n .../jgit/transport/DefaultSshSessionFactory.java   |   22 ++-\n .../org/spearce/jgit/transport/OpenSshConfig.java  |  318 ++++++++++++++++++++\n 4 files changed, 499 insertions(+), 16 deletions(-)\n create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/transport/OpenSshConfigTest.java\n create mode 100644 org.spearce.jgit/src/org/spearce/jgit/transport/OpenSshConfig.java\n\ndiff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/EclipseSshSessionFactory.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/EclipseSshSessionFactory.java\nindex 8f80373..640a165 100644\n--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/EclipseSshSessionFactory.java\n+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/EclipseSshSessionFactory.java\n@@ -8,13 +8,15 @@\n  *******************************************************************************/\n package org.spearce.egit.ui;\n \n+import java.io.File;\n import java.io.IOException;\n import java.io.OutputStream;\n-import java.security.AccessController;\n-import java.security.PrivilegedAction;\n+import java.util.HashSet;\n+import java.util.Set;\n \n import org.eclipse.jsch.core.IJSchService;\n import org.eclipse.jsch.ui.UserInfoPrompter;\n+import org.spearce.jgit.transport.OpenSshConfig;\n import org.spearce.jgit.transport.SshSessionFactory;\n \n import com.jcraft.jsch.JSchException;\n@@ -23,15 +25,27 @@\n class EclipseSshSessionFactory extends SshSessionFactory {\n \tprivate final IJSchService provider;\n \n+\tprivate final Set<String> loadedIdentities = new HashSet<String>();\n+\n+\tprivate OpenSshConfig config;\n+\n \tEclipseSshSessionFactory(final IJSchService p) {\n \t\tprovider = p;\n \t}\n \n \t@Override\n-\tpublic Session getSession(final String user, final String pass,\n-\t\t\tfinal String host, final int port) throws JSchException {\n-\t\tfinal Session session = provider.createSession(host, port > 0 ? port\n-\t\t\t\t: -1, user != null ? user : userName());\n+\tpublic Session getSession(String user, String pass, String host, int port)\n+\t\t\tthrows JSchException {\n+\t\tfinal OpenSshConfig.Host hc = getConfig().lookup(host);\n+\t\thost = hc.getHostName();\n+\t\tif (port <= 0)\n+\t\t\tport = hc.getPort();\n+\t\tif (user == null)\n+\t\t\tuser = hc.getUser();\n+\n+\t\tfinal Session session = provider.createSession(host, port, user);\n+\t\tif (hc.getIdentityFile() != null)\n+\t\t\taddIdentity(hc.getIdentityFile());\n \t\tif (pass != null)\n \t\t\tsession.setPassword(pass);\n \t\telse\n@@ -39,12 +53,17 @@ public Session getSession(final String user, final String pass,\n \t\treturn session;\n \t}\n \n-\tprivate static String userName() {\n-\t\treturn AccessController.doPrivileged(new PrivilegedAction<String>() {\n-\t\t\tpublic String run() {\n-\t\t\t\treturn System.getProperty(\"user.name\");\n-\t\t\t}\n-\t\t});\n+\tprivate synchronized OpenSshConfig getConfig() {\n+\t\tif (config == null)\n+\t\t\tconfig = OpenSshConfig.get();\n+\t\treturn config;\n+\t}\n+\n+\tprivate void addIdentity(final File identityFile)\n+\t\t\tthrows JSchException {\n+\t\tfinal String path = identityFile.getAbsolutePath();\n+\t\tif (loadedIdentities.add(path))\n+\t\t\tprovider.getJSch().addIdentity(path);\n \t}\n \n \t@Override\n@@ -52,6 +71,7 @@ public OutputStream getErrorStream() {\n \t\treturn new OutputStream() {\n \n \t\t\tStringBuilder all = new StringBuilder();\n+\n \t\t\tStringBuilder sb = new StringBuilder();\n \n \t\t\tpublic String toString() {\ndiff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/transport/OpenSshConfigTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/transport/OpenSshConfigTest.java\nnew file mode 100644\nindex 0000000..a250f9d\n--- /dev/null\n+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/transport/OpenSshConfigTest.java\n@@ -0,0 +1,131 @@\n+/*\n+ * Copyright (C) 2008, Google Inc.\n+ *\n+ * All rights reserved.\n+ *\n+ * Redistribution and use in source and binary forms, with or\n+ * without modification, are permitted provided that the following\n+ * conditions are met:\n+ *\n+ * - Redistributions of source code must retain the above copyright\n+ *   notice, this list of conditions and the following disclaimer.\n+ *\n+ * - Redistributions in binary form must reproduce the above\n+ *   copyright notice, this list of conditions and the following\n+ *   disclaimer in the documentation and/or other materials provided\n+ *   with the distribution.\n+ *\n+ * - Neither the name of the Git Development Community nor the\n+ *   names of its contributors may be used to endorse or promote\n+ *   products derived from this software without specific prior\n+ *   written permission.\n+ *\n+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\n+ * CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\n+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\n+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n+ */\n+\n+package org.spearce.jgit.transport;\n+\n+import java.io.File;\n+import java.io.FileOutputStream;\n+import java.io.IOException;\n+import java.io.OutputStreamWriter;\n+\n+import org.spearce.jgit.lib.RepositoryTestCase;\n+import org.spearce.jgit.transport.OpenSshConfig.Host;\n+\n+public class OpenSshConfigTest extends RepositoryTestCase {\n+\tprivate File home;\n+\n+\tprivate File configFile;\n+\n+\tprivate OpenSshConfig osc;\n+\n+\tpublic void setUp() throws Exception {\n+\t\tsuper.setUp();\n+\n+\t\thome = new File(trash, \"home\");\n+\t\thome.mkdir();\n+\n+\t\tconfigFile = new File(new File(home, \".ssh\"), \"config\");\n+\t\tconfigFile.getParentFile().mkdir();\n+\n+\t\tSystem.setProperty(\"user.name\", \"jex_junit\");\n+\t\tosc = new OpenSshConfig(home, configFile);\n+\t}\n+\n+\tprivate void config(final String data) throws IOException {\n+\t\tfinal OutputStreamWriter fw = new OutputStreamWriter(\n+\t\t\t\tnew FileOutputStream(configFile), \"UTF-8\");\n+\t\tfw.write(data);\n+\t\tfw.close();\n+\t}\n+\n+\tpublic void testNoConfig() {\n+\t\tfinal Host h = osc.lookup(\"repo.or.cz\");\n+\t\tassertNotNull(h);\n+\t\tassertEquals(\"repo.or.cz\", h.getHostName());\n+\t\tassertEquals(\"jex_junit\", h.getUser());\n+\t\tassertEquals(22, h.getPort());\n+\t\tassertNull(h.getIdentityFile());\n+\t}\n+\n+\tpublic void testAlias_DoesNotMatch() throws Exception {\n+\t\tconfig(\"Host orcz\\n\" + \"\\tHostName repo.or.cz\\n\");\n+\t\tfinal Host h = osc.lookup(\"repo.or.cz\");\n+\t\tassertNotNull(h);\n+\t\tassertEquals(\"repo.or.cz\", h.getHostName());\n+\t\tassertEquals(\"jex_junit\", h.getUser());\n+\t\tassertEquals(22, h.getPort());\n+\t\tassertNull(h.getIdentityFile());\n+\t}\n+\n+\tpublic void testAlias_OptionsSet() throws Exception {\n+\t\tconfig(\"Host orcz\\n\" + \"\\tHostName repo.or.cz\\n\" + \"\\tPort 2222\\n\"\n+\t\t\t\t+ \"\\tUser jex\\n\" + \"\\tIdentityFile .ssh/id_jex\\n\"\n+\t\t\t\t+ \"\\tForwardX11 no\\n\");\n+\t\tfinal Host h = osc.lookup(\"orcz\");\n+\t\tassertNotNull(h);\n+\t\tassertEquals(\"repo.or.cz\", h.getHostName());\n+\t\tassertEquals(\"jex\", h.getUser());\n+\t\tassertEquals(2222, h.getPort());\n+\t\tassertEquals(new File(home, \".ssh/id_jex\"), h.getIdentityFile());\n+\t}\n+\n+\n+\tpublic void testAlias_OptionsKeywordCaseInsensitive() throws Exception {\n+\t\tconfig(\"hOsT orcz\\n\" + \"\\thOsTnAmE repo.or.cz\\n\" + \"\\tPORT 2222\\n\"\n+\t\t\t\t+ \"\\tuser jex\\n\" + \"\\tidentityfile .ssh/id_jex\\n\"\n+\t\t\t\t+ \"\\tForwardX11 no\\n\");\n+\t\tfinal Host h = osc.lookup(\"orcz\");\n+\t\tassertNotNull(h);\n+\t\tassertEquals(\"repo.or.cz\", h.getHostName());\n+\t\tassertEquals(\"jex\", h.getUser());\n+\t\tassertEquals(2222, h.getPort());\n+\t\tassertEquals(new File(home, \".ssh/id_jex\"), h.getIdentityFile());\n+\t}\n+\n+\tpublic void testAlias_OptionsInherit() throws Exception {\n+\t\tconfig(\"Host orcz\\n\" + \"\\tHostName repo.or.cz\\n\" + \"\\n\" + \"Host *\\n\"\n+\t\t\t\t+ \"\\tHostName not.a.host.example.com\\n\" + \"\\tPort 2222\\n\"\n+\t\t\t\t+ \"\\tUser jex\\n\" + \"\\tIdentityFile .ssh/id_jex\\n\"\n+\t\t\t\t+ \"\\tForwardX11 no\\n\");\n+\t\tfinal Host h = osc.lookup(\"orcz\");\n+\t\tassertNotNull(h);\n+\t\tassertEquals(\"repo.or.cz\", h.getHostName());\n+\t\tassertEquals(\"jex\", h.getUser());\n+\t\tassertEquals(2222, h.getPort());\n+\t\tassertEquals(new File(home, \".ssh/id_jex\"), h.getIdentityFile());\n+\t}\n+}\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/transport/DefaultSshSessionFactory.java b/org.spearce.jgit/src/org/spearce/jgit/transport/DefaultSshSessionFactory.java\nindex 0484fc0..a2437c2 100644\n--- a/org.spearce.jgit/src/org/spearce/jgit/transport/DefaultSshSessionFactory.java\n+++ b/org.spearce.jgit/src/org/spearce/jgit/transport/DefaultSshSessionFactory.java\n@@ -49,6 +49,8 @@\n import java.io.OutputStream;\n import java.security.AccessController;\n import java.security.PrivilegedAction;\n+import java.util.HashSet;\n+import java.util.Set;\n \n import javax.swing.JLabel;\n import javax.swing.JOptionPane;\n@@ -76,21 +78,27 @@\n  */\n class DefaultSshSessionFactory extends SshSessionFactory {\n \t/** IANA assigned port number for SSH. */\n-\tprivate static final int SSH_PORT = 22;\n+\tstatic final int SSH_PORT = 22;\n \n \tprivate Set<String> loadedIdentities;\n \n \tprivate JSch userJSch;\n \n+\tprivate OpenSshConfig config;\n+\n \t@Override\n \tpublic synchronized Session getSession(String user, String pass,\n \t\t\tString host, int port) throws JSchException {\n+\t\tfinal OpenSshConfig.Host hc = getConfig().lookup(host);\n+\t\thost = hc.getHostName();\n \t\tif (port <= 0)\n-\t\t\tport = SSH_PORT;\n+\t\t\tport = hc.getPort();\n \t\tif (user == null)\n-\t\t\tuser = userName();\n+\t\t\tuser = hc.getUser();\n \n \t\tfinal Session session = getUserJSch().getSession(user, host, port);\n+\t\tif (hc.getIdentityFile() != null)\n+\t\t\taddIdentity(hc.getIdentityFile());\n \t\tif (pass != null)\n \t\t\tsession.setPassword(pass);\n \t\telse\n@@ -98,7 +106,7 @@ public synchronized Session getSession(String user, String pass,\n \t\treturn session;\n \t}\n \n-\tprivate static String userName() {\n+\tstatic String userName() {\n \t\treturn AccessController.doPrivileged(new PrivilegedAction<String>() {\n \t\t\tpublic String run() {\n \t\t\t\treturn System.getProperty(\"user.name\");\n@@ -116,6 +124,12 @@ private JSch getUserJSch() throws JSchException {\n \t\treturn userJSch;\n \t}\n \n+\tprivate OpenSshConfig getConfig() {\n+\t\tif (config == null)\n+\t\t\tconfig = OpenSshConfig.get();\n+\t\treturn config;\n+\t}\n+\n \tprivate void knownHosts(final JSch sch) throws JSchException {\n \t\tfinal File home = FS.userHome();\n \t\tif (home == null)\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/transport/OpenSshConfig.java b/org.spearce.jgit/src/org/spearce/jgit/transport/OpenSshConfig.java\nnew file mode 100644\nindex 0000000..f927b1a\n--- /dev/null\n+++ b/org.spearce.jgit/src/org/spearce/jgit/transport/OpenSshConfig.java\n@@ -0,0 +1,318 @@\n+/*\n+ * Copyright (C) 2008, Google Inc.\n+ *\n+ * All rights reserved.\n+ *\n+ * Redistribution and use in source and binary forms, with or\n+ * without modification, are permitted provided that the following\n+ * conditions are met:\n+ *\n+ * - Redistributions of source code must retain the above copyright\n+ *   notice, this list of conditions and the following disclaimer.\n+ *\n+ * - Redistributions in binary form must reproduce the above\n+ *   copyright notice, this list of conditions and the following\n+ *   disclaimer in the documentation and/or other materials provided\n+ *   with the distribution.\n+ *\n+ * - Neither the name of the Git Development Community nor the\n+ *   names of its contributors may be used to endorse or promote\n+ *   products derived from this software without specific prior\n+ *   written permission.\n+ *\n+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\n+ * CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\n+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\n+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n+ */\n+\n+package org.spearce.jgit.transport;\n+\n+import java.io.BufferedReader;\n+import java.io.File;\n+import java.io.FileInputStream;\n+import java.io.FileNotFoundException;\n+import java.io.IOException;\n+import java.io.InputStream;\n+import java.io.InputStreamReader;\n+import java.util.ArrayList;\n+import java.util.Collections;\n+import java.util.LinkedHashMap;\n+import java.util.List;\n+import java.util.Map;\n+\n+import org.spearce.jgit.errors.InvalidPatternException;\n+import org.spearce.jgit.fnmatch.FileNameMatcher;\n+import org.spearce.jgit.util.FS;\n+\n+/**\n+ * Simple configuration parser for the OpenSSH ~/.ssh/config file.\n+ * <p>\n+ * Since JSch does not (currently) have the ability to parse an OpenSSH\n+ * configuration file this is a simple parser to read that file and make the\n+ * critical options available to {@link SshSessionFactory}.\n+ */\n+public class OpenSshConfig {\n+\t/**\n+\t * Obtain the user's configuration data.\n+\t * <p>\n+\t * The configuration file is always returned to the caller, even if no file\n+\t * exists in the user's home directory at the time the call was made. Lookup\n+\t * requests are cached and are automatically updated if the user modifies\n+\t * the configuration file since the last time it was cached.\n+\t * \n+\t * @return a caching reader of the user's configuration file.\n+\t */\n+\tpublic static OpenSshConfig get() {\n+\t\tFile home = FS.userHome();\n+\t\tif (home == null)\n+\t\t\thome = new File(\".\").getAbsoluteFile();\n+\n+\t\tfinal File config = new File(new File(home, \".ssh\"), \"config\");\n+\t\tfinal OpenSshConfig osc = new OpenSshConfig(home, config);\n+\t\tosc.refresh();\n+\t\treturn osc;\n+\t}\n+\n+\t/** The user's home directory, as key files may be relative to here. */\n+\tprivate final File home;\n+\n+\t/** The .ssh/config file we read and monitor for updates. */\n+\tprivate final File configFile;\n+\n+\t/** Modification time of {@link #configFile} when {@link #hosts} loaded. */\n+\tprivate long lastModified;\n+\n+\t/** Cached entries read out of the configuration file. */\n+\tprivate Map<String, Host> hosts;\n+\n+\tprotected OpenSshConfig(final File h, final File cfg) {\n+\t\thome = h;\n+\t\tconfigFile = cfg;\n+\t\thosts = Collections.emptyMap();\n+\t}\n+\n+\t/**\n+\t * Locate the configuration for a specific host request.\n+\t * \n+\t * @param hostName\n+\t *            the name the user has supplied to the SSH tool. This may be a\n+\t *            real host name, or it may just be a \"Host\" block in the\n+\t *            configuration file.\n+\t * @return r configuration for the requested name. Never null.\n+\t */\n+\tpublic Host lookup(final String hostName) {\n+\t\tfinal Map<String, Host> cache = refresh();\n+\t\tHost h = cache.get(hostName);\n+\t\tif (h == null)\n+\t\t\th = new Host();\n+\t\tif (h.patternsApplied)\n+\t\t\treturn h;\n+\n+\t\tfor (final Map.Entry<String, Host> e : cache.entrySet()) {\n+\t\t\tif (!isHostPattern(e.getKey()))\n+\t\t\t\tcontinue;\n+\t\t\tif (!isHostMatch(e.getKey(), hostName))\n+\t\t\t\tcontinue;\n+\t\t\th.copyFrom(e.getValue());\n+\t\t}\n+\n+\t\tif (h.hostName == null)\n+\t\t\th.hostName = hostName;\n+\t\tif (h.user == null)\n+\t\t\th.user = DefaultSshSessionFactory.userName();\n+\t\tif (h.port == 0)\n+\t\t\th.port = DefaultSshSessionFactory.SSH_PORT;\n+\t\th.patternsApplied = true;\n+\t\treturn h;\n+\t}\n+\n+\tprivate synchronized Map<String, Host> refresh() {\n+\t\tfinal long mtime = configFile.lastModified();\n+\t\tif (mtime != lastModified) {\n+\t\t\ttry {\n+\t\t\t\tfinal FileInputStream in = new FileInputStream(configFile);\n+\t\t\t\ttry {\n+\t\t\t\t\thosts = parse(in);\n+\t\t\t\t} finally {\n+\t\t\t\t\tin.close();\n+\t\t\t\t}\n+\t\t\t} catch (FileNotFoundException none) {\n+\t\t\t\thosts = Collections.emptyMap();\n+\t\t\t} catch (IOException err) {\n+\t\t\t\thosts = Collections.emptyMap();\n+\t\t\t}\n+\t\t\tlastModified = mtime;\n+\t\t}\n+\t\treturn hosts;\n+\t}\n+\n+\tprivate Map<String, Host> parse(final InputStream in) throws IOException {\n+\t\tfinal Map<String, Host> m = new LinkedHashMap<String, Host>();\n+\t\tfinal BufferedReader br = new BufferedReader(new InputStreamReader(in));\n+\t\tfinal List<Host> current = new ArrayList<Host>(4);\n+\t\tString line;\n+\n+\t\twhile ((line = br.readLine()) != null) {\n+\t\t\tline = line.trim();\n+\t\t\tif (line.length() == 0 || line.startsWith(\"#\"))\n+\t\t\t\tcontinue;\n+\n+\t\t\tfinal int sp = line.indexOf(' ');\n+\t\t\tfinal int eq = line.indexOf('=');\n+\t\t\tfinal int splitAt;\n+\t\t\tif (sp >= 0 && eq >= 0)\n+\t\t\t\tsplitAt = Math.min(sp, eq);\n+\t\t\telse if (sp < 0)\n+\t\t\t\tsplitAt = eq;\n+\t\t\telse\n+\t\t\t\tsplitAt = sp;\n+\t\t\tfinal String keyword = line.substring(0, splitAt).trim();\n+\t\t\tfinal String argValue = line.substring(splitAt + 1).trim();\n+\n+\t\t\tif (\"Host\".equalsIgnoreCase(keyword)) {\n+\t\t\t\tcurrent.clear();\n+\t\t\t\tfor (final String name : argValue.split(\"[ \\t]\")) {\n+\t\t\t\t\tHost c = m.get(name);\n+\t\t\t\t\tif (c == null) {\n+\t\t\t\t\t\tc = new Host();\n+\t\t\t\t\t\tm.put(name, c);\n+\t\t\t\t\t}\n+\t\t\t\t\tcurrent.add(c);\n+\t\t\t\t}\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif (current.isEmpty()) {\n+\t\t\t\t// We received an option outside of a Host block. We\n+\t\t\t\t// don't know who this should match against, so skip.\n+\t\t\t\t//\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif (\"HostName\".equalsIgnoreCase(keyword)) {\n+\t\t\t\tfor (final Host c : current)\n+\t\t\t\t\tif (c.hostName == null)\n+\t\t\t\t\t\tc.hostName = dequote(argValue);\n+\t\t\t} else if (\"User\".equalsIgnoreCase(keyword)) {\n+\t\t\t\tfor (final Host c : current)\n+\t\t\t\t\tif (c.user == null)\n+\t\t\t\t\t\tc.user = dequote(argValue);\n+\t\t\t} else if (\"Port\".equalsIgnoreCase(keyword)) {\n+\t\t\t\ttry {\n+\t\t\t\t\tfinal int port = Integer.parseInt(dequote(argValue));\n+\t\t\t\t\tfor (final Host c : current)\n+\t\t\t\t\t\tif (c.port == 0)\n+\t\t\t\t\t\t\tc.port = port;\n+\t\t\t\t} catch (NumberFormatException nfe) {\n+\t\t\t\t\t// Bad port number. Don't set it.\n+\t\t\t\t}\n+\t\t\t} else if (\"IdentityFile\".equalsIgnoreCase(keyword)) {\n+\t\t\t\tfor (final Host c : current)\n+\t\t\t\t\tif (c.identityFile == null)\n+\t\t\t\t\t\tc.identityFile = toFile(dequote(argValue));\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn m;\n+\t}\n+\n+\tprivate static boolean isHostPattern(final String s) {\n+\t\treturn s.indexOf('*') >= 0 || s.indexOf('?') >= 0;\n+\t}\n+\n+\tprivate static boolean isHostMatch(final String pattern, final String name) {\n+\t\tfinal FileNameMatcher fn;\n+\t\ttry {\n+\t\t\tfn = new FileNameMatcher(pattern, null);\n+\t\t} catch (InvalidPatternException e) {\n+\t\t\treturn false;\n+\t\t}\n+\t\tfn.append(name);\n+\t\treturn fn.isMatch();\n+\t}\n+\n+\tprivate static String dequote(final String value) {\n+\t\tif (value.startsWith(\"\\\"\") && value.endsWith(\"\\\"\"))\n+\t\t\treturn value.substring(1, value.length() - 2);\n+\t\treturn value;\n+\t}\n+\n+\tprivate File toFile(final String path) {\n+\t\tif (path.startsWith(\"~/\"))\n+\t\t\treturn new File(home, path.substring(2));\n+\t\treturn new File(home, path);\n+\t}\n+\n+\t/**\n+\t * Configuration of one \"Host\" block in the configuration file.\n+\t * <p>\n+\t * If returned from {@link OpenSshConfig#lookup(String)} some or all of the\n+\t * properties may not be populated. The properties which are not populated\n+\t * should be defaulted by the caller.\n+\t * <p>\n+\t * When returned from {@link OpenSshConfig#lookup(String)} any wildcard\n+\t * entries which appear later in the configuration file will have been\n+\t * already merged into this block.\n+\t */\n+\tpublic static class Host {\n+\t\tboolean patternsApplied;\n+\n+\t\tString hostName;\n+\n+\t\tint port;\n+\n+\t\tFile identityFile;\n+\n+\t\tString user;\n+\n+\t\tvoid copyFrom(final Host src) {\n+\t\t\tif (hostName == null)\n+\t\t\t\thostName = src.hostName;\n+\t\t\tif (port == 0)\n+\t\t\t\tport = src.port;\n+\t\t\tif (identityFile == null)\n+\t\t\t\tidentityFile = src.identityFile;\n+\t\t\tif (user == null)\n+\t\t\t\tuser = src.user;\n+\t\t}\n+\n+\t\t/**\n+\t\t * @return the real IP address or host name to connect to; never null.\n+\t\t */\n+\t\tpublic String getHostName() {\n+\t\t\treturn hostName;\n+\t\t}\n+\n+\t\t/**\n+\t\t * @return the real port number to connect to; never 0.\n+\t\t */\n+\t\tpublic int getPort() {\n+\t\t\treturn port;\n+\t\t}\n+\n+\t\t/**\n+\t\t * @return path of the private key file to use for authentication; null\n+\t\t *         if the caller should use default authentication strategies.\n+\t\t */\n+\t\tpublic File getIdentityFile() {\n+\t\t\treturn identityFile;\n+\t\t}\n+\n+\t\t/**\n+\t\t * @return the real user name to connect as; never null.\n+\t\t */\n+\t\tpublic String getUser() {\n+\t\t\treturn user;\n+\t\t}\n+\t}\n+}\n-- \n1.6.0.rc3.250.g8dd0\n"},{"id":"87870","messageId":"200808202234.08348.robin.rosenberg.lists@dewire.com","threadId":"15027","inReplyTo":"1218821705-2631-2-git-send-email-spearce@spearce.org","subject":"Re: [EGIT PATCH 2/2] Honor ~/.ssh/config whenever possible during SSH based transport","fromName":"Robin Rosenberg","fromEmail":"robin.rosenberg.lists@dewire.com","sentAt":"2008-08-20T20:34:05Z","receivedAt":"2008-08-20T20:34:05Z","isPatch":true,"sender":{"key":"robin.rosenberg@dewire.com","avatar":"https://avatars.githubusercontent.com/u/46357?v=4"},"body":"fredagen den 15 augusti 2008 19.35.05 skrev Shawn O. Pearce:\n> +\tprivate void addIdentity(final File identityFile)\n> +\t\t\tthrows JSchException {\n> +\t\tfinal String path = identityFile.getAbsolutePath();\n> +\t\tif (loadedIdentities.add(path))\n> +\t\t\tprovider.getJSch().addIdentity(path);\n>  \t}\n\nOk, this breaks 3.3 compatibility and I accidentally pushed it to master. Now Marek also\nhas stuff that only works (well) with 3.4 becaujse of bugs in 3.3, that won't be fixed. So maybe\nwe'd better skip 3.3 in order to work faster. I.e. I won't revert this commit, but I am willing to\naccept a patch to \"fix\" it in order to prolong 3.3 compatibilty.\n\n-- robin\n"},{"id":"87872","messageId":"20080820203818.GL3483@spearce.org","threadId":"15027","inReplyTo":"200808202234.08348.robin.rosenberg.lists@dewire.com","subject":"Re: [EGIT PATCH 2/2] Honor ~/.ssh/config whenever possible during SSH based transport","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2008-08-20T20:38:18Z","receivedAt":"2008-08-20T20:38:18Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"Robin Rosenberg <robin.rosenberg.lists@dewire.com> wrote:\n> fredagen den 15 augusti 2008 19.35.05 skrev Shawn O. Pearce:\n> > +\tprivate void addIdentity(final File identityFile)\n> > +\t\t\tthrows JSchException {\n> > +\t\tfinal String path = identityFile.getAbsolutePath();\n> > +\t\tif (loadedIdentities.add(path))\n> > +\t\t\tprovider.getJSch().addIdentity(path);\n> >  \t}\n> \n> Ok, this breaks 3.3 compatibility and I accidentally pushed it to master. Now Marek also\n> has stuff that only works (well) with 3.4 becaujse of bugs in 3.3, that won't be fixed. So maybe\n> we'd better skip 3.3 in order to work faster. I.e. I won't revert this commit, but I am willing to\n> accept a patch to \"fix\" it in order to prolong 3.3 compatibilty.\n\nI agree.  But I'm not going to be supplying that fix myself.  I don't\nwant to spend a lot of time messing around with the SSH transport,\nthat was just annoying me one morning.\n\n-- \nShawn.\n"}]}