From 79ac955b33d4b462ddb5479990dad3acfc7e3935 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 14 Jul 2011 20:06:31 +0000 Subject: [PATCH] * I2PTunnelIRCClient: - Big refactoring into multiple class files - Allow AWAY and CAP messages - First cut at DCC support - not for SOCKS (yet) --- .../i2p/i2ptunnel/I2PTunnelClientBase.java | 5 +- .../net/i2p/i2ptunnel/I2PTunnelIRCClient.java | 441 +++--------------- .../net/i2p/i2ptunnel/I2PTunnelServer.java | 29 +- .../i2p/i2ptunnel/irc/DCCClientManager.java | 105 +++++ .../src/net/i2p/i2ptunnel/irc/DCCHelper.java | 37 ++ .../i2p/i2ptunnel/irc/I2PTunnelDCCClient.java | 77 +++ .../i2p/i2ptunnel/irc/I2PTunnelDCCServer.java | 173 +++++++ .../src/net/i2p/i2ptunnel/irc/IRCFilter.java | 428 +++++++++++++++++ .../i2p/i2ptunnel/irc/IrcInboundFilter.java | 101 ++++ .../i2p/i2ptunnel/irc/IrcOutboundFilter.java | 101 ++++ .../i2ptunnel/socks/I2PSOCKSIRCTunnel.java | 7 +- 11 files changed, 1124 insertions(+), 380 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/DCCClientManager.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/DCCHelper.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/I2PTunnelDCCClient.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/I2PTunnelDCCServer.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IRCFilter.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IrcInboundFilter.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IrcOutboundFilter.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java index dadb2a58d..1d98798bc 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java @@ -193,7 +193,10 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna // no need to load the netDb with leaseSets for destinations that will never // be looked up - tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true"); + boolean dccEnabled = (this instanceof I2PTunnelIRCClient) && + Boolean.valueOf(tunnel.getClientOptions().getProperty(I2PTunnelIRCClient.PROP_DCC)).booleanValue(); + if (!dccEnabled) + tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true"); boolean openNow = !Boolean.valueOf(tunnel.getClientOptions().getProperty("i2cp.delayOpen")).booleanValue(); if (openNow) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java index ebf6cc0a1..dc128d1c4 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java @@ -10,7 +10,13 @@ import java.util.List; import java.util.StringTokenizer; import net.i2p.client.streaming.I2PSocket; +import net.i2p.data.Base32; import net.i2p.data.Destination; +import net.i2p.i2ptunnel.irc.DCCClientManager; +import net.i2p.i2ptunnel.irc.DCCHelper; +import net.i2p.i2ptunnel.irc.I2PTunnelDCCServer; +import net.i2p.i2ptunnel.irc.IrcInboundFilter; +import net.i2p.i2ptunnel.irc.IrcOutboundFilter; import net.i2p.util.EventDispatcher; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; @@ -18,7 +24,7 @@ import net.i2p.util.Log; /** * Todo: Can we extend I2PTunnelClient instead and remove some duplicated code? */ -public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable { +public class I2PTunnelIRCClient extends I2PTunnelClientBase implements DCCHelper { /** used to assign unique IDs to the threads / clients. no logic or functionality */ private static volatile long __clientId = 0; @@ -27,6 +33,14 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable protected List dests; private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1 protected long readTimeout = DEFAULT_READ_TIMEOUT; + private final boolean _dccEnabled; + private I2PTunnelDCCServer _DCCServer; + private DCCClientManager _DCCClientManager; + + /** + * @since 0.8.9 + */ + public static final String PROP_DCC = "i2ptunnel.ircclient.enableDCC"; /** * @throws IllegalArgumentException if the I2PTunnel does not contain @@ -75,6 +89,9 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable setName("IRC Client on " + tunnel.listenHost + ':' + localPort); + _dccEnabled = Boolean.valueOf(tunnel.getClientOptions().getProperty(PROP_DCC)).booleanValue(); + // TODO add some prudent tunnel options (or is it too late?) + startRunning(); notifyEvent("openIRCClientResult", "ok"); @@ -89,9 +106,9 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable i2ps = createI2PSocket(clientDest); i2ps.setReadTimeout(readTimeout); StringBuffer expectedPong = new StringBuffer(); - Thread in = new I2PAppThread(new IrcInboundFilter(s,i2ps, expectedPong, _log), "IRC Client " + __clientId + " in", true); + Thread in = new I2PAppThread(new IrcInboundFilter(s,i2ps, expectedPong, _log, this), "IRC Client " + __clientId + " in", true); in.start(); - Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong, _log), "IRC Client " + __clientId + " out", true); + Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong, _log, this), "IRC Client " + __clientId + " out", true); out.start(); } catch (Exception ex) { if (_log.shouldLog(Log.ERROR)) @@ -120,388 +137,62 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable return dests.get(index); } - /************************************************************************* - * - */ - public static class IrcInboundFilter implements Runnable { - - private final Socket local; - private final I2PSocket remote; - private final StringBuffer expectedPong; - private final Log _log; - - public IrcInboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong, Log log) { - local=_local; - remote=_remote; - expectedPong=pong; - _log = log; - } - - public void run() { - // Todo: Don't use BufferedReader - IRC spec limits line length to 512 but... - BufferedReader in; - OutputStream output; - try { - in = new BufferedReader(new InputStreamReader(remote.getInputStream(), "ISO-8859-1")); - output=local.getOutputStream(); - } catch (IOException e) { - if (_log.shouldLog(Log.ERROR)) - _log.error("IrcInboundFilter: no streams",e); - return; - } - if (_log.shouldLog(Log.DEBUG)) - _log.debug("IrcInboundFilter: Running."); - try { - while(true) - { - try { - String inmsg = in.readLine(); - if(inmsg==null) - break; - if(inmsg.endsWith("\r")) - inmsg=inmsg.substring(0,inmsg.length()-1); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("in: [" + inmsg + "]"); - String outmsg = inboundFilter(inmsg, expectedPong); - if(outmsg!=null) - { - if(!inmsg.equals(outmsg)) { - if (_log.shouldLog(Log.WARN)) { - _log.warn("inbound FILTERED: "+outmsg); - _log.warn(" - inbound was: "+inmsg); - } - } else { - if (_log.shouldLog(Log.INFO)) - _log.info("inbound: "+outmsg); - } - outmsg=outmsg+"\r\n"; // rfc1459 sec. 2.3 - output.write(outmsg.getBytes("ISO-8859-1")); - // probably doesn't do much but can't hurt - output.flush(); - } else { - if (_log.shouldLog(Log.WARN)) - _log.warn("inbound BLOCKED: "+inmsg); - } - } catch (IOException e1) { - if (_log.shouldLog(Log.WARN)) - _log.warn("IrcInboundFilter: disconnected",e1); - break; - } - } - } catch (RuntimeException re) { - _log.error("Error filtering inbound data", re); - } finally { - try { local.close(); } catch (IOException e) {} - } - if(_log.shouldLog(Log.DEBUG)) - _log.debug("IrcInboundFilter: Done."); - } - - } - - /************************************************************************* - * - */ - public static class IrcOutboundFilter implements Runnable { - - private final Socket local; - private final I2PSocket remote; - private final StringBuffer expectedPong; - private final Log _log; - - public IrcOutboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong, Log log) { - local=_local; - remote=_remote; - expectedPong=pong; - _log = log; - } - - public void run() { - // Todo: Don't use BufferedReader - IRC spec limits line length to 512 but... - BufferedReader in; - OutputStream output; - try { - in = new BufferedReader(new InputStreamReader(local.getInputStream(), "ISO-8859-1")); - output=remote.getOutputStream(); - } catch (IOException e) { - if (_log.shouldLog(Log.ERROR)) - _log.error("IrcOutboundFilter: no streams",e); - return; - } - if (_log.shouldLog(Log.DEBUG)) - _log.debug("IrcOutboundFilter: Running."); - try { - while(true) - { - try { - String inmsg = in.readLine(); - if(inmsg==null) - break; - if(inmsg.endsWith("\r")) - inmsg=inmsg.substring(0,inmsg.length()-1); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("out: [" + inmsg + "]"); - String outmsg = outboundFilter(inmsg, expectedPong); - if(outmsg!=null) - { - if(!inmsg.equals(outmsg)) { - if (_log.shouldLog(Log.WARN)) { - _log.warn("outbound FILTERED: "+outmsg); - _log.warn(" - outbound was: "+inmsg); - } - } else { - if (_log.shouldLog(Log.INFO)) - _log.info("outbound: "+outmsg); - } - outmsg=outmsg+"\r\n"; // rfc1459 sec. 2.3 - output.write(outmsg.getBytes("ISO-8859-1")); - // save 250 ms in streaming - output.flush(); - } else { - if (_log.shouldLog(Log.WARN)) - _log.warn("outbound BLOCKED: "+"\""+inmsg+"\""); - } - } catch (IOException e1) { - if (_log.shouldLog(Log.WARN)) - _log.warn("IrcOutboundFilter: disconnected",e1); - break; - } - } - } catch (RuntimeException re) { - _log.error("Error filtering outbound data", re); - } finally { - try { remote.close(); } catch (IOException e) {} - } - if (_log.shouldLog(Log.DEBUG)) - _log.debug("IrcOutboundFilter: Done."); + @Override + public boolean close(boolean forced) { + synchronized(this) { + if (_DCCServer != null) { + _DCCServer.close(forced); + _DCCServer = null; } } - - - /************************************************************************* - * - */ - - public static String inboundFilter(String s, StringBuffer expectedPong) { - - String field[]=s.split(" ",4); - String command; - int idx=0; - final String[] allowedCommands = - { - // "NOTICE", // can contain CTCP - //"PING", - //"PONG", - "MODE", - "JOIN", - "NICK", - "QUIT", - "PART", - "WALLOPS", - "ERROR", - "KICK", - "H", // "hide operator status" (after kicking an op) - "TOPIC" - }; - - if(field[0].charAt(0)==':') - idx++; - - try { command = field[idx++]; } - catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command? - { - //_log.warn("Dropping defective message: index out of bounds while extracting command."); - return null; - } - - idx++; //skip victim - - // Allow numerical responses - try { - new Integer(command); - return s; - } catch(NumberFormatException nfe){} - - - if ("PING".equalsIgnoreCase(command)) - return "PING 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works - if ("PONG".equalsIgnoreCase(command)) { - // Turn the received ":irc.freshcoffee.i2p PONG irc.freshcoffee.i2p :127.0.0.1" - // into ":127.0.0.1 PONG 127.0.0.1 " so that the caller can append the client's extra parameter - // though, does 127.0.0.1 work for irc clients connecting remotely? and for all of them? sure would - // be great if irc clients actually followed the RFCs here, but i guess thats too much to ask. - // If we haven't PINGed them, or the PING we sent isn't something we know how to filter, this - // is blank. - // - // String pong = expectedPong.length() > 0 ? expectedPong.toString() : null; - // If we aren't going to rewrite it, pass it through - String pong = expectedPong.length() > 0 ? expectedPong.toString() : s; - expectedPong.setLength(0); - return pong; - } - - // Allow all allowedCommands - for(int i=0;i= 0) // CTCP marker ^A can be anywhere, not just immediately after the ':' - { - // CTCP - msg=msg.substring(2); - if(msg.startsWith("ACTION ")) { - // /me says hello - return s; - } - return null; // Block all other ctcp - } - return s; - } - - // Block the rest - return null; + return super.close(forced); } - - public static String outboundFilter(String s, StringBuffer expectedPong) { - - String field[]=s.split(" ",3); - String command; - final String[] allowedCommands = - { - // "NOTICE", // can contain CTCP - "MODE", - "JOIN", - "NICK", - "WHO", - "WHOIS", - "LIST", - "NAMES", - "NICK", - // "QUIT", // replace with a filtered QUIT to hide client quit messages - "SILENCE", - "MAP", // seems safe enough, the ircd should protect themselves though - // "PART", // replace with filtered PART to hide client part messages - "OPER", - // "PONG", // replaced with a filtered PING/PONG since some clients send the server IP (thanks aardvax!) - // "PING", - "KICK", - "HELPME", - "RULES", - "TOPIC", - "ISON", // jIRCii uses this for a ping (response is 303) - "INVITE" - }; - if(field[0].length()==0) - return null; // W T F? - - - if(field[0].charAt(0)==':') - return null; // wtf - - command = field[0].toUpperCase(); + // + // Start of the DCCHelper interface + // - if ("PING".equals(command)) { - // Most clients just send a PING and are happy with any old PONG. Others, - // like BitchX, actually expect certain behavior. It sends two different pings: - // "PING :irc.freshcoffee.i2p" and "PING 1234567890 127.0.0.1" (where the IP is the proxy) - // the PONG to the former seems to be "PONG 127.0.0.1", while the PONG to the later is - // ":irc.freshcoffee.i2p PONG irc.freshcoffe.i2p :1234567890". - // We don't want to send them our proxy's IP address, so we need to rewrite the PING - // sent to the server, but when we get a PONG back, use what we expected, rather than - // what they sent. - // - // Yuck. + public boolean isEnabled() { + return _dccEnabled; + } - String rv = null; - expectedPong.setLength(0); - if (field.length == 1) { // PING - rv = "PING"; - // If we aren't rewriting the PING don't rewrite the PONG - // expectedPong.append("PONG 127.0.0.1"); - } else if (field.length == 2) { // PING nonce - rv = "PING " + field[1]; - // If we aren't rewriting the PING don't rewrite the PONG - // expectedPong.append("PONG ").append(field[1]); - } else if (field.length == 3) { // PING nonce serverLocation - rv = "PING " + field[1]; - expectedPong.append("PONG ").append(field[2]).append(" :").append(field[1]); // PONG serverLocation nonce - } else { - //if (_log.shouldLog(Log.ERROR)) - // _log.error("IRC client sent a PING we don't understand, filtering it (\"" + s + "\")"); - rv = null; + public String getB32Hostname() { + return Base32.encode(sockMgr.getSession().getMyDestination().calculateHash().getData()) + ".b32.i2p"; + } + + + public int newOutgoing(byte[] ip, int port, String type) { + I2PTunnelDCCServer server; + synchronized(this) { + if (_DCCServer == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Starting DCC Server"); + _DCCServer = new I2PTunnelDCCServer(sockMgr, l, this, getTunnel()); + // TODO add some prudent tunnel options (or is it too late?) + _DCCServer.startRunning(); } - - //if (_log.shouldLog(Log.WARN)) - // _log.warn("sending ping [" + rv + "], waiting for [" + expectedPong + "] orig was [" + s + "]"); - - return rv; + server = _DCCServer; } - if ("PONG".equals(command)) - return "PONG 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works + int rv = server.newOutgoing(ip, port, type); + if (_log.shouldLog(Log.INFO)) + _log.info("New outgoing " + type + ' ' + port + " returns " + rv); + return rv; + } - // Allow all allowedCommands - for(int i=0;i= 0) // CTCP marker ^A can be anywhere, not just immediately after the ':' - { - // CTCP - msg=msg.substring(2); - if(msg.startsWith("ACTION ")) { - // /me says hello - return s; - } - return null; // Block all other ctcp + public int newIncoming(String b32, int port, String type) { + DCCClientManager tracker; + synchronized(this) { + if (_DCCClientManager == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Starting DCC Client"); + _DCCClientManager = new DCCClientManager(sockMgr, l, this, getTunnel()); } - return s; + tracker = _DCCClientManager; } - - if("USER".equals(command)) { - int idx = field[2].lastIndexOf(":"); - if(idx<0) - return "USER user hostname localhost :realname"; - String realname = field[2].substring(idx+1); - String ret = "USER "+field[1]+" hostname localhost :"+realname; - return ret; - } - - if ("PART".equals(command)) { - // hide client message - return "PART " + field[1] + " :leaving"; - } - - if ("QUIT".equals(command)) { - return "QUIT :leaving"; - } - - // Block the rest - return null; + // The tracker starts our client + int rv = tracker.newIncoming(b32, port, type); + if (_log.shouldLog(Log.INFO)) + _log.info("New incoming " + type + ' ' + b32 + ' ' + port + " returns " + rv); + return rv; } } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java index 6947b520f..94edfac46 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java @@ -47,7 +47,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { protected int remotePort; private boolean _usePool; - private Logging l; + protected Logging l; private static final long DEFAULT_READ_TIMEOUT = -1; // 3*60*1000; /** default timeout to 3 minutes - override if desired */ @@ -69,10 +69,13 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { protected boolean bidir = false; private ThreadPoolExecutor _executor; + /** unused? port should always be specified */ private int DEFAULT_LOCALPORT = 4488; protected int localPort = DEFAULT_LOCALPORT; /** + * @param privData Base64-encoded private key data, + * format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile} * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ @@ -84,6 +87,9 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { } /** + * @param privkey file containing the private key data, + * format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile} + * @param privkeyname the name of the privKey file, not clear why we need this too * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ @@ -105,6 +111,9 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { } /** + * @param privData stream containing the private key data, + * format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile} + * @param privkeyname the name of the privKey file, not clear why we need this too * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ @@ -114,10 +123,28 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { init(host, port, privData, privkeyname, l); } + /** + * @param sktMgr the existing socket manager + * @since 0.8.9 + */ + public I2PTunnelServer(InetAddress host, int port, I2PSocketManager sktMgr, + Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { + super("Server at " + host + ':' + port, notifyThis, tunnel); + this.l = l; + this.remoteHost = host; + this.remotePort = port; + _log = tunnel.getContext().logManager().getLog(getClass()); + sockMgr = sktMgr; + open = true; + } + private static final int RETRY_DELAY = 20*1000; private static final int MAX_RETRIES = 4; /** + * @param privData stream containing the private key data, + * format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile} + * @param privkeyname the name of the privKey file, not clear why we need this too * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/DCCClientManager.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/DCCClientManager.java new file mode 100644 index 000000000..ac9ed249b --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/DCCClientManager.java @@ -0,0 +1,105 @@ +package net.i2p.i2ptunnel.irc; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.Logging; +import net.i2p.util.EventDispatcher; +import net.i2p.util.Log; + +/** + * Start, track, and expire the I2PTunnelDCCClients. + * + *
+ *
+ *                <---  I2PTunnelDCCServer <--------------- I2PTunnelDCCClient <----
+ *   originating                                                                     responding
+ *   chat client                                                                     chat client
+ *                ---> I2PTunnelIRCClient --> IRC server --> I2TunnelIRCClient ----->
+ *
+ * 
+ * + * @since 0.8.9 + */ +public class DCCClientManager { + + private final I2PSocketManager sockMgr; + private final EventDispatcher _dispatch; + private final Logging l; + private final I2PTunnel _tunnel; + private final Log _log; + + private final ConcurrentHashMap _incoming; + // list of client tunnels? + private static long _id; + + private static final int MAX_INCOMING_PENDING = 10; + private static final int MAX_INCOMING_ACTIVE = 10; + private static final long INBOUND_EXPIRE = 30*60*1000; + + public DCCClientManager(I2PSocketManager sktMgr, Logging logging, + EventDispatcher dispatch, I2PTunnel tunnel) { + sockMgr = sktMgr; + l = logging; + _dispatch = dispatch; + _tunnel = tunnel; + _log = tunnel.getContext().logManager().getLog(DCCClientManager.class); + _incoming = new ConcurrentHashMap(8); + } + + /** + * An incoming DCC request + * + * @param b32 remote dcc server address + * @param port remote dcc server port + * @param type ignored + * @return local server port or -1 on error + */ + public int newIncoming(String b32, int port, String type) { + expireInbound(); + if (_incoming.size() >= MAX_INCOMING_PENDING) { + _log.error("Too many incoming DCC, max is " + MAX_INCOMING_PENDING); + return -1; + } + I2PAddress client = new I2PAddress(b32, port, _tunnel.getContext().clock().now() + INBOUND_EXPIRE); + try { + // Transparent tunnel used for all types... + // Do we need to do any filtering for chat? + I2PTunnelDCCClient cTunnel = new I2PTunnelDCCClient(b32, port, l, sockMgr, + _dispatch, _tunnel, ++_id); + int lport = cTunnel.getLocalPort(); + if (_log.shouldLog(Log.WARN)) + _log.warn("Opened client tunnel at port " + lport + + " pointing to " + b32 + ':' + port); + _incoming.put(Integer.valueOf(lport), client); + return lport; + } catch (IllegalArgumentException uhe) { + l.log("Could not find listen host to bind to [" + _tunnel.host + "]"); + _log.error("Error finding host to bind", uhe); + return -1; + } + } + + private void expireInbound() { + for (Iterator iter = _incoming.values().iterator(); iter.hasNext(); ) { + I2PAddress a = iter.next(); + if (a.expire < _tunnel.getContext().clock().now()) + iter.remove(); + } + } + + private static class I2PAddress { + public final String dest; + public final int port; + public final long expire; + + public I2PAddress(String b32, int p, long exp) { + dest = b32; + port = p; + expire = exp; + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/DCCHelper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/DCCHelper.java new file mode 100644 index 000000000..c5162f148 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/DCCHelper.java @@ -0,0 +1,37 @@ +package net.i2p.i2ptunnel.irc; + +/** + * Hooks to create and maintain DCC client and server tunnels + * + * @since 0.8.9 + */ +public interface DCCHelper { + + public boolean isEnabled(); + + /** + * String to put in the outgoing DCC + */ + public String getB32Hostname(); + + /** + * An outgoing DCC request + * + * @param ip local irc client IP + * @param port local irc client port + * @param type string + * @return i2p port or -1 on error + */ + public int newOutgoing(byte[] ip, int port, String type); + + /** + * An incoming DCC request + * + * @param b32 remote dcc server address + * @param port remote dcc server port + * @param type string + * @return local server port or -1 on error + */ + public int newIncoming(String b32, int port, String type); + +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/I2PTunnelDCCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/I2PTunnelDCCClient.java new file mode 100644 index 000000000..e6a9c96cd --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/I2PTunnelDCCClient.java @@ -0,0 +1,77 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel.irc; + +import java.net.Socket; +import java.io.IOException; + +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.client.streaming.I2PSocketOptions; +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.I2PTunnelClientBase; +import net.i2p.i2ptunnel.I2PTunnelRunner; +import net.i2p.i2ptunnel.Logging; +import net.i2p.util.EventDispatcher; +import net.i2p.util.Log; + +/** + * A standard client, using an existing socket manager. + * Targets a single destination and port. + * Naming resolution is delayed until connect time. + * + * @since 0.8.9 + */ +public class I2PTunnelDCCClient extends I2PTunnelClientBase { + + // delay resolution until connect time + private final String _dest; + private final int _remotePort; + + /** + * @param dest the target, presumably b32 + * @throws IllegalArgumentException if the I2PTunnel does not contain + * valid config to contact the router + */ + public I2PTunnelDCCClient(String dest, int remotePort, Logging l, + I2PSocketManager sktMgr, EventDispatcher notifyThis, + I2PTunnel tunnel, long clientId) throws IllegalArgumentException { + super(0, l, sktMgr, tunnel, notifyThis, clientId); + _dest = dest; + _remotePort = remotePort; + + setName("DCC send -> " + dest + ':' + remotePort); + + startRunning(); + + notifyEvent("openClientResult", "ok"); + } + + protected void clientConnectionRun(Socket s) { + I2PSocket i2ps = null; + if (_log.shouldLog(Log.INFO)) + _log.info("Opening DCC connection to " + _dest + ':' + _remotePort); + Destination dest = _context.namingService().lookup(_dest); + if (dest == null) { + _log.error("Could not find leaseset for DCC connection to " + _dest + ':' + _remotePort); + closeSocket(s); + // shutdown? + return; + } + + I2PSocketOptions opts = sockMgr.buildOptions(); + opts.setPort(_remotePort); + try { + i2ps = createI2PSocket(dest, opts); + new I2PTunnelRunner(s, i2ps, sockLock, null, mySockets); + } catch (Exception ex) { + _log.error("Could not make DCC connection to " + _dest + ':' + _remotePort, ex); + closeSocket(s); + if (i2ps != null) { + try { i2ps.close(); } catch (IOException ioe) {} + } + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/I2PTunnelDCCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/I2PTunnelDCCServer.java new file mode 100644 index 000000000..d2a16cee9 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/I2PTunnelDCCServer.java @@ -0,0 +1,173 @@ +package net.i2p.i2ptunnel.irc; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.I2PTunnelRunner; +import net.i2p.i2ptunnel.I2PTunnelServer; +import net.i2p.i2ptunnel.Logging; +import net.i2p.util.EventDispatcher; +import net.i2p.util.Log; + +/** + * A standard server that only answers for registered ports, + * and each port can only be used once. + * + *
+ *
+ *                <---  I2PTunnelDCCServer <--------------- I2PTunnelDCCClient <----
+ *   originating                                                                     responding
+ *   chat client                                                                     chat client
+ *                ---> I2PTunnelIRCClient --> IRC server --> I2TunnelIRCClient ----->
+ *
+ * 
+ * + * @since 0.8.9 + */ +public class I2PTunnelDCCServer extends I2PTunnelServer { + + private final ConcurrentHashMap _outgoing; + // list of client tunnels? + private static long _id; + + /** just to keep super() happy */ + private static final InetAddress DUMMY; + static { + InetAddress dummy = null; + try { + dummy = InetAddress.getByAddress(new byte[4]); + } catch (UnknownHostException uhe) {} + DUMMY = dummy; + } + + private static final int MIN_I2P_PORT = 1; + private static final int MAX_I2P_PORT = 65535; + private static final int MAX_OUTGOING_PENDING = 20; + private static final int MAX_OUTGOING_ACTIVE = 20; + private static final long OUTBOUND_EXPIRE = 30*60*1000; + + /** + * There's no support for unsolicited incoming I2P connections, + * so there's no server host or port parameters. + * + * @param sktMgr an existing socket manager + * @throws IllegalArgumentException if the I2PTunnel does not contain + * valid config to contact the router + */ + public I2PTunnelDCCServer(I2PSocketManager sktMgr, Logging l, + EventDispatcher notifyThis, I2PTunnel tunnel) { + super(DUMMY, 0, sktMgr, l, notifyThis, tunnel); + _outgoing = new ConcurrentHashMap(8); + } + + /** + * An incoming DCC connection, only accept for a known port. + * Passed through without filtering. + */ + @Override + protected void blockingHandle(I2PSocket socket) { + if (_log.shouldLog(Log.INFO)) + _log.info("Incoming connection to '" + toString() + "' from: " + socket.getPeerDestination().calculateHash().toBase64()); + + try { + expireOutbound(); + int myPort = socket.getLocalPort(); + // TODO remove, add to active + LocalAddress local = _outgoing.get(Integer.valueOf(myPort)); + if (local == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Incoming DCC connection for unknown port " + myPort); + try { + socket.close(); + } catch (IOException ioe) {} + return; + } + if (_log.shouldLog(Log.WARN)) + _log.warn("Incoming DCC connection for I2P port " + myPort + + " sending to " + local.ia + ':' + local.port); + Socket s = new Socket(local.ia, local.port); + new I2PTunnelRunner(s, socket, slock, null, null); + } catch (SocketException ex) { + try { + socket.close(); + } catch (IOException ioe) {} + if (_log.shouldLog(Log.ERROR)) + _log.error("Error connecting to server " + remoteHost + ':' + remotePort, ex); + } catch (IOException ex) { + _log.error("Error while waiting for I2PConnections", ex); + } + } + + /** + * An outgoing DCC request + * + * @param ip local irc client IP + * @param port local irc client port + * @param type ignored + * @return i2p port or -1 on error + */ + public int newOutgoing(byte[] ip, int port, String type) { + expireOutbound(); + if (_outgoing.size() >= MAX_OUTGOING_PENDING) { + _log.error("Too many outgoing DCC, max is " + MAX_OUTGOING_PENDING); + return -1; + } + InetAddress ia; + try { + ia = InetAddress.getByAddress(ip); + } catch (UnknownHostException uhe) { + return -1; + } + LocalAddress client = new LocalAddress(ia, port, getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE); + for (int i = 0; i < 10; i++) { + int iport = MIN_I2P_PORT + getTunnel().getContext().random().nextInt(1 + MAX_I2P_PORT - MIN_I2P_PORT); + LocalAddress old = _outgoing.putIfAbsent(Integer.valueOf(iport), client); + if (old != null) + continue; + // TODO expire in a few minutes + return iport; + } + // couldn't find an unused i2p port + return -1; + } + + private InetAddress getListenHost(Logging l) { + try { + return InetAddress.getByName(getTunnel().listenHost); + } catch (UnknownHostException uhe) { + l.log("Could not find listen host to bind to [" + getTunnel().host + "]"); + _log.error("Error finding host to bind", uhe); + notifyEvent("openBaseClientResult", "error"); + return null; + } + } + + private void expireOutbound() { + for (Iterator iter = _outgoing.values().iterator(); iter.hasNext(); ) { + LocalAddress a = iter.next(); + if (a.expire < getTunnel().getContext().clock().now()) + iter.remove(); + } + } + + private static class LocalAddress { + public final InetAddress ia; + public final int port; + public final long expire; + + public LocalAddress(InetAddress a, int p, long exp) { + ia = a; + port = p; + expire = exp; + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IRCFilter.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IRCFilter.java new file mode 100644 index 000000000..2f386225d --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IRCFilter.java @@ -0,0 +1,428 @@ +package net.i2p.i2ptunnel.irc; + +import net.i2p.data.DataHelper; + + +/** + * Static methods to filter individual lines. + * Moved from I2PTunnelIRCClient.java + * + * @since 0.8.9 + */ +abstract class IRCFilter { + + private static final boolean ALLOW_ALL_DCC_IN = false; + private static final boolean ALLOW_ALL_DCC_OUT = false; + /** does not override DCC handling */ + private static final boolean ALLOW_ALL_CTCP_IN = false; + /** does not override DCC handling */ + private static final boolean ALLOW_ALL_CTCP_OUT = false; + + /************************************************************************* + * + * Modify or filter a single inbound line. + * + * @param helper may be null + * @return the original or modified line, or null if it should be dropped. + */ + public static String inboundFilter(String s, StringBuffer expectedPong, DCCHelper helper) { + + String field[]=s.split(" ",4); + String command; + int idx=0; + final String[] allowedCommands = + { + // "NOTICE", // can contain CTCP + //"PING", + //"PONG", + "MODE", + "JOIN", + "NICK", + "QUIT", + "PART", + "WALLOPS", + "ERROR", + "KICK", + "H", // "hide operator status" (after kicking an op) + "TOPIC", + // http://tools.ietf.org/html/draft-mitchell-irc-capabilities-01 + "CAP" + }; + + if(field[0].charAt(0)==':') + idx++; + + try { command = field[idx++]; } + catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command? + { + //_log.warn("Dropping defective message: index out of bounds while extracting command."); + return null; + } + + idx++; //skip victim + + // Allow numerical responses + try { + new Integer(command); + return s; + } catch(NumberFormatException nfe){} + + + if ("PING".equalsIgnoreCase(command)) + return "PING 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works + if ("PONG".equalsIgnoreCase(command)) { + // Turn the received ":irc.freshcoffee.i2p PONG irc.freshcoffee.i2p :127.0.0.1" + // into ":127.0.0.1 PONG 127.0.0.1 " so that the caller can append the client's extra parameter + // though, does 127.0.0.1 work for irc clients connecting remotely? and for all of them? sure would + // be great if irc clients actually followed the RFCs here, but i guess thats too much to ask. + // If we haven't PINGed them, or the PING we sent isn't something we know how to filter, this + // is blank. + // + // String pong = expectedPong.length() > 0 ? expectedPong.toString() : null; + // If we aren't going to rewrite it, pass it through + String pong = expectedPong.length() > 0 ? expectedPong.toString() : s; + expectedPong.setLength(0); + return pong; + } + + // Allow all allowedCommands + for(int i=0;i= 0) // CTCP marker ^A can be anywhere, not just immediately after the ':' + { + // CTCP + + // don't even try to parse multiple CTCP in the same message + int count = 0; + for (int i = 0; i < msg.length(); i++) { + if (msg.charAt(i) == 0x01) + count++; + } + if (count != 2) + return null; + + msg=msg.substring(2); + if(msg.startsWith("ACTION ")) { + // /me says hello + return s; + } + if (msg.startsWith("DCC ")) { + StringBuilder buf = new StringBuilder(128); + for (int i = 0; i <= idx - 2; i++) { + buf.append(field[i]).append(' '); + } + buf.append(":\001DCC "); + return filterDCCIn(buf.toString(), msg.substring(4), helper); + } + if (ALLOW_ALL_CTCP_IN) + return s; + return null; // Block all other ctcp + } + return s; + } + + // Block the rest + return null; + } + + /************************************************************************* + * + * Modify or filter a single outbound line. + * + * @param helper may be null + * @return the original or modified line, or null if it should be dropped. + */ + public static String outboundFilter(String s, StringBuffer expectedPong, DCCHelper helper) { + + String field[]=s.split(" ",3); + String command; + final String[] allowedCommands = + { + // "NOTICE", // can contain CTCP + "MODE", + "JOIN", + "NICK", + "WHO", + "WHOIS", + "LIST", + "NAMES", + "NICK", + // "QUIT", // replace with a filtered QUIT to hide client quit messages + "SILENCE", + "MAP", // seems safe enough, the ircd should protect themselves though + // "PART", // replace with filtered PART to hide client part messages + "OPER", + // "PONG", // replaced with a filtered PING/PONG since some clients send the server IP (thanks aardvax!) + // "PING", + "KICK", + "HELPME", + "RULES", + "TOPIC", + "ISON", // jIRCii uses this for a ping (response is 303) + "INVITE", + "AWAY", // should be harmless + // http://tools.ietf.org/html/draft-mitchell-irc-capabilities-01 + "CAP" + }; + + if(field[0].length()==0) + return null; // W T F? + + + if(field[0].charAt(0)==':') + return null; // wtf + + command = field[0].toUpperCase(); + + if ("PING".equals(command)) { + // Most clients just send a PING and are happy with any old PONG. Others, + // like BitchX, actually expect certain behavior. It sends two different pings: + // "PING :irc.freshcoffee.i2p" and "PING 1234567890 127.0.0.1" (where the IP is the proxy) + // the PONG to the former seems to be "PONG 127.0.0.1", while the PONG to the later is + // ":irc.freshcoffee.i2p PONG irc.freshcoffe.i2p :1234567890". + // We don't want to send them our proxy's IP address, so we need to rewrite the PING + // sent to the server, but when we get a PONG back, use what we expected, rather than + // what they sent. + // + // Yuck. + + String rv = null; + expectedPong.setLength(0); + if (field.length == 1) { // PING + rv = "PING"; + // If we aren't rewriting the PING don't rewrite the PONG + // expectedPong.append("PONG 127.0.0.1"); + } else if (field.length == 2) { // PING nonce + rv = "PING " + field[1]; + // If we aren't rewriting the PING don't rewrite the PONG + // expectedPong.append("PONG ").append(field[1]); + } else if (field.length == 3) { // PING nonce serverLocation + rv = "PING " + field[1]; + expectedPong.append("PONG ").append(field[2]).append(" :").append(field[1]); // PONG serverLocation nonce + } else { + //if (_log.shouldLog(Log.ERROR)) + // _log.error("IRC client sent a PING we don't understand, filtering it (\"" + s + "\")"); + rv = null; + } + + //if (_log.shouldLog(Log.WARN)) + // _log.warn("sending ping [" + rv + "], waiting for [" + expectedPong + "] orig was [" + s + "]"); + + return rv; + } + if ("PONG".equals(command)) + return "PONG 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works + + // Allow all allowedCommands + for(int i=0;i= 0) // CTCP marker ^A can be anywhere, not just immediately after the ':' + { + // CTCP + + // don't even try to parse multiple CTCP in the same message + int count = 0; + for (int i = 0; i < msg.length(); i++) { + if (msg.charAt(i) == 0x01) + count++; + } + if (count != 2) + return null; + + msg=msg.substring(2); + if(msg.startsWith("ACTION ")) { + // /me says hello + return s; + } + if (msg.startsWith("DCC ")) + return filterDCCOut(field[0] + ' ' + field[1] + " :\001DCC ", msg.substring(4), helper); + if (ALLOW_ALL_CTCP_OUT) + return s; + return null; // Block all other ctcp + } + return s; + } + + if("USER".equals(command)) { + int idx = field[2].lastIndexOf(":"); + if(idx<0) + return "USER user hostname localhost :realname"; + String realname = field[2].substring(idx+1); + String ret = "USER "+field[1]+" hostname localhost :"+realname; + return ret; + } + + if ("PART".equals(command)) { + // hide client message + return "PART " + field[1] + " :leaving"; + } + + if ("QUIT".equals(command)) { + return "QUIT :leaving"; + } + + // Block the rest + return null; + } + + /** + * @param pfx the message through the "DCC " part + * @param msg the message after the "DCC " part + * @param helper may be null + * @return the sanitized message or null to block + * @since 0.8.9 + */ + private static String filterDCCIn(String pfx, String msg, DCCHelper helper) { + // strip trailing ctcp (other one is in pfx) + int ctcp = msg.indexOf(0x01); + if (ctcp > 0) + msg = msg.substring(0, ctcp); + String[] args = msg.split(" ", 5); + if (args.length <= 0) + return null; + String type = args[0]; + // no IP in these but port needs to be fixed still + //if (type == "RESUME" || type == "ACCEPT") + // return msg; + if (!(type.equals("CHAT") || type.equals("SEND"))) { + if (ALLOW_ALL_DCC_IN) { + if (ctcp > 0) + return pfx + msg + (char) 0x01; + return pfx + msg; + } + return null; + } + if (helper == null || !helper.isEnabled()) + return null; + if (args.length < 4) + return null; + String arg = args[1]; + String b32 = args[2]; + int cPort; + try { + String cp = args[3]; + cPort = Integer.parseInt(cp); + } catch (NumberFormatException nfe) { + return null; + } + + int port = helper.newIncoming(b32, cPort, type); + if (port < 0) + return null; + StringBuilder buf = new StringBuilder(256); + // fixme what is our address? + byte[] myIP = { 127, 0, 0, 1 }; + buf.append(pfx) + .append(type).append(' ').append(arg).append(' ') + .append(DataHelper.fromLong(myIP, 0, myIP.length)).append(' ') + .append(port); + if (args.length > 4) + buf.append(' ').append(args[4]); + if (pfx.indexOf(0x01) >= 0) + buf.append((char) 0x01); + return buf.toString(); + } + + /** + * @param pfx the message through the "DCC " part + * @param msg the message after the "DCC " part + * @param helper may be null + * @return the sanitized message or null to block + * @since 0.8.9 + */ + private static String filterDCCOut(String pfx, String msg, DCCHelper helper) { + // strip trailing ctcp (other one is in pfx) + int ctcp = msg.indexOf(0x01); + if (ctcp > 0) + msg = msg.substring(0, ctcp); + String[] args = msg.split(" ", 5); + if (args.length <= 0) + return null; + String type = args[0]; + // no IP in these but port needs to be fixed still + //if (type == "RESUME" || type == "ACCEPT") + // return msg; + if (!(type.equals("CHAT") || type.equals("SEND"))) { + if (ALLOW_ALL_DCC_OUT) { + if (ctcp > 0) + return pfx + msg + (char) 0x01; + return pfx + msg; + } + } + if (helper == null || !helper.isEnabled()) + return null; + if (args.length < 4) + return null; + String arg = args[1]; + byte[] ip; + try { + String ips = args[2]; + long ipl = Long.parseLong(ips); + if (ipl < 0x01000000) { + // "reverse/firewall DCC" + // http://en.wikipedia.org/wiki/Direct_Client-to-Client + // xchat sends an IP of 199 and a port of 0 + System.err.println("Reverse / Firewall DCC not supported IP = 0x" + Long.toHexString(ipl)); + return null; + } + ip = DataHelper.toLong(4, ipl); + } catch (NumberFormatException nfe) { + return null; + } + int cPort; + try { + String cp = args[3]; + cPort = Integer.parseInt(cp); + } catch (NumberFormatException nfe) { + return null; + } + if (cPort <= 0) { + // "reverse/firewall DCC" + // http://en.wikipedia.org/wiki/Direct_Client-to-Client + System.err.println("Reverse / Firewall DCC not supported"); + return null; + } + int port = helper.newOutgoing(ip, cPort, type); + if (port < 0) + return null; + StringBuilder buf = new StringBuilder(256); + buf.append(pfx) + .append(type).append(' ').append(arg).append(' ') + .append(helper.getB32Hostname()).append(' ') + .append(port); + if (args.length > 4) + buf.append(' ').append(args[4]); + if (pfx.indexOf(0x01) >= 0) + buf.append((char) 0x01); + return buf.toString(); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IrcInboundFilter.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IrcInboundFilter.java new file mode 100644 index 000000000..ce301a48e --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IrcInboundFilter.java @@ -0,0 +1,101 @@ +package net.i2p.i2ptunnel.irc; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; + +import net.i2p.client.streaming.I2PSocket; +import net.i2p.util.Log; + +/** + * Thread to do inbound filtering. + * Moved from I2PTunnelIRCClient.java + * + * @since 0.8.9 + */ +public class IrcInboundFilter implements Runnable { + + private final Socket local; + private final I2PSocket remote; + private final StringBuffer expectedPong; + private final Log _log; + private final DCCHelper _dccHelper; + + public IrcInboundFilter(Socket lcl, I2PSocket rem, StringBuffer pong, Log log) { + this(lcl, rem, pong, log, null); + } + + /** + * @param helper may be null + * @since 0.8.9 + */ + public IrcInboundFilter(Socket lcl, I2PSocket rem, StringBuffer pong, Log log, DCCHelper helper) { + local = lcl; + remote = rem; + expectedPong = pong; + _log = log; + _dccHelper = helper; + } + + public void run() { + // Todo: Don't use BufferedReader - IRC spec limits line length to 512 but... + BufferedReader in; + OutputStream output; + try { + in = new BufferedReader(new InputStreamReader(remote.getInputStream(), "ISO-8859-1")); + output=local.getOutputStream(); + } catch (IOException e) { + if (_log.shouldLog(Log.ERROR)) + _log.error("IrcInboundFilter: no streams",e); + return; + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("IrcInboundFilter: Running."); + try { + while(true) + { + try { + String inmsg = in.readLine(); + if(inmsg==null) + break; + if(inmsg.endsWith("\r")) + inmsg=inmsg.substring(0,inmsg.length()-1); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("in: [" + inmsg + "]"); + String outmsg = IRCFilter.inboundFilter(inmsg, expectedPong, _dccHelper); + if(outmsg!=null) + { + if(!inmsg.equals(outmsg)) { + if (_log.shouldLog(Log.WARN)) { + _log.warn("inbound FILTERED: "+outmsg); + _log.warn(" - inbound was: "+inmsg); + } + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("inbound: "+outmsg); + } + outmsg=outmsg+"\r\n"; // rfc1459 sec. 2.3 + output.write(outmsg.getBytes("ISO-8859-1")); + // probably doesn't do much but can't hurt + output.flush(); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("inbound BLOCKED: "+inmsg); + } + } catch (IOException e1) { + if (_log.shouldLog(Log.WARN)) + _log.warn("IrcInboundFilter: disconnected",e1); + break; + } + } + } catch (RuntimeException re) { + _log.error("Error filtering inbound data", re); + } finally { + try { local.close(); } catch (IOException e) {} + } + if(_log.shouldLog(Log.DEBUG)) + _log.debug("IrcInboundFilter: Done."); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IrcOutboundFilter.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IrcOutboundFilter.java new file mode 100644 index 000000000..ce68835c2 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/irc/IrcOutboundFilter.java @@ -0,0 +1,101 @@ +package net.i2p.i2ptunnel.irc; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; + +import net.i2p.client.streaming.I2PSocket; +import net.i2p.util.Log; + +/** + * Thread to do inbound filtering. + * Moved from I2PTunnelIRCClient.java + * + * @since 0.8.9 + */ +public class IrcOutboundFilter implements Runnable { + + private final Socket local; + private final I2PSocket remote; + private final StringBuffer expectedPong; + private final Log _log; + private final DCCHelper _dccHelper; + + public IrcOutboundFilter(Socket lcl, I2PSocket rem, StringBuffer pong, Log log) { + this(lcl, rem, pong, log, null); + } + + /** + * @param helper may be null + * @since 0.8.9 + */ + public IrcOutboundFilter(Socket lcl, I2PSocket rem, StringBuffer pong, Log log, DCCHelper helper) { + local = lcl; + remote = rem; + expectedPong = pong; + _log = log; + _dccHelper = helper; + } + + public void run() { + // Todo: Don't use BufferedReader - IRC spec limits line length to 512 but... + BufferedReader in; + OutputStream output; + try { + in = new BufferedReader(new InputStreamReader(local.getInputStream(), "ISO-8859-1")); + output=remote.getOutputStream(); + } catch (IOException e) { + if (_log.shouldLog(Log.ERROR)) + _log.error("IrcOutboundFilter: no streams",e); + return; + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("IrcOutboundFilter: Running."); + try { + while(true) + { + try { + String inmsg = in.readLine(); + if(inmsg==null) + break; + if(inmsg.endsWith("\r")) + inmsg=inmsg.substring(0,inmsg.length()-1); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("out: [" + inmsg + "]"); + String outmsg = IRCFilter.outboundFilter(inmsg, expectedPong, _dccHelper); + if(outmsg!=null) + { + if(!inmsg.equals(outmsg)) { + if (_log.shouldLog(Log.WARN)) { + _log.warn("outbound FILTERED: "+outmsg); + _log.warn(" - outbound was: "+inmsg); + } + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("outbound: "+outmsg); + } + outmsg=outmsg+"\r\n"; // rfc1459 sec. 2.3 + output.write(outmsg.getBytes("ISO-8859-1")); + // save 250 ms in streaming + output.flush(); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("outbound BLOCKED: "+"\""+inmsg+"\""); + } + } catch (IOException e1) { + if (_log.shouldLog(Log.WARN)) + _log.warn("IrcOutboundFilter: disconnected",e1); + break; + } + } + } catch (RuntimeException re) { + _log.error("Error filtering outbound data", re); + } finally { + try { remote.close(); } catch (IOException e) {} + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("IrcOutboundFilter: Done."); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java index a19763678..ee5156f8d 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java @@ -11,7 +11,8 @@ import java.net.Socket; import net.i2p.I2PAppContext; import net.i2p.client.streaming.I2PSocket; import net.i2p.i2ptunnel.I2PTunnel; -import net.i2p.i2ptunnel.I2PTunnelIRCClient; +import net.i2p.i2ptunnel.irc.IrcInboundFilter; +import net.i2p.i2ptunnel.irc.IrcOutboundFilter; import net.i2p.i2ptunnel.Logging; import net.i2p.util.EventDispatcher; import net.i2p.util.I2PAppThread; @@ -50,10 +51,10 @@ public class I2PSOCKSIRCTunnel extends I2PSOCKSTunnel { Socket clientSock = serv.getClientSocket(); I2PSocket destSock = serv.getDestinationI2PSocket(this); StringBuffer expectedPong = new StringBuffer(); - Thread in = new I2PAppThread(new I2PTunnelIRCClient.IrcInboundFilter(clientSock, destSock, expectedPong, _log), + Thread in = new I2PAppThread(new IrcInboundFilter(clientSock, destSock, expectedPong, _log), "SOCKS IRC Client " + (++__clientId) + " in", true); in.start(); - Thread out = new I2PAppThread(new I2PTunnelIRCClient.IrcOutboundFilter(clientSock, destSock, expectedPong, _log), + Thread out = new I2PAppThread(new IrcOutboundFilter(clientSock, destSock, expectedPong, _log), "SOCKS IRC Client " + __clientId + " out", true); out.start(); } catch (SOCKSException e) {