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>tags/v5.8.0.202006091008-r
@@ -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 @@ import org.junit.Test; | |||
*/ | |||
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 @@ 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); |
@@ -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 |
@@ -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 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) { |
@@ -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(); | |||
} | |||
}; | |||
} |