Java tutorial
// $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(); } } }