forked from I2P_Developers/i2p.i2p
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user