This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push:
new 204c0bc [SSHD-1047] Support for SSH jumps (#154)
204c0bc is described below
commit 204c0bc48de5a32c43a3b201f8c739bb38f161f8
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Jul 31 14:55:28 2020 +0200
[SSHD-1047] Support for SSH jumps (#154)
---
CHANGES.md | 9 +-
docs/proxies.md | 22 ++
.../org/apache/sshd/cli/client/ScpCommandMain.java | 2 +-
.../apache/sshd/cli/client/SftpCommandMain.java | 2 +-
.../sshd/cli/client/SshClientCliSupport.java | 33 ++-
.../org/apache/sshd/cli/client/SshClientMain.java | 2 +-
.../apache/sshd/cli/client/ChannelExecMain.java | 2 +-
.../config/hosts/ConfigFileHostEntryResolver.java | 22 +-
.../hosts/DefaultConfigFileHostEntryResolver.java | 11 +-
.../sshd/client/config/hosts/HostConfigEntry.java | 58 +++++-
.../config/hosts/HostConfigEntryResolver.java | 6 +-
.../hosts/ConfigFileHostEntryResolverTest.java | 8 +-
.../java/org/apache/sshd/client/SshClient.java | 226 +++++++++++++-------
.../sshd/client/session/AbstractClientSession.java | 4 +
.../sshd/client/session/ClientSessionCreator.java | 14 ++
.../simple/AbstractSimpleClientSessionCreator.java | 15 ++
.../sshd/client/simple/SimpleSessionClient.java | 20 ++
.../java/org/apache/sshd/client/ProxyTest.java | 228 ++++++++++++++++++---
.../config/hosts/HostConfigEntryResolverTest.java | 4 +-
19 files changed, 550 insertions(+), 138 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 6477914..fc54a85 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -13,16 +13,17 @@
## Major code re-factoring
* [SSHD-506](https://issues.apache.org/jira/browse/SSHD-506) Added support for
AES-GCM ciphers.
+* [SSHD-1004](https://issues.apache.org/jira/browse/SSHD-1004) Deprecate DES,
RC4 and Blowfish ciphers from default setup.
* [SSHD-1034](https://issues.apache.org/jira/browse/SSHD-1034) Rename
`org.apache.sshd.common.ForwardingFilter` to `Forwarder`.
* [SSHD-1035](https://issues.apache.org/jira/browse/SSHD-1035) Move property
definitions to common locations.
-* [SSHD-1038](https://issues.apache.org/jira/browse/SSHD-1035) Refactor
packages from a module into a cleaner hierarchy.
-* [SSHD-1004](https://issues.apache.org/jira/browse/SSHD-1035) Deprecate DES,
RC4 and Blowfish ciphers from default setup.
+* [SSHD-1038](https://issues.apache.org/jira/browse/SSHD-1038) Refactor
packages from a module into a cleaner hierarchy.
+* [SSHD-1047](https://issues.apache.org/jira/browse/SSHD-1047) Support for SSH
jumps.
## Minor code helpers
-* [SSHD-1040](https://issues.apache.org/jira/browse/SSHD-1040) Make server key
available after KEX completed.
* [SSHD-1030](https://issues.apache.org/jira/browse/SSHD-1030) Added a
NoneFileSystemFactory implementation
-* [SSHD-1042](https://issues.apache.org/jira/browse/SSHD-1030) Added more
callbacks to SftpEventListener
+* [SSHD-1042](https://issues.apache.org/jira/browse/SSHD-1042) Added more
callbacks to SftpEventListener
+* [SSHD-1040](https://issues.apache.org/jira/browse/SSHD-1040) Make server key
available after KEX completed.
## Behavioral changes and enhancements
diff --git a/docs/proxies.md b/docs/proxies.md
new file mode 100644
index 0000000..4047179
--- /dev/null
+++ b/docs/proxies.md
@@ -0,0 +1,22 @@
+# Proxies
+
+## SSH Jumps
+
+The SSH client can be configured to use SSH jumps. A *jump host* (also known
as a *jump server*) is an
+intermediary host or an SSH gateway to a remote network, through which a
connection can be made to another
+host in a dissimilar security zone, for example a demilitarized zone (DMZ). It
bridges two dissimilar
+security zones and offers controlled access between them.
+
+Starting from SSHD 2.6.0, the *ProxyJump* host configuration entry is honored
when using the `SshClient`
+to connect to a host. The `SshClient` built by default reads the
`~/.ssh/config` file. The various CLI clients
+also honor the `-J` command line option to specify one or more jumps.
+
+In order to manually configure jumps, you need to build a `HostConfigEntry`
with a `proxyJump` and use it
+to connect to the server:
+```
+ConnectFuture future = client.connect(new HostConfigEntry(
+ "", host, port, user,
+ proxyUser + "@" + proxyHost + ":" + proxyPort));
+```
+
+The configuration options specified in the configuration file for the jump
hosts are also honored.
diff --git
a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
index a0a842b..a23eff5 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
@@ -184,7 +184,7 @@ public class ScpCommandMain extends SshClientCliSupport {
if (session == null) {
stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i
identity] [-io nio2|mina|netty]"
+ " [-v[v][v]] [-E logoutput] [-r] [-p] [-q]
[-o option=value] [-o creator=class name]"
- + " [-c cipherlist] [-m maclist] [-w password]
[-C] <source> <target>");
+ + " [-c cipherlist] [-m maclist] [-J proxyJump]
[-w password] [-C] <source> <target>");
stderr.println();
stderr.println("Where <source> or <target> are either
'user@host:file' or a local file path");
stderr.println("NOTE: exactly ONE of the source or target must
be remote and the other one local");
diff --git
a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
index a1501b9..399208f 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
@@ -329,7 +329,7 @@ public class SftpCommandMain extends SshClientCliSupport
implements Channel {
: setupClientSession(SFTP_PORT_OPTION, stdin, level,
stdout, stderr, args);
if (session == null) {
System.err.println("usage: sftp [-v[v][v]] [-E logoutput] [-i
identity] [-io nio2|mina|netty]"
- + " [-l login] [" + SFTP_PORT_OPTION + "
port] [-o option=value]"
+ + " [-J proxyJump] [-l login] [" +
SFTP_PORT_OPTION + " port] [-o option=value]"
+ " [-w password] [-c cipherlist] [-m
maclist] [-C] hostname/user@host");
System.exit(-1);
return;
diff --git
a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
index 2eff66d..fca0403 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
@@ -51,6 +51,7 @@ import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.auth.keyboard.UserInteraction;
import org.apache.sshd.client.config.SshClientConfigFileReader;
import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
import org.apache.sshd.client.config.keys.ClientIdentity;
import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier;
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
@@ -79,6 +80,7 @@ import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.NoCloseOutputStream;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.common.util.threads.ThreadUtils;
import org.apache.sshd.core.CoreModuleProperties;
@@ -106,10 +108,12 @@ public abstract class SshClientCliSupport extends
CliSupport {
|| "-w".equals(argName)
|| "-c".equals(argName)
|| "-m".equals(argName)
- || "-E".equals(argName);
+ || "-E".equals(argName)
+ || "-J".equals(argName);
}
// NOTE: ClientSession#getFactoryManager is the SshClient
+ // CHECKSTYLE:OFF
public static ClientSession setupClientSession(
String portOption, BufferedReader stdin, Level level,
PrintStream stdout, PrintStream stderr, String... args)
@@ -117,6 +121,7 @@ public abstract class SshClientCliSupport extends
CliSupport {
int port = -1;
String host = null;
String login = null;
+ String proxyJump = null;
String password = null;
boolean error = false;
List<Path> identities = new ArrayList<>();
@@ -149,6 +154,12 @@ public abstract class SshClientCliSupport extends
CliSupport {
error = showError(stderr, "Bad option value for " +
argName + ": " + port);
break;
}
+ } else if ("-J".equals(argName)) {
+ if (proxyJump != null) {
+ error = showError(stderr, argName + " option value
re-specified: " + proxyJump);
+ break;
+ }
+ proxyJump = argVal;
} else if ("-w".equals(argName)) {
if (GenericUtils.length(password) > 0) {
error = showError(stderr, argName + " option value
re-specified: " + password);
@@ -245,8 +256,9 @@ public abstract class SshClientCliSupport extends
CliSupport {
port = SshConstants.DEFAULT_PORT;
}
+ HostConfigEntry entry = resolveHost(client, login, host, port,
proxyJump);
// TODO use a configurable wait time
- ClientSession session = client.connect(login, host, port)
+ ClientSession session = client.connect(entry, null, null)
.verify()
.getSession();
try {
@@ -264,6 +276,23 @@ public abstract class SshClientCliSupport extends
CliSupport {
throw e;
}
}
+ // CHECKSTYLE:ON
+
+ public static HostConfigEntry resolveHost(SshClient client, String
username, String host, int port, String proxyJump)
+ throws IOException {
+ HostConfigEntryResolver resolver = client.getHostConfigEntryResolver();
+ HostConfigEntry entry = resolver.resolveEffectiveHost(host, port,
null, username, proxyJump, null);
+ if (entry == null) {
+ // IPv6 addresses have a format which means they need special
treatment, separate from pattern validation
+ if (SshdSocketAddress.isIPv6Address(host)) {
+ // Not using a pattern as the host name passed in was a valid
IPv6 address
+ entry = new HostConfigEntry("", host, port, username, null);
+ } else {
+ entry = new HostConfigEntry(host, host, port, username,
proxyJump);
+ }
+ }
+ return entry;
+ }
public static Path resolveIdentityFile(String id) throws IOException {
BuiltinIdentities identity = BuiltinIdentities.fromName(id);
diff --git
a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
index 75d28e2..7c1265e 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
@@ -135,7 +135,7 @@ public class SshClientMain extends SshClientCliSupport {
if (error) {
System.err.println("usage: ssh [-A|-a] [-v[v][v]] [-E
logoutputfile] [-D socksPort]"
- + " [-l login] [" + SSH_CLIENT_PORT_OPTION
+ " port] [-o option=value]"
+ + " [-J proxyJump] [-l login] [" +
SSH_CLIENT_PORT_OPTION + " port] [-o option=value]"
+ " [-w password] [-c cipherslist] [-m
maclist] [-C]"
+ " hostname/user@host [command]");
System.exit(-1);
diff --git
a/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
b/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
index 93da3ba..45424d4 100644
--- a/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
@@ -82,7 +82,7 @@ public class ChannelExecMain extends BaseTestSupport {
CliSupport.resolveLoggingVerbosity(args), stdout, stderr,
args);
if (session == null) {
System.err.println("usage: channelExec [-i identity] [-l
login] [-P port] [-o option=value]"
- + " [-w password] [-c cipherlist] [-m
maclist] [-C] hostname/user@host");
+ + " [-J proxyJump] [-w password] [-c
cipherlist] [-m maclist] [-C] hostname/user@host");
System.exit(-1);
return;
}
diff --git
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
index 1159e50..0ee867c 100644
---
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
+++
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
@@ -52,21 +52,21 @@ public class ConfigFileHostEntryResolver extends
ModifiableFileWatcher implement
@Override
public HostConfigEntry resolveEffectiveHost(
- String host, int port, SocketAddress localAddress, String
username, AttributeRepository context)
+ String host, int port, SocketAddress localAddress, String
username, String proxyJump, AttributeRepository context)
throws IOException {
try {
HostConfigEntryResolver delegate
- = Objects.requireNonNull(resolveEffectiveResolver(host,
port, username), "No delegate");
- HostConfigEntry entry = delegate.resolveEffectiveHost(host, port,
localAddress, username, context);
+ = Objects.requireNonNull(resolveEffectiveResolver(host,
port, username, proxyJump), "No delegate");
+ HostConfigEntry entry = delegate.resolveEffectiveHost(host, port,
localAddress, username, proxyJump, context);
if (log.isDebugEnabled()) {
- log.debug("resolveEffectiveHost({}@{}:{}) => {}", username,
host, port, entry);
+ log.debug("resolveEffectiveHost({}@{}:{}/{}) => {}", username,
host, port, proxyJump, entry);
}
return entry;
} catch (Throwable e) {
if (log.isDebugEnabled()) {
- log.debug("resolveEffectiveHost({}@{}:{}) failed ({}) to
resolve: {}",
- username, host, port, e.getClass().getSimpleName(),
e.getMessage());
+ log.debug("resolveEffectiveHost({}@{}:{}/{}) failed ({}) to
resolve: {}",
+ username, host, port, proxyJump,
e.getClass().getSimpleName(), e.getMessage());
}
if (log.isTraceEnabled()) {
@@ -80,18 +80,20 @@ public class ConfigFileHostEntryResolver extends
ModifiableFileWatcher implement
}
}
- protected HostConfigEntryResolver resolveEffectiveResolver(String host,
int port, String username) throws IOException {
+ protected HostConfigEntryResolver resolveEffectiveResolver(String host,
int port, String username, String proxyJump)
+ throws IOException {
if (checkReloadRequired()) {
delegateHolder.set(HostConfigEntryResolver.EMPTY); // start fresh
Path path = getPath();
if (exists()) {
- Collection<HostConfigEntry> entries =
reloadHostConfigEntries(path, host, port, username);
+ Collection<HostConfigEntry> entries =
reloadHostConfigEntries(path, host, port, username, proxyJump);
if (GenericUtils.size(entries) > 0) {
delegateHolder.set(HostConfigEntry.toHostConfigEntryResolver(entries));
}
} else {
- log.info("resolveEffectiveResolver({}@{}:{}) no configuration
file at {}", username, host, port, path);
+ log.info("resolveEffectiveResolver({}@{}:{}/{}) no
configuration file at {}", username, host, port, proxyJump,
+ path);
}
}
@@ -99,7 +101,7 @@ public class ConfigFileHostEntryResolver extends
ModifiableFileWatcher implement
}
protected List<HostConfigEntry> reloadHostConfigEntries(
- Path path, String host, int port, String username)
+ Path path, String host, int port, String username, String
proxyJump)
throws IOException {
List<HostConfigEntry> entries =
HostConfigEntry.readHostConfigEntries(path);
log.info("resolveEffectiveResolver({}@{}:{}) loaded {} entries from
{}", username, host, port,
diff --git
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
index 6b44026..010d28a 100644
---
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
+++
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
@@ -64,22 +64,23 @@ public class DefaultConfigFileHostEntryResolver extends
ConfigFileHostEntryResol
}
@Override
- protected List<HostConfigEntry> reloadHostConfigEntries(Path path, String
host, int port, String username)
+ protected List<HostConfigEntry> reloadHostConfigEntries(Path path, String
host, int port, String username, String proxyJump)
throws IOException {
if (isStrict()) {
if (log.isDebugEnabled()) {
- log.debug("reloadHostConfigEntries({}@{}:{}) check permissions
of {}", username, host, port, path);
+ log.debug("reloadHostConfigEntries({}@{}:{}/{}) check
permissions of {}", username, host, port, proxyJump,
+ path);
}
Map.Entry<String, ?> violation =
validateStrictConfigFilePermissions(path);
if (violation != null) {
- log.warn("reloadHostConfigEntries({}@{}:{}) invalid file={}
permissions: {}",
- username, host, port, path, violation.getKey());
+ log.warn("reloadHostConfigEntries({}@{}:{}/{}) invalid file={}
permissions: {}",
+ username, host, port, proxyJump, path,
violation.getKey());
updateReloadAttributes();
return Collections.emptyList();
}
}
- return super.reloadHostConfigEntries(path, host, port, username);
+ return super.reloadHostConfigEntries(path, host, port, username,
proxyJump);
}
}
diff --git
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
index 6a5bb70..26f839b 100644
---
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
+++
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -77,6 +77,7 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
public static final String HOST_NAME_CONFIG_PROP = "HostName";
public static final String PORT_CONFIG_PROP =
ConfigFileReaderSupport.PORT_CONFIG_PROP;
public static final String USER_CONFIG_PROP = "User";
+ public static final String PROXY_JUMP_CONFIG_PROP = "ProxyJump";
public static final String IDENTITY_FILE_CONFIG_PROP = "IdentityFile";
/**
* Use only the identities specified in the host entry (if any)
@@ -116,6 +117,7 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
private String hostName;
private int port;
private String username;
+ private String proxyJump;
private Boolean exclusiveIdentites;
private Collection<String> identities = Collections.emptyList();
private Map<String, String> properties = Collections.emptyMap();
@@ -125,10 +127,15 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
}
public HostConfigEntry(String pattern, String host, int port, String
username) {
+ this(pattern, host, port, username, null);
+ }
+
+ public HostConfigEntry(String pattern, String host, int port, String
username, String proxyJump) {
setHost(pattern);
setHostName(host);
setPort(port);
setUsername(username);
+ setProxyJump(proxyJump);
}
/**
@@ -211,6 +218,29 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
}
/**
+ * @return the host to use as a proxy
+ */
+ public String getProxyJump() {
+ return proxyJump;
+ }
+
+ public void setProxyJump(String proxyJump) {
+ this.proxyJump = proxyJump;
+ }
+
+ /**
+ * Resolves the effective proxyJump
+ *
+ * @param originalProxyJump The original requested proxyJump
+ * @return If the configured host entry proxyJump is not
{@code null}/empty then it is used,
+ * otherwise the original one.
+ * @see #resolveUsername(String)
+ */
+ public String resolveProxyJump(String originalProxyJump) {
+ return resolveProxyJump(originalProxyJump, getProxyJump());
+ }
+
+ /**
* @return The current identities file paths - may be {@code null}/empty
*/
public Collection<String> getIdentities() {
@@ -472,6 +502,11 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
setIdentitiesOnly(
ConfigFileReaderSupport.parseBooleanValue(
ValidateUtils.checkNotNullAndNotEmpty(joinedValue,
"No identities option value")));
+ } else if (PROXY_JUMP_CONFIG_PROP.equalsIgnoreCase(key)) {
+ String curValue = getProxyJump();
+ ValidateUtils.checkTrue(GenericUtils.isEmpty(curValue) ||
ignoreAlreadyInitialized, "Already initialized %s: %s",
+ key, curValue);
+ setProxyJump(joinedValue);
}
}
@@ -659,7 +694,7 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
if (GenericUtils.isEmpty(entries)) {
return HostConfigEntryResolver.EMPTY;
} else {
- return (host1, port1, lclAddress, username1, ctx) -> {
+ return (host1, port1, lclAddress, username1, proxyJump1, ctx) -> {
List<HostConfigEntry> matches = findMatchingEntries(host1,
entries);
int numMatches = GenericUtils.size(matches);
if (numMatches <= 0) {
@@ -672,7 +707,7 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
host1, port1, numMatches);
}
- return normalizeEntry(match, host1, port1, username1);
+ return normalizeEntry(match, host1, port1, username1,
proxyJump1);
};
}
}
@@ -691,7 +726,7 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
* @see #resolveIdentityFilePath(String, String, int,
String)
*/
public static HostConfigEntry normalizeEntry(
- HostConfigEntry entry, String host, int port, String username)
+ HostConfigEntry entry, String host, int port, String username,
String proxyJump)
throws IOException {
if (entry == null) {
return null;
@@ -702,6 +737,7 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
normal.setHostName(entry.resolveHostName(host));
normal.setPort(entry.resolvePort(port));
normal.setUsername(entry.resolveUsername(username));
+ normal.setProxyJump(entry.resolveProxyJump(proxyJump));
Map<String, String> props = entry.getProperties();
if (GenericUtils.size(props) > 0) {
@@ -772,6 +808,22 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
}
}
+ /**
+ * Resolves the effective proxyJump
+ *
+ * @param originalProxyJump The original requested proxyJump
+ * @param entryProxyJump The configured host entry proxyJump
+ * @return If the configured host entry proxyJump is not
{@code null}/empty then it is used,
+ * otherwise the original one.
+ */
+ public static String resolveProxyJump(String originalProxyJump, String
entryProxyJump) {
+ if (GenericUtils.isEmpty(entryProxyJump)) {
+ return originalProxyJump;
+ } else {
+ return entryProxyJump;
+ }
+ }
+
public static List<HostConfigEntry> readHostConfigEntries(Path path,
OpenOption... options) throws IOException {
try (InputStream input = Files.newInputStream(path, options)) {
return readHostConfigEntries(input, true);
diff --git
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
index d27c053..cc0766a 100644
---
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
+++
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
@@ -36,7 +36,8 @@ public interface HostConfigEntryResolver {
HostConfigEntryResolver EMPTY = new HostConfigEntryResolver() {
@Override
public HostConfigEntry resolveEffectiveHost(
- String host, int port, SocketAddress localAddress, String
username, AttributeRepository context)
+ String host, int port, SocketAddress localAddress, String
username, String proxyJump,
+ AttributeRepository context)
throws IOException {
return null;
}
@@ -54,6 +55,7 @@ public interface HostConfigEntryResolver {
* @param port The requested port
* @param localAddress Optional binding endpoint for the local peer
* @param username The requested username
+ * @param proxyJump The requested proxyJump
* @param context An optional "context" provided during
the connection request (to be attached to
* the established session if successfully connected)
* @return A {@link HostConfigEntry} for the actual target -
{@code null} if use original parameters.
@@ -63,6 +65,6 @@ public interface HostConfigEntryResolver {
* @throws IOException If failed to resolve the configuration
*/
HostConfigEntry resolveEffectiveHost(
- String host, int port, SocketAddress localAddress, String
username, AttributeRepository context)
+ String host, int port, SocketAddress localAddress, String
username, String proxyJump, AttributeRepository context)
throws IOException;
}
diff --git
a/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
b/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
index 4a9a070..54085ca 100644
---
a/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
+++
b/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
@@ -53,10 +53,11 @@ public class ConfigFileHostEntryResolverTest extends
JUnitTestSupport {
ConfigFileHostEntryResolver resolver = new ConfigFileHostEntryResolver(
assertHierarchyTargetFolderExists(dir).resolve(getCurrentTestName() +
".config.txt")) {
@Override
- protected List<HostConfigEntry> reloadHostConfigEntries(Path path,
String host, int port, String username)
+ protected List<HostConfigEntry> reloadHostConfigEntries(
+ Path path, String host, int port, String username, String
proxyJump)
throws IOException {
reloadCount.incrementAndGet();
- return super.reloadHostConfigEntries(path, host, port,
username);
+ return super.reloadHostConfigEntries(path, host, port,
username, proxyJump);
}
};
Path path = resolver.getPath();
@@ -116,7 +117,8 @@ public class ConfigFileHostEntryResolverTest extends
JUnitTestSupport {
for (int index = 1; index < Byte.SIZE; index++) {
HostConfigEntry actual
- = resolver.resolveEffectiveHost(query.getHostName(),
query.getPort(), null, query.getUsername(), null);
+ = resolver.resolveEffectiveHost(query.getHostName(),
query.getPort(), null, query.getUsername(), null,
+ null);
if (entries == null) {
assertEquals(phase + "[" + index + "]: mismatched reload
count", 0, reloadCount.get());
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 8633268..049edca 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -22,6 +22,8 @@ import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.nio.channels.UnsupportedAddressTypeException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -52,6 +54,7 @@ import
org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
import org.apache.sshd.client.config.keys.ClientIdentity;
import org.apache.sshd.client.config.keys.ClientIdentityLoader;
import org.apache.sshd.client.config.keys.DefaultClientIdentitiesWatcher;
+import org.apache.sshd.client.future.AuthFuture;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.future.DefaultConnectFuture;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
@@ -62,6 +65,7 @@ import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.ClientSessionCreator;
import org.apache.sshd.client.session.ClientUserAuthServiceFactory;
import org.apache.sshd.client.session.SessionFactory;
+import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
import org.apache.sshd.client.simple.AbstractSimpleClientSessionCreator;
import org.apache.sshd.client.simple.SimpleClient;
import org.apache.sshd.common.AttributeRepository;
@@ -466,100 +470,120 @@ public class SshClient extends AbstractFactoryManager
implements ClientFactoryMa
}
@Override
- public ConnectFuture connect(
- String username, String host, int port, AttributeRepository
context, SocketAddress localAddress)
- throws IOException {
- HostConfigEntryResolver resolver = getHostConfigEntryResolver();
- HostConfigEntry entry = resolver.resolveEffectiveHost(host, port,
localAddress, username, context);
- if (entry == null) {
- // generate a synthetic entry
- if (log.isDebugEnabled()) {
- log.debug("connect({}@{}:{}) no overrides", username, host,
port);
- }
-
- // IPv6 addresses have a format which means they need special
treatment, separate from pattern validation
- if (SshdSocketAddress.isIPv6Address(host)) {
- // Not using a pattern as the host name passed in was a valid
IPv6 address
- entry = new HostConfigEntry("", host, port, username);
- } else {
- entry = new HostConfigEntry(host, host, port, username);
- }
- } else {
- if (log.isDebugEnabled()) {
- log.debug("connect({}@{}:{}) effective: {}", username, host,
port, entry);
- }
+ public ConnectFuture connect(String uriStr) throws IOException {
+ Objects.requireNonNull(uriStr, "No uri address");
+ URI uri = URI.create(uriStr.contains("//") ? uriStr : "ssh://" +
uriStr);
+ if (GenericUtils.isNotEmpty(uri.getScheme()) &&
!"ssh".equals(uri.getScheme())) {
+ throw new IllegalArgumentException("Unsupported scheme for uri: "
+ uri);
}
-
- return connect(entry, context, localAddress);
+ String host = uri.getHost();
+ int port = uri.getPort();
+ String userInfo = uri.getUserInfo();
+ return connect(userInfo, host, port);
}
@Override
public ConnectFuture connect(
- String username, SocketAddress targetAddress, AttributeRepository
context, SocketAddress localAddress)
+ String username, SocketAddress targetAddress,
+ AttributeRepository context, SocketAddress localAddress)
throws IOException {
Objects.requireNonNull(targetAddress, "No target address");
- if (targetAddress instanceof InetSocketAddress) {
- InetSocketAddress inetAddress = (InetSocketAddress) targetAddress;
- String host =
ValidateUtils.checkNotNullAndNotEmpty(inetAddress.getHostString(), "No host");
- int port = inetAddress.getPort();
- ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port);
-
- HostConfigEntryResolver resolver = getHostConfigEntryResolver();
- HostConfigEntry entry = resolver.resolveEffectiveHost(host, port,
localAddress, username, context);
- if (entry == null) {
- if (log.isDebugEnabled()) {
- log.debug("connect({}@{}:{}) no overrides", username,
host, port);
- }
-
- return doConnect(
- username, targetAddress, context, localAddress,
KeyIdentityProvider.EMPTY_KEYS_PROVIDER, true);
- } else {
- if (log.isDebugEnabled()) {
- log.debug("connect({}@{}:{}) effective: {}", username,
host, port, entry);
- }
-
- return connect(entry, context, localAddress);
- }
- } else {
- if (log.isDebugEnabled()) {
- log.debug("connect({}@{}) not an InetSocketAddress: {}",
- username, targetAddress,
targetAddress.getClass().getName());
- }
- return doConnect(
- username, targetAddress, context, localAddress,
KeyIdentityProvider.EMPTY_KEYS_PROVIDER, true);
+ if (!(targetAddress instanceof InetSocketAddress)) {
+ throw new UnsupportedAddressTypeException();
}
+ InetSocketAddress inetAddress = (InetSocketAddress) targetAddress;
+ String host =
ValidateUtils.checkNotNullAndNotEmpty(inetAddress.getHostString(), "No host");
+ int port = inetAddress.getPort();
+ ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port);
+ return connect(username, host, port, context, localAddress);
+ }
+
+ @Override
+ public ConnectFuture connect(
+ String username, String host, int port,
+ AttributeRepository context, SocketAddress localAddress)
+ throws IOException {
+ HostConfigEntry entry = resolveHost(username, host, port, context,
localAddress);
+ return connect(entry, context, localAddress);
}
@Override
public ConnectFuture connect(
HostConfigEntry hostConfig, AttributeRepository context,
SocketAddress localAddress)
throws IOException {
+ List<HostConfigEntry> jumps =
parseProxyJumps(hostConfig.getProxyJump(), context);
+ return doConnect(hostConfig, jumps, context, localAddress);
+ }
+
+ protected ConnectFuture doConnect(
+ HostConfigEntry hostConfig, List<HostConfigEntry> jumps,
+ AttributeRepository context, SocketAddress localAddress)
+ throws IOException {
Objects.requireNonNull(hostConfig, "No host configuration");
String host =
ValidateUtils.checkNotNullAndNotEmpty(hostConfig.getHostName(), "No target
host");
int port = hostConfig.getPort();
ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port);
-
Collection<String> hostIds = hostConfig.getIdentities();
- Collection<PathResource> idFiles = GenericUtils.isEmpty(hostIds)
- ? Collections.emptyList()
- : hostIds.stream()
- .map(Paths::get)
- .map(PathResource::new)
- .collect(Collectors.toCollection(() -> new
ArrayList<>(hostIds.size())));
+ Collection<PathResource> idFiles = GenericUtils.stream(hostIds)
+ .map(Paths::get)
+ .map(PathResource::new)
+ .collect(Collectors.toCollection(() -> new
ArrayList<>(hostIds.size())));
KeyIdentityProvider keys = preloadClientIdentities(idFiles);
- return doConnect(hostConfig.getUsername(), new InetSocketAddress(host,
port),
- context, localAddress, keys, !hostConfig.isIdentitiesOnly());
- }
-
- protected KeyIdentityProvider preloadClientIdentities(
- Collection<? extends NamedResource> locations)
- throws IOException {
- return GenericUtils.isEmpty(locations)
- ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER
- : ClientIdentityLoader.asKeyIdentityProvider(
- Objects.requireNonNull(getClientIdentityLoader(), "No
ClientIdentityLoader"),
- locations, getFilePasswordProvider(),
-
CoreModuleProperties.IGNORE_INVALID_IDENTITIES.getRequired(this));
+ String username = hostConfig.getUsername();
+ InetSocketAddress targetAddress = new
InetSocketAddress(hostConfig.getHostName(), hostConfig.getPort());
+ if (GenericUtils.isNotEmpty(jumps)) {
+ ConnectFuture connectFuture = new DefaultConnectFuture(username +
"@" + targetAddress, null);
+ HostConfigEntry jump = jumps.remove(0);
+ ConnectFuture f1 = doConnect(jump, jumps, context, null);
+ f1.addListener(f2 -> {
+ if (f2.isConnected()) {
+ ClientSession proxySession = f2.getClientSession();
+ try {
+ AuthFuture auth = proxySession.auth();
+ auth.addListener(f3 -> {
+ if (f3.isSuccess()) {
+ try {
+ SshdSocketAddress address
+ = new
SshdSocketAddress(hostConfig.getHostName(), hostConfig.getPort());
+ ExplicitPortForwardingTracker tracker =
proxySession
+
.createLocalPortForwardingTracker(SshdSocketAddress.LOCALHOST_ADDRESS, address);
+ SshdSocketAddress bound =
tracker.getBoundAddress();
+ ConnectFuture f4 =
doConnect(hostConfig.getUsername(), bound.toInetSocketAddress(),
+ context, localAddress, keys,
!hostConfig.isIdentitiesOnly());
+ f4.addListener(f5 -> {
+ if (f5.isConnected()) {
+ ClientSession clientSession =
f5.getClientSession();
+
clientSession.setAttribute(TARGET_SERVER, address);
+
connectFuture.setSession(clientSession);
+
proxySession.addCloseFutureListener(f6 -> clientSession.close(true));
+
clientSession.addCloseFutureListener(f6 -> proxySession.close(true));
+ } else {
+ proxySession.close(true);
+
connectFuture.setException(f5.getException());
+ }
+ });
+ } catch (IOException e) {
+ proxySession.close(true);
+ connectFuture.setException(e);
+ }
+ } else {
+ proxySession.close(true);
+ connectFuture.setException(f3.getException());
+ }
+ });
+ } catch (IOException e) {
+ proxySession.close(true);
+ connectFuture.setException(e);
+ }
+ } else {
+ connectFuture.setException(f2.getException());
+ }
+ });
+ return connectFuture;
+ } else {
+ return doConnect(hostConfig.getUsername(), new
InetSocketAddress(host, port),
+ context, localAddress, keys,
!hostConfig.isIdentitiesOnly());
+ }
}
protected ConnectFuture doConnect(
@@ -580,6 +604,60 @@ public class SshClient extends AbstractFactoryManager
implements ClientFactoryMa
return connectFuture;
}
+ protected List<HostConfigEntry> parseProxyJumps(String proxyJump,
AttributeRepository context) throws IOException {
+ List<HostConfigEntry> jumps = new ArrayList<>();
+ for (String jump : GenericUtils.split(proxyJump, ',')) {
+ String j = jump.trim();
+ URI uri = URI.create(j.contains("//") ? j : "ssh://" + j);
+ if (GenericUtils.isNotEmpty(uri.getScheme()) &&
!"ssh".equals(uri.getScheme())) {
+ throw new IllegalArgumentException("Unsupported scheme for
proxy jump: " + jump);
+ }
+ String host = uri.getHost();
+ int port = uri.getPort();
+ String userInfo = uri.getUserInfo();
+ HostConfigEntry entry = resolveHost(userInfo, host, port, context,
null);
+ jumps.add(entry);
+ }
+ return jumps;
+ }
+
+ protected HostConfigEntry resolveHost(
+ String username, String host, int port, AttributeRepository
context, SocketAddress localAddress)
+ throws IOException {
+ HostConfigEntryResolver resolver = getHostConfigEntryResolver();
+ HostConfigEntry entry = resolver.resolveEffectiveHost(host, port,
localAddress, username, null, context);
+ if (entry == null) {
+ // generate a synthetic entry
+ if (log.isDebugEnabled()) {
+ log.debug("connect({}@{}:{}) no overrides", username, host,
port);
+ }
+
+ // IPv6 addresses have a format which means they need special
treatment, separate from pattern validation
+ if (SshdSocketAddress.isIPv6Address(host)) {
+ // Not using a pattern as the host name passed in was a valid
IPv6 address
+ entry = new HostConfigEntry("", host, port, username, null);
+ } else {
+ entry = new HostConfigEntry(host, host, port, username, null);
+ }
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug("connect({}@{}:{}) effective: {}", username, host,
port, entry);
+ }
+ }
+ return entry;
+ }
+
+ protected KeyIdentityProvider preloadClientIdentities(
+ Collection<? extends NamedResource> locations)
+ throws IOException {
+ return GenericUtils.isEmpty(locations)
+ ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER
+ : ClientIdentityLoader.asKeyIdentityProvider(
+ Objects.requireNonNull(getClientIdentityLoader(), "No
ClientIdentityLoader"),
+ locations, getFilePasswordProvider(),
+
CoreModuleProperties.IGNORE_INVALID_IDENTITIES.getRequired(this));
+ }
+
protected SshFutureListener<IoConnectFuture>
createConnectCompletionListener(
ConnectFuture connectFuture, String username, SocketAddress
address,
KeyIdentityProvider identities, boolean useDefaultIdentities) {
diff --git
a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 5421ee6..805df8b 100644
---
a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++
b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -560,6 +560,10 @@ public abstract class AbstractClientSession extends
AbstractSession implements C
IoSession networkSession = getIoSession();
SocketAddress remoteAddress = networkSession.getRemoteAddress();
PublicKey serverKey = Objects.requireNonNull(getServerKey(), "No
server key to verify");
+ SshdSocketAddress targetServerAddress =
getAttribute(ClientSessionCreator.TARGET_SERVER);
+ if (targetServerAddress != null) {
+ remoteAddress = targetServerAddress.toInetSocketAddress();
+ }
boolean verified = false;
if (serverKey instanceof OpenSshCertificate) {
diff --git
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionCreator.java
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionCreator.java
index 6f13584..d0e3c7e 100644
---
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionCreator.java
+++
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionCreator.java
@@ -24,11 +24,25 @@ import java.net.SocketAddress;
import org.apache.sshd.client.config.hosts.HostConfigEntry;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.common.AttributeRepository;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
/**
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
*/
public interface ClientSessionCreator {
+
+ AttributeRepository.AttributeKey<SshdSocketAddress> TARGET_SERVER = new
AttributeRepository.AttributeKey<>();
+
+ /**
+ * Resolves the <U>effective</U> {@link HostConfigEntry} and connects to it
+ *
+ * @param uri The server uri to connect to
+ * @return A {@link ConnectFuture}
+ * @throws IOException If failed to resolve the effective target or
connect to it
+ * @see #connect(HostConfigEntry)
+ */
+ ConnectFuture connect(String uri) throws IOException;
+
/**
* Resolves the <U>effective</U> {@link HostConfigEntry} and connects to it
*
diff --git
a/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClientSessionCreator.java
b/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClientSessionCreator.java
index a5045aa..e32bf28 100644
---
a/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClientSessionCreator.java
+++
b/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClientSessionCreator.java
@@ -82,6 +82,16 @@ public abstract class AbstractSimpleClientSessionCreator
extends AbstractSimpleC
return loginSession(connect(username, target), identity);
}
+ @Override
+ public ClientSession sessionLogin(String uri, String password) throws
IOException {
+ return loginSession(connect(uri), password);
+ }
+
+ @Override
+ public ClientSession sessionLogin(String uri, KeyPair identity) throws
IOException {
+ return loginSession(connect(uri), identity);
+ }
+
protected ClientSession loginSession(ConnectFuture future, String
password) throws IOException {
return authSession(future.verify(getConnectTimeout()), password);
}
@@ -143,6 +153,11 @@ public abstract class AbstractSimpleClientSessionCreator
extends AbstractSimpleC
Objects.requireNonNull(channel, "No channel");
return new AbstractSimpleClientSessionCreator() {
@Override
+ public ConnectFuture connect(String uri) throws IOException {
+ return creator.connect(uri);
+ }
+
+ @Override
public ConnectFuture connect(String username, String host, int
port) throws IOException {
return creator.connect(username, host, port);
}
diff --git
a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleSessionClient.java
b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleSessionClient.java
index a83fbaa..658abe1 100644
---
a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleSessionClient.java
+++
b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleSessionClient.java
@@ -167,4 +167,24 @@ public interface SimpleSessionClient extends
SimpleClientConfigurator, Channel {
* @throws IOException If failed to login or authenticate
*/
ClientSession sessionLogin(SocketAddress target, String username, KeyPair
identity) throws IOException;
+
+ /**
+ * Creates a session and logs in using the provided credentials
+ *
+ * @param uri The target uri
+ * @param password Password
+ * @return Created {@link ClientSession}
+ * @throws IOException If failed to login or authenticate
+ */
+ ClientSession sessionLogin(String uri, String password) throws IOException;
+
+ /**
+ * Creates a session and logs in using the provided credentials
+ *
+ * @param uri The target uri
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link ClientSession}
+ * @throws IOException If failed to login or authenticate
+ */
+ ClientSession sessionLogin(String uri, KeyPair identity) throws
IOException;
}
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ProxyTest.java
b/sshd-core/src/test/java/org/apache/sshd/client/ProxyTest.java
index 1500729..2a8297b 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ProxyTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ProxyTest.java
@@ -18,21 +18,38 @@
*/
package org.apache.sshd.client;
+import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.rmi.RemoteException;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.apache.sshd.client.config.hosts.KnownHostHashValue;
+import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.RejectAllServerKeyVerifier;
import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
import org.apache.sshd.util.test.BaseTestSupport;
import org.apache.sshd.util.test.CommandExecutionHelper;
import org.junit.FixMethodOrder;
+import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runners.MethodSorters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
@@ -40,6 +57,11 @@ import org.junit.runners.MethodSorters;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class ProxyTest extends BaseTestSupport {
+ @Rule
+ public TemporaryFolder tmpClientDir = new TemporaryFolder();
+
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
+
private ClientSession proxySession;
public ProxyTest() {
@@ -69,45 +91,193 @@ public class ProxyTest extends BaseTestSupport {
// setup client
client.start();
- String command = "ls -la";
- String result;
+ logger.info("Proxy: " + proxy.getPort() + ", server: " +
server.getPort());
+
+ // Connect through to the proxy.
+ client.addPasswordIdentity("user2");
try (ClientSession session = createSession(
client,
"localhost", server.getPort(), "user1", "user1",
- "localhost", proxy.getPort(), "user2", "user2")) {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- ByteArrayOutputStream errors = new ByteArrayOutputStream();
- session.executeRemoteCommand(command, out, errors,
StandardCharsets.UTF_8);
- result = out.toString();
+ "user2@localhost:" + proxy.getPort())) {
+ assertTrue(session.isOpen());
+ doTestCommand(session, "ls -al");
+ }
+ assertTrue(proxySession == null || proxySession.isClosing() ||
proxySession.isClosed());
+ }
+ }
+
+ @Test
+ public void testDirectWithHostKeyVerification() throws Exception {
+ // This test exists only to show that the knownhosts setup is correct
+ try (SshServer server = setupTestServer();
+ SshServer proxy = setupTestServer();
+ SshClient client = setupTestClient()) {
+
+ File knownHosts = prepareHostKeySetup(server, proxy);
+ // Setup client with a standard ServerKeyVerifier
+ client.setServerKeyVerifier(
+ new
KnownHostsServerKeyVerifier(RejectAllServerKeyVerifier.INSTANCE,
knownHosts.toPath()));
+ client.start();
+
+ logger.info("Proxy: " + proxy.getPort() + ", server: " +
server.getPort());
+
+ // Connect to the server directly to verify the knownhosts setup.
+ try (ClientSession session = createSession(
+ client, "localhost", server.getPort(), "user1", "user1",
null)) {
+ assertTrue(session.isOpen());
+ doTestCommand(session, "ls -al");
+ }
+ assertTrue(proxySession == null || proxySession.isClosing() ||
proxySession.isClosed());
+
+ // Connect through to the proxy.
+ try (ClientSession session = createSession(
+ client, "localhost", proxy.getPort(), "user2", "user2",
null)) {
+ assertTrue(session.isOpen());
+ assertThrows(RemoteException.class,
+ () -> doTestCommand(session, "ls -al"));
+ }
+ assertTrue(proxySession == null || proxySession.isClosing() ||
proxySession.isClosed());
+ }
+ }
+
+ @Test
+ public void testProxyWithHostKeyVerification() throws Exception {
+ try (SshServer server = setupTestServer();
+ SshServer proxy = setupTestServer();
+ SshClient client = setupTestClient()) {
+
+ File knownHosts = prepareHostKeySetup(server, proxy);
+ // Setup client with a standard ServerKeyVerifier
+ client.setServerKeyVerifier(
+ new
KnownHostsServerKeyVerifier(RejectAllServerKeyVerifier.INSTANCE,
knownHosts.toPath()));
+ client.start();
+
+ logger.info("Proxy: " + proxy.getPort() + ", server: " +
server.getPort());
+
+ // Connect via the proxy
+ client.addPasswordIdentity("user2");
+ try (ClientSession session = createSession(
+ client, "localhost", server.getPort(), "user1", "user1",
+ "user2@localhost:" + proxy.getPort())) {
+
+ assertTrue(session.isOpen());
+ doTestCommand(session, "ls -la");
}
- assertEquals(command, result);
// make sure the proxy session is closed / closing
assertTrue(proxySession == null || proxySession.isClosing() ||
proxySession.isClosed());
}
}
+ @Test
+ public void testProxyWithHostKeyVerificationAndCustomConfig() throws
Exception {
+ try (SshServer server = setupTestServer();
+ SshServer proxy = setupTestServer();
+ SshClient client = setupTestClient()) {
+
+ File knownHosts = prepareHostKeySetup(server, proxy);
+ // Setup client with a standard ServerKeyVerifier
+ client.setServerKeyVerifier(
+ new
KnownHostsServerKeyVerifier(RejectAllServerKeyVerifier.INSTANCE,
knownHosts.toPath()));
+ client.start();
+
client.setHostConfigEntryResolver(HostConfigEntry.toHostConfigEntryResolver(Arrays.asList(
+ new HostConfigEntry("server", "localhost",
server.getPort(), "user1", "proxy"),
+ new HostConfigEntry("proxy", "localhost", proxy.getPort(),
"user2"))));
+
+ logger.info("Proxy: " + proxy.getPort() + ", server: " +
server.getPort());
+
+ // Connect via the proxy
+ client.addPasswordIdentity("user1");
+ client.addPasswordIdentity("user2");
+ try (ClientSession session = client.connect("server")
+ .verify(CONNECT_TIMEOUT).getSession()) {
+ session.auth().verify(AUTH_TIMEOUT);
+
+ assertTrue(session.isOpen());
+ doTestCommand(session, "ls -la");
+ }
+ // make sure the proxy session is closed / closing
+ assertTrue(proxySession == null || proxySession.isClosing() ||
proxySession.isClosed());
+ }
+ }
+
+ @Test
+ @Ignore
+ public void testExternal() throws Exception {
+ try (SshServer server = setupTestServer();
+ SshServer proxy = setupTestServer()) {
+
+ server.setCommandFactory((session, command) -> new
CommandExecutionHelper(command) {
+ @Override
+ protected boolean handleCommandLine(String command) throws
Exception {
+ OutputStream stdout = getOutputStream();
+ stdout.write(command.getBytes(StandardCharsets.US_ASCII));
+ stdout.flush();
+ return false;
+ }
+ });
+ server.start();
+ // setup proxy with a forwarding filter to allow the local port
forwarding
+ proxy.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
+ proxy.start();
+
+ logger.info("Proxy: " + proxy.getPort() + ", server: " +
server.getPort());
+ Thread.sleep(TimeUnit.MINUTES.toMillis(5));
+ }
+ }
+
+ protected void doTestCommand(ClientSession session, String command) throws
IOException {
+ String result;
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayOutputStream errors = new ByteArrayOutputStream();
+ session.executeRemoteCommand(command, out, errors,
StandardCharsets.UTF_8);
+ result = out.toString();
+ assertEquals(command, result);
+ }
+
+ protected File prepareHostKeySetup(SshServer server, SshServer proxy)
throws Exception {
+ // setup server with an echo command
+ server.setCommandFactory((session, command) -> new
CommandExecutionHelper(command) {
+ @Override
+ protected boolean handleCommandLine(String command) throws
Exception {
+ OutputStream stdout = getOutputStream();
+ stdout.write(command.getBytes(StandardCharsets.US_ASCII));
+ stdout.flush();
+ return true;
+ }
+ });
+
+ server.start();
+ File knownHosts = tmpClientDir.newFile("knownhosts");
+ writeKnownHosts(server, knownHosts);
+ // setup proxy with a forwarding filter to allow the local port
forwarding
+ proxy.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
+ proxy.start();
+ writeKnownHosts(proxy, knownHosts);
+ return knownHosts;
+ }
+
+ protected File writeKnownHosts(SshServer server, File knownHosts) throws
Exception {
+ KeyPair serverHostKey =
GenericUtils.head(server.getKeyPairProvider().loadKeys(null));
+ try (BufferedWriter writer =
Files.newBufferedWriter(knownHosts.toPath(), StandardCharsets.US_ASCII,
+ StandardOpenOption.CREATE, StandardOpenOption.WRITE,
StandardOpenOption.APPEND)) {
+ KnownHostHashValue.appendHostPattern(writer, "localhost",
server.getPort());
+ writer.append(' ');
+ PublicKeyEntry.appendPublicKeyEntry(writer,
serverHostKey.getPublic());
+ writer.append('\n');
+ }
+ return knownHosts;
+ }
+
@SuppressWarnings("checkstyle:ParameterNumber")
protected ClientSession createSession(
SshClient client,
String host, int port, String user, String password,
- String proxyHost, int proxyPort, String proxyUser, String
proxyPassword)
- throws java.io.IOException {
- ClientSession session;
- if (proxyHost != null) {
- proxySession = client.connect(proxyUser, proxyHost, proxyPort)
- .verify(CONNECT_TIMEOUT).getSession();
- proxySession.addPasswordIdentity(proxyPassword);
- proxySession.auth().verify(AUTH_TIMEOUT);
- SshdSocketAddress address = new SshdSocketAddress(host, port);
- ExplicitPortForwardingTracker tracker =
proxySession.createLocalPortForwardingTracker(
- SshdSocketAddress.LOCALHOST_ADDRESS, address);
- SshdSocketAddress bound = tracker.getBoundAddress();
- session = client.connect(user, bound.getHostName(),
bound.getPort())
- .verify(CONNECT_TIMEOUT).getSession();
- session.addCloseFutureListener(f -> IoUtils.closeQuietly(tracker,
proxySession));
- } else {
- session = client.connect(user, host,
port).verify(CONNECT_TIMEOUT).getSession();
- }
+ String proxyJump)
+ throws IOException {
+ ClientSession session = client.connect(new HostConfigEntry(
+ "", host, port, user,
+ proxyJump))
+ .verify(CONNECT_TIMEOUT).getSession();
session.addPasswordIdentity(password);
session.auth().verify(AUTH_TIMEOUT);
return session;
diff --git
a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
index 495ba82..c45f8e0 100644
---
a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
+++
b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
@@ -92,7 +92,7 @@ public class HostConfigEntryResolverTest extends
BaseTestSupport {
@Test
public void testEffectiveHostConfigResolution() throws Exception {
HostConfigEntry entry = new HostConfigEntry(getCurrentTestName(),
TEST_LOCALHOST, port, getCurrentTestName());
- client.setHostConfigEntryResolver((host, portValue, lclAddress,
username, context) -> entry);
+ client.setHostConfigEntryResolver((host, portValue, lclAddress,
username, proxy, context) -> entry);
client.start();
try (ClientSession session = client.connect(
@@ -169,7 +169,7 @@ public class HostConfigEntryResolverTest extends
BaseTestSupport {
String host = getClass().getSimpleName();
HostConfigEntry entry = new HostConfigEntry(host, TEST_LOCALHOST,
port, user);
entry.addIdentity(clientIdentity);
- client.setHostConfigEntryResolver((host1, portValue, lclAddress,
username, context) -> entry);
+ client.setHostConfigEntryResolver((host1, portValue, lclAddress,
username, proxy, context) -> entry);
client.start();
try (ClientSession session = client.connect(