Tunnel Library - Tutorials
[MAIN] [LICENSE] [CHANGELOG] [TUTORIALS] [JAVADOC] [FORUM]Contents
Http Tunnel Tutorial
In this tutorial we will show you how to set up an HTTP tunnel. First we will look at the client side and then at the server side.
Client
To set up an HTTP tunnel we need to know the server URI. In this example we will pretend the server is located at
http://my.server.com/tunnel
. Then we can setup the tunnel as simply as:
TunnelClient client = new HttpTunnelClient(new URI("http://my.server.com/tunnel"));
Socket socket = client.connect();
To close the tunnel, simply close the socket. The HTTP tunnel client can be used to set up multiple tunnels by calling the connect method repeatedly. However, in general it is probably best to set up one HTTP tunnel to the server and to use the multiplexer (see below) to make multiple virtual connections.
If you need to tune the parameters of the HTTP tunnel client use a parameters object as follows:
TunnelClient client = new HttpTunnelClient(
new DefaultHttpTunnelClientParams(new URI("http://my.server.com/tunnel"))
.setChunked(true)
.setHttpUsername("joe")
.setHttpPassword("secret")
);
Socket socket = client.connect();
Note that the parameter object is copied by the HTTP tunnel client at construction time, so keeping a reference to it and changing the parameters after the construction of the HTTP tunnel client will have no effect.
When dealing with HTTP authentication and proxy authorization it is probably better to use the
AuthenticatingHttpTunnelClient
class, which has callbacks to ask for the credentials:
final TunnelClient client = new AuthenticatingHttpTunnelClient(tunnelUri, 3) {
@Override
protected Credentials getHttpCredentials() {
System.out.print("enter HTTP username: ");
final String username = System.console().readLine();
if (username == null) {
// Abort.
return null;
}
System.out.print("enter HTTP password: ");
final char[] password = System.console().readPassword();
if (password == null) {
// Abort.
return null;
}
return new Credentials(username, new String(password));
}
};
Socket socket = client.connect();
Server
On the server side we must install the
HttpTunnelServlet
in the servlet container and tie the lifecycle of the service that we want to implement to that of the servlet (since our service can only accept
connections when the servlet is running). The servlet can be installed in your servlet container by adding the following to your web.xml file:
<servlet>
<servlet-name>http-tunnel-servlet</servlet-name>
<servlet-class>
com.sebster.tunnel.http.server.servlet.HttpTunnelServlet
</servlet-class>
<init-param>
<param-name>eventListenerLocatorClass</param-name>
<param-value>mypackage.MyServletEventListenerLocator</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>http-tunnel-servlet</servlet-name>
<url-pattern>/tunnel</url-pattern>
</servlet-mapping>
Next we must implement the
MyServletEventListenerLocator
class, which is the class that the servlet uses to locate the tunnel server. The standard way to implement it is as follows:
public class MyServletEventListenerLocator implements ServletEventListenerLocator {
// Create a new HTTP tunnel server.
private final HttpTunnelServer tunnelServer = new HttpTunnelServer();
// The accept thread.
private volatile Thread acceptThread;
// Create a new delegating servlet event listener to hook up to the servlet lifecycle.
private final ServletEventListener servletEventListener = new DelegatingServletEventListener() {
@Override
public void onServletInit(final Servlet servlet) throws ServletException {
// Set the delegate to the HTTP tunnel server's event listener.
setDelegate(tunnelServer.getServletEventListener());
// Initialize the delegate.
super.onServletInit(servlet);
// Start my service in a separate thread.
acceptThread = new Thread(new Runnable() {
public void run() {
while (acceptThread != null) {
try {
final Socket s = tunnelServer.accept();
handleInAnotherThread(s);
} catch (final Exception e) {
e.printStackTrace();
}
}
}
}, "accept-thread");
acceptThread.setDaemon(true);
acceptThread.start();
}
@Override
public void onServletDestroy() {
// Stop my service.
acceptThread = null;
// This will close the tunnel server and cause the accept thread to terminate.
super.onServletDestroy();
}
// This method handles sockets in a new thread so that it does not block the accept thread.
protected void handleInAnotherThread(Socket s) { /* your handle code */ }
};
// Return our servlet event listener.
public ServletEventListener getServletEventListener() {
return servletEventListener;
}
}
When the
HttpTunnelServlet
starts it will instantiate the
MyServletEventListenerLocator
class and get the servlet event listener. It will then use this listener to communicate servlet events to the tunnel server. We hooked up to the init and
destroy lifecycle events to start and stop our own service, which is a basic acceptor thread. The accept/handle part is just standard socket programming.
Multiplexer Tutorial
To use the multiplexer you first need an underlying connection. The connection endpoint is just a regular Java socket. Thus, to create a multiplexer to multiplex connections over an HTTP tunnel you could use the following code:
TunnelClient client = new HttpTunnelClient(new URI("http://my.server.com"));
Socket socket = client.connect();
Multiplexer multiplexer = new Multiplexer(socket);
Socket socket2 = multiplexer.connect();
On the server side you would also need to wrap connection with a multiplexer, and thus you would have in your accept loop:
Socket socket = tunnelServer.accept(); Multiplexer multiplexer = new Multiplexer(socket);
To accept new connections from the multiplexer an accept thread must be started which calls
multiplexer.accept()
. Note that to be able to accept connections on the client side, the client must also start an accept loop.
If the underlying connection fails then the multiplexer also fails. If you wish to be notified of this failure you can override the
onFail(Exception cause)
method of the multiplexer:
Multiplexer multiplexer = new Multiplexer(socket) {
@Override
protected void onFail(final Exception cause) {
// Do something to handle the failure.
}
};
Multiplexed RMI Tutorial
To do RMI over a tunnel you probably want to multiplex RMI connections over one single tunnel instead of creating a new tunnel for each RMI connection. This will cause the RMI connections to share the bandwidth fairly which can reduce the overall latency of the system and it lower the connection setup times.
Client
To use multiplexed RMI on the client side, just create the required client RMI socket factory provider and use the provided socket factories in your RMI calls:
final HttpTunnelClient tunnelClient = new HttpTunnelClient(new URI("http://my.server.com/tunnel"));
// There is only one server, so the server id is null.
final ClientMultiplexedRmiSocketFactoryProvider sfp =
new ClientMultiplexedRmiSocketFactoryProvider(null, tunnelClient, new DefaultMultiplexerParams());
// Lookup the registry on the server (which is pretending to be bound on port 1099)
final Registry rmiRegistry = LocateRegistry.getRegistry("my.server.com", 1099,
// RMI needs to make an outgoing connection from the client to the server.
sfp.getOutgoingClientSocketFactory()
);
// Lookup a remote object and make a remote method call.
final MyRemote myRemote = (MyRemote) rmiRegistry.lookup("myRemote");
myRemote.myRemoteCall();
// Create and export remote object.
final MyRemote2 myRemote2 = new MyRemote2();
UnicastRemoteObject.exportObject(myRemote2, 1099,
// The remote RMI client needs to make incoming connections to this RMI server.
sfp.getIncomingClientSocketFactory(), sfp.getServerSocketFactory()
);
// Push the object to the server end of the tunnel, which can then do callbacks on it.
myRemote.sendObjectToServer(myRemote2);
Server
On the server side we can create a remote object and bind it as follows:
// This is the only server we're using, so use null as the server id.
final ServerMultiplexedRmiSocketFactoryProvider sfp =
new ServerMultiplexedRmiSocketFactoryProvider(null, tunnelServer, new DefaultMultiplexerParams());
final Registry rmiRegistry = LocateRegistry.createRegistry(1099,
// The remote RMI client needs to make incoming connections to this RMI server.
sfp.getIncomingClientSocketFactory(), sfp.getServerSocketFactory()
);
final MyRemote myRemote = new MyRemoteImpl();
UnicastRemoteObject.exportObject(myRemote, 1099,
// The remote RMI client needs to make incoming connections to this RMI server.
sfp.getIncomingClientSocketFactory(), sfp.getServerSocketFactory()
);
rmiRegistry.bind("myRemote", myRemote);
After this, the remote client can lookup the
myRemote
instance and make remote method calls on it. Any objects exported by the remote client to the server can be used without problems on the server side. That
means that callbacks to the client will just work, even though there may be NAT routers or firewalls between the client and server.
Port Forward Tutorial
The port forward client and server are used to create a tunnel for TCP oriented services (which need not be written in Java). The port forward client listens on a port and receives incoming TCP connections. For each connection received, it creates a tunnel to the port forward tunnel server. The port forward server then creates a new connection to a specified TCP host and port. The data that is sent and received over the TCP connection is forwarded transparently over the tunnel. This way, TCP aware services can be transparently forwarded over for example an HTTP tunnel.
Client
To start a port forward client, you need to specify the socket address on which to listen for incoming connections and the tunnel client to use to set up the tunnel to the port forward server. For example:
PortForwardClient pfc = new PortForwardClient(
new InetSocketAddress(2222),
new HttpTunnelClient(new URI("http://my.server.com/tunnel"))
);
pfc.start();
This starts a port forward client which listens for connections on port 2222 on all addresses and forwards connections over an HTTP tunnel on my.server.com.
To stop the port forward client you must call
pfc.stop()
.
Server
To start a port forward server, you need to specify the tunnel server to accept connections from and the target socket addres to which the connections should be forwarded. For example:
PortForwardServer pfc = new PortForwardServer(
new InetSocketAddress("localhost", 22),
tunnelServer
);
pfc.start();
In the above example, incoming connection to the tunnel server would be forwarded to localhost port 22 (ssh).
In the case that the port forward server works with an HTTP tunnel server, there is a special event listener locator which can be used out of the box. Just add the following to your web.xml file.
<servlet>
<servlet-name>ssh-tunnel-servlet</servlet-name>
<servlet-class>
com.sebster.tunnel.http.server.servlet.HttpTunnelServlet
</servlet-class>
<init-param>
<param-name>eventListenerLocatorClass</param-name>
<param-value>
com.sebster.tunnel.http.portforward.PortForwardServletEventListenerLocator
</param-value>
</init-param>
<init-param>
<param-name>inputBufferSize</param-name>
<param-value>65536</param-value>
</init-param>
<init-param>
<param-name>acceptQueueSize</param-name>
<param-value>50</param-value>
</init-param>
<init-param>
<param-name>targetAddress</param-name>
<param-value>localhost</param-value>
</init-param>
<init-param>
<param-name>targetPort</param-name>
<param-value>22</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
Note that the input buffer size, accept queue size, and target address parameters are optional.
To use the above port forward to make an SSH connection over an HTTP tunnel you would simply issue the following ssh commandline:
ssh -p 2222 localhost
This tells ssh to make a connection to the local host on port 2222, which is forwarded to the ssh server running locally (not necessarily on the public ip address) on my.server.com port 22.