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
This commit is contained in:
zzz
2015-04-05 17:36:30 +00:00
parent 013c79bc45
commit cce710e377
3 changed files with 156 additions and 43 deletions

View File

@@ -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);
}
}
}