From f4c3607c4d7725997fc5d9627d70a305d3a2d0e4 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 22 Feb 2009 00:35:24 +0000 Subject: [PATCH] * I2PTunnel: - Add new IRCServer tunnel type - Catch OOMs in HTTPServer - Name the IRCClient filter threads --- .../java/src/net/i2p/i2ptunnel/I2PTunnel.java | 49 +++++ .../i2p/i2ptunnel/I2PTunnelHTTPServer.java | 9 + .../net/i2p/i2ptunnel/I2PTunnelIRCClient.java | 4 +- .../net/i2p/i2ptunnel/I2PTunnelIRCServer.java | 193 ++++++++++++++++++ .../net/i2p/i2ptunnel/TunnelController.java | 15 +- .../src/net/i2p/i2ptunnel/web/IndexBean.java | 1 + apps/i2ptunnel/jsp/edit.jsp | 4 +- apps/i2ptunnel/jsp/index.jsp | 1 + 8 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java index b72ae18b3..18b517cd2 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -234,6 +234,8 @@ public class I2PTunnel implements Logging, EventDispatcher { runServer(args, l); } else if ("httpserver".equals(cmdname)) { runHttpServer(args, l); + } else if ("ircserver".equals(cmdname)) { + runIrcServer(args, l); } else if ("textserver".equals(cmdname)) { runTextServer(args, l); } else if ("client".equals(cmdname)) { @@ -383,6 +385,53 @@ public class I2PTunnel implements Logging, EventDispatcher { } } + /** + * Same args as runServer + * (we should stop duplicating all this code...) + */ + public void runIrcServer(String args[], Logging l) { + if (args.length == 3) { + InetAddress serverHost = null; + int portNum = -1; + File privKeyFile = null; + try { + serverHost = InetAddress.getByName(args[0]); + } catch (UnknownHostException uhe) { + l.log("unknown host"); + _log.error(getPrefix() + "Error resolving " + args[0], uhe); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + return; + } + + try { + portNum = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error(getPrefix() + "Port specified is not valid: " + args[1], nfe); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + return; + } + + privKeyFile = new File(args[2]); + if (!privKeyFile.canRead()) { + l.log("private key file does not exist"); + _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[2]); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + return; + } + I2PTunnelServer serv = new I2PTunnelIRCServer(serverHost, portNum, privKeyFile, args[2], l, (EventDispatcher) this, this); + serv.setReadTimeout(readTimeout); + serv.startRunning(); + addtask(serv); + notifyEvent("serverTaskId", Integer.valueOf(serv.getId())); + return; + } else { + l.log("server "); + l.log(" creates a server that sends all incoming data\n" + " of its destination to host:port."); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + } + } + /** * Run the HTTP server pointing at the host and port specified using the private i2p * destination loaded from the specified file, replacing the HTTP headers diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index 658dd5e32..536844af9 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -124,8 +124,17 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { _log.error("Error while closing the received i2p con", ex); } } catch (IOException ex) { + try { + socket.close(); + } catch (IOException ioe) {} if (_log.shouldLog(Log.WARN)) _log.warn("Error while receiving the new HTTP request", ex); + } catch (OutOfMemoryError oom) { + try { + socket.close(); + } catch (IOException ioe) {} + if (_log.shouldLog(Log.ERROR)) + _log.error("OOM in HTTP server", oom); } long afterHandle = getTunnel().getContext().clock().now(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java index e6708aa21..5b223b1a4 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java @@ -83,9 +83,9 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable i2ps = createI2PSocket(dest); i2ps.setReadTimeout(readTimeout); StringBuffer expectedPong = new StringBuffer(); - Thread in = new I2PThread(new IrcInboundFilter(s,i2ps, expectedPong)); + Thread in = new I2PThread(new IrcInboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " in"); in.start(); - Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps, expectedPong)); + Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " out"); out.start(); } catch (Exception ex) { if (_log.shouldLog(Log.ERROR)) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java new file mode 100644 index 000000000..970d90ff0 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java @@ -0,0 +1,193 @@ +package net.i2p.i2ptunnel; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.crypto.SHA256Generator; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.util.EventDispatcher; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Simple extension to the I2PTunnelServer that filters the registration + * sequence to pass the destination hash of the client through as the hostname, + * so an IRC Server may track users across nick changes. + * + * Of course, this requires the ircd actually use the hostname sent by + * the client rather than the IP. It is common for ircds to ignore the + * hostname in the USER message (unless it's coming from another server) + * since it is easily spoofed. So you have to fix or, if you are lucky, + * configure your ircd first. At least in unrealircd and ngircd this is + * not configurable. + * + * There are three options for mangling the desthash. Put the option in the + * "custom options" section of i2ptunnel. + * - ircserver.cloakKey unset: Cloak with a random value that is persistent for + * the life of this tunnel. This is the default. + * - ircserver.cloakKey=none: Don't cloak. Users may be correlated with their + * (probably) shared clients destination. + * Of course if the ircd does cloaking than this is ok. + * - ircserver.cloakKey=somepassphrase: Cloak with the hash of the passphrase. Use this to + * have consistent mangling across restarts, or to + * have multiple IRC servers cloak consistently to + * be able to track users even when they switch servers. + * Note: don't quote or put spaces in the passphrase, + * the i2ptunnel gui can't handle it. + * + * There is no outbound filtering. + * + * @author zzz + */ +public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { + + private static final Log _log = new Log(I2PTunnelIRCServer.class); + private static final String PROP_CLOAK="ircserver.cloakKey"; + private boolean _cloak; + private byte[] _cloakKey; // 32 bytes of stuff to scramble the dest with + + /** + * @throws IllegalArgumentException if the I2PTunnel does not contain + * valid config to contact the router + */ + public I2PTunnelIRCServer(InetAddress host, int port, String privData, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { + super(host, port, privData, l, notifyThis, tunnel); + initCloak(tunnel); + } + + public I2PTunnelIRCServer(InetAddress host, int port, File privkey, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { + super(host, port, privkey, privkeyname, l, notifyThis, tunnel); + initCloak(tunnel); + } + + public I2PTunnelIRCServer(InetAddress host, int port, InputStream privData, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { + super(host, port, privData, privkeyname, l, notifyThis, tunnel); + initCloak(tunnel); + } + + /** generate a random 32 bytes, or the hash of the passphrase */ + private void initCloak(I2PTunnel tunnel) { + Properties opts = tunnel.getClientOptions(); + String passphrase = opts.getProperty(PROP_CLOAK); + _cloak = passphrase == null || !"none".equals(passphrase); + if (_cloak) { + if (passphrase == null) { + _cloakKey = new byte[Hash.HASH_LENGTH]; + tunnel.getContext().random().nextBytes(_cloakKey); + } else { + _cloakKey = SHA256Generator.getInstance().calculateHash(passphrase.trim().getBytes()).getData(); + } + } + } + + protected void blockingHandle(I2PSocket socket) { + try { + // give them 15 seconds to send in the request + socket.setReadTimeout(15*1000); + InputStream in = socket.getInputStream(); + String modifiedRegistration = filterRegistration(in, cloakDest(socket.getPeerDestination())); + socket.setReadTimeout(readTimeout); + Socket s = new Socket(remoteHost, remotePort); + new I2PTunnelRunner(s, socket, slock, null, modifiedRegistration.getBytes(), null); + } catch (SocketException ex) { + try { + socket.close(); + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error while closing the received i2p con", ex); + } + } catch (IOException ex) { + try { + socket.close(); + } catch (IOException ioe) {} + if (_log.shouldLog(Log.WARN)) + _log.warn("Error while receiving the new IRC Connection", ex); + } catch (OutOfMemoryError oom) { + try { + socket.close(); + } catch (IOException ioe) {} + if (_log.shouldLog(Log.ERROR)) + _log.error("OOM in IRC server", oom); + } + } + + /** + * (Optionally) append 32 bytes of crap to the destination then return + * the first few characters of the hash of the whole thing, + ".i2p". + * Or do we want the full hash if the ircd is going to use this for + * nickserv auto-login? Or even Base32 if it will be used in a + * case-insensitive manner? + * + */ + String cloakDest(Destination d) { + Hash h; + if (_cloak) { + byte[] b = new byte[d.size() + _cloakKey.length]; + System.arraycopy(b, 0, d.toByteArray(), 0, d.size()); + System.arraycopy(b, d.size(), _cloakKey, 0, _cloakKey.length); + h = SHA256Generator.getInstance().calculateHash(b); + } else { + h = d.calculateHash(); + } + return h.toBase64().substring(0, 8) + ".i2p"; + } + + /** keep reading until we see USER or SERVER */ + private String filterRegistration(InputStream in, String newHostname) throws IOException { + StringBuffer buf = new StringBuffer(128); + int lineCount = 0; + + while (true) { + String s = DataHelper.readLine(in); + if (s == null) + throw new IOException("EOF reached before the end of the headers [" + buf.toString() + "]"); + if (++lineCount > 10) + throw new IOException("Too many lines before USER or SERVER, giving up"); + s = s.trim(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got line: " + s); + + String field[]=s.split(" ",5); + String command; + int idx=0; + + if(field[0].charAt(0)==':') + idx++; + + try { command = field[idx++]; } + catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command? + { + throw new IOException("Dropping defective message: index out of bounds while extracting command."); + } + + if ("USER".equalsIgnoreCase(command)) { + if (field.length < idx + 4) + throw new IOException("Too few parameters in USER message: " + s); + // USER zzz1 hostname localhost :zzz + // => + // USER zzz1 abcd1234.i2p localhost :zzz + // this whole class is for these two lines... + buf.append("USER ").append(field[idx]).append(' ').append(newHostname).append(".i2p "); + buf.append(field[idx+2]).append(' ').append(field[idx+3]).append("\r\n"); + break; + } + buf.append(s).append("\r\n"); + if ("SERVER".equalsIgnoreCase(command)) + break; + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("All done, sending: " + buf.toString()); + return buf.toString(); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 3c9640ce5..82b253985 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -58,7 +58,7 @@ public class TunnelController implements Logging { setConfig(config, prefix); _messages = new ArrayList(4); _running = false; - if (createKey && ("server".equals(getType()) || "httpserver".equals(getType())) ) + if (createKey && getType().endsWith("server")) createPrivateKey(); _starting = getStartOnLoad(); } @@ -148,6 +148,8 @@ public class TunnelController implements Logging { startServer(); } else if ("httpserver".equals(type)) { startHttpServer(); + } else if ("ircserver".equals(type)) { + startIrcServer(); } else { if (_log.shouldLog(Log.ERROR)) _log.error("Cannot start tunnel - unknown type [" + type + "]"); @@ -274,6 +276,17 @@ public class TunnelController implements Logging { _running = true; } + private void startIrcServer() { + setI2CPOptions(); + setSessionOptions(); + String targetHost = getTargetHost(); + String targetPort = getTargetPort(); + String privKeyFile = getPrivKeyFile(); + _tunnel.runIrcServer(new String[] { targetHost, targetPort, privKeyFile }, this); + acquire(); + _running = true; + } + private void setListenOn() { String listenOn = getListenOnInterface(); if ( (listenOn != null) && (listenOn.length() > 0) ) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 46b555772..87d8e26f6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -386,6 +386,7 @@ public class IndexBean { else if ("httpserver".equals(internalType)) return "HTTP server"; else if ("sockstunnel".equals(internalType)) return "SOCKS 5 proxy"; else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy"; + else if ("ircserver".equals(internalType)) return "IRC server"; else return internalType; } diff --git a/apps/i2ptunnel/jsp/edit.jsp b/apps/i2ptunnel/jsp/edit.jsp index 67fdf016c..b58798b20 100644 --- a/apps/i2ptunnel/jsp/edit.jsp +++ b/apps/i2ptunnel/jsp/edit.jsp @@ -16,10 +16,8 @@ String tun = request.getParameter("tunnel"); int curTunnel = -1; if (EditBean.isClient(type)) { %><% - } else if ("server".equals(type) || "httpserver".equals(type)) { - %><% } else { - %>Invalid tunnel type<% + %><% } } %> diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index b96236ae1..77303267c 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -260,6 +260,7 @@