package net.i2p.i2ptunnel; import java.io.IOException; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.StringTokenizer; import net.i2p.I2PException; import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocketAddress; import net.i2p.data.DataHelper; 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; import net.i2p.util.PortMapper; /** * Todo: Can we extend I2PTunnelClient instead and remove some duplicated code? */ public class I2PTunnelIRCClient extends I2PTunnelClientBase { /** list of Destination objects that we point at */ private final List<I2PSocketAddress> _addrs; 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"; /** * As of 0.9.20 this is fast, and does NOT connect the manager to the router, * or open the local socket. You MUST call startRunning() for that. * * @param destinations peers we target, comma- or space-separated. Since 0.9.9, each dest may be appended with :port * @throws IllegalArgumentException if the I2PTunnel does not contain * valid config to contact the router */ public I2PTunnelIRCClient( int localPort, String destinations, Logging l, boolean ownDest, EventDispatcher notifyThis, I2PTunnel tunnel, String pkf) throws IllegalArgumentException { super(localPort, ownDest, l, notifyThis, "IRC Client on " + tunnel.listenHost + ':' + localPort, tunnel, pkf); _addrs = new ArrayList<I2PSocketAddress>(4); buildAddresses(destinations); if (_addrs.isEmpty()) { l.log("No target destinations found"); notifyEvent("openClientResult", "error"); // Nothing is listening for the above event, so it's useless // Maybe figure out where to put a waitEventValue("openClientResult") ?? // In the meantime, let's do this the easy way // Don't close() here, because it does a removeSession() and then // TunnelController can't acquire() it to release() it. //close(true); // Unfortunately, super() built the whole tunnel before we get here. throw new IllegalArgumentException("No valid target destinations found"); //return; } setName("IRC Client on " + tunnel.listenHost + ':' + localPort); _dccEnabled = Boolean.parseBoolean(tunnel.getClientOptions().getProperty(PROP_DCC)); // TODO add some prudent tunnel options (or is it too late?) notifyEvent("openIRCClientResult", "ok"); } /** @since 0.9.9 moved from constructor */ private void buildAddresses(String destinations) { if (destinations == null) return; StringTokenizer tok = new StringTokenizer(destinations, ", "); synchronized(_addrs) { _addrs.clear(); while (tok.hasMoreTokens()) { String destination = tok.nextToken(); try { // Try to resolve here but only log if it doesn't. // Note that b32 _addrs will often not be resolvable at instantiation time. // We will try again to resolve in clientConnectionRun() I2PSocketAddress addr = new I2PSocketAddress(destination); _addrs.add(addr); if (addr.isUnresolved()) { String name = addr.getHostName(); if (name.length() == 60 && name.endsWith(".b32.i2p")) l.log("Warning - Could not resolve " + name + ", perhaps it is not up, will retry when connecting."); else l.log("Warning - Could not resolve " + name + ", you must add it to your address book for it to work."); } } catch (IllegalArgumentException iae) { l.log("Bad destination " + destination + " - " + iae); } } } } protected void clientConnectionRun(Socket s) { if (_log.shouldLog(Log.INFO)) _log.info("New connection local addr is: " + s.getLocalAddress() + " from: " + s.getInetAddress()); I2PSocket i2ps = null; I2PSocketAddress addr = pickDestination(); try { if (addr == null) throw new UnknownHostException("No valid destination configured"); Destination clientDest = addr.getAddress(); if (clientDest == null) throw new UnknownHostException("Could not resolve " + addr.getHostName()); int port = addr.getPort(); i2ps = createI2PSocket(clientDest, port); i2ps.setReadTimeout(readTimeout); StringBuffer expectedPong = new StringBuffer(); DCCHelper dcc = _dccEnabled ? new DCC(s.getLocalAddress().getAddress()) : null; Thread in = new I2PAppThread(new IrcInboundFilter(s,i2ps, expectedPong, _log, dcc), "IRC Client " + _clientId + " in", true); in.start(); //Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong, _log, dcc), "IRC Client " + _clientId + " out", true); Runnable out = new IrcOutboundFilter(s,i2ps, expectedPong, _log, dcc); // we are called from an unlimited thread pool, so run inline //out.start(); out.run(); } catch (IOException ex) { // generally NoRouteToHostException if (_log.shouldLog(Log.WARN)) _log.warn("Error connecting", ex); //l.log("Error connecting: " + ex.getMessage()); try { // Send a response so the user doesn't just see a disconnect // and blame his router or the network. String name = addr != null ? addr.getHostName() : "undefined"; String msg = ":" + name + " 499 you :" + ex + "\r\n"; s.getOutputStream().write(DataHelper.getUTF8(msg)); } catch (IOException ioe) {} closeSocket(s); if (i2ps != null) { synchronized (sockLock) { mySockets.remove(sockLock); } } } catch (I2PException ex) { if (_log.shouldLog(Log.WARN)) _log.warn("Error connecting", ex); //l.log("Error connecting: " + ex.getMessage()); try { // Send a response so the user doesn't just see a disconnect // and blame his router or the network. String name = addr != null ? addr.getHostName() : "undefined"; String msg = ":" + name + " 499 you :" + ex + "\r\n"; s.getOutputStream().write(DataHelper.getUTF8(msg)); } catch (IOException ioe) {} closeSocket(s); if (i2ps != null) { synchronized (sockLock) { mySockets.remove(sockLock); } } } } private final I2PSocketAddress pickDestination() { synchronized(_addrs) { int size = _addrs.size(); if (size <= 0) { if (_log.shouldLog(Log.ERROR)) _log.error("No client targets?!"); return null; } if (size == 1) // skip the rand in the most common case return _addrs.get(0); int index = _context.random().nextInt(size); return _addrs.get(index); } } /** * Update the dests then call super. * * @since 0.9.9 */ @Override public void optionsUpdated(I2PTunnel tunnel) { if (getTunnel() != tunnel) return; Properties props = tunnel.getClientOptions(); // see TunnelController.setSessionOptions() String targets = props.getProperty("targetDestination"); buildAddresses(targets); super.optionsUpdated(tunnel); } @Override public void startRunning() { super.startRunning(); if (open) _context.portMapper().register(PortMapper.SVC_IRC, getTunnel().listenHost, getLocalPort()); } @Override public boolean close(boolean forced) { int reg = _context.portMapper().getPort(PortMapper.SVC_IRC); if (reg == getLocalPort()) _context.portMapper().unregister(PortMapper.SVC_IRC); synchronized(this) { if (_DCCServer != null) { _DCCServer.close(forced); _DCCServer = null; } if (_DCCClientManager != null) { _DCCClientManager.close(forced); _DCCClientManager = null; } } return super.close(forced); } // // Start of the DCCHelper interface // private class DCC implements DCCHelper { private final byte[] _localAddr; /** * @param local Our IP address, from the IRC client's perspective */ public DCC(byte[] local) { if (local.length == 4) _localAddr = local; else _localAddr = new byte[] {127, 0, 0, 1}; } public boolean isEnabled() { return _dccEnabled; } public String getB32Hostname() { return sockMgr.getSession().getMyDestination().toBase32(); } public byte[] getLocalAddress() { return _localAddr; } 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, I2PTunnelIRCClient.this, getTunnel()); // TODO add some prudent tunnel options (or is it too late?) _DCCServer.startRunning(); } server = _DCCServer; } int rv = server.newOutgoing(ip, port, type); if (_log.shouldLog(Log.INFO)) _log.info("New outgoing " + type + ' ' + port + " returns " + rv); return rv; } 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, I2PTunnelIRCClient.this, getTunnel()); } tracker = _DCCClientManager; } // 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; } public int resumeOutgoing(int port) { DCCClientManager tracker = _DCCClientManager; if (tracker != null) return tracker.resumeOutgoing(port); return -1; } public int resumeIncoming(int port) { I2PTunnelDCCServer server = _DCCServer; if (server != null) return server.resumeIncoming(port); return -1; } public int acceptOutgoing(int port) { I2PTunnelDCCServer server = _DCCServer; if (server != null) return server.acceptOutgoing(port); return -1; } public int acceptIncoming(int port) { DCCClientManager tracker = _DCCClientManager; if (tracker != null) return tracker.acceptIncoming(port); return -1; } } }