/**
* Copyright 2003-2016 SSHTOOLS Limited. All Rights Reserved.
*
* For product documentation visit https://www.sshtools.com/
*
* This file is part of J2SSH Maverick.
*
* J2SSH Maverick is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* J2SSH Maverick is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with J2SSH Maverick. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sshtools.net;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import com.sshtools.events.Event;
import com.sshtools.events.EventServiceImplementation;
import com.sshtools.events.J2SSHEventCodes;
import com.sshtools.logging.Log;
import com.sshtools.ssh.ChannelAdapter;
import com.sshtools.ssh.Client;
import com.sshtools.ssh.ForwardingRequestListener;
import com.sshtools.ssh.SshChannel;
import com.sshtools.ssh.SshClient;
import com.sshtools.ssh.SshException;
import com.sshtools.ssh.SshTransport;
import com.sshtools.ssh.SshTunnel;
import com.sshtools.ssh.components.ComponentManager;
import com.sshtools.util.ByteArrayReader;
import com.sshtools.util.IOStreamConnector;
/**
* <p>
* Provides management of port forwarding connections and active tunnels.
* </p>
*
* <p>
* Three types of forwarding are provided by both SSH protocol versions; local
* forwarding, remote forwarding, X forwarding.
* </p>
*
* <p>
* Local forwarding allows you to transfer data (or socket connections if you
* prefer) from the local computer to a destination on the remote
* computer/network. For example you could setup a local forwarding to listen
* for connection's on port 110 (the POP3 protocol) and forwarding those
* connections to port 110 at the remote side of the connection. This secures
* the data by encrypting it within the SSH connection making the insecure POP3
* protocol secure. It is normally the practice to deliver the connection to a
* socket on the localhost of the remote computer to ensure that the data is not
* transmitted over any other insecure network.
* </p>
*
* <p>
* Remote forwarding is simply the reverse of this operation; a request is made
* to the server to listen on a particular port and any connections made are
* forwarded to the local computer where they are delivered to the required
* destination.
* </p>
*
* <p>
* X forwarding is available for you to forward X11 data from the remote machine
* to a local X server.
* </p>
*
* <p>
* The use of this client is a simple procedure. First create an instance once
* you have an authenticated <a
* href="../../maverick/ssh/SshClient.html">SshClient</a>. You can then use the
* methods to start local forwarding or request remote forwarding. This
* implementation manages all the connections and manages threads to transfer
* the data from sockets to the forwarding channels. All you are required to do
* is to select the forwarding configuration you require. <blockquote>
*
* <pre>
* // Create an SshClient instance into the variable ssh
* ...
*
* // Create a forwarding client
* ForwardingClient fwd = new ForwardingClient(ssh);
*
* // Configure X forwarding to deliver to a local X server
* fwd.allowX11Forwarding("localhost:0");
*
* // Request that HTTP requests on the port 8080 be forwarding from the
* // remote computer to the local computers HTTP server.
* if(!fwd.requestRemoteForwarding("127.0.0.1", 8080, "127.0.0.1", 80)) {
* System.out.println("Forwarding request failed!");
* }
*
* // Create a session to start the user's shell (see notes below)
* SshSession session = ssh.openSessionChannel();
* session.requestPseudoTerminal("vt100",80,24,0,0);
* session.startShell();
*
* // Forwarding POP3 connections made to the local computer to the remote server's POP3 port
* fwd.startLocalForwarding("127.0.0.1", 110, "127.0.0.1", 110);
*
* // Read the output of the users shell until EOF.
* InputStream in = session.getInputStream();
* try {
* int read;
* while((read = in.read()) > -1) {
* if(read > 0)
* System.out.print((char)read);
* }
* } catch(Throwable t) {
* t.printStackTrace();
* } finally {
* System.exit(0);
* }
*
* </pre>
*
* </blockquote>
* </p>
*
* <p>
* The are several restrictions you must follow if you require protocol
* independence so that your code will work with both SSH1 and SSH2 servers.<br>
* SSH1 remote forwarding requests MUST be made before you start the users shell
* and local forwarding's MUST only be started once you have started the users
* shell. With SSH1 you must always start the user's shell in order to perform
* port forwarding as this places the protocol into interactive mode.<br>
* SSH2 does not place any restrictions as to when a remote forwarding is
* requesting or local forwarding started.
* </p>
*
* <p>
* Additionally the single threaded nature of the API means there is no
* background thread available to service remote forwarding connection requests.
* In order that these requests are dealt with in a timely fashion you can
* either ensure that:<br>
* Your implementation will be required to start the users shell and read from
* its InputStream until it reaches EOF. This provides a thread to service the
* incoming requests and conforms to the requirements of using SSH1 forwarding
* so we recommend you follow this procedure even if you only require SSH2
* connections.<br>
* Alternatively you can create a background thread by passing true into the
* SshConnector.connect method for the buffered parameter.
* </p>
*
* <p>
* The X forwarding managed by this class should be requested before starting
* any sessions. When X forwarding is requested a fake MIT-MAGIC-COOKIE is
* supplied to the remote machine which protects your real authentication
* cookies from being detected. When an X11 request comes in the fake cookie is
* replaced with your real cookie by looking at your .Xauthority file. If in the
* event that a real cookie cannot be found there are additional methods to
* either specify an alternative path to your .Xauthority file or to specify the
* cookie itself. Please note that X forwarding provided by this class does not
* operate over Unix Domain sockets so you should ensure that your X server is
* listening on a TCP port.
* </p>
*
* @author Lee David Painter
*/
public class ForwardingClient implements Client {
SshClient ssh;
protected Hashtable<String, Vector<ActiveTunnel>> incomingtunnels = new Hashtable<String, Vector<ActiveTunnel>>();
protected Hashtable<String, String> remoteforwardings = new Hashtable<String, String>();
protected Hashtable<String, Vector<ActiveTunnel>> outgoingtunnels = new Hashtable<String, Vector<ActiveTunnel>>();
protected Hashtable<String, SocketListener> socketlisteners = new Hashtable<String, SocketListener>();
protected Vector<ForwardingClientListener> clientlisteners = new Vector<ForwardingClientListener>();
ForwardingListener forwardinglistener = new ForwardingListener();
TunnelListener tunnellistener = new TunnelListener();
boolean isXForwarding = false;
/** The key used to identify X11 forwarding **/
public static final String X11_KEY = "X11";
/** The lowest possible random port to select * */
public final int LOWEST_RANDOM_PORT = 49152;
/** The highest possible random port to select * */
public final int HIGHEST_RANDOM_PORT = 65535;
/**
* Create an forwarding client.
*/
public ForwardingClient(SshClient ssh) {
this.ssh = ssh;
}
/**
* Add a {@link ForwardingClientListener} to receive forwarding events.
*
* @param listener
* listener
*/
public void addListener(ForwardingClientListener listener) {
if (listener != null) {
clientlisteners.addElement(listener);
SocketListener s;
Enumeration<SocketListener> en = socketlisteners.elements();
while (en.hasMoreElements()) {
s = (SocketListener) en.nextElement();
if (s.isListening()) {
listener.forwardingStarted(
ForwardingClientListener.LOCAL_FORWARDING,
generateKey(s.addressToBind, s.portToBind),
s.hostToConnect, s.portToConnect);
}
}
if (Log.isDebugEnabled()) {
Log.debug(this, "enumerated socketlisteners");
}
Enumeration<String> en2 = incomingtunnels.keys();
String key;
String destination;
String hostToConnect;
int portToConnect;
while (en2.hasMoreElements()) {
key = en2.nextElement();
if (key.equals(X11_KEY)
|| ssh.getContext().getX11Display() != null
&& ssh.getContext().getX11Display().equals(key))
continue;
destination = (String) remoteforwardings.get(key);
hostToConnect = destination.substring(0,
destination.indexOf(':'));
portToConnect = Integer.parseInt(destination
.substring(destination.indexOf(':') + 1));
listener.forwardingStarted(
ForwardingClientListener.REMOTE_FORWARDING, key,
hostToConnect, portToConnect);
}
if (Log.isDebugEnabled()) {
Log.debug(this, "enumerated incomingtunnels");
}
String display = ssh.getContext().getX11Display();
if (Log.isDebugEnabled()) {
Log.debug(this, "display is " + display);
}
if (display != null && isXForwarding) {
String hostname = "localhost";
int screen;
int idx = display.indexOf(':');
if (idx != -1) {
hostname = display.substring(0, idx);
screen = Integer.parseInt(display.substring(idx + 1));
} else {
screen = Integer.parseInt(display);
}
listener.forwardingStarted(
ForwardingClientListener.X11_FORWARDING, X11_KEY,
hostname, screen);
}
}
}
public boolean hasRemoteForwarding(String addressBound, int portBound) {
return remoteforwardings.containsKey(generateKey(addressBound,
portBound));
}
public boolean hasLocalForwarding(String addressBound, int portBound) {
return socketlisteners
.containsKey(generateKey(addressBound, portBound));
}
/**
* Remove a {@link ForwardingClientListener} from the list receiving
* forwarding events.
*
* @param listener
* listener
*/
public void removeListener(ForwardingClientListener listener) {
clientlisteners.removeElement(listener);
}
/**
* Start's a local listening socket and forwards any connections made to the
* to the remote side.
*
* @param addressToBind
* the listening address
* @param portToBind
* the listening port
* @param hostToConnect
* the host to connect on the remote side
* @param portToConnect
* the port to connect on the remote side
* @throws IOException
*/
public void startLocalForwarding(String addressToBind, int portToBind,
String hostToConnect, int portToConnect) throws SshException {
String key = generateKey(addressToBind, portToBind);
SocketListener listener = new SocketListener(addressToBind, portToBind,
hostToConnect, portToConnect);
listener.start();
socketlisteners.put(key, listener);
if (!outgoingtunnels.containsKey(key)) {
outgoingtunnels.put(key, new Vector<ActiveTunnel>());
}
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.forwardingStarted(
ForwardingClientListener.LOCAL_FORWARDING, key,
hostToConnect, portToConnect);
}
EventServiceImplementation
.getInstance()
.fireEvent(
(new Event(this,
J2SSHEventCodes.EVENT_FORWARDING_LOCAL_STARTED,
true))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_FORWARDING_TUNNEL_ENTRANCE,
key)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_FORWARDING_TUNNEL_EXIT,
hostToConnect + ":" + portToConnect));
}
/**
* Start's a local listening socket and forwards any connections made to the
* to the remote side.
*
* @param addressToBind
* the listening address
* @param maxFailedPorts
* the number of times to retry if the randomly selected port is
* in use.
* @param hostToConnect
* the host to connect on the remote side
* @param portToConnect
* the port to connect on the remote side
* @return the random port on which the tunnel is now listening
* @throws IOException
*/
public int startLocalForwardingOnRandomPort(String addressToBind,
int maxFailedPorts, String hostToConnect, int portToConnect)
throws SshException {
for (int x = 0; x < maxFailedPorts; x++) {
try {
int portToBind = selectRandomPort();
String key = generateKey(addressToBind, portToBind);
SocketListener listener = new SocketListener(addressToBind,
portToBind, hostToConnect, portToConnect);
listener.start();
socketlisteners.put(key, listener);
if (!outgoingtunnels.containsKey(key)) {
outgoingtunnels.put(key, new Vector<ActiveTunnel>());
}
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.forwardingStarted(
ForwardingClientListener.LOCAL_FORWARDING,
key, hostToConnect, portToConnect);
}
EventServiceImplementation
.getInstance()
.fireEvent(
(new Event(
this,
J2SSHEventCodes.EVENT_FORWARDING_LOCAL_STARTED,
true))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_FORWARDING_TUNNEL_ENTRANCE,
key)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_FORWARDING_TUNNEL_EXIT,
hostToConnect + ":"
+ portToConnect));
return portToBind;
} catch (Throwable ex) {
}
}
throw new SshException(
"Maximum retry limit reached for random port selection",
SshException.FORWARDING_ERROR);
}
/**
* Returns the currently active remote forwarding listeners.
*
* @return String[]
*/
public String[] getRemoteForwardings() {
String[] r = new String[remoteforwardings.size()
- (remoteforwardings.containsKey(X11_KEY) ? 1 : 0)];
int index = 0;
for (Enumeration<String> e = remoteforwardings.keys(); e
.hasMoreElements();) {
String key = e.nextElement();
if (!key.equals(X11_KEY))
r[index++] = key;
}
return r;
}
/**
* Return the currently active local forwarding listeners.
*
* @return String[]
*/
public String[] getLocalForwardings() {
String[] r = new String[socketlisteners.size()];
int index = 0;
for (Enumeration<String> e = socketlisteners.keys(); e
.hasMoreElements();) {
r[index++] = e.nextElement();
}
return r;
}
/**
* Get the active tunnels for a local forwarding listener.
*
* @param key
* @return ActiveTunnel[]
* @throws IOException
*/
public ActiveTunnel[] getLocalForwardingTunnels(String key)
throws IOException {
if (outgoingtunnels.containsKey(key)) {
Vector<ActiveTunnel> v = outgoingtunnels.get(key);
ActiveTunnel[] t = new ActiveTunnel[v.size()];
v.copyInto(t);
return t;
}
if (!socketlisteners.containsKey(key)) {
throw new IOException(key
+ " is not a valid local forwarding configuration");
}
return new ActiveTunnel[] {};
}
/**
* Get the active tunnels for a local forwarding listener.
*
* @param addressToBind
* @param portToBind
* @return ActiveTunnel[]
* @throws IOException
*/
public ActiveTunnel[] getLocalForwardingTunnels(String addressToBind,
int portToBind) throws IOException {
return getLocalForwardingTunnels(generateKey(addressToBind, portToBind));
}
/**
* Get all the active remote forwarding tunnels
*
* @return
* @throws IOException
*/
public ActiveTunnel[] getRemoteForwardingTunnels() throws IOException {
Vector<ActiveTunnel> v = new Vector<ActiveTunnel>();
String[] remoteForwardings = getRemoteForwardings();
for (int i = 0; i < remoteForwardings.length; i++) {
ActiveTunnel[] tmp = getRemoteForwardingTunnels(remoteForwardings[i]);
for (int x = 0; x < tmp.length; x++) {
v.add(tmp[x]);
}
}
return (ActiveTunnel[]) v.toArray(new ActiveTunnel[v.size()]);
}
/**
* Get all the active local forwarding tunnels
*
* @return
* @throws IOException
*/
public ActiveTunnel[] getLocalForwardingTunnels() throws IOException {
Vector<ActiveTunnel> v = new Vector<ActiveTunnel>();
String[] localForwardings = getLocalForwardings();
for (int i = 0; i < localForwardings.length; i++) {
ActiveTunnel[] tmp = getLocalForwardingTunnels(localForwardings[i]);
for (int x = 0; x < tmp.length; x++) {
v.add(tmp[x]);
}
}
return (ActiveTunnel[]) v.toArray(new ActiveTunnel[v.size()]);
}
/**
* Get the active tunnels for a remote forwarding listener.
*
* @param key
* @return ActiveTunnel[]
* @throws IOException
*/
public ActiveTunnel[] getRemoteForwardingTunnels(String key)
throws IOException {
synchronized (incomingtunnels) {
if (incomingtunnels.containsKey(key)) {
Vector<ActiveTunnel> v = incomingtunnels.get(key);
ActiveTunnel[] t = new ActiveTunnel[v.size()];
v.copyInto(t);
return t;
}
}
if (!remoteforwardings.containsKey(key)) {
throw new IOException(key
+ " is not a valid remote forwarding configuration");
}
return new ActiveTunnel[] {};
}
/**
* Is X forwarding currently active?
*
* @return boolean
*/
public boolean isXForwarding() {
return isXForwarding;
}
/**
* Get the active tunnels for a remote forwarding listener.
*
* @param addressToBind
* @param portToBind
* @return ActiveTunnel[]
* @throws IOException
*/
public ActiveTunnel[] getRemoteForwardingTunnels(String addressToBind,
int portToBind) throws IOException {
return getRemoteForwardingTunnels(generateKey(addressToBind, portToBind));
}
/**
* Get the active X11 forwarding channels.
*
* @return ActiveTunnel[]
* @throws IOException
*/
public ActiveTunnel[] getX11ForwardingTunnels() throws IOException {
if (incomingtunnels.containsKey(X11_KEY)) {
Vector<ActiveTunnel> v = incomingtunnels.get(X11_KEY);
ActiveTunnel[] t = new ActiveTunnel[v.size()];
v.copyInto(t);
return t;
}
return new ActiveTunnel[] {};
}
/**
* Requests that the remote side start listening for socket connections so
* that they may be forwarded to to the local destination.
*
* @param addressToBind
* the listening address on the remote server
* @param portToBind
* the listening port on the remote server
* @param hostToConnect
* the host to connect on the local side
* @param portToConnect
* the port to connect on the local side
* @return boolean
* @throws IOException
*/
public boolean requestRemoteForwarding(String addressToBind,
int portToBind, String hostToConnect, int portToConnect)
throws SshException {
if (ssh.requestRemoteForwarding(addressToBind, portToBind,
hostToConnect, portToConnect, forwardinglistener)) {
String key = generateKey(addressToBind, portToBind);
if (!incomingtunnels.containsKey(key)) {
incomingtunnels.put(key, new Vector<ActiveTunnel>());
}
remoteforwardings.put(key, hostToConnect + ":" + portToConnect);
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.forwardingStarted(
ForwardingClientListener.REMOTE_FORWARDING,
key, hostToConnect, portToConnect);
}
return true;
}
return false;
}
/**
* Configure the forwarding client to manage X11 connections. This method
* will configure the {@link com.sshtools.ssh.SshClient} for X11 forwarding
* and will generate a fake cookie which will be used to spoof incoming X11
* requests. When a request is received the fake cookie will be replaced in
* the authentication packet by a real cookie provided and passed onto the X
* server.
*
* @param display
* String
* @param magicCookie
* String
* @throws IOException
*/
public void allowX11Forwarding(String display, String magicCookie)
throws SshException {
if (remoteforwardings.containsKey(X11_KEY))
throw new SshException("X11 forwarding is already in use!",
SshException.FORWARDING_ERROR);
if (!incomingtunnels.containsKey(X11_KEY)) {
incomingtunnels.put(X11_KEY, new Vector<ActiveTunnel>());
}
ssh.getContext().setX11Display(display);
ssh.getContext().setX11RequestListener(forwardinglistener);
byte[] cookie = new byte[16];
if (magicCookie.length() != 32)
throw new SshException("Invalid MIT-MAGIC_COOKIE-1 value "
+ magicCookie, SshException.FORWARDING_ERROR);
for (int i = 0; i < 32; i += 2) {
cookie[i / 2] = (byte) Integer.parseInt(
magicCookie.substring(i, i + 2), 16);
}
ssh.getContext().setX11RealCookie(cookie);
String hostname = "localhost";
int screen = 0;
int idx = display.indexOf(':');
if (idx != -1) {
hostname = display.substring(0, idx);
display = display.substring(idx + 1);
}
if ((idx = display.indexOf('.')) > -1) {
screen = Integer.parseInt(display.substring(idx + 1));
}
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.forwardingStarted(ForwardingClientListener.X11_FORWARDING,
X11_KEY, hostname, screen);
}
isXForwarding = true;
}
/**
* Configure the forwarding client to manage X11 connections. This method
* will configure the {@link com.sshtools.ssh.SshClient} for X11 forwarding
* and will generate a fake cookie which will be used to spoof incoming X11
* requests. When a request is received the fake cookie will be replaced in
* the authentication packet by a real cookie which is extracted from the
* users .Xauthority file.
*
* @param display
* String
* @throws IOException
*/
public void allowX11Forwarding(String display) throws SshException {
String homeDir = "";
try {
homeDir = System.getProperty("user.home");
} catch (SecurityException e) {
// ignore
}
allowX11Forwarding(display, new File(homeDir, ".Xauthority"));
}
/**
* Configure the forwarding client to manage X11 connections. This method
* will configure the {@link com.sshtools.ssh.SshClient} for X11 forwarding
* and will generate a fake cookie which will be used to spoof incoming X11
* requests. When a request is received the fake cookie will be replaced in
* the authentication packet by a real cookie which is extracted from the
* .Xauthority file provided in the File parameter.
*
* @param display
* String
* @throws IOException
*/
public void allowX11Forwarding(String display, File f) throws SshException {
if (remoteforwardings.containsKey(X11_KEY))
throw new SshException("X11 forwarding is already in use!",
SshException.FORWARDING_ERROR);
if (!incomingtunnels.containsKey(X11_KEY)) {
incomingtunnels.put(X11_KEY, new Vector<ActiveTunnel>());
}
ssh.getContext().setX11Display(display);
ssh.getContext().setX11RequestListener(forwardinglistener);
try {
// Find the users real cookie
if (f.exists()) {
String hostname = "";
int screen = 0;
int idx = display.indexOf(':');
if (idx != -1) {
hostname = display.substring(0, idx);
screen = Integer.parseInt(display.substring(idx + 1));
}
FileInputStream in = new FileInputStream(f);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int read;
while ((read = in.read()) != -1)
out.write(read);
in.close();
byte[] tmp = out.toByteArray();
ByteArrayReader bar = new ByteArrayReader(tmp);
try {
while (bar.available() > 0) {
short family = bar.readShort();
short len = bar.readShort();
byte[] address = new byte[len];
bar.read(address);
len = bar.readShort();
byte[] number = new byte[len];
bar.read(number);
len = bar.readShort();
byte[] name = new byte[len];
bar.read(name);
len = bar.readShort();
byte[] data = new byte[len];
bar.read(data);
String n = new String(number);
int d = Integer.parseInt(n);
String protocol = new String(name);
if (protocol.equals("MIT-MAGIC-COOKIE-1")) {
if (family == 0) {
// We cannot use InetAddress.getByAddress since
// it
// was only introduced in 1.4 :(
// So we're going to do this really crude
// formating
// of the IP Address and get by name
// which works just as well!
String ip = (address[0] & 0xFF) + "."
+ (address[1] & 0xFF) + "."
+ (address[2] & 0xFF) + "."
+ (address[3] & 0xFF);
InetAddress addr = java.net.InetAddress
.getByName(ip);
if (addr.getHostAddress().equals(hostname)
|| addr.getHostName().equals(hostname)) {
if (screen == d) {
ssh.getContext().setX11RealCookie(data);
break;
}
}
} else if (family == 256) {
String h = new String(address);
if (h.equals(hostname)) {
if (screen == d) {
ssh.getContext().setX11RealCookie(data);
break;
}
}
}
}
}
} finally {
bar.close();
}
}
String hostname = "localhost";
int screen = 0;
int idx = display.indexOf(':');
if (idx != -1) {
hostname = display.substring(0, idx);
display = display.substring(idx + 1);
}
if ((idx = display.indexOf('.')) > -1) {
screen = Integer.parseInt(display.substring(idx + 1));
}
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.forwardingStarted(
ForwardingClientListener.X11_FORWARDING,
X11_KEY, hostname, screen);
}
isXForwarding = true;
} catch (IOException ioe) {
throw new SshException(ioe.getMessage(),
SshException.FORWARDING_ERROR);
}
}
/**
* Requests that the remote side stop listening for socket connections.
* Please note that this feature is not available on SSH1 connections. The
* only way to stop the server from listening is to disconnect the
* connection.
*
* @param bindAddress
* the listening address on the remote side
* @param bindPort
* the listening port on the remote side
* @throws IOException
*/
public void cancelRemoteForwarding(String bindAddress, int bindPort)
throws SshException {
cancelRemoteForwarding(bindAddress, bindPort, false);
}
/**
* Requests that the remote side stop listening for socket connections.
* Please note that this feature is not available on SSH1 connections. The
* only way to stop the server from listening is to disconnect the
* connection.
*
* @param bindAddress
* the listening address on the remote side
* @param bindPort
* the listening port on the remote side
* @param killActiveTunnels
* should any active tunnels be closed
* @throws IOException
*/
public void cancelRemoteForwarding(String bindAddress, int bindPort,
boolean killActiveTunnels) throws SshException {
String key = generateKey(bindAddress, bindPort);
boolean killedTunnels = false;
if (killActiveTunnels) {
try {
ActiveTunnel[] tunnels = getRemoteForwardingTunnels(
bindAddress, bindPort);
if (tunnels != null) {
for (int i = 0; i < tunnels.length; i++) {
killedTunnels = true;
tunnels[i].stop();
}
}
} catch (IOException ex) {
}
incomingtunnels.remove(key);
}
if (!remoteforwardings.containsKey(key)) {
if (killActiveTunnels && killedTunnels) {
return;
}
throw new SshException("Remote forwarding has not been started on "
+ key, SshException.FORWARDING_ERROR);
}
// Check to see whether this is local or remote
if (ssh == null)
return;
ssh.cancelRemoteForwarding(bindAddress, bindPort);
String destination = (String) remoteforwardings.get(key);
int idx = destination.indexOf(":");
String hostToConnect;
int portToConnect;
if (idx == -1) {
throw new SshException(
"Invalid port reference in remote forwarding key!",
SshException.INTERNAL_ERROR);
}
hostToConnect = destination.substring(0, idx);
portToConnect = Integer.parseInt(destination.substring(idx + 1));
for (int i = 0; i < clientlisteners.size(); i++) {
if (clientlisteners.elementAt(i) != null) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.forwardingStopped(
ForwardingClientListener.REMOTE_FORWARDING,
key, hostToConnect, portToConnect);
}
}
remoteforwardings.remove(key);
}
/**
* Stop all remote forwarding
*
* @throws SshException
*/
public synchronized void cancelAllRemoteForwarding() throws SshException {
cancelAllRemoteForwarding(false);
}
/**
* Stop all remote forwarding.
*
* @param killActiveTunnels
* Should any active tunnels be closed.
* @throws SshException
*/
public synchronized void cancelAllRemoteForwarding(boolean killActiveTunnels)
throws SshException {
if (remoteforwardings == null) {
return;
}
for (Enumeration<String> e = remoteforwardings.keys(); e
.hasMoreElements();) {
String host = (String) e.nextElement();
if (host == null)
return;
try {
int idx = host.indexOf(':');
int port = -1;
if (idx == -1) {
port = Integer.parseInt(host);
host = "";
} else {
port = Integer.parseInt(host.substring(idx + 1));
host = host.substring(0, idx);
}
cancelRemoteForwarding(host, port, killActiveTunnels);
} catch (NumberFormatException nfe) {
}
}
}
/**
* Select a random port. NOTE: this method does not guarantee that the port
* is available.
*
* Simply generates a random number in the range LOWEST_RANDOM_PORT to
* HIGHEST_RANDOM_PORT
*
* @return int
*/
protected int selectRandomPort() {
try {
int n = HIGHEST_RANDOM_PORT - LOWEST_RANDOM_PORT + 1;
int i = ComponentManager.getInstance().getRND().nextInt() % n;
if (i < 0)
i = -i;
return LOWEST_RANDOM_PORT + i;
} catch (SshException e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* Stop all local forwarding
*/
public synchronized void stopAllLocalForwarding() throws SshException {
stopAllLocalForwarding(false);
}
/**
* Stop all local forwarding
*
* @param killActiveTunnels
* should any active tunnels be closed
*/
public synchronized void stopAllLocalForwarding(boolean killActiveTunnels)
throws SshException {
for (Enumeration<String> e = socketlisteners.keys(); e
.hasMoreElements();) {
stopLocalForwarding((String) e.nextElement(), killActiveTunnels);
}
}
/**
* Stops a local listening socket from accepting connections.
*
* @param bindAddress
* the listening address
* @param bindPort
* the listening port
*/
public synchronized void stopLocalForwarding(String bindAddress,
int bindPort) throws SshException {
stopLocalForwarding(bindAddress, bindPort, false);
}
/**
* Stops a local listening socket from accepting connections.
*
* @param bindAddress
* the listening address
* @param bindPort
* the listening port
* @param killActiveTunnels
* should any active tunnels be closed.
*/
public synchronized void stopLocalForwarding(String bindAddress,
int bindPort, boolean killActiveTunnels) throws SshException {
String key = generateKey(bindAddress, bindPort);
stopLocalForwarding(key, killActiveTunnels);
}
/**
* Stop a local listening socket from accepting connections.
*
* @param key
* the bound address and port in the format "127.0.0.1:8080"
* @param killActiveTunnels
* should any active tunnels be closed.
* @throws SshException
*/
public synchronized void stopLocalForwarding(String key,
boolean killActiveTunnels) throws SshException {
if (key == null)
return;
boolean killedTunnels = false;
if (killActiveTunnels) {
try {
ActiveTunnel[] tunnels = getLocalForwardingTunnels(key);
if (tunnels != null) {
for (int i = 0; i < tunnels.length; i++) {
tunnels[i].stop();
killedTunnels = true;
}
}
} catch (IOException ex) {
}
outgoingtunnels.remove(key);
}
if (!socketlisteners.containsKey(key)) {
if (killActiveTunnels && killedTunnels) {
return;
}
throw new SshException("Local forwarding has not been started for "
+ key, SshException.FORWARDING_ERROR);
}
// Stop the ServerSocket
SocketListener listener = (SocketListener) socketlisteners.get(key);
listener.stop();
// Remove the listener
socketlisteners.remove(key);
for (int i = 0; i < clientlisteners.size(); i++) {
if (clientlisteners.elementAt(i) != null) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.forwardingStopped(
ForwardingClientListener.LOCAL_FORWARDING, key,
listener.hostToConnect, listener.portToConnect);
}
}
EventServiceImplementation
.getInstance()
.fireEvent(
(new Event(this,
J2SSHEventCodes.EVENT_FORWARDING_LOCAL_STOPPED,
true))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_FORWARDING_TUNNEL_ENTRANCE,
key)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_FORWARDING_TUNNEL_EXIT,
listener.hostToConnect + ":"
+ listener.portToConnect));
}
String generateKey(String host, int port) {
return host.equals("") ? String.valueOf(port) : (host + ":" + String
.valueOf(port));
}
protected class ForwardingListener implements ForwardingRequestListener {
public SshTransport createConnection(String hostToConnect,
int portToConnect) throws SshException {
try {
SocketTransport t = new SocketTransport(hostToConnect,
portToConnect);
t.setSoTimeout(30000);
return t;
} catch (IOException ex) {
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.channelFailure(
ForwardingClientListener.REMOTE_FORWARDING,
hostToConnect + ":" + portToConnect,
hostToConnect, portToConnect,
ssh.isConnected(), ex);
}
throw new SshException("Failed to connect",
SshException.CONNECT_FAILED);
}
}
public void initializeTunnel(SshTunnel tunnel) {
tunnel.addChannelEventListener(tunnellistener);
}
}
class TunnelListener extends ChannelAdapter {
public void channelOpened(SshChannel channel) {
if (channel instanceof SshTunnel) {
ActiveTunnel t = new ActiveTunnel((SshTunnel) channel);
try {
t.start();
} catch (IOException ex) {
}
}
}
}
/**
* This class represents an active tunnel.
*
* @author Lee David Painter
*/
public class ActiveTunnel {
SshTunnel channel;
IOStreamConnector tx;
IOStreamConnector rx;
IOStreamListener listener = new IOStreamListener();
ActiveTunnel(SshTunnel channel) {
this.channel = channel;
}
SshTunnel getChannel() {
return channel;
}
void start() throws IOException {
try {
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.channelOpened(
channel.isLocal() ? ForwardingClientListener.LOCAL_FORWARDING
: channel.isX11() ? ForwardingClientListener.X11_FORWARDING
: ForwardingClientListener.REMOTE_FORWARDING,
channel.isX11() ? X11_KEY : generateKey(
channel.getListeningAddress(),
channel.getListeningPort()),
channel);
}
// glue forwarding channel in to connection to server out
rx = new IOStreamConnector();
rx.addListener(listener);
// rx.setCloseInput(true);
rx.connect(channel.getInputStream(), channel.getTransport()
.getOutputStream());
// glue connection to server in to forwarding channel out
tx = new IOStreamConnector();
tx.addListener(listener);
// tx.setCloseOutput(false);
tx.connect(channel.getTransport().getInputStream(),
channel.getOutputStream());
String key = generateKey(channel.getListeningAddress(),
channel.getListeningPort());
Hashtable<String, Vector<ActiveTunnel>> owner = channel
.isLocal() ? outgoingtunnels : incomingtunnels;
if (!owner.containsKey(key)) {
owner.put(key, new Vector<ActiveTunnel>());
}
Vector<ActiveTunnel> tunnels = owner.get(key);
tunnels.addElement(this);
} catch (Exception ex) {
Log.error(this, "Exception whilst opening channel", ex);
try {
channel.close();
} catch (Exception e) {
}
throw new IOException("The tunnel failed to start: "
+ ex.getMessage());
}
}
/**
* Stop's the tunnel from transferring data, closing the channel and the
* attached socket. This is now synchronized to avoid two threads
* stopping this at the same time
*/
public synchronized void stop() {
if (!rx.isClosed()) {
rx.close();
}
if (!tx.isClosed()) {
tx.close();
}
String key = generateKey(channel.getListeningAddress(),
channel.getListeningPort());
Hashtable<String, Vector<ActiveTunnel>> owner = channel.isLocal() ? outgoingtunnels
: incomingtunnels;
Vector<ActiveTunnel> tunnels = owner.get(key);
if (tunnels != null && tunnels.contains(this)) {
tunnels.removeElement(this);
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners.elementAt(i))
.channelClosed(
channel.isLocal() ? ForwardingClientListener.LOCAL_FORWARDING
: channel.isX11() ? ForwardingClientListener.X11_FORWARDING
: ForwardingClientListener.REMOTE_FORWARDING,
channel.isX11() ? X11_KEY : key, channel);
}
}
}
class IOStreamListener implements
IOStreamConnector.IOStreamConnectorListener {
public synchronized void connectorClosed(IOStreamConnector connector) {
if (Log.isDebugEnabled()) {
Log.debug(
this,
"Tunnel connector closed id="
+ channel.getChannelId() + " localEOF="
+ channel.isLocalEOF() + " remoteEOF="
+ channel.isRemoteEOF() + " closed="
+ channel.isClosed());
}
if (!channel.isClosed()) {
try {
channel.getTransport().close();
} catch (IOException ex) {
}
try {
channel.close();
} catch (Exception ex1) {
}
}
stop();
}
public void dataTransfered(byte[] buffer, int count) {
}
public void connectorTimeout(IOStreamConnector connector) {
if (Log.isDebugEnabled()) {
Log.debug(
this,
"IO timeout detected in tunnel id="
+ channel.getChannelId() + " localEOF="
+ channel.isLocalEOF() + " remoteEOF="
+ channel.isRemoteEOF() + " closed="
+ channel.isClosed());
}
if (channel.isLocalEOF() || channel.isRemoteEOF()) {
try {
channel.close();
} catch (IOException e) {
}
}
}
}
}
protected class SocketListener implements Runnable {
String addressToBind;
int portToBind;
String hostToConnect;
int portToConnect;
ServerSocket server;
private Thread thread;
private boolean listening;
public SocketListener(String addressToBind, int portToBind,
String hostToConnect, int portToConnect) {
this.addressToBind = addressToBind;
this.portToBind = portToBind;
this.hostToConnect = hostToConnect;
this.portToConnect = portToConnect;
}
public int getLocalPort() {
return (server == null) ? (-1) : server.getLocalPort();
}
public boolean isListening() {
return listening;
}
public void run() {
try {
// Socket socket;
listening = true;
while (listening && ssh.isConnected()) {
final Socket socket = server.accept();
if (!listening || (socket == null)) {
break;
}
Enumeration<ForwardingClientListener> en = clientlisteners
.elements();
ForwardingClientListener listener;
boolean accepted = true;
while (en.hasMoreElements()) {
listener = en.nextElement();
if (!listener.acceptLocalForwarding(
socket.getRemoteSocketAddress(), hostToConnect,
portToConnect)) {
accepted = false;
try {
socket.close();
} catch (Exception e) {
if (Log.isDebugEnabled()) {
Log.debug(this,
"Listener denied local forwarding to "
+ hostToConnect + ":"
+ portToConnect);
}
}
break;
}
}
if (!accepted) {
continue;
}
Thread t = new Thread() {
public void run() {
try {
// Open a forwarding channel and bind to the
// socket
ssh.openForwardingChannel(hostToConnect,
portToConnect, addressToBind,
portToBind, socket.getInetAddress()
.getHostAddress(), socket
.getPort(), new SocketWrapper(
socket), tunnellistener);
socket.setSoTimeout(30000);
} catch (Exception ex) {
Log.error(this,
"Exception whilst opening channel", ex);
try {
socket.close();
} catch (IOException ioe) {
} finally {
for (int i = 0; i < clientlisteners.size(); i++) {
((ForwardingClientListener) clientlisteners
.elementAt(i))
.channelFailure(
ForwardingClientListener.LOCAL_FORWARDING,
addressToBind + ":"
+ portToBind,
hostToConnect,
portToConnect,
ssh.isConnected(), ex);
}
}
}
}
};
t.start();
}
} catch (IOException ioe) {
} finally {
stop();
thread = null;
}
}
public String getHostToConnect() {
return hostToConnect;
}
public int getPortToConnect() {
return portToConnect;
}
public boolean isRunning() {
return (thread != null) && thread.isAlive();
}
public void start() throws SshException {
/* Bind server socket */
try {
server = new ServerSocket(portToBind, 1000,
addressToBind.equals("") ? null
: InetAddress.getByName(addressToBind));
/* Create a thread and start it */
thread = new Thread(this);
thread.setDaemon(true);
thread.setName("SocketListener " + addressToBind + ":"
+ String.valueOf(portToBind));
thread.start();
} catch (IOException ioe) {
throw new SshException("Failed to local forwarding server. ",
SshException.CHANNEL_FAILURE, ioe);
}
}
public void stop() {
try {
/* Close the server socket */
if (server != null) {
server.close();
}
} catch (IOException ioe) {
}
server = null;
listening = false;
}
}
public void exit() throws SshException {
stopAllLocalForwarding();
cancelAllRemoteForwarding();
}
}