From cce710e377f9629de766272980aa7a813881bfdf Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 5 Apr 2015 17:36:30 +0000 Subject: [PATCH] IRC Server: Better timeout handling when reading initial lines (ticket #723) Send error responses for timeout, EOF, and bad registration. Only affects "user" mode, not webirc. detab move private fields to top --- .../net/i2p/i2ptunnel/I2PTunnelIRCServer.java | 194 ++++++++++++++---- history.txt | 3 + .../src/net/i2p/router/RouterVersion.java | 2 +- 3 files changed, 156 insertions(+), 43 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java index 2d652861c..2288c27ec 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java @@ -1,17 +1,18 @@ 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; @@ -54,23 +55,44 @@ import net.i2p.util.Log; * @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_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 byte[] ERR_UNAVAILABLE = - (":ircserver.i2p 499 you :" + + 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") - .getBytes(); + "\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" @@ -97,8 +119,8 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { // 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 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); @@ -119,35 +141,66 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { _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 - socket.setReadTimeout(HEADER_TIMEOUT); - InputStream in = socket.getInputStream(); - modifiedRegistration = filterRegistration(in, 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(); - } + 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, modifiedRegistration.getBytes(), 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); + socket.getOutputStream().write(ERR_UNAVAILABLE.getBytes("ISO-8859-1")); } catch (IOException ioe) {} try { socket.close(); @@ -191,27 +244,35 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { return this.hostname.replace("%f", hf).replace("%c", hc); } - /** keep reading until we see USER or SERVER */ - private static String filterRegistration(InputStream in, String newHostname) throws IOException { + /** + * 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 = DataHelper.readLine(in); + String s = readLine(socket, expire - System.currentTimeMillis()); if (s == null) - throw new IOException("EOF reached before the end of the headers [" + buf.toString() + "]"); + 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 IOException("Bad protocol " + BAD_PROTOCOLS[i]); + throw new RegistrationException("Bad protocol " + BAD_PROTOCOLS[i]); } } if (++lineCount > 10) - throw new IOException("Too many lines before USER or SERVER, giving up"); + throw new RegistrationException("Too many lines before USER or SERVER, giving up"); if (System.currentTimeMillis() > expire) - throw new IOException("Headers took too long [" + buf.toString() + "]"); + throw new SocketTimeoutException("Headers took too long"); s = s.trim(); //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Got line: " + s); @@ -225,12 +286,12 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { idx++; command = field[idx++].toUpperCase(Locale.US); } catch (IndexOutOfBoundsException ioobe) { - throw new IOException("Dropping defective message: [" + s + ']'); + throw new RegistrationException("Dropping defective message: [" + s + ']'); } if ("USER".equals(command)) { if (field.length < idx + 4) - throw new IOException("Too few parameters in USER message: " + s); + throw new RegistrationException("Too few parameters in USER message: " + s); // USER zzz1 hostname localhost :zzz // => // USER zzz1 abcd1234.i2p localhost :zzz @@ -249,9 +310,58 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { return buf.toString(); } - 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; + /** + * 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); + } + } } diff --git a/history.txt b/history.txt index 4f731d065..b2c2c809c 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,6 @@ +2015-04-05 zzz + * IRC Server: Better timeout handling reading initial lines (ticket #723) + 2015-04-04 zzz * i2ptunnel: - Better timeout handling when reading headers in HTTP server (ticket #723) diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 098e16d49..14408f1ed 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 17; + public final static long BUILD = 18; /** for example "-test" */ public final static String EXTRA = "-rc";