package net.i2p.i2ptunnel; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.Locale; import java.util.Properties; import net.i2p.client.streaming.I2PSocket; import net.i2p.crypto.SHA256Generator; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.Base32; import net.i2p.util.EventDispatcher; 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.method unset: Defaults to user. * - ircserver.method=user: Use method described above. * - ircserver.method=webirc: Use the WEBIRC protocol. * - ircserver.cloakKey unset: Cloak with a random value that is persistent for * the life of this tunnel. This is the default. * - 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. * - ircserver.webircPassword=password The password to use for the WEBIRC protocol. * - ircserver.webircSpoofIP=IP The IP * - ircserver.fakeHostname=%f.b32.i2p: Set the fake hostname sent by I2PTunnel, * %f is the full B32 destination hash * %c is the cloaked hash. * * There is no outbound filtering. * * @author zzz */ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { private final byte[] cloakKey; // 32 bytes of stuff to scramble the dest with private final String hostname; private final String method; private final String webircPassword; private final String webircSpoofIP; public static final String PROP_METHOD="ircserver.method"; public static final String PROP_METHOD_DEFAULT="user"; public static final String PROP_CLOAK="ircserver.cloakKey"; public static final String PROP_WEBIRC_PASSWORD="ircserver.webircPassword"; public static final String PROP_WEBIRC_SPOOF_IP="ircserver.webircSpoofIP"; public static final String PROP_WEBIRC_SPOOF_IP_DEFAULT="127.0.0.1"; public static final String PROP_HOSTNAME="ircserver.fakeHostname"; public static final String PROP_HOSTNAME_DEFAULT="%f.b32.i2p"; private static final long HEADER_TIMEOUT = 15*1000; private static final long TOTAL_HEADER_TIMEOUT = 2 * HEADER_TIMEOUT; private static final int MAX_LINE_LENGTH = 1024; private final static String ERR_UNAVAILABLE = ":ircserver.i2p 499 you :" + "This I2P IRC server is unavailable. It may be down or undergoing maintenance. " + "Please try again later." + "\r\n"; private final static String ERR_REGISTRATION = ":ircserver.i2p 499 you :" + "Bad registration." + "\r\n"; private final static String ERR_TIMEOUT = ":ircserver.i2p 499 you :" + "Timeout registering." + "\r\n"; private final static String ERR_EOF = ":ircserver.i2p 499 you :" + "EOF while registering." + "\r\n"; private static final String[] BAD_PROTOCOLS = { "GET ", "HEAD ", "POST ", "GNUTELLA CONNECT", "\023BitTorrent protocol" }; /** * @throws IllegalArgumentException if the I2PTunnel does not contain * valid config to contact the router */ public I2PTunnelIRCServer(InetAddress host, int port, File privkey, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super(host, port, privkey, privkeyname, l, notifyThis, tunnel); // generate a random 32 bytes, or the hash of the passphrase // get the properties of this server-tunnel Properties opts = tunnel.getClientOptions(); // get method of host faking this.method = opts.getProperty(PROP_METHOD, PROP_METHOD_DEFAULT); assert this.method != null; // get the password for the webirc method this.webircPassword = opts.getProperty(PROP_WEBIRC_PASSWORD); // get the spoof IP for the webirc method this.webircSpoofIP = opts.getProperty(PROP_WEBIRC_SPOOF_IP, PROP_WEBIRC_SPOOF_IP_DEFAULT); // get the cloaking passphrase String passphrase = opts.getProperty(PROP_CLOAK); if (passphrase == null) { this.cloakKey = new byte[Hash.HASH_LENGTH]; tunnel.getContext().random().nextBytes(this.cloakKey); } else { this.cloakKey = SHA256Generator.getInstance().calculateHash(DataHelper.getUTF8(passphrase.trim())).getData(); } // get the fake hostmask to use this.hostname = opts.getProperty(PROP_HOSTNAME, PROP_HOSTNAME_DEFAULT); } @Override protected void blockingHandle(I2PSocket socket) { if (_log.shouldLog(Log.INFO)) _log.info("Incoming connection to '" + toString() + "' port " + socket.getLocalPort() + " from: " + socket.getPeerDestination().calculateHash() + " port " + socket.getPort()); try { String modifiedRegistration; if(!this.method.equals("webirc")) { // The headers _should_ be in the first packet, but // may not be, depending on the client-side options modifiedRegistration = filterRegistration(socket, cloakDest(socket.getPeerDestination())); socket.setReadTimeout(readTimeout); } else { StringBuffer buf = new StringBuffer("WEBIRC "); buf.append(this.webircPassword); buf.append(" cgiirc "); buf.append(cloakDest(socket.getPeerDestination())); buf.append(' '); buf.append(this.webircSpoofIP); buf.append("\r\n"); modifiedRegistration = buf.toString(); } Socket s = getSocket(socket.getPeerDestination().calculateHash(), socket.getLocalPort()); Thread t = new I2PTunnelRunner(s, socket, slock, null, DataHelper.getUTF8(modifiedRegistration), null, (I2PTunnelRunner.FailCallback) null); // run in the unlimited client pool //t.start(); _clientExecutor.execute(t); } catch (RegistrationException ex) { try { // Send a response so the user doesn't just see a disconnect // and blame his router or the network. socket.getOutputStream().write(ERR_REGISTRATION.getBytes("ISO-8859-1")); } catch (IOException ioe) { } finally { try { socket.close(); } catch (IOException ioe) {} } if (_log.shouldLog(Log.WARN)) _log.warn("Error while receiving the new IRC Connection", ex); } catch (EOFException ex) { try { // Send a response so the user doesn't just see a disconnect // and blame his router or the network. socket.getOutputStream().write(ERR_EOF.getBytes("ISO-8859-1")); } catch (IOException ioe) { } finally { try { socket.close(); } catch (IOException ioe) {} } if (_log.shouldLog(Log.WARN)) _log.warn("Error while receiving the new IRC Connection", ex); } catch (SocketTimeoutException ex) { try { // Send a response so the user doesn't just see a disconnect // and blame his router or the network. socket.getOutputStream().write(ERR_TIMEOUT.getBytes("ISO-8859-1")); } catch (IOException ioe) { } finally { try { socket.close(); } catch (IOException ioe) {} } if (_log.shouldLog(Log.WARN)) _log.warn("Error while receiving the new IRC Connection", ex); } catch (SocketException ex) { try { // Send a response so the user doesn't just see a disconnect // and blame his router or the network. socket.getOutputStream().write(ERR_UNAVAILABLE.getBytes("ISO-8859-1")); } catch (IOException ioe) {} try { socket.close(); } catch (IOException ioe) {} if (_log.shouldLog(Log.ERROR)) _log.error("Error connecting to IRC server " + remoteHost + ':' + remotePort, ex); } catch (IOException ex) { try { socket.reset(); } catch (IOException ioe) {} if (_log.shouldLog(Log.WARN)) _log.warn("Error while receiving the new IRC Connection", ex); } catch (OutOfMemoryError oom) { try { socket.reset(); } 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) { String hf; String hc; byte[] b = new byte[d.size() + this.cloakKey.length]; System.arraycopy(b, 0, d.toByteArray(), 0, d.size()); System.arraycopy(b, d.size(), this.cloakKey, 0, this.cloakKey.length); hc = Base32.encode(SHA256Generator.getInstance().calculateHash(b).getData()); hf = Base32.encode(d.calculateHash().getData()); return this.hostname.replace("%f", hf).replace("%c", hc); } /** * Keep reading until we see USER or SERVER. * This modifies the socket readTimeout, caller must save and restore. * * @throws SocketTimeoutException if timeout is reached before newline * @throws EOFException if EOF is reached before newline * @throws RegistrationException if line too long * @throws IOException on other errors in the underlying stream */ private static String filterRegistration(I2PSocket socket, String newHostname) throws IOException { StringBuilder buf = new StringBuilder(128); int lineCount = 0; // slowloris / darkloris long expire = System.currentTimeMillis() + TOTAL_HEADER_TIMEOUT; while (true) { String s = readLine(socket, expire - System.currentTimeMillis()); if (s == null) throw new EOFException("EOF reached before the end of the headers"); if (lineCount == 0) { for (int i = 0; i < BAD_PROTOCOLS.length; i++) { if (s.startsWith(BAD_PROTOCOLS[i])) throw new RegistrationException("Bad protocol " + BAD_PROTOCOLS[i]); } } if (++lineCount > 10) throw new RegistrationException("Too many lines before USER or SERVER, giving up"); if (System.currentTimeMillis() > expire) throw new SocketTimeoutException("Headers took too long"); s = s.trim(); //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Got line: " + s); String field[] = DataHelper.split(s, " ", 5); String command; int idx=0; try { if (field[0].charAt(0) == ':') idx++; command = field[idx++].toUpperCase(Locale.US); } catch (IndexOutOfBoundsException ioobe) { throw new RegistrationException("Dropping defective message: [" + s + ']'); } if ("USER".equals(command)) { if (field.length < idx + 4) throw new RegistrationException("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); buf.append(' '); buf.append(field[idx+2]).append(' ').append(field[idx+3]).append("\r\n"); break; } buf.append(s).append("\r\n"); if ("SERVER".equals(command)) break; } //if (_log.shouldLog(Log.DEBUG)) // _log.debug("All done, sending: " + buf.toString()); return buf.toString(); } /** * Read a line teriminated by newline, with a total read timeout. * * Warning - strips \n but not \r * Warning - 8KB line length limit as of 0.7.13, @throws IOException if exceeded * Warning - not UTF-8 * * @param timeout throws SocketTimeoutException immediately if zero or negative * @throws SocketTimeoutException if timeout is reached before newline * @throws EOFException if EOF is reached before newline * @throws RegistrationException if line too long * @throws IOException on other errors in the underlying stream * @since 0.9.19 modified from DataHelper and I2PTunnelHTTPServer */ private static String readLine(I2PSocket socket, long timeout) throws IOException { StringBuilder buf = new StringBuilder(128); if (timeout <= 0) throw new SocketTimeoutException(); long expires = System.currentTimeMillis() + timeout; InputStream in = socket.getInputStream(); int c; int i = 0; socket.setReadTimeout(timeout); while ( (c = in.read()) != -1) { if (++i > MAX_LINE_LENGTH) throw new RegistrationException("Line too long - max " + MAX_LINE_LENGTH); if (c == '\n') break; long newTimeout = expires - System.currentTimeMillis(); if (newTimeout <= 0) throw new SocketTimeoutException(); buf.append((char)c); if (newTimeout != timeout) { timeout = newTimeout; socket.setReadTimeout(timeout); } } if (c == -1) { if (System.currentTimeMillis() >= expires) throw new SocketTimeoutException(); else throw new EOFException(); } return buf.toString(); } /** * @since 0.9.19 */ private static class RegistrationException extends IOException { public RegistrationException(String s) { super(s); } } }