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.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; 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. * * <pre> * * direct conn * <---> I2PTunnelDCCServer <--------------->I2PTunnelDCCClient <----> * originating responding * chat client chat client * CHAT ---> I2PTunnelIRCClient --> IRC server --> I2TunnelIRCClient -----> * SEND ---> I2PTunnelIRCClient --> IRC server --> I2TunnelIRCClient -----> * RESUME <--- I2PTunnelIRCClient <-- IRC server <-- I2TunnelIRCClient <----- * ACCEPT ---> I2PTunnelIRCClient --> IRC server --> I2TunnelIRCClient -----> * * </pre> * * @since 0.8.9 */ public class I2PTunnelDCCServer extends I2PTunnelServer { /** key is the server's local I2P port */ private final ConcurrentHashMap<Integer, LocalAddress> _outgoing; /** key is the server's local I2P port */ private final ConcurrentHashMap<Integer, LocalAddress> _active; /** key is the server's local I2P port */ private final ConcurrentHashMap<Integer, LocalAddress> _resume; private final List<I2PSocket> _sockList; /** 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<Integer, LocalAddress>(8); _active = new ConcurrentHashMap<Integer, LocalAddress>(8); _resume = new ConcurrentHashMap<Integer, LocalAddress>(8); _sockList = new CopyOnWriteArrayList<I2PSocket>(); } /** * 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(); // Port is a one-time-use only LocalAddress local = _outgoing.remove(Integer.valueOf(myPort)); if (local == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Rejecting 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); try { Socket s = new Socket(local.ia, local.port); _sockList.add(socket); Thread t = new I2PTunnelRunner(s, socket, slock, null, null, _sockList, (I2PTunnelRunner.FailCallback) null); // run in the unlimited client pool //t.start(); _clientExecutor.execute(t); local.socket = socket; local.expire = getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE; _active.put(Integer.valueOf(myPort), local); } catch (SocketException ex) { try { socket.reset(); } catch (IOException ioe) {} _log.error("Error relaying incoming DCC connection to IRC client at " + local.ia + ':' + local.port, ex); } } catch (IOException ex) { _log.error("Error while waiting for I2PConnections", ex); } } @Override public boolean close(boolean forced) { _outgoing.clear(); _active.clear(); for (I2PSocket s : _sockList) { try { s.close(); } catch (IOException ioe) {} } _sockList.clear(); return super.close(forced); } /** * 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) { return newOutgoing(ip, port, type, 0); } /** * @param port local dcc server I2P port or 0 to pick one at random */ private int newOutgoing(byte[] ip, int port, String type, int i2pPort) { expireOutbound(); if (_outgoing.size() >= MAX_OUTGOING_PENDING || _active.size() >= MAX_OUTGOING_ACTIVE) { _log.error("Too many outgoing DCC, max is " + MAX_OUTGOING_PENDING + '/' + MAX_OUTGOING_ACTIVE + " pending/active"); return -1; } InetAddress ia; try { ia = InetAddress.getByAddress(ip); } catch (UnknownHostException uhe) { return -1; } int limit = i2pPort > 0 ? 10 : 1; LocalAddress client = new LocalAddress(ia, port, getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE); for (int i = 0; i < limit; i++) { int iport; if (i2pPort > 0) iport = i2pPort; else iport = MIN_I2P_PORT + getTunnel().getContext().random().nextInt(1 + MAX_I2P_PORT - MIN_I2P_PORT); if (_active.containsKey(Integer.valueOf(iport))) continue; 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; } /** * An incoming RESUME request * * @param port local dcc server I2P port * @return local IRC client DCC port or -1 on error */ public int resumeIncoming(int port) { Integer iport = Integer.valueOf(port); LocalAddress local = _active.remove(iport); if (local != null) { local.expire = getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE; _resume.put(Integer.valueOf(local.port), local); return local.port; } local = _outgoing.get(iport); if (local != null) { // shouldn't happen local.expire = getTunnel().getContext().clock().now() + OUTBOUND_EXPIRE; return local.port; } return -1; } /** * An outgoing ACCEPT response * * @param port local irc client DCC port * @return local DCC server i2p port or -1 on error */ public int acceptOutgoing(int port) { // do a reverse lookup for (Iterator<Map.Entry<Integer, LocalAddress>> iter = _resume.entrySet().iterator(); iter.hasNext(); ) { Map.Entry<Integer, LocalAddress> e = iter.next(); LocalAddress local = e.getValue(); if (local.port == port) { iter.remove(); return newOutgoing(local.ia.getAddress(), port, "ACCEPT", e.getKey().intValue()); } } return -1; } private void expireOutbound() { for (Iterator<LocalAddress> iter = _outgoing.values().iterator(); iter.hasNext(); ) { LocalAddress a = iter.next(); if (a.expire < getTunnel().getContext().clock().now()) iter.remove(); } for (Iterator<LocalAddress> iter = _active.values().iterator(); iter.hasNext(); ) { LocalAddress a = iter.next(); I2PSocket s = a.socket; if (s != null && s.isClosed()) iter.remove(); } } private static class LocalAddress { public final InetAddress ia; public final int port; public long expire; public I2PSocket socket; public LocalAddress(InetAddress a, int p, long exp) { ia = a; port = p; expire = exp; } } }