diff options
Diffstat (limited to 'org.eclipse.jgit.ssh.apache')
5 files changed, 468 insertions, 48 deletions
diff --git a/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.transport.SshSessionFactory b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.transport.SshSessionFactory new file mode 100644 index 0000000000..8289411149 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.transport.SshSessionFactory @@ -0,0 +1 @@ +org.eclipse.jgit.transport.sshd.SshdSessionFactory diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java index 2ce69901c1..79b3637caa 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java @@ -170,7 +170,7 @@ public class CachingKeyPairProvider extends FileKeyPairProvider } catch (CancellationException cancelled) { throw cancelled; } catch (Exception other) { - log.warn(other.toString()); + log.warn(other.getMessage(), other); } } return nextItem != null; diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java index e770134fa1..97e0fcc7d2 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java @@ -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); diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java index 4c1b49b67f..bb4e49be8e 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java @@ -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; @@ -64,6 +66,8 @@ import org.eclipse.jgit.util.FS; */ public class SshdSessionFactory extends SshSessionFactory implements Closeable { + private static final String MINA_SSHD = "mina-sshd"; //$NON-NLS-1$ + private final AtomicBoolean closing = new AtomicBoolean(); private final Set<SshdSession> sessions = new HashSet<>(); @@ -131,6 +135,11 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { BCryptKdfOptions.setMaxAllowedRounds(16384); } + @Override + public String getType() { + return MINA_SSHD; + } + /** A simple general map key. */ private static final class Tuple { private Object[] objects; @@ -327,8 +336,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 +356,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 +396,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 index 0000000000..2147c2bd58 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java @@ -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); + } + } + } +} |