/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) * (c) 2003 - 2004 mihi */ package net.i2p.i2ptunnel; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InterruptedIOException; import java.net.ConnectException; import java.net.InetAddress; import java.net.NoRouteToHostException; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicLong; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.I2PClient; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocketManager; import net.i2p.client.streaming.I2PSocketManagerFactory; import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.crypto.SigType; import net.i2p.data.Destination; import net.i2p.util.EventDispatcher; import net.i2p.util.I2PAppThread; import net.i2p.util.I2PSSLSocketFactory; import net.i2p.util.Log; public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runnable { protected final Log _log; protected final I2PAppContext _context; protected final Logging l; static final long DEFAULT_CONNECT_TIMEOUT = 60 * 1000; private static final AtomicLong __clientId = new AtomicLong(); protected long _clientId; protected final Object sockLock = new Object(); // Guards sockMgr and mySockets protected I2PSocketManager sockMgr; // should be final and use a factory. LINT protected final List<I2PSocket> mySockets = new ArrayList<I2PSocket>(); protected boolean _ownDest; protected Destination dest; private volatile int localPort; private final String _handlerName; /** * Protected for I2Ping since 0.9.11. Not for use outside package. */ protected boolean listenerReady; protected ServerSocket ss; private final Object startLock = new Object(); private boolean startRunning; // private Object closeLock = new Object(); // private byte[] pubkey; private String privKeyFile; // true if we are chained from a server. private boolean chained; private volatile ThreadPoolExecutor _executor; /** this is ONLY for shared clients */ private static I2PSocketManager socketManager; /** * Only destroy and replace a static shared client socket manager if it's been connected before * @since 0.9.20 */ private enum SocketManagerState { INIT, CONNECTED } private static SocketManagerState _socketManagerState = SocketManagerState.INIT; public static final String PROP_USE_SSL = I2PTunnelServer.PROP_USE_SSL; /** * This constructor is used to add a client to an existing socket manager. * <p> * As of 0.9.21 this does NOT open the local socket. You MUST call * {@link #startRunning()} for that. The local socket will be opened * immediately (ignoring the <code>i2cp.delayOpen</code> option). * * @param localPort if 0, use any port, get actual port selected with getLocalPort() * @param sktMgr the existing socket manager */ public I2PTunnelClientBase(int localPort, Logging l, I2PSocketManager sktMgr, I2PTunnel tunnel, EventDispatcher notifyThis, long clientId ) throws IllegalArgumentException { super(localPort + " (uninitialized)", notifyThis, tunnel); chained = true; sockMgr = sktMgr; _clientId = clientId; _handlerName = "chained"; this.localPort = localPort; this.l = l; _ownDest = true; // == ! shared client _context = tunnel.getContext(); initStats(); _log = _context.logManager().getLog(getClass()); } /** * The main constructor. * <p> * As of 0.9.21 this is fast, and does NOT connect the manager to the router, * or open the local socket. You MUST call startRunning() for that. * <p> * (0.9.20 claimed to be fast, but due to a bug it DID connect the manager * to the router. It did NOT open the local socket however, so it was still * necessary to call startRunning() for that.) * * @param localPort if 0, use any port, get actual port selected with getLocalPort() * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we can't create a socketManager */ public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l, EventDispatcher notifyThis, String handlerName, I2PTunnel tunnel) throws IllegalArgumentException { this(localPort, ownDest, l, notifyThis, handlerName, tunnel, null); } /** * Use this to build a client with a persistent private key. * <p> * As of 0.9.21 this is fast, and does NOT connect the manager to the router, * or open the local socket. You MUST call startRunning() for that. * <p> * (0.9.20 claimed to be fast, but due to a bug it DID connect the manager * to the router. It did NOT open the local socket however, so it was still * necessary to call startRunning() for that.) * * @param localPort if 0, use any port, get actual port selected with getLocalPort() * @param pkf Path to the private key file, or null to generate a transient key * * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we can't create a socketManager */ public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l, EventDispatcher notifyThis, String handlerName, I2PTunnel tunnel, String pkf) throws IllegalArgumentException{ super(localPort + " (uninitialized)", notifyThis, tunnel); _clientId = __clientId.incrementAndGet(); this.localPort = localPort; this.l = l; _ownDest = ownDest; // == ! shared client _handlerName = handlerName; _context = tunnel.getContext(); initStats(); _log = _context.logManager().getLog(getClass()); // normalize path so we can find it if (pkf != null) { File keyFile = new File(pkf); if (!keyFile.isAbsolute()) keyFile = new File(_context.getConfigDir(), pkf); this.privKeyFile = keyFile.getAbsolutePath(); } // no need to load the netDb with leaseSets for destinations that will never // be looked up boolean dccEnabled = (this instanceof I2PTunnelIRCClient) && Boolean.parseBoolean(tunnel.getClientOptions().getProperty(I2PTunnelIRCClient.PROP_DCC)); if (!dccEnabled) tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true"); if (tunnel.getClientOptions().getProperty("i2p.streaming.answerPings") == null) tunnel.getClientOptions().setProperty("i2p.streaming.answerPings", "false"); } private void initStats() { //_context.statManager().createRateStat("i2ptunnel.client.closeBacklog", "How many pending sockets remain when we close one due to backlog?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); //_context.statManager().createRateStat("i2ptunnel.client.closeNoBacklog", "How many pending sockets remain when it was removed prior to backlog timeout?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); //_context.statManager().createRateStat("i2ptunnel.client.manageTime", "How long it takes to accept a socket and fire it into an i2ptunnel runner (or queue it for the pool)?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); //_context.statManager().createRateStat("i2ptunnel.client.buildRunTime", "How long it takes to run a queued socket into an i2ptunnel runner?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); } /** * Create the manager if it doesn't exist, AND connect it to the router and * build tunnels. * * Sets the this.sockMgr field if it is null, or if we want a new one. * This may take a LONG time if building a new manager. * * We need a socket manager before getDefaultOptions() and most other things * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ protected void verifySocketManager() { synchronized(sockLock) { boolean newManager = false; // other shared client could have destroyed it if (this.sockMgr == null || this.sockMgr.isDestroyed()) { newManager = true; } else { I2PSession sess = sockMgr.getSession(); if (sess.isClosed() && Boolean.parseBoolean(getTunnel().getClientOptions().getProperty("i2cp.closeOnIdle")) && Boolean.parseBoolean(getTunnel().getClientOptions().getProperty("i2cp.newDestOnResume"))) { // build a new socket manager and a new dest if the session is closed. getTunnel().removeSession(sess); if (_log.shouldLog(Log.WARN)) _log.warn(getTunnel().getClientOptions().getProperty("inbound.nickname") + ": Built a new destination on resume"); // make sure the old one is closed // if it's shared client, it will be destroyed in getSocketManager() // with the correct locking boolean shouldDestroy; synchronized(I2PTunnelClientBase.class) { shouldDestroy = sockMgr != socketManager; } if (shouldDestroy) sockMgr.destroySocketManager(); newManager = true; } // else the old socket manager will reconnect the old session if necessary } if (newManager) { if (_ownDest) this.sockMgr = buildSocketManager(); else this.sockMgr = getSocketManager(); } } connectManager(); } /** * This is ONLY for shared clients. * As of 0.9.20 this is fast, and does NOT connect the manager to the router. * Call verifySocketManager() for that. * * @return non-null * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ protected I2PSocketManager getSocketManager() { return getSocketManager(getTunnel(), this.privKeyFile); } /** * This is ONLY for shared clients. * As of 0.9.20 this is fast, and does NOT connect the manager to the router. * Call verifySocketManager() for that. * * @return non-null * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ protected static I2PSocketManager getSocketManager(I2PTunnel tunnel) { return getSocketManager(tunnel, null); } /** * This is ONLY for shared clients. * As of 0.9.20 this is fast, and does NOT connect the manager to the router. * Call verifySocketManager() for that. * * @return non-null * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ protected static synchronized I2PSocketManager getSocketManager(I2PTunnel tunnel, String pkf) { // shadows instance _log Log _log = tunnel.getContext().logManager().getLog(I2PTunnelClientBase.class); if (socketManager != null && !socketManager.isDestroyed()) { I2PSession s = socketManager.getSession(); if (s.isClosed() && _socketManagerState != SocketManagerState.INIT) { if (_log.shouldLog(Log.INFO)) _log.info(tunnel.getClientOptions().getProperty("inbound.nickname") + ": Building a new socket manager since the old one closed [s=" + s + "]"); tunnel.removeSession(s); // make sure the old one is closed socketManager.destroySocketManager(); _socketManagerState = SocketManagerState.INIT; // We could be here a LONG time, holding the lock socketManager = buildSocketManager(tunnel, pkf); // FIXME may not be the right place for this I2PSession sub = addSubsession(tunnel); if (sub != null && _log.shouldLog(Log.WARN)) _log.warn("Added subsession " + sub); } else { if (_log.shouldLog(Log.INFO)) _log.info(tunnel.getClientOptions().getProperty("inbound.nickname") + ": Not building a new socket manager since the old one is open [s=" + s + "]"); // If some other tunnel created the session, we need to add it // as our session too. // It's a Set in I2PTunnel tunnel.addSession(s); } } else { if (_log.shouldLog(Log.INFO)) _log.info(tunnel.getClientOptions().getProperty("inbound.nickname") + ": Building a new socket manager since there is no other one"); socketManager = buildSocketManager(tunnel, pkf); I2PSession sub = addSubsession(tunnel); if (sub != null && _log.shouldLog(Log.WARN)) _log.warn("Added subsession " + sub); } return socketManager; } /** * Add a DSA_SHA1 subsession to the shared client if necessary. * * @return subsession, or null if none was added * @since 0.9.20 */ protected static synchronized I2PSession addSubsession(I2PTunnel tunnel) { I2PSession sess = socketManager.getSession(); if (sess.getMyDestination().getSigType() == SigType.DSA_SHA1) return null; Properties props = new Properties(); props.putAll(tunnel.getClientOptions()); String name = props.getProperty("inbound.nickname"); if (name != null) props.setProperty("inbound.nickname", name + " (DSA)"); name = props.getProperty("outbound.nickname"); if (name != null) props.setProperty("outbound.nickname", name + " (DSA)"); props.setProperty(I2PClient.PROP_SIGTYPE, "DSA_SHA1"); try { return socketManager.addSubsession(null, props); } catch (I2PSessionException ise) { Log log = tunnel.getContext().logManager().getLog(I2PTunnelClientBase.class); if (log.shouldLog(Log.WARN)) log.warn("Failed to add subssession", ise); return null; } } /** * Kill the shared client, so that on restart in android * we won't latch onto the old one * * @since 0.9.18 */ protected static synchronized void killSharedClient() { socketManager = null; } /** * For NON-SHARED clients (ownDest = true). * * As of 0.9.20 this is fast, and does NOT connect the manager to the router. * Call verifySocketManager() for that. * * @return non-null * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ protected I2PSocketManager buildSocketManager() { return buildSocketManager(getTunnel(), this.privKeyFile, this.l); } /** * As of 0.9.20 this is fast, and does NOT connect the manager to the router. * Call verifySocketManager() for that. * * @return non-null * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel) { return buildSocketManager(tunnel, null); } private static final int RETRY_DELAY = 20*1000; private static final int MAX_RETRIES = 4; /** * As of 0.9.20 this is fast, and does NOT connect the manager to the router. * Call verifySocketManager() for that. * * @param pkf absolute path or null * @return non-null * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel, String pkf) { return buildSocketManager(tunnel, pkf, null); } /** * As of 0.9.20 this is fast, and does NOT connect the manager to the router. * Call verifySocketManager() for that. * * @param pkf absolute path or null * @return non-null * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel, String pkf, Logging log) { // shadows instance _log Log _log = tunnel.getContext().logManager().getLog(I2PTunnelClientBase.class); Properties props = new Properties(); props.putAll(tunnel.getClientOptions()); int portNum = 7654; if (tunnel.port != null) { try { portNum = Integer.parseInt(tunnel.port); } catch (NumberFormatException nfe) { throw new IllegalArgumentException("Invalid port specified [" + tunnel.port + "]", nfe); } } I2PSocketManager sockManager = null; FileInputStream fis = null; try { if (pkf != null) { // Persistent client dest fis = new FileInputStream(pkf); sockManager = I2PSocketManagerFactory.createDisconnectedManager(fis, tunnel.host, portNum, props); } else { sockManager = I2PSocketManagerFactory.createDisconnectedManager(null, tunnel.host, portNum, props); } } catch (I2PSessionException ise) { throw new IllegalArgumentException("Can't create socket manager", ise); } catch (IOException ioe) { if (log != null) log.log("Error opening key file " + ioe); _log.error("Error opening key file", ioe); throw new IllegalArgumentException("Error opening key file", ioe); } finally { if (fis != null) try { fis.close(); } catch (IOException ioe) {} } sockManager.setName("Client"); if (_log.shouldLog(Log.INFO)) _log.info(tunnel.getClientOptions().getProperty("inbound.nickname") + ": Built a new socket manager [s=" + sockManager.getSession() + "]"); tunnel.addSession(sockManager.getSession()); return sockManager; } /** * Warning, blocks while connecting to router and building tunnels; * This may take a LONG time. * * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager * @since 0.9.20 */ private void connectManager() { int retries = 0; while (sockMgr.getSession().isClosed()) { try { sockMgr.getSession().connect(); synchronized(I2PTunnelClientBase.class) { if (sockMgr == socketManager) _socketManagerState = SocketManagerState.CONNECTED; } } catch (I2PSessionException ise) { // shadows instance _log Log _log = getTunnel().getContext().logManager().getLog(I2PTunnelClientBase.class); Logging log = this.l; // try to make this error sensible as it will happen... String portNum = getTunnel().port; if (portNum == null) portNum = "7654"; String msg; if (getTunnel().getContext().isRouterContext()) msg = "Unable to build tunnels for the client"; else msg = "Unable to connect to the router at " + getTunnel().host + ':' + portNum + " and build tunnels for the client"; if (++retries < MAX_RETRIES) { if (log != null) log.log(msg + ", retrying in " + (RETRY_DELAY / 1000) + " seconds"); _log.error(msg + ", retrying in " + (RETRY_DELAY / 1000) + " seconds", ise); } else { if (log != null) log.log(msg + ", giving up"); _log.log(Log.CRIT, msg + ", giving up", ise); throw new IllegalArgumentException(msg, ise); } try { Thread.sleep(RETRY_DELAY); } catch (InterruptedException ie) {} } } } public final int getLocalPort() { return localPort; } protected final 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; } } /** * Actually open the local socket and start working on incoming connections. *Must* be * called by derived classes after initialization. * * (this wasn't actually true until 0.9.20) * * This will be fast if i2cp.delayOpen is true, but could take * a LONG TIME if it is false, as it connects to the router and builds tunnels. * * Extending classes must check the value of boolean open after calling * super.startRunning(), if false then something went wrong. * */ public void startRunning() { boolean openNow = !Boolean.parseBoolean(getTunnel().getClientOptions().getProperty("i2cp.delayOpen")); if (openNow) { while (sockMgr == null) { verifySocketManager(); if (sockMgr == null) { _log.error("Unable to connect to router and build tunnels for " + _handlerName); // FIXME there is a loop in buildSocketManager(), do we really need another one here? // no matter, buildSocketManager() now throws an IllegalArgumentException try { Thread.sleep(10*1000); } catch (InterruptedException ie) {} } else { l.log("Tunnels ready for client: " + _handlerName); } } // can't be null unless we limit the loop above //if (sockMgr == null) { // l.log("Invalid I2CP configuration"); // throw new IllegalArgumentException("Socket manager could not be created"); //} } // else delay creating session until createI2PSocket() is called startup(); } private void startup() { if (_log.shouldLog(Log.DEBUG)) _log.debug("startup " + _clientId, new Exception("I did it")); // prevent JVM exit when running outside the router boolean isDaemon = getTunnel().getContext().isRouterContext(); open = true; Thread t = new I2PAppThread(this, "I2PTunnel Client " + getTunnel().listenHost + ':' + localPort, isDaemon); t.start(); synchronized (this) { while (!listenerReady && open) { try { wait(); } catch (InterruptedException e) { // ignore } } } if (open && listenerReady) { boolean openNow = !Boolean.parseBoolean(getTunnel().getClientOptions().getProperty("i2cp.delayOpen")); if (openNow || chained) l.log("Client ready, listening on " + getTunnel().listenHost + ':' + localPort); else l.log("Client ready, listening on " + getTunnel().listenHost + ':' + localPort + ", delaying tunnel open until required"); notifyEvent("openBaseClientResult", "ok"); } else { l.log("Client error for " + getTunnel().listenHost + ':' + localPort + ", check logs"); notifyEvent("openBaseClientResult", "error"); } synchronized (startLock) { startRunning = true; startLock.notify(); } } /** * Create the default options (using the default timeout, etc). * Warning, this does not make a copy of I2PTunnel's client options, * it modifies them directly. */ protected I2PSocketOptions getDefaultOptions() { Properties defaultOpts = getTunnel().getClientOptions(); I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts); if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT)) opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); return opts; } /** * Create the default options (using the default timeout, etc). * Warning, this does not make a copy of I2PTunnel's client options, * it modifies them directly. * Do not use overrides for per-socket options. */ protected I2PSocketOptions getDefaultOptions(Properties overrides) { Properties defaultOpts = getTunnel().getClientOptions(); defaultOpts.putAll(overrides); I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts); if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT)) opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); return opts; } /** * Update the I2PSocketManager. * * @since 0.9.1 */ @Override public void optionsUpdated(I2PTunnel tunnel) { if (getTunnel() != tunnel) return; I2PSocketManager sm = _ownDest ? sockMgr : socketManager; if (sm == null) return; Properties props = tunnel.getClientOptions(); sm.setDefaultOptions(sm.buildOptions(props)); } /** * Create a new I2PSocket towards to the specified destination, * adding it to the list of connections actually managed by this * tunnel. * * @param dest The destination to connect to, non-null * @return a new I2PSocket */ public I2PSocket createI2PSocket(Destination dest) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException { return createI2PSocket(dest, 0); } /** * Create a new I2PSocket towards to the specified destination, * adding it to the list of connections actually managed by this * tunnel. * * @param dest The destination to connect to, non-null * @param port The destination port to connect to 0 - 65535 * @return a new I2PSocket * @since 0.9.9 */ public I2PSocket createI2PSocket(Destination dest, int port) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException { verifySocketManager(); I2PSocketOptions opts = getDefaultOptions(); opts.setPort(port); return createI2PSocket(dest, opts); } /** * Create a new I2PSocket towards to the specified destination, * adding it to the list of connections actually managed by this * tunnel. * * @param dest The destination to connect to, non-null * @param opt Option to be used to open when opening the socket * @return a new I2PSocket * * @throws ConnectException if the peer refuses the connection * @throws NoRouteToHostException if the peer is not found or not reachable * @throws InterruptedIOException if the connection timeouts * @throws I2PException if there is some other I2P-related problem */ public I2PSocket createI2PSocket(Destination dest, I2PSocketOptions opt) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException { if (dest == null) throw new NullPointerException(); I2PSocket i2ps; verifySocketManager(); i2ps = sockMgr.connect(dest, opt); synchronized (sockLock) { mySockets.add(i2ps); } return i2ps; } /** * Non-final since 0.9.11. * open will be true before being called. * Any overrides must set listenerReady = true and then notifyAll() if setup is successful, * and must call close() and then notifyAll() on failure or termination. */ public void run() { InetAddress addr = getListenHost(l); if (addr == null) { close(true); open = false; synchronized (this) { notifyAll(); } return; } try { Properties opts = getTunnel().getClientOptions(); boolean useSSL = Boolean.parseBoolean(opts.getProperty(PROP_USE_SSL)); if (useSSL) { // was already done in web/IndexBean.java when saving the config boolean wasCreated = SSLClientUtil.verifyKeyStore(opts); if (wasCreated) { // From here, we can't save the config. // We shouldn't get here, as SSL isn't the default, so it would // be enabled via the GUI only. // If it was done manually, the keys will be regenerated at every startup, // which is bad. _log.logAlways(Log.WARN, "Created new i2ptunnel SSL keys but can't save the config, disable and enable via i2ptunnel GUI"); } SSLServerSocketFactory fact = SSLClientUtil.initializeFactory(opts); ss = fact.createServerSocket(localPort, 0, addr); I2PSSLSocketFactory.setProtocolsAndCiphers((SSLServerSocket) ss); } else { ss = new ServerSocket(localPort, 0, addr); } // If a free port was requested, find out what we got if (localPort == 0) { localPort = ss.getLocalPort(); } notifyEvent("clientLocalPort", Integer.valueOf(ss.getLocalPort())); // duplicates message in constructor //l.log("Listening for clients on port " + localPort + " of " + getTunnel().listenHost); // Notify constructor that port is ready synchronized (this) { listenerReady = true; notifyAll(); } // Wait until we are authorized to process data synchronized (startLock) { while (!startRunning) { try { startLock.wait(); } catch (InterruptedException ie) { } } } TunnelControllerGroup tcg = TunnelControllerGroup.getInstance(); if (tcg != null) { _executor = tcg.getClientExecutor(); } else { // Fallback in case TCG.getInstance() is null, never instantiated // and we were not started by TCG. // Maybe a plugin loaded before TCG? Should be rare. // Never shut down. _executor = new TunnelControllerGroup.CustomThreadPoolExecutor(); } while (open) { Socket s = ss.accept(); manageConnection(s); } } catch (IOException ex) { synchronized (sockLock) { mySockets.clear(); } if (open) { _log.error("Error listening for connections on " + addr + " port " + localPort, ex); l.log("Error listening for connections on " + addr + " port " + localPort + ": " + ex); notifyEvent("openBaseClientResult", "error"); close(true); } synchronized (this) { notifyAll(); } } } /** * Manage the connection just opened on the specified socket * * @param s Socket to take care of */ protected void manageConnection(Socket s) { if (s == null) return; ThreadPoolExecutor tpe = _executor; if (tpe == null) { _log.error("No executor for socket!"); try { s.close(); } catch (IOException ioe) {} return; } try { tpe.execute(new BlockingRunner(s)); } catch (RejectedExecutionException ree) { // should never happen, we have an unbounded pool and never stop the executor try { s.close(); } catch (IOException ioe) {} } } /** * Blocking runner, used during the connection establishment */ private class BlockingRunner implements Runnable { private final Socket _s; public BlockingRunner(Socket s) { _s = s; } public void run() { try { clientConnectionRun(_s); } catch (Throwable t) { // probably an IllegalArgumentException from // connecting to the router in a delay-open or // close-on-idle tunnel (in connectManager() above) _log.error("Uncaught error in i2ptunnel client", t); } } } /** * Note that the tunnel can be reopened after this by calling startRunning(). * This may not release all resources. In particular, the I2PSocketManager remains * and it may have timer threads that continue running. * * To release all resources permanently, call destroy(). * * Does nothing if open is already false. * Sets open = false but does not notifyAll(). * * @return success */ public boolean close(boolean forced) { if (_log.shouldLog(Log.INFO)) _log.info("close() called: forced = " + forced + " open = " + open + " sockMgr = " + sockMgr); if (!open) return true; // FIXME: here we might have to wait quite a long time if // there is a connection attempt atm. But without waiting we // might risk to create an orphan socket. Would be better // to return with an error in that situation quickly. synchronized (sockLock) { if (sockMgr != null) { mySockets.retainAll(sockMgr.listSockets()); if ((!forced) && (!mySockets.isEmpty())) { l.log("Not closing, there are still active connections!"); _log.debug("can't close: there are still active connections!"); for (I2PSocket s : mySockets) { l.log(" -> " + s.toString()); } return false; } if (!chained) { I2PSession session = sockMgr.getSession(); getTunnel().removeSession(session); if (_ownDest) { try { session.destroySession(); } catch (I2PException ex) {} } // TCG will try to destroy it too } // else the app chaining to this one closes it! } l.log("Stopping client " + toString()); open = false; try { if (ss != null) ss.close(); } catch (IOException ex) { if (_log.shouldDebug()) _log.debug("error closing", ex); return false; } //l.log("Client closed."); } return true; } /** * Note that the tunnel cannot be reopened after this by calling startRunning(), * as it will destroy the underlying socket manager. * This releases all resources if not a shared client. * For shared client, the router will kill all the remaining streaming timers at shutdown. * * @since 0.9.17 */ @Override public synchronized boolean destroy() { close(true); if (_ownDest) sockMgr.destroySocketManager(); return true; } public static void closeSocket(Socket s) { try { s.close(); } catch (IOException ex) { //_log.error("Could not close socket", ex); } } /** * Manage a connection in a separate thread. This only works if * you do not override manageConnection(). * * This is run in a thread from an unlimited-size thread pool, * so it may block or run indefinitely. */ protected abstract void clientConnectionRun(Socket s); }