diff --git a/examples/src/main/java/net/schmizz/sshj/examples/Jump.java b/examples/src/main/java/net/schmizz/sshj/examples/Jump.java new file mode 100644 index 00000000..2fcec29e --- /dev/null +++ b/examples/src/main/java/net/schmizz/sshj/examples/Jump.java @@ -0,0 +1,50 @@ +package net.schmizz.sshj.examples; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.connection.channel.direct.DirectConnection; +import net.schmizz.sshj.connection.channel.direct.Session; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * This example demonstrates connecting via an intermediate "jump" server using a direct TCP/IP channel. + */ +public class Jump { + public static void main(String... args) + throws IOException { + SSHClient firstHop = new SSHClient(); + + firstHop.loadKnownHosts(); + + firstHop.connect("localhost"); + try { + + firstHop.authPublickey(System.getProperty("user.name")); + + DirectConnection tunnel = firstHop.newDirectConnection("localhost", 22); + + SSHClient ssh = new SSHClient(); + try { + ssh.loadKnownHosts(); + ssh.connectVia(tunnel); + ssh.authPublickey(System.getProperty("user.name")); + + final Session session = ssh.startSession(); + try { + final Session.Command cmd = session.exec("ping -c 1 google.com"); + System.out.println(IOUtils.readFully(cmd.getInputStream()).toString()); + cmd.join(5, TimeUnit.SECONDS); + System.out.println("\n** exit status: " + cmd.getExitStatus()); + } finally { + session.close(); + } + } finally { + ssh.disconnect(); + } + } finally { + firstHop.disconnect(); + } + } +} diff --git a/src/main/java/net/schmizz/sshj/SSHClient.java b/src/main/java/net/schmizz/sshj/SSHClient.java index f210cd8e..63bae506 100644 --- a/src/main/java/net/schmizz/sshj/SSHClient.java +++ b/src/main/java/net/schmizz/sshj/SSHClient.java @@ -23,6 +23,7 @@ import net.schmizz.sshj.common.SecurityUtils; import net.schmizz.sshj.connection.Connection; import net.schmizz.sshj.connection.ConnectionException; import net.schmizz.sshj.connection.ConnectionImpl; +import net.schmizz.sshj.connection.channel.direct.DirectConnection; import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder; import net.schmizz.sshj.connection.channel.direct.Session; import net.schmizz.sshj.connection.channel.direct.SessionChannel; @@ -672,6 +673,20 @@ public class SSHClient return forwarder; } + /** Create a {@link DirectConnection} channel that connects to a remote address from the server. + * + * This can be used to open a tunnel to, for example, an HTTP server that is only + * accessible from the SSH server, or opening an SSH connection via a 'jump' server. + * + * @param hostname name of the host to connect to from the server. + * @param port remote port number. + */ + public DirectConnection newDirectConnection(String hostname, int port) throws IOException { + DirectConnection tunnel = new DirectConnection(conn, hostname, port); + tunnel.open(); + return tunnel; + } + /** * Register a {@code listener} for handling forwarded X11 channels. Without having done this, an incoming X11 * forwarding will be summarily rejected. diff --git a/src/main/java/net/schmizz/sshj/SocketClient.java b/src/main/java/net/schmizz/sshj/SocketClient.java index e831bffa..3158a819 100644 --- a/src/main/java/net/schmizz/sshj/SocketClient.java +++ b/src/main/java/net/schmizz/sshj/SocketClient.java @@ -17,6 +17,7 @@ package net.schmizz.sshj; import com.hierynomus.sshj.backport.JavaVersion; import com.hierynomus.sshj.backport.Jdk7HttpProxySocket; +import net.schmizz.sshj.connection.channel.direct.DirectConnection; import javax.net.SocketFactory; import java.io.IOException; @@ -43,6 +44,9 @@ public abstract class SocketClient { private int timeout = 0; private String hostname; + private int port; + + private boolean tunneled = false; SocketClient(int defaultPort) { this.defaultPort = defaultPort; @@ -71,6 +75,7 @@ public abstract class SocketClient { @Deprecated public void connect(String hostname, int port, Proxy proxy) throws IOException { this.hostname = hostname; + this.port = port; if (JavaVersion.isJava7OrEarlier() && proxy.type() == Proxy.Type.HTTP) { // Java7 and earlier have no support for HTTP Connect proxies, return our custom socket. socket = new Jdk7HttpProxySocket(proxy); @@ -103,6 +108,7 @@ public abstract class SocketClient { */ @Deprecated public void connect(InetAddress host, int port, Proxy proxy) throws IOException { + this.port = port; if (JavaVersion.isJava7OrEarlier() && proxy.type() == Proxy.Type.HTTP) { // Java7 and earlier have no support for HTTP Connect proxies, return our custom socket. socket = new Jdk7HttpProxySocket(proxy); @@ -122,6 +128,7 @@ public abstract class SocketClient { connect(InetAddress.getByName(null), port); } else { this.hostname = hostname; + this.port = port; socket = socketFactory.createSocket(); socket.connect(new InetSocketAddress(hostname, port), connectTimeout); onConnect(); @@ -133,6 +140,7 @@ public abstract class SocketClient { connect(InetAddress.getByName(null), port, localAddr, localPort); } else { this.hostname = hostname; + this.port = port; socket = socketFactory.createSocket(); socket.bind(new InetSocketAddress(localAddr, localPort)); socket.connect(new InetSocketAddress(hostname, port), connectTimeout); @@ -140,11 +148,22 @@ public abstract class SocketClient { } } + /** Connect to a remote address via a direct TCP/IP connection from the server. */ + public void connectVia(DirectConnection directConnection) throws IOException { + this.hostname = directConnection.getRemoteHost(); + this.port = directConnection.getRemotePort(); + this.input = directConnection.getInputStream(); + this.output = directConnection.getOutputStream(); + this.tunneled = true; + onConnect(); + } + public void connect(InetAddress host) throws IOException { connect(host, defaultPort); } public void connect(InetAddress host, int port) throws IOException { + this.port = port; socket = socketFactory.createSocket(); socket.connect(new InetSocketAddress(host, port), connectTimeout); onConnect(); @@ -152,6 +171,7 @@ public abstract class SocketClient { public void connect(InetAddress host, int port, InetAddress localAddr, int localPort) throws IOException { + this.port = port; socket = socketFactory.createSocket(); socket.bind(new InetSocketAddress(localAddr, localPort)); socket.connect(new InetSocketAddress(host, port), connectTimeout); @@ -174,15 +194,15 @@ public abstract class SocketClient { } public boolean isConnected() { - return (socket != null) && socket.isConnected(); + return tunneled || ((socket != null) && socket.isConnected()); } public int getLocalPort() { - return socket.getLocalPort(); + return tunneled ? 65536 : socket.getLocalPort(); } public InetAddress getLocalAddress() { - return socket.getLocalAddress(); + return socket == null ? null : socket.getLocalAddress(); } public String getRemoteHostname() { @@ -190,11 +210,11 @@ public abstract class SocketClient { } public int getRemotePort() { - return socket.getPort(); + return socket == null ? this.port : socket.getPort(); } public InetAddress getRemoteAddress() { - return socket.getInetAddress(); + return socket == null ? null : socket.getInetAddress(); } public void setSocketFactory(SocketFactory factory) { @@ -238,9 +258,11 @@ public abstract class SocketClient { } void onConnect() throws IOException { - socket.setSoTimeout(timeout); - input = socket.getInputStream(); - output = socket.getOutputStream(); + if (socket != null) { + socket.setSoTimeout(timeout); + input = socket.getInputStream(); + output = socket.getOutputStream(); + } } } diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/DirectConnection.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/DirectConnection.java new file mode 100644 index 00000000..5e79530c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/DirectConnection.java @@ -0,0 +1,47 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.schmizz.sshj.connection.channel.direct; + +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.Connection; + +/** A channel for creating a direct TCP/IP connection from the server to a remote address. */ +public class DirectConnection extends AbstractDirectChannel { + private final String remoteHost; + private final int remotePort; + + public DirectConnection(Connection conn, String remoteHost, int remotePort) { + super(conn, "direct-tcpip"); + this.remoteHost = remoteHost; + this.remotePort = remotePort; + } + + @Override protected SSHPacket buildOpenReq() { + return super.buildOpenReq() + .putString(getRemoteHost()) + .putUInt32(getRemotePort()) + .putString("localhost") + .putUInt32(65536); // it looks like OpenSSH uses this value in stdio-forward + } + + public String getRemoteHost() { + return remoteHost; + } + + public int getRemotePort() { + return remotePort; + } +} \ No newline at end of file