Proxy.java Source code

Java tutorial

Introduction

Here is the source code for Proxy.java

Source

// $Id: Proxy.java,v 1.3.4.1 2008/01/22 10:01:16 belaban Exp $

import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Redirects incoming TCP connections to other hosts/ports. All redirections are defined in a file as for example
 * <pre>
 * 127.0.0.1:8888=www.ibm.com:80
 * localhost:80=pop.mail.yahoo.com:110
 * </pre>
 * The first line forwards all requests to port 8888 on to www.ibm.com at port 80 (it also forwards the HTTP
 * response back to the sender. The second line essentially provides a POP-3 service on port 8110, using
 * Yahoo's POP service. This is neat when you're behind a firewall and one of the few services in the outside
 * world that are not blocked is port 80 (HHTP).<br/>
 * Note that JDK 1.4 is required for this class. Also, concurrent.jar has to be on the classpath. Note that
 * you also need to include jsse.jar/jce.jar (same location as rt.jar) if you want SSL sockets.<br>
 * To create SSLServerSockets you'll need to do the following:
 * Generate a certificate as follows:
 * <pre>
 * keytool -genkey -keystore /home/bela/.keystore -keyalg rsa -alias bela -storepass <passwd> -keypass <passwd>
 * </pre>
 *
 * Start the Proxy as follows:
 * <pre>
 * java -Djavax.net.ssl.keyStore=/home/bela/.keystore -Djavax.net.ssl.keyStorePassword=<passwd>
 *      -Djavax.net.ssl.trustStore=/home/bela/.keystore -Djavax.net.ssl.trustStorePassword=<passwd>
 *      org.jgroups.util.Proxy -file /home/bela/map.properties
 * </pre>
 * Start client as follows:
 * <pre>
 * java -Djavax.net.ssl.trustStore=/home/bela/.keystore -Djavax.net.ssl.trustStorePassword=<passwd> sslclient
 * </pre>
 * <br/>
 * To import a certificate into the keystore, use the following steps:
 * <pre>
 * openssl x509 -in server.crt -out server.crt.der -outform DER
 * keytool -import -trustcacerts -alias <your alias name> -file server.crt.der
 * </pre>
 * This will store the server's certificate in the ${user.home}/.keystore key store.
 * <br/>
 * Note that an SSL client or server can be debugged by starting it as follows:
 * <pre>-Djava.protocol.handler.pkgs=com.sun.net.ssl.internal.www.protocol -Djavax.net.debug=ssl</pre>
 * <br/>
 * If you run a web browser, simply enter https://<host>:<port> as URL to connect to an SSLServerSocket
 * <br/>Note that we cannot use JDK 1.4's selectors for SSL sockets, as
 * getChannel() on an SSL socket doesn't seem to work.
 * @todo Check whether SSLSocket.getChannel() or SSLServerSocket.getChannel() works.
 * @author Bela Ban
 */
public class Proxy {
    InetAddress local = null, remote = null;
    int local_port = 0, remote_port = 0;
    static boolean verbose = false;
    static boolean debug = false;
    String mapping_file = null; // contains a list of src and dest host:port pairs
    final HashMap mappings = new HashMap(); // keys=MyInetSocketAddr (src), values=MyInetSocketAddr (dest)
    Executor executor; // maintains a thread pool
    static final int MIN_THREAD_POOL_SIZE = 2;
    static final int MAX_THREAD_POOL_SIZE = 64; // for processing requests
    static final int BUFSIZE = 1024; // size of data transfer buffer

    public Proxy(InetAddress local, int local_port, InetAddress remote, int remote_port, boolean verbose,
            boolean debug) {
        this.local = local;
        this.local_port = local_port;
        this.remote = remote;
        this.remote_port = remote_port;
        Proxy.verbose = verbose;
        Proxy.debug = debug;
    }

    public Proxy(InetAddress local, int local_port, InetAddress remote, int remote_port, boolean verbose,
            boolean debug, String mapping_file) {
        this(local, local_port, remote, remote_port, verbose, debug);
        this.mapping_file = mapping_file;
    }

    public void start() throws Exception {
        Map.Entry entry;
        Selector selector;
        ServerSocketChannel sock_channel;
        MyInetSocketAddress key, value;

        if (remote != null && local != null)
            mappings.put(new InetSocketAddress(local, local_port), new InetSocketAddress(remote, remote_port));

        if (mapping_file != null) {
            try {
                populateMappings(mapping_file);
            } catch (Exception ex) {
                log("Failed reading " + mapping_file);
                throw ex;
            }
        }

        log("\nProxy started at " + new java.util.Date());

        if (verbose) {
            log("\nMappings:\n---------");
            for (Iterator it = mappings.entrySet().iterator(); it.hasNext();) {
                entry = (Map.Entry) it.next();
                log(toString((InetSocketAddress) entry.getKey()) + " <--> "
                        + toString((InetSocketAddress) entry.getValue()));
            }
            log("\n");
        }

        // 1. Create a Selector
        selector = Selector.open();

        // Create a thread pool (Executor)
        executor = new ThreadPoolExecutor(MIN_THREAD_POOL_SIZE, MAX_THREAD_POOL_SIZE, 30000, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue(1000));

        for (Iterator it = mappings.keySet().iterator(); it.hasNext();) {
            key = (MyInetSocketAddress) it.next();
            value = (MyInetSocketAddress) mappings.get(key);

            // if either source or destination are SSL, we cannot use JDK 1.4
            // NIO selectors, but have to fall back on separate threads per connection

            if (key.ssl() || value.ssl()) {
                // if(2 == 2) {
                SocketAcceptor acceptor = new SocketAcceptor(key, value);
                executor.execute(acceptor);
                continue;
            }

            // 2. Create a ServerSocketChannel
            sock_channel = ServerSocketChannel.open();
            sock_channel.configureBlocking(false);
            sock_channel.socket().bind(key);

            // 3. Register the selector with all server sockets. 'Key' is attachment, so we get it again on
            //    select(). That way we can associate it with the mappings hashmap to find the corresponding
            //    value
            sock_channel.register(selector, SelectionKey.OP_ACCEPT, key);
        }

        // 4. Start main loop. won't return until CTRL-C'ed        
        loop(selector);
    }

    /** We handle only non-SSL connections */
    void loop(Selector selector) {
        Set ready_keys;
        SelectionKey key;
        ServerSocketChannel srv_sock;
        SocketChannel in_sock, out_sock;
        InetSocketAddress src, dest;

        while (true) {
            if (verbose)
                log("[Proxy] ready to accept connection");

            // 4. Call Selector.select()
            try {
                selector.select();

                // get set of ready objects
                ready_keys = selector.selectedKeys();
                for (Iterator it = ready_keys.iterator(); it.hasNext();) {
                    key = (SelectionKey) it.next();
                    it.remove();

                    if (key.isAcceptable()) {
                        srv_sock = (ServerSocketChannel) key.channel();
                        // get server socket and attachment
                        src = (InetSocketAddress) key.attachment();
                        in_sock = srv_sock.accept(); // accept request
                        if (verbose)
                            log("Proxy.loop()", "accepted connection from " + toString(in_sock));
                        dest = (InetSocketAddress) mappings.get(src);
                        // find corresponding dest
                        if (dest == null) {
                            in_sock.close();
                            log("Proxy.loop()", "did not find a destination host for " + src);
                            continue;
                        } else {
                            if (verbose)
                                log("Proxy.loop()",
                                        "relaying traffic from " + toString(src) + " to " + toString(dest));
                        }

                        // establish connection to destination host
                        try {
                            out_sock = SocketChannel.open(dest);
                            // uses thread pool (Executor) to handle request, closes socks at end
                            handleConnection(in_sock, out_sock);
                        } catch (Exception ex) {
                            in_sock.close();
                            throw ex;
                        }
                    }
                }
            } catch (Exception ex) {
                log("Proxy.loop()", "exception: " + ex);
            }
        }
    }

    //    void handleConnection(Socket in_sock, Socket out_sock) {
    //        try {
    //            Relayer r=new Relayer(in_sock, out_sock);
    //            executor.execute(r);
    //            r=new Relayer(out_sock, in_sock);
    //            executor.execute(r);
    //        }
    //        catch (Exception ex) {
    //            log("Proxy.handleConnection()", "exception: " + ex);
    //        }
    //        finally {
    //            close(in_sock, out_sock);
    //        }
    //    }

    void handleConnection(SocketChannel in, SocketChannel out) {
        try {
            _handleConnection(in, out);
        } catch (Exception ex) {
            log("Proxy.handleConnection()", "exception: " + ex);
        }
    }

    void _handleConnection(final SocketChannel in_channel, final SocketChannel out_channel) throws Exception {
        executor.execute(new Runnable() {
            public void run() {
                Selector sel = null;
                SocketChannel tmp;
                Set ready_keys;
                SelectionKey key;
                ByteBuffer transfer_buf = ByteBuffer.allocate(BUFSIZE);

                try {
                    sel = Selector.open();
                    in_channel.configureBlocking(false);
                    out_channel.configureBlocking(false);
                    in_channel.register(sel, SelectionKey.OP_READ);
                    out_channel.register(sel, SelectionKey.OP_READ);

                    while (sel.select() > 0) {
                        ready_keys = sel.selectedKeys();
                        for (Iterator it = ready_keys.iterator(); it.hasNext();) {
                            key = (SelectionKey) it.next();
                            it.remove(); // remove current entry (why ?)
                            tmp = (SocketChannel) key.channel();
                            if (tmp == null) {
                                log("Proxy._handleConnection()", "attachment is null, continuing");
                                continue;
                            }
                            if (key.isReadable()) { // data is available to be read from tmp
                                if (tmp == in_channel) {
                                    // read all data from in_channel and forward it to out_channel (request)
                                    if (relay(tmp, out_channel, transfer_buf) == false)
                                        return;
                                }
                                if (tmp == out_channel) {
                                    // read all data from out_channel and forward it 
                                    // to in_channel (response)
                                    if (relay(tmp, in_channel, transfer_buf) == false)
                                        return;
                                }
                            }
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                } finally {
                    close(sel, in_channel, out_channel);
                }
            }
        });
    }

    void close(Selector sel, SocketChannel in_channel, SocketChannel out_channel) {
        try {
            if (sel != null)
                sel.close();
        } catch (Exception ex) {
        }
        try {
            if (in_channel != null)
                in_channel.close();
        } catch (Exception ex) {
        }
        try {
            if (out_channel != null)
                out_channel.close();
        } catch (Exception ex) {
        }
    }

    /**
     * Read all data from <code>from</code> and write it to <code>to</code>.
     * Returns false if channel was closed
     */
    boolean relay(SocketChannel from, SocketChannel to, ByteBuffer buf) throws Exception {
        int num;
        StringBuilder sb;

        buf.clear();
        while (true) {
            num = from.read(buf);
            if (num < 0)
                return false;
            else if (num == 0)
                return true;
            buf.flip();
            if (verbose) {
                log(printRelayedData(toString(from), toString(to), buf.remaining()));
            }
            if (debug) {
                sb = new StringBuilder();
                sb.append(new String(buf.array()).trim());
                sb.append('\n');
                log(sb.toString());
            }
            to.write(buf);
            buf.flip();
        }
    }

    String toString(SocketChannel ch) {
        StringBuilder sb = new StringBuilder();
        Socket sock;

        if (ch == null)
            return null;
        if ((sock = ch.socket()) == null)
            return null;
        sb.append(sock.getInetAddress().getHostName()).append(':').append(sock.getPort());
        return sb.toString();
    }

    String toString(InetSocketAddress addr) {
        StringBuilder sb;
        sb = new StringBuilder();

        if (addr == null)
            return null;
        sb.append(addr.getAddress().getHostName()).append(':').append(addr.getPort());
        if (addr instanceof MyInetSocketAddress)
            sb.append(" [ssl=").append(((MyInetSocketAddress) addr).ssl()).append(']');
        return sb.toString();
    }

    static String printRelayedData(String from, String to, int num_bytes) {
        StringBuilder sb;
        sb = new StringBuilder();
        sb.append("\n[PROXY] ").append(from);
        sb.append(" to ").append(to);
        sb.append(" (").append(num_bytes).append(" bytes)");
        // log("Proxy.relay()", sb.toString());
        return sb.toString();
    }

    /**
     * Populates <code>mappings</code> hashmap. An example of a definition file is:
     * <pre>
     * http://localhost:8888=http://www.yahoo.com:80
     * https://localhost:2200=https://cvs.sourceforge.net:22
     * http://localhost:8000=https://www.ibm.com:443
     * </pre>
     * Mappings can be http-https, https-http, http-http or https-https
     */
    void populateMappings(String filename) throws Exception {
        FileInputStream in = new FileInputStream(filename);
        BufferedReader reader;
        String line;
        URI key, value;
        int index;
        boolean ssl_key, ssl_value;
        final String HTTPS = "https";

        reader = new BufferedReader(new InputStreamReader(in));
        while ((line = reader.readLine()) != null) {
            line = line.trim();
            if (line.startsWith("//") || line.startsWith("#") || line.length() == 0)
                continue;
            index = line.indexOf('=');
            if (index == -1)
                throw new Exception("Proxy.populateMappings(): detected no '=' character in " + line);
            key = new URI(line.substring(0, index));
            ssl_key = key.getScheme().trim().equals(HTTPS);

            value = new URI(line.substring(index + 1));
            ssl_value = value.getScheme().trim().equals(HTTPS);

            check(key);
            check(value);

            log("key: " + key + ", value: " + value);

            mappings.put(new MyInetSocketAddress(key.getHost(), key.getPort(), ssl_key),
                    new MyInetSocketAddress(value.getHost(), value.getPort(), ssl_value));
        }
        in.close();
    }

    /** Checks whether a URI is http(s)://<host>:<port> */
    void check(URI u) throws Exception {
        if (u.getScheme() == null)
            throw new Exception("scheme is null in " + u + ", (valid URI is \"http(s)://<host>:<port>\")");

        if (u.getHost() == null)
            throw new Exception("host is null in " + u + ", (valid URI is \"http(s)://<host>:<port>\")");

        if (u.getPort() <= 0)
            throw new Exception("port is <=0 in " + u + ", (valid URI is \"http(s)://<host>:<port>\")");

    }

    /** Input is "host:port" */
    SocketAddress strToAddr(String input) throws Exception {
        StringTokenizer tok = new StringTokenizer(input, ":");
        String host, port;

        host = tok.nextToken();
        port = tok.nextToken();
        return new InetSocketAddress(host, Integer.parseInt(port));
    }

    String printSelectionOps(SelectionKey key) {
        StringBuilder sb = new StringBuilder();
        if ((key.readyOps() & SelectionKey.OP_ACCEPT) != 0)
            sb.append("OP_ACCEPT ");
        if ((key.readyOps() & SelectionKey.OP_CONNECT) != 0)
            sb.append("OP_CONNECT ");
        if ((key.readyOps() & SelectionKey.OP_READ) != 0)
            sb.append("OP_READ ");
        if ((key.readyOps() & SelectionKey.OP_WRITE) != 0)
            sb.append("OP_WRITE ");
        return sb.toString();
    }

    public static void main(String[] args) {
        Proxy p;
        InetAddress local = null, remote = null;
        int local_port = 0, remote_port = 0;
        String tmp, tmp_addr, tmp_port;
        boolean verbose = false, debug = false;
        int index;
        String mapping_file = null;

        try {
            for (int i = 0; i < args.length; i++) {
                tmp = args[i];
                if ("-help".equals(tmp)) {
                    help();
                    return;
                }
                if ("-verbose".equals(tmp)) {
                    verbose = true;
                    continue;
                }
                if ("-local".equals(tmp)) {
                    tmp_addr = args[++i];
                    index = tmp_addr.indexOf(':');
                    if (index > -1) { // it is in the format address:port
                        tmp_port = tmp_addr.substring(index + 1);
                        local_port = Integer.parseInt(tmp_port);
                        tmp_addr = tmp_addr.substring(0, index);
                        local = InetAddress.getByName(tmp_addr);
                    } else
                        local = InetAddress.getByName(args[++i]);
                    continue;
                }
                if ("-local_port".equals(tmp)) {
                    local_port = Integer.parseInt(args[++i]);
                    continue;
                }
                if ("-remote".equals(tmp)) {
                    tmp_addr = args[++i];
                    index = tmp_addr.indexOf(':');
                    if (index > -1) { // it is in the format address:port
                        tmp_port = tmp_addr.substring(index + 1);
                        remote_port = Integer.parseInt(tmp_port);
                        tmp_addr = tmp_addr.substring(0, index);
                        remote = InetAddress.getByName(tmp_addr);
                    } else
                        remote = InetAddress.getByName(args[++i]);
                    continue;
                }
                if ("-remote_port".equals(tmp)) {
                    remote_port = Integer.parseInt(args[++i]);
                    continue;
                }
                if ("-file".equals(tmp)) {
                    mapping_file = args[++i];
                    continue;
                }
                if ("-debug".equals(tmp)) {
                    debug = true;
                    continue;
                }
                help();
                return;
            }

            if (local == null)
                local = InetAddress.getLocalHost();

            p = new Proxy(local, local_port, remote, remote_port, verbose, debug, mapping_file);
            p.start();
        } catch (Throwable ex) {
            ex.printStackTrace();
        }
    }

    static void help() {
        System.out.println("Proxy [-help] [-local <local address>] [-local_port <port>] "
                + "[-remote <remote address>] [-remote_port <port>] [-verbose] "
                + "[-file <mapping file>] [-debug]");
    }

    static void log(String method_name, String msg) {
        System.out.println('[' + method_name + "]: " + msg);
    }

    static void log(String msg) {
        System.out.println(msg);
    }

    static void close(Socket in, Socket out) {
        if (in != null) {
            try {
                in.close();
            } catch (Exception ex) {
            }
        }
        if (out != null) {
            try {
                out.close();
            } catch (Exception ex) {
            }
        }
    }

    static void close(Socket sock) {
        if (sock != null) {
            try {
                sock.close();
            } catch (Exception ex) {
            }
        }
    }

    static class Relayer implements Runnable {
        final Socket in_sock;
        final Socket out_sock;
        final InputStream in;
        final OutputStream out;
        Thread t = null;
        final java.util.List listeners = new ArrayList();
        String name = null;

        interface Listener {
            void connectionClosed();
        }

        public Relayer(Socket in_sock, Socket out_sock, String name) throws Exception {
            this.in_sock = in_sock;
            this.out_sock = out_sock;
            this.name = name;
            in = in_sock.getInputStream();
            out = out_sock.getOutputStream();
        }

        public void addListener(Listener l) {
            if (l != null && !listeners.contains(l))
                listeners.add(l);
        }

        public void run() {
            byte[] buf = new byte[1024];
            int num;
            StringBuilder sb;

            try {
                while (t != null) {
                    if ((num = in.read(buf)) == -1)
                        break;

                    if (verbose) {

                        //sb=new StringBuilder();

                        //sb.append("forwarding ").append(num).append(" bytes from ").append(toString(in_sock));
                        //sb.append(" to ").append(toString(out_sock));
                        // log("Proxy.Relayer.run()", sb.toString());
                        log(printRelayedData(toString(in_sock), toString(out_sock), num));
                    }
                    if (debug) {
                        sb = new StringBuilder();
                        sb.append(new String(buf, 0, num).trim());
                        log(sb.toString());
                    }

                    out.write(buf, 0, num);
                    //if(debug)
                    //    System.out.println(new String(buf));
                }

            } catch (Exception ex) {
                log("Proxy.Relayer.run(): [" + name + "] exception=" + ex + ", in_sock=" + in_sock + ", out_sock="
                        + out_sock);
            } finally {
                stop();
            }
        }

        public void start() {
            if (t == null) {
                t = new Thread(this, "Proxy.Relayer");
                t.setDaemon(true);
                t.start();
            }
        }

        public void stop() {
            t = null;
            close(in_sock);
            close(out_sock);
        }

        String toString(Socket s) {
            if (s == null)
                return null;
            return s.getInetAddress().getHostName() + ':' + s.getPort();
        }

        void notifyListeners() {
            for (Iterator it = listeners.iterator(); it.hasNext();) {
                try {
                    ((Listener) it.next()).connectionClosed();
                } catch (Throwable ex) {
                    ;
                }
            }
        }
    }

    static class MyInetSocketAddress extends InetSocketAddress {
        boolean is_ssl = false;

        public MyInetSocketAddress(InetAddress addr, int port) {
            super(addr, port);
        }

        public MyInetSocketAddress(InetAddress addr, int port, boolean is_ssl) {
            super(addr, port);
            this.is_ssl = is_ssl;
        }

        public MyInetSocketAddress(int port) {
            super(port);
        }

        public MyInetSocketAddress(int port, boolean is_ssl) {
            super(port);
            this.is_ssl = is_ssl;
        }

        public MyInetSocketAddress(String hostname, int port) {
            super(hostname, port);
        }

        public MyInetSocketAddress(String hostname, int port, boolean is_ssl) {
            super(hostname, port);
            this.is_ssl = is_ssl;
        }

        public boolean ssl() {
            return is_ssl;
        }

        public String toString() {
            return super.toString() + " [ssl: " + ssl() + ']';
        }
    }

    /**
     * Handles accepts on an SSLServerSocket or ServerSocket. Creates a {@link
     * Connection} for each successful accept().
     * 
     * @author bela Dec 19, 2002
     */
    class SocketAcceptor implements Runnable {
        ServerSocket srv_sock = null;
        MyInetSocketAddress dest = null;

        /**
         * Create an SSLServerSocket or ServerSocket and continuously call
         * accept() on it.
         * @param sock_addr
         */
        public SocketAcceptor(MyInetSocketAddress sock_addr, MyInetSocketAddress dest) throws Exception {
            this.dest = dest;
            if (sock_addr.ssl()) {
                srv_sock = createSSLServerSocket(sock_addr);
            } else {
                srv_sock = createServerSocket(sock_addr);
            }
            executor.execute(this);
        }

        public void run() {
            Connection conn;
            Socket s, dest_sock;

            while (srv_sock != null) {
                try {
                    s = srv_sock.accept();
                    dest_sock = dest.ssl() ? createSSLSocket(dest) : createSocket(dest);
                    conn = new Connection(s, dest_sock);
                    conn.start();
                } catch (Exception e) {
                    log("Proxy.SSLServerSocketAcceptor.run(): exception=" + e);
                    break;
                }
            }
        }

        Socket createSocket(InetSocketAddress addr) throws Exception {
            return new Socket(addr.getAddress(), addr.getPort());
        }

        Socket createSSLSocket(InetSocketAddress addr) throws Exception {
            SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
            return sslsocketfactory.createSocket(addr.getAddress(), addr.getPort());
        }

        ServerSocket createServerSocket(InetSocketAddress addr) throws Exception {
            return new ServerSocket(addr.getPort(), 10, addr.getAddress());
        }

        ServerSocket createSSLServerSocket(InetSocketAddress addr) throws Exception {
            SSLServerSocketFactory sslserversocketfactory = (SSLServerSocketFactory) SSLServerSocketFactory
                    .getDefault();
            SSLServerSocket sslserversocket;
            sslserversocket = (SSLServerSocket) sslserversocketfactory.createServerSocket(addr.getPort(), 10,
                    addr.getAddress());
            return sslserversocket;
        }
    }

    /**
     * Handles an incoming SSLSocket or Socket. Looks up the destination in the
     * mapping hashmap, key is the incoming socket address. Creates an outgoing
     * socket (regular or SSL, depending on settings) and relays data between
     * incoming and outgoing sockets. Closes the connection when either incoming
     * or outgoing socket is closed, or when stop() is called.
     * 
     * @author bela Dec 19, 2002
     */
    static class Connection implements Relayer.Listener {
        Relayer in_to_out = null;
        Relayer out_to_in = null;

        /**
         * Creates an outgoing (regular or SSL) socket according to the mapping
         * table. Sets both input and output stream. Caller needs to call
         * start() after the instance has been created.
         * @param in The Socket we got as result of accept()
         * @throws Exception Thrown if either the input or output streams cannot
         * be created.
         */
        public Connection(Socket in, Socket out) throws Exception {
            in_to_out = new Relayer(in, out, "in-out");
            in_to_out.addListener(this);
            out_to_in = new Relayer(out, in, "out-in");
            out_to_in.addListener(this);
        }

        /** Starts relaying between incoming and outgoing sockets.
         * Returns immediately (thread is started). 
         * 
         */
        public void start() {
            in_to_out.start();
            out_to_in.start();
        }

        public void stop() {
            if (in_to_out != null) {
                in_to_out.stop();
            }
            if (out_to_in != null) {
                out_to_in.stop();
            }
        }

        public void connectionClosed() {
            stop();
        }
    }

}