--- /dev/null
+/*
+ * 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());
+ }
+
+}
*/
public class NoFilesSshTest extends SshTestHarness {
-
private PublicKey testServerKey;
private KeyPair testUserKey;
/*
- * 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
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;
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
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);
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;
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;
@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())));
}
/**
}
/**
- * 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
@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
--- /dev/null
+/*
+ * 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);
+ }
+ }
+ }
+}
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;
* @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
* 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) {
* 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.,
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")}.
*
* 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) {
* 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;
*
* @return all single-valued options
*/
+ @Override
@NonNull
public Map<String, String> getOptions() {
if (options == null) {
*
* @return all multi-valued options
*/
+ @Override
@NonNull
public Map<String, List<String>> getMultiValuedOptions() {
if (listOptions == null && multiOptions == null) {
--- /dev/null
+/*
+ * 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();
+ }
+
+ };
+}