Browse Source
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>master
Thomas Wolf
5 years ago
7 changed files with 746 additions and 52 deletions
@ -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()); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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