]> source.dussan.org Git - jgit.git/commitdiff
Builder API to configure SshdSessionFactories 48/161448/4
authorThomas Wolf <thomas.wolf@paranor.ch>
Thu, 23 Apr 2020 16:30:19 +0000 (18:30 +0200)
committerThomas Wolf <thomas.wolf@paranor.ch>
Sat, 23 May 2020 14:46:22 +0000 (16:46 +0200)
A builder API provides a more convenient way to define a customized
SshdSessionFactory by hiding the subclassing.

Also provide a new interface SshConfigStore to abstract away the
specifics of reading a ssh config file, and provide a way to customize
the concrete ssh config implementation to be used. This facilitates
using an alternate ssh config implementation that may or may not be
based on files.

Change-Id: Ib9038e8ff2a4eb3a9ce7b3554d1450befec8e1e1
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshBuilderTest.java [new file with mode: 0644]
org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigStore.java [new file with mode: 0644]

diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshBuilderTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshBuilderTest.java
new file mode 100644 (file)
index 0000000..04208fe
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport.sshd;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.transport.ssh.SshTestHarness;
+import org.eclipse.jgit.util.FS;
+import org.junit.After;
+import org.junit.Test;
+
+/**
+ * Test for using the SshdSessionFactory without files in ~/.ssh but with an
+ * in-memory setup, creating the factory via the builder API.
+ */
+public class NoFilesSshBuilderTest extends SshTestHarness {
+
+       private PublicKey testServerKey;
+
+       private KeyPair testUserKey;
+
+       @Override
+       protected SshSessionFactory createSessionFactory() {
+               return new SshdSessionFactoryBuilder() //
+                               .setConfigStoreFactory((h, f, u) -> null)
+                               .setDefaultKeysProvider(f -> new KeyAuthenticator())
+                               .setServerKeyDatabase((h, s) -> new ServerKeyDatabase() {
+
+                                       @Override
+                                       public List<PublicKey> lookup(String connectAddress,
+                                                       InetSocketAddress remoteAddress,
+                                                       Configuration config) {
+                                               return Collections.singletonList(testServerKey);
+                                       }
+
+                                       @Override
+                                       public boolean accept(String connectAddress,
+                                                       InetSocketAddress remoteAddress,
+                                                       PublicKey serverKey, Configuration config,
+                                                       CredentialsProvider provider) {
+                                               return KeyUtils.compareKeys(serverKey, testServerKey);
+                                       }
+
+                               }) //
+                               .setPreferredAuthentications("publickey")
+                               .setHomeDirectory(FS.DETECTED.userHome())
+                               .setSshDirectory(sshDir) //
+                               .build(new JGitKeyCache());
+       }
+
+       private class KeyAuthenticator
+                       implements KeyIdentityProvider, Iterable<KeyPair> {
+
+               @Override
+               public Iterator<KeyPair> iterator() {
+                       // Should not be called. The use of the Iterable interface in
+                       // SshdSessionFactory.getDefaultKeys() made sense in sshd 2.0.0,
+                       // but sshd 2.2.0 added the SessionContext, which although good
+                       // (without it we couldn't check here) breaks the Iterable analogy.
+                       // But we're stuck now with that interface for getDefaultKeys, and
+                       // so this override throwing an exception is unfortunately needed.
+                       throw new UnsupportedOperationException();
+               }
+
+               @Override
+               public Iterable<KeyPair> loadKeys(SessionContext session)
+                               throws IOException, GeneralSecurityException {
+                       if (!TEST_USER.equals(session.getUsername())) {
+                               return Collections.emptyList();
+                       }
+                       SshdSocketAddress remoteAddress = SshdSocketAddress
+                                       .toSshdSocketAddress(session.getRemoteAddress());
+                       switch (remoteAddress.getHostName()) {
+                       case "localhost":
+                       case "127.0.0.1":
+                               return Collections.singletonList(testUserKey);
+                       default:
+                               return Collections.emptyList();
+                       }
+               }
+       }
+
+       @After
+       public void cleanUp() {
+               testServerKey = null;
+               testUserKey = null;
+       }
+
+       @Override
+       protected void installConfig(String... config) {
+               File configFile = new File(sshDir, Constants.CONFIG);
+               if (config != null) {
+                       try {
+                               Files.write(configFile.toPath(), Arrays.asList(config));
+                       } catch (IOException e) {
+                               throw new UncheckedIOException(e);
+                       }
+               }
+       }
+
+       private KeyPair load(Path path) throws Exception {
+               try (InputStream in = Files.newInputStream(path)) {
+                       return SecurityUtils
+                                       .loadKeyPairIdentities(null,
+                                                       NamedResource.ofName(path.toString()), in, null)
+                                       .iterator().next();
+               }
+       }
+
+       @Test
+       public void testCloneWithBuiltInKeys() throws Exception {
+               // This test should fail unless our in-memory setup is taken: no
+               // known_hosts file, and a config that specifies a non-existing key.
+               File newHostKey = new File(getTemporaryDirectory(), "newhostkey");
+               copyTestResource("id_ed25519", newHostKey);
+               server.addHostKey(newHostKey.toPath(), true);
+               testServerKey = load(newHostKey.toPath()).getPublic();
+               assertTrue(newHostKey.delete());
+               testUserKey = load(privateKey1.getAbsoluteFile().toPath());
+               assertNotNull(testServerKey);
+               assertNotNull(testUserKey);
+               cloneWith(
+                               "ssh://" + TEST_USER + "@localhost:" + testPort
+                                               + "/doesntmatter",
+                               new File(getTemporaryDirectory(), "cloned"), null, //
+                               "Host localhost", //
+                               "IdentityFile "
+                                               + new File(sshDir, "does_not_exist").getAbsolutePath());
+       }
+
+}
index 608f647bc5eda856782781096c9623a0b2c7810c..fa026a5c031ae956f9a4b9e0c2300879fce30693 100644 (file)
@@ -47,7 +47,6 @@ import org.junit.Test;
  */
 public class NoFilesSshTest extends SshTestHarness {
 
-
        private PublicKey testServerKey;
 
        private KeyPair testUserKey;
index e770134fa16a18bc979069c3ad36eb2e92e209bf..97e0fcc7d239d4ca2e30960dfb00d58b778276ba 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -12,7 +12,6 @@ package org.eclipse.jgit.internal.transport.sshd;
 import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.flag;
 import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
 
-import java.io.File;
 import java.io.IOException;
 import java.net.SocketAddress;
 import java.util.Map;
@@ -22,61 +21,36 @@ import org.apache.sshd.client.config.hosts.HostConfigEntry;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.common.AttributeRepository;
 import org.apache.sshd.common.util.net.SshdSocketAddress;
-import org.eclipse.jgit.annotations.NonNull;
-import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
-import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry;
+import org.eclipse.jgit.transport.SshConfigStore;
 import org.eclipse.jgit.transport.SshConstants;
+import org.eclipse.jgit.transport.SshSessionFactory;
 
 /**
- * A {@link HostConfigEntryResolver} adapted specifically for JGit.
- * <p>
- * We use our own config file parser and entry resolution since the default
- * {@link org.apache.sshd.client.config.hosts.ConfigFileHostEntryResolver
- * ConfigFileHostEntryResolver} has a number of problems:
- * </p>
- * <ul>
- * <li>It does case-insensitive pattern matching. Matching in OpenSsh is
- * case-sensitive! Compare also bug 531118.</li>
- * <li>It only merges values from the global items (before the first "Host"
- * line) into the host entries. Otherwise it selects the most specific match.
- * OpenSsh processes <em>all</em> entries in the order they appear in the file
- * and whenever one matches, it updates values as appropriate.</li>
- * <li>We have to ensure that ~ replacement uses the same HOME directory as
- * JGit. Compare bug bug 526175.</li>
- * </ul>
- * Therefore, this re-uses the parsing and caching from
- * {@link OpenSshConfigFile}.
- *
+ * A bridge between a JGit {@link SshConfigStore} and the Apache MINA sshd
+ * {@link HostConfigEntryResolver}.
  */
 public class JGitSshConfig implements HostConfigEntryResolver {
 
-       private final OpenSshConfigFile configFile;
-
-       private final String localUserName;
+       private final SshConfigStore configFile;
 
        /**
-        * Creates a new {@link OpenSshConfigFile} that will read the config from
-        * file {@code config} use the given file {@code home} as "home" directory.
+        * Creates a new {@link JGitSshConfig} that will read the config from the
+        * given {@link SshConfigStore}.
         *
-        * @param home
-        *            user's home directory for the purpose of ~ replacement
-        * @param config
-        *            file to load; may be {@code null} if no ssh config file
-        *            handling is desired
-        * @param localUserName
-        *            user name of the current user on the local host OS
+        * @param store
+        *            to use
         */
-       public JGitSshConfig(@NonNull File home, File config,
-                       @NonNull String localUserName) {
-               this.localUserName = localUserName;
-               configFile = config == null ?  null : new OpenSshConfigFile(home, config, localUserName);
+       public JGitSshConfig(SshConfigStore store) {
+               configFile = store;
        }
 
        @Override
        public HostConfigEntry resolveEffectiveHost(String host, int port,
                        SocketAddress localAddress, String username,
                        AttributeRepository attributes) throws IOException {
-               HostEntry entry = configFile == null ? new HostEntry() : configFile.lookup(host, port, username);
+               SshConfigStore.HostConfig entry = configFile == null
+                               ? SshConfigStore.EMPTY_CONFIG
+                               : configFile.lookup(host, port, username);
                JGitHostConfigEntry config = new JGitHostConfigEntry();
                // Apache MINA conflates all keys, even multi-valued ones, in one map
                // and puts multiple values separated by commas in one string. See
@@ -102,7 +76,7 @@ public class JGitSshConfig implements HostConfigEntryResolver {
                String user = username != null && !username.isEmpty() ? username
                                : entry.getValue(SshConstants.USER);
                if (user == null || user.isEmpty()) {
-                       user = localUserName;
+                       user = SshSessionFactory.getLocalUserName();
                }
                config.setUsername(user);
                config.setProperty(SshConstants.USER, user);
index 4c1b49b67ff918a2be9a8f229c2e87f178787ae4..cb6b7d674a6855b626178d22e6a1dbf44916b196 100644 (file)
@@ -39,6 +39,7 @@ import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions;
 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
 import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
 import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
 import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
@@ -50,6 +51,7 @@ import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase;
 import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper;
 import org.eclipse.jgit.internal.transport.sshd.SshdText;
 import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.SshConfigStore;
 import org.eclipse.jgit.transport.SshConstants;
 import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.transport.URIish;
@@ -327,8 +329,8 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
                        @NonNull File homeDir, @NonNull File sshDir) {
                return defaultHostConfigEntryResolver.computeIfAbsent(
                                new Tuple(new Object[] { homeDir, sshDir }),
-                               t -> new JGitSshConfig(homeDir, getSshConfig(sshDir),
-                                               getLocalUserName()));
+                               t -> new JGitSshConfig(createSshConfigStore(homeDir,
+                                               getSshConfig(sshDir), getLocalUserName())));
        }
 
        /**
@@ -347,7 +349,29 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
        }
 
        /**
-        * Obtain a {@link ServerKeyDatabase} to verify server host keys. The
+        * Obtains a {@link SshConfigStore}, or {@code null} if not SSH config is to
+        * be used. The default implementation returns {@code null} if
+        * {@code configFile == null} and otherwise an OpenSSH-compatible store
+        * reading host entries from the given file.
+        *
+        * @param homeDir
+        *            may be used for ~-replacements by the returned config store
+        * @param configFile
+        *            to use, or {@code null} if none
+        * @param localUserName
+        *            user name of the current user on the local OS
+        * @return A {@link SshConfigStore}, or {@code null} if none is to be used
+        *
+        * @since 5.8
+        */
+       protected SshConfigStore createSshConfigStore(@NonNull File homeDir,
+                       File configFile, String localUserName) {
+               return configFile == null ? null
+                               : new OpenSshConfigFile(homeDir, configFile, localUserName);
+       }
+
+       /**
+        * Obtains a {@link ServerKeyDatabase} to verify server host keys. The
         * default implementation returns a {@link ServerKeyDatabase} that
         * recognizes the two openssh standard files {@code ~/.ssh/known_hosts} and
         * {@code ~/.ssh/known_hosts2} as well as any files configured via the
@@ -365,10 +389,31 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
                        @NonNull File sshDir) {
                return defaultServerKeyDatabase.computeIfAbsent(
                                new Tuple(new Object[] { homeDir, sshDir }),
-                               t -> new OpenSshServerKeyDatabase(true,
-                                               getDefaultKnownHostsFiles(sshDir)));
+                               t -> createServerKeyDatabase(homeDir, sshDir));
+
+       }
 
+       /**
+        * Creates a {@link ServerKeyDatabase} to verify server host keys. The
+        * default implementation returns a {@link ServerKeyDatabase} that
+        * recognizes the two openssh standard files {@code ~/.ssh/known_hosts} and
+        * {@code ~/.ssh/known_hosts2} as well as any files configured via the
+        * {@code UserKnownHostsFile} option in the ssh config file.
+        *
+        * @param homeDir
+        *            home directory to use for ~ replacement
+        * @param sshDir
+        *            representing ~/.ssh/
+        * @return the {@link ServerKeyDatabase}
+        * @since 5.8
+        */
+       @NonNull
+       protected ServerKeyDatabase createServerKeyDatabase(@NonNull File homeDir,
+                       @NonNull File sshDir) {
+               return new OpenSshServerKeyDatabase(true,
+                               getDefaultKnownHostsFiles(sshDir));
        }
+
        /**
         * Gets the list of default user known hosts files. The default returns
         * ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java
new file mode 100644 (file)
index 0000000..2147c2b
--- /dev/null
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport.sshd;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.SshConfigStore;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * A builder API to configure {@link SshdSessionFactory SshdSessionFactories}.
+ *
+ * @since 5.8
+ */
+public final class SshdSessionFactoryBuilder {
+
+       private final State state = new State();
+
+       /**
+        * Sets the {@link ProxyDataFactory} to use for {@link SshdSessionFactory
+        * SshdSessionFactories} created by {@link #build(KeyCache)}.
+        *
+        * @param proxyDataFactory
+        *            to use
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setProxyDataFactory(
+                       ProxyDataFactory proxyDataFactory) {
+               this.state.proxyDataFactory = proxyDataFactory;
+               return this;
+       }
+
+       /**
+        * Sets the home directory to use for {@link SshdSessionFactory
+        * SshdSessionFactories} created by {@link #build(KeyCache)}.
+        *
+        * @param homeDirectory
+        *            to use; may be {@code null}, in which case the home directory
+        *            as defined by {@link org.eclipse.jgit.util.FS#userHome()
+        *            FS.userHome()} is assumed
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setHomeDirectory(File homeDirectory) {
+               this.state.homeDirectory = homeDirectory;
+               return this;
+       }
+
+       /**
+        * Sets the SSH directory to use for {@link SshdSessionFactory
+        * SshdSessionFactories} created by {@link #build(KeyCache)}.
+        *
+        * @param sshDirectory
+        *            to use; may be {@code null}, in which case ".ssh" under the
+        *            {@link #setHomeDirectory(File) home directory} is assumed
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setSshDirectory(File sshDirectory) {
+               this.state.sshDirectory = sshDirectory;
+               return this;
+       }
+
+       /**
+        * Sets the default preferred authentication mechanisms to use for
+        * {@link SshdSessionFactory SshdSessionFactories} created by
+        * {@link #build(KeyCache)}.
+        *
+        * @param authentications
+        *            comma-separated list of authentication mechanism names; if
+        *            {@code null} or empty, the default as specified by
+        *            {@link SshdSessionFactory#getDefaultPreferredAuthentications()}
+        *            will be used
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setPreferredAuthentications(
+                       String authentications) {
+               this.state.preferredAuthentications = authentications;
+               return this;
+       }
+
+       /**
+        * Sets a function that returns the SSH config file, given the SSH
+        * directory. The function may return {@code null}, in which case no SSH
+        * config file will be used. If a non-null file is returned, it will be used
+        * when it exists. If no supplier has been set, or the supplier has been set
+        * explicitly to {@code null}, by default a file named
+        * {@link org.eclipse.jgit.transport.SshConstants#CONFIG
+        * SshConstants.CONFIG} in the {@link #setSshDirectory(File) SSH directory}
+        * is used.
+        *
+        * @param supplier
+        *            returning a {@link File} for the SSH config file to use, or
+        *            returning {@code null} if no config file is to be used
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setConfigFile(
+                       Function<File, File> supplier) {
+               this.state.configFileFinder = supplier;
+               return this;
+       }
+
+       /**
+        * A factory interface for creating a @link SshConfigStore}.
+        */
+       @FunctionalInterface
+       public interface ConfigStoreFactory {
+
+               /**
+                * Creates a {@link SshConfigStore}. May return {@code null} if none is
+                * to be used.
+                *
+                * @param homeDir
+                *            to use for ~-replacements
+                * @param configFile
+                *            to use, may be {@code null} if none
+                * @param localUserName
+                *            name of the current user in the local OS
+                * @return the {@link SshConfigStore}, or {@code null} if none is to be
+                *         used
+                */
+               SshConfigStore create(@NonNull File homeDir, File configFile,
+                               String localUserName);
+       }
+
+       /**
+        * Sets a factory for the {@link SshConfigStore} to use. If not set or
+        * explicitly set to {@code null}, the default as specified by
+        * {@link SshdSessionFactory#createSshConfigStore(File, File, String)} is
+        * used.
+        *
+        * @param factory
+        *            to set
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setConfigStoreFactory(
+                       ConfigStoreFactory factory) {
+               this.state.configFactory = factory;
+               return this;
+       }
+
+       /**
+        * Sets a function that returns the default known hosts files, given the SSH
+        * directory. If not set or explicitly set to {@code null}, the defaults as
+        * specified by {@link SshdSessionFactory#getDefaultKnownHostsFiles(File)}
+        * are used.
+        *
+        * @param supplier
+        *            to get the default known hosts files
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setDefaultKnownHostsFiles(
+                       Function<File, List<Path>> supplier) {
+               this.state.knownHostsFileFinder = supplier;
+               return this;
+       }
+
+       /**
+        * Sets a function that returns the default private key files, given the SSH
+        * directory. If not set or explicitly set to {@code null}, the defaults as
+        * specified by {@link SshdSessionFactory#getDefaultIdentities(File)} are
+        * used.
+        *
+        * @param supplier
+        *            to get the default private key files
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setDefaultIdentities(
+                       Function<File, List<Path>> supplier) {
+               this.state.defaultKeyFileFinder = supplier;
+               return this;
+       }
+
+       /**
+        * Sets a function that returns the default private keys, given the SSH
+        * directory. If not set or explicitly set to {@code null}, the defaults as
+        * specified by {@link SshdSessionFactory#getDefaultKeys(File)} are used.
+        *
+        * @param provider
+        *            to get the default private key files
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setDefaultKeysProvider(
+                       Function<File, Iterable<KeyPair>> provider) {
+               this.state.defaultKeysProvider = provider;
+               return this;
+       }
+
+       /**
+        * Sets a factory function to create a {@link KeyPasswordProvider}. If not
+        * set or explicitly set to {@code null}, or if the factory returns
+        * {@code null}, the default as specified by
+        * {@link SshdSessionFactory#createKeyPasswordProvider(CredentialsProvider)}
+        * is used.
+        *
+        * @param factory
+        *            to create a {@link KeyPasswordProvider}
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setKeyPasswordProvider(
+                       Function<CredentialsProvider, KeyPasswordProvider> factory) {
+               this.state.passphraseProviderFactory = factory;
+               return this;
+       }
+
+       /**
+        * Sets a function that creates a new {@link ServerKeyDatabase}, given the
+        * SSH and home directory. If not set or explicitly set to {@code null}, or
+        * if the {@code factory} returns {@code null}, the default as specified by
+        * {@link SshdSessionFactory#createServerKeyDatabase(File, File)} is used.
+        *
+        * @param factory
+        *            to create a {@link ServerKeyDatabase}
+        * @return this {@link SshdSessionFactoryBuilder}
+        */
+       public SshdSessionFactoryBuilder setServerKeyDatabase(
+                       BiFunction<File, File, ServerKeyDatabase> factory) {
+               this.state.serverKeyDatabaseCreator = factory;
+               return this;
+       }
+
+       /**
+        * Builds a {@link SshdSessionFactory} as configured, using the given
+        * {@link KeyCache} for caching keys.
+        * <p>
+        * Different {@link SshdSessionFactory SshdSessionFactories} should
+        * <em>not</em> share the same {@link KeyCache} since the cache is
+        * invalidated when the factory itself or when the last {@link SshdSession}
+        * created from the factory is closed.
+        * </p>
+        *
+        * @param cache
+        *            to use for caching ssh keys; may be {@code null} if no caching
+        *            is desired.
+        * @return the {@link SshdSessionFactory}
+        */
+       public SshdSessionFactory build(KeyCache cache) {
+               // Use a copy to avoid that subsequent calls to setters affect an
+               // already created SshdSessionFactory.
+               return state.copy().build(cache);
+       }
+
+       private static class State {
+
+               ProxyDataFactory proxyDataFactory;
+
+               File homeDirectory;
+
+               File sshDirectory;
+
+               String preferredAuthentications;
+
+               Function<File, File> configFileFinder;
+
+               ConfigStoreFactory configFactory;
+
+               Function<CredentialsProvider, KeyPasswordProvider> passphraseProviderFactory;
+
+               Function<File, List<Path>> knownHostsFileFinder;
+
+               Function<File, List<Path>> defaultKeyFileFinder;
+
+               Function<File, Iterable<KeyPair>> defaultKeysProvider;
+
+               BiFunction<File, File, ServerKeyDatabase> serverKeyDatabaseCreator;
+
+               State copy() {
+                       State c = new State();
+                       c.proxyDataFactory = proxyDataFactory;
+                       c.homeDirectory = homeDirectory;
+                       c.sshDirectory = sshDirectory;
+                       c.preferredAuthentications = preferredAuthentications;
+                       c.configFileFinder = configFileFinder;
+                       c.configFactory = configFactory;
+                       c.passphraseProviderFactory = passphraseProviderFactory;
+                       c.knownHostsFileFinder = knownHostsFileFinder;
+                       c.defaultKeyFileFinder = defaultKeyFileFinder;
+                       c.defaultKeysProvider = defaultKeysProvider;
+                       c.serverKeyDatabaseCreator = serverKeyDatabaseCreator;
+                       return c;
+               }
+
+               SshdSessionFactory build(KeyCache cache) {
+                       SshdSessionFactory factory = new SessionFactory(cache,
+                                       proxyDataFactory);
+                       factory.setHomeDirectory(homeDirectory);
+                       factory.setSshDirectory(sshDirectory);
+                       return factory;
+               }
+
+               private class SessionFactory extends SshdSessionFactory {
+
+                       public SessionFactory(KeyCache cache,
+                                       ProxyDataFactory proxyDataFactory) {
+                               super(cache, proxyDataFactory);
+                       }
+
+                       @Override
+                       protected File getSshConfig(File sshDir) {
+                               if (configFileFinder != null) {
+                                       return configFileFinder.apply(sshDir);
+                               }
+                               return super.getSshConfig(sshDir);
+                       }
+
+                       @Override
+                       protected List<Path> getDefaultKnownHostsFiles(File sshDir) {
+                               if (knownHostsFileFinder != null) {
+                                       List<Path> result = knownHostsFileFinder.apply(sshDir);
+                                       return result == null ? Collections.emptyList() : result;
+                               }
+                               return super.getDefaultKnownHostsFiles(sshDir);
+                       }
+
+                       @Override
+                       protected List<Path> getDefaultIdentities(File sshDir) {
+                               if (defaultKeyFileFinder != null) {
+                                       List<Path> result = defaultKeyFileFinder.apply(sshDir);
+                                       return result == null ? Collections.emptyList() : result;
+                               }
+                               return super.getDefaultIdentities(sshDir);
+                       }
+
+                       @Override
+                       protected String getDefaultPreferredAuthentications() {
+                               if (!StringUtils.isEmptyOrNull(preferredAuthentications)) {
+                                       return preferredAuthentications;
+                               }
+                               return super.getDefaultPreferredAuthentications();
+                       }
+
+                       @Override
+                       protected Iterable<KeyPair> getDefaultKeys(File sshDir) {
+                               if (defaultKeysProvider != null) {
+                                       Iterable<KeyPair> result = defaultKeysProvider
+                                                       .apply(sshDir);
+                                       return result == null ? Collections.emptyList() : result;
+                               }
+                               return super.getDefaultKeys(sshDir);
+                       }
+
+                       @Override
+                       protected KeyPasswordProvider createKeyPasswordProvider(
+                                       CredentialsProvider provider) {
+                               if (passphraseProviderFactory != null) {
+                                       KeyPasswordProvider result = passphraseProviderFactory
+                                                       .apply(provider);
+                                       if (result != null) {
+                                               return result;
+                                       }
+                               }
+                               return super.createKeyPasswordProvider(provider);
+                       }
+
+                       @Override
+                       protected ServerKeyDatabase createServerKeyDatabase(File homeDir,
+                                       File sshDir) {
+                               if (serverKeyDatabaseCreator != null) {
+                                       ServerKeyDatabase result = serverKeyDatabaseCreator
+                                                       .apply(homeDir, sshDir);
+                                       if (result != null) {
+                                               return result;
+                                       }
+                               }
+                               return super.createServerKeyDatabase(homeDir, sshDir);
+                       }
+
+                       @Override
+                       protected SshConfigStore createSshConfigStore(File homeDir,
+                                       File configFile, String localUserName) {
+                               if (configFactory != null) {
+                                       return configFactory.create(homeDir, configFile,
+                                                       localUserName);
+                               }
+                               return super.createSshConfigStore(homeDir, configFile,
+                                               localUserName);
+                       }
+               }
+       }
+}
index 2fbc9122f117aec040b34607de250b95c48808b0..98c63cdcdd62af80ab2276ad48716701c8bcbcce 100644 (file)
@@ -32,6 +32,7 @@ import java.util.TreeSet;
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.fnmatch.FileNameMatcher;
+import org.eclipse.jgit.transport.SshConfigStore;
 import org.eclipse.jgit.transport.SshConstants;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.StringUtils;
@@ -80,7 +81,7 @@ import org.eclipse.jgit.util.SystemReader;
  * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
  *      ssh-config</a>
  */
-public class OpenSshConfigFile {
+public class OpenSshConfigFile implements SshConfigStore {
 
        /**
         * "Host" name of the HostEntry for the default options before the first
@@ -152,8 +153,9 @@ public class OpenSshConfigFile {
         *            the user supplied; <= 0 if none
         * @param userName
         *            the user supplied, may be {@code null} or empty if none given
-        * @return r configuration for the requested name.
+        * @return the configuration for the requested name.
         */
+       @Override
        @NonNull
        public HostEntry lookup(@NonNull String hostName, int port,
                        String userName) {
@@ -446,7 +448,7 @@ public class OpenSshConfigFile {
         * of several matching host entries, %-substitutions, and ~ replacement have
         * all been done.
         */
-       public static class HostEntry {
+       public static class HostEntry implements SshConfigStore.HostConfig {
 
                /**
                 * Keys that can be specified multiple times, building up a list. (I.e.,
@@ -489,7 +491,7 @@ public class OpenSshConfigFile {
                private Map<String, List<String>> listOptions;
 
                /**
-                * Retrieves the value of a single-valued key, or the first is the key
+                * Retrieves the value of a single-valued key, or the first if the key
                 * has multiple values. Keys are case-insensitive, so
                 * {@code getValue("HostName") == getValue("HOSTNAME")}.
                 *
@@ -497,6 +499,7 @@ public class OpenSshConfigFile {
                 *            to get the value of
                 * @return the value, or {@code null} if none
                 */
+               @Override
                public String getValue(String key) {
                        String result = options != null ? options.get(key) : null;
                        if (result == null) {
@@ -524,6 +527,7 @@ public class OpenSshConfigFile {
                 *            to get the values of
                 * @return a possibly empty list of values
                 */
+               @Override
                public List<String> getValues(String key) {
                        List<String> values = listOptions != null ? listOptions.get(key)
                                        : null;
@@ -778,6 +782,7 @@ public class OpenSshConfigFile {
                 *
                 * @return all single-valued options
                 */
+               @Override
                @NonNull
                public Map<String, String> getOptions() {
                        if (options == null) {
@@ -792,6 +797,7 @@ public class OpenSshConfigFile {
                 *
                 * @return all multi-valued options
                 */
+               @Override
                @NonNull
                public Map<String, List<String>> getMultiValuedOptions() {
                        if (listOptions == null && multiOptions == null) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigStore.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigStore.java
new file mode 100644 (file)
index 0000000..04a4922
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * An abstraction for a SSH config storage, like the OpenSSH ~/.ssh/config file.
+ *
+ * @since 5.8
+ */
+public interface SshConfigStore {
+
+       /**
+        * Locate the configuration for a specific host request.
+        *
+        * @param hostName
+        *            to look up
+        * @param port
+        *            the user supplied; <= 0 if none
+        * @param userName
+        *            the user supplied, may be {@code null} or empty if none given
+        * @return the configuration for the requested name.
+        */
+       @NonNull
+       HostConfig lookup(@NonNull String hostName, int port, String userName);
+
+       /**
+        * A host entry from the ssh config. Any merging of global values and of
+        * several matching host entries, %-substitutions, and ~ replacement have
+        * all been done.
+        */
+       interface HostConfig {
+
+               /**
+                * Retrieves the value of a single-valued key, or the first if the key
+                * has multiple values. Keys are case-insensitive, so
+                * {@code getValue("HostName") == getValue("HOSTNAME")}.
+                *
+                * @param key
+                *            to get the value of
+                * @return the value, or {@code null} if none
+                */
+               String getValue(String key);
+
+               /**
+                * Retrieves the values of a multi- or list-valued key. Keys are
+                * case-insensitive, so
+                * {@code getValue("HostName") == getValue("HOSTNAME")}.
+                *
+                * @param key
+                *            to get the values of
+                * @return a possibly empty list of values
+                */
+               List<String> getValues(String key);
+
+               /**
+                * Retrieves an unmodifiable map of all single-valued options, with
+                * case-insensitive lookup by keys.
+                *
+                * @return all single-valued options
+                */
+               @NonNull
+               Map<String, String> getOptions();
+
+               /**
+                * Retrieves an unmodifiable map of all multi- or list-valued options,
+                * with case-insensitive lookup by keys.
+                *
+                * @return all multi-valued options
+                */
+               @NonNull
+               Map<String, List<String>> getMultiValuedOptions();
+
+       }
+
+       /**
+        * An empty {@link HostConfig}.
+        */
+       static final HostConfig EMPTY_CONFIG = new HostConfig() {
+
+               @Override
+               public String getValue(String key) {
+                       return null;
+               }
+
+               @Override
+               public List<String> getValues(String key) {
+                       return Collections.emptyList();
+               }
+
+               @Override
+               public Map<String, String> getOptions() {
+                       return Collections.emptyMap();
+               }
+
+               @Override
+               public Map<String, List<String>> getMultiValuedOptions() {
+                       return Collections.emptyMap();
+               }
+
+       };
+}