Builder API to configure SshdSessionFactories
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>
This commit is contained in:
parent
bdb7357228
commit
3a499606b1
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -47,7 +47,6 @@
|
|||
*/
|
||||
public class NoFilesSshTest extends SshTestHarness {
|
||||
|
||||
|
||||
private PublicKey testServerKey;
|
||||
|
||||
private KeyPair testUserKey;
|
||||
|
|
|
@ -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 @@
|
|||
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.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 HostConfigEntry resolveEffectiveHost(String host, int port,
|
|||
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);
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
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.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 @@ private HostConfigEntryResolver getHostConfigEntryResolver(
|
|||
@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 @@ protected File getSshConfig(@NonNull File sshDir) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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 @@ protected ServerKeyDatabase getServerKeyDatabase(@NonNull File homeDir,
|
|||
@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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@
|
|||
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 @@
|
|||
* @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 OpenSshConfigFile(@NonNull File home, @NonNull File config,
|
|||
* 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 String getLocalUserName() {
|
|||
* 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 static class HostEntry {
|
|||
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 static class HostEntry {
|
|||
* 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 String getValue(String key) {
|
|||
* 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 @@ void substitute(String originalHostName, int port, String userName,
|
|||
*
|
||||
* @return all single-valued options
|
||||
*/
|
||||
@Override
|
||||
@NonNull
|
||||
public Map<String, String> getOptions() {
|
||||
if (options == null) {
|
||||
|
@ -792,6 +797,7 @@ public Map<String, String> getOptions() {
|
|||
*
|
||||
* @return all multi-valued options
|
||||
*/
|
||||
@Override
|
||||
@NonNull
|
||||
public Map<String, List<String>> getMultiValuedOptions() {
|
||||
if (listOptions == null && multiOptions == 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();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue