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