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