diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java index d53ddde5d435c75c62354f2384c977f7ca38289b..034f74ae71e86bccb7d1ae44cb3ed4cb52b60588 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java @@ -428,7 +428,7 @@ public class PeerCoordinator implements PeerListener peer.runConnection(_util, listener, bitfield); } }; - String threadName = peer.toString(); + String threadName = "Snark peer " + peer.toString(); new I2PAppThread(r, threadName).start(); return true; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index e3cc8ae9f00ebd1c63b759497160d72ab258496b..a1a3f4cc21a2e5c0f11952594e85e728493a96c0 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -72,8 +72,10 @@ public class TrackerClient extends I2PAppThread public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator) { + super(); // Set unique name. - super("TrackerClient-" + urlencode(coordinator.getID())); + String id = urlencode(coordinator.getID()); + setName("TrackerClient " + id.substring(id.length() - 12)); _util = util; this.meta = meta; this.coordinator = coordinator; diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java index 890a4f1414b510402fb6f791492435f6b94b9517..7fc808643b078992e2b2fb65e70d6eb610ef9fca 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java @@ -16,6 +16,7 @@ import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.util.zip.GZIPInputStream; +import java.util.concurrent.RejectedExecutionException; import net.i2p.I2PAppContext; import net.i2p.data.ByteArray; @@ -228,7 +229,15 @@ class HTTPResponseOutputStream extends FilterOutputStream { //out.flush(); PipedInputStream pi = new PipedInputStream(); PipedOutputStream po = new PipedOutputStream(pi); - new I2PAppThread(new Pusher(pi, out), "HTTP decompressor").start(); + // Run in the client thread pool, as there should be an unused thread + // there after the accept(). + // Overridden in I2PTunnelHTTPServer, where it does not use the client pool. + try { + I2PTunnelClientBase._executor.execute(new Pusher(pi, out)); + } catch (RejectedExecutionException ree) { + // shouldn't happen + throw ree; + } out = po; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java index 053fc61ceab5227bb5d7570f1a2a1e6d93f4a70e..bfde2fb32432864bcade361925f7ff33b647c3f4 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java @@ -16,8 +16,6 @@ import net.i2p.util.Log; public class I2PTunnelClient extends I2PTunnelClientBase { - private static final Log _log = new Log(I2PTunnelClient.class); - /** list of Destination objects that we point at */ protected List<Destination> dests; private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1 diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java index e48bae1a9a9ab1758290307d48b71bec6194bed7..a98551b8c5ba2643536acd48e1436a2b569d894c 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java @@ -17,6 +17,13 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Properties; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadFactory; import net.i2p.I2PAppContext; import net.i2p.I2PException; @@ -34,9 +41,9 @@ import net.i2p.util.SimpleTimer; public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runnable { - private static final Log _log = new Log(I2PTunnelClientBase.class); - protected I2PAppContext _context; - protected Logging l; + protected final Log _log; + protected final I2PAppContext _context; + protected final Logging l; static final long DEFAULT_CONNECT_TIMEOUT = 60 * 1000; @@ -64,35 +71,24 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna private String handlerName; private String privKeyFile; - // private Object conLock = new Object(); - - /** List of Socket for those accept()ed but not yet started up */ - protected final List _waitingSockets = new ArrayList(4); // FIXME should be final and use a factory. FIXME - /** How many connections will we allow to be in the process of being built at once? */ - private int _numConnectionBuilders; - /** How long will we allow sockets to sit in the _waitingSockets map before killing them? */ - private int _maxWaitTime; - - /** - * How many concurrent connections this I2PTunnel instance will allow to be - * in the process of connecting (or if less than 1, there is no limit)? - */ - public static final String PROP_NUM_CONNECTION_BUILDERS = "i2ptunnel.numConnectionBuilders"; - /** - * How long will we let a socket wait after being accept()ed without getting - * pumped through a connection builder (in milliseconds). If this time is - * reached, the socket is unceremoniously closed and discarded. If the max - * wait time is less than 1, there is no limit. - * - */ - public static final String PROP_MAX_WAIT_TIME = "i2ptunnel.maxWaitTime"; - - private static final int DEFAULT_NUM_CONNECTION_BUILDERS = 5; - private static final int DEFAULT_MAX_WAIT_TIME = 30*1000; - // true if we are chained from a server. private boolean chained = false; + /** how long to wait before dropping an idle thread */ + private static final long HANDLER_KEEPALIVE_MS = 2*60*1000; + + /** + * We keep a static pool of socket handlers for all clients, + * as there is no need for isolation on the client side. + * Extending classes may use it for other purposes. + * Not for use by servers, as there is no limit on threads. + */ + static final Executor _executor; + private static int _executorThreadCount; + static { + _executor = new CustomThreadPoolExecutor(); + } + public I2PTunnelClientBase(int localPort, Logging l, I2PSocketManager sktMgr, I2PTunnel tunnel, EventDispatcher notifyThis, long clientId ) throws IllegalArgumentException { @@ -109,9 +105,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna _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 }); + _log = _context.logManager().getLog(getClass()); - Thread t = new I2PAppThread(this); - t.setName("Client " + _clientId); + Thread t = new I2PAppThread(this, "Client " + tunnel.listenHost + ':' + localPort); listenerReady = false; t.start(); open = true; @@ -125,8 +121,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna } } - configurePool(tunnel); - if (open && listenerReady) { l.log("Client ready, listening on " + tunnel.listenHost + ':' + localPort); notifyEvent("openBaseClientResult", "ok"); @@ -135,6 +129,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna notifyEvent("openBaseClientResult", "error"); } } + public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l, EventDispatcher notifyThis, String handlerName, I2PTunnel tunnel) throws IllegalArgumentException { @@ -163,6 +158,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna _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 }); + _log = _context.logManager().getLog(getClass()); // normalize path so we can find it if (pkf != null) { @@ -210,8 +206,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna } } - configurePool(tunnel); - if (open && listenerReady) { if (openNow) l.log("Client ready, listening on " + tunnel.listenHost + ':' + localPort); @@ -224,37 +218,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna } } - /** - * build and configure the pool handling accept()ed but not yet - * established connections - * - */ - private void configurePool(I2PTunnel tunnel) { - //_waitingSockets = new ArrayList(4); - - Properties opts = tunnel.getClientOptions(); - String maxWait = opts.getProperty(PROP_MAX_WAIT_TIME, DEFAULT_MAX_WAIT_TIME+""); - try { - _maxWaitTime = Integer.parseInt(maxWait); - } catch (NumberFormatException nfe) { - _maxWaitTime = DEFAULT_MAX_WAIT_TIME; - } - - String numBuild = opts.getProperty(PROP_NUM_CONNECTION_BUILDERS, DEFAULT_NUM_CONNECTION_BUILDERS+""); - try { - _numConnectionBuilders = Integer.parseInt(numBuild); - } catch (NumberFormatException nfe) { - _numConnectionBuilders = DEFAULT_NUM_CONNECTION_BUILDERS; - } - - for (int i = 0; i < _numConnectionBuilders; i++) { - String name = "ClientBuilder" + _clientId + '.' + i; - I2PAppThread b = new I2PAppThread(new TunnelConnectionBuilder(), name); - b.setDaemon(true); - b.start(); - } - } - /** * Sets the this.sockMgr field if it is null, or if we want a new one * @@ -321,6 +284,8 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna * 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) { I2PSession s = socketManager.getSession(); if ( (s == null) || (s.isClosed()) ) { @@ -378,6 +343,8 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna * 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; @@ -537,7 +504,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna synchronized (this) { notifyAll(); } - synchronized (_waitingSockets) { _waitingSockets.notifyAll(); } return; } ss = new ServerSocket(localPort, 0, addr); @@ -566,12 +532,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna } } - while (true) { + while (open) { Socket s = ss.accept(); - long before = System.currentTimeMillis(); manageConnection(s); - long total = System.currentTimeMillis() - before; - _context.statManager().addRateData("i2ptunnel.client.manageTime", total, total); } } catch (IOException ex) { if (open) { @@ -586,9 +549,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna notifyAll(); } } - synchronized (_waitingSockets) { - _waitingSockets.notifyAll(); - } } /** @@ -598,24 +558,38 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna */ protected void manageConnection(Socket s) { if (s == null) return; - if (_numConnectionBuilders <= 0) { - new I2PAppThread(new BlockingRunner(s), "Clinet run").start(); - return; + try { + _executor.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) {} } - - if (_maxWaitTime > 0) - SimpleScheduler.getInstance().addEvent(new CloseEvent(s), _maxWaitTime); + } + + /** + * Not really needed for now but in case we want to add some hooks like afterExecute(). + */ + private static class CustomThreadPoolExecutor extends ThreadPoolExecutor { + public CustomThreadPoolExecutor() { + super(0, Integer.MAX_VALUE, HANDLER_KEEPALIVE_MS, TimeUnit.MILLISECONDS, + new SynchronousQueue(), new CustomThreadFactory()); + } + } - synchronized (_waitingSockets) { - _waitingSockets.add(s); - _waitingSockets.notifyAll(); + /** just to set the name and set Daemon */ + private static class CustomThreadFactory implements ThreadFactory { + public Thread newThread(Runnable r) { + Thread rv = Executors.defaultThreadFactory().newThread(r); + rv.setName("I2PTunnel Client Runner " + (++_executorThreadCount)); + rv.setDaemon(true); + return rv; } } /** - * Blocking runner, used during the connection establishment whenever we - * are not using the queued builders. - * + * Blocking runner, used during the connection establishment */ private class BlockingRunner implements Runnable { private Socket _s; @@ -625,32 +599,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna } } - /** - * Remove and close the socket from the waiting list, if it is still there. - * - */ - private class CloseEvent implements SimpleTimer.TimedEvent { - private Socket _s; - public CloseEvent(Socket s) { _s = s; } - public void timeReached() { - int remaining = 0; - boolean stillWaiting = false; - synchronized (_waitingSockets) { - stillWaiting = _waitingSockets.remove(_s); - remaining = _waitingSockets.size(); - } - if (stillWaiting) { - try { _s.close(); } catch (IOException ioe) {} - if (_log.shouldLog(Log.INFO)) { - _context.statManager().addRateData("i2ptunnel.client.closeBacklog", remaining, 0); - _log.info("Closed a waiting socket because of backlog"); - } - } else { - _context.statManager().addRateData("i2ptunnel.client.closeNoBacklog", remaining, 0); - } - } - } - public boolean close(boolean forced) { if (_log.shouldLog(Log.INFO)) _log.info("close() called: forced = " + forced + " open = " + open + " sockMgr = " + sockMgr); @@ -688,7 +636,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna //l.log("Client closed."); } - synchronized (_waitingSockets) { _waitingSockets.notifyAll(); } return true; } @@ -696,40 +643,10 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna try { s.close(); } catch (IOException ex) { - _log.error("Could not close socket", ex); + //_log.error("Could not close socket", ex); } } - /** - * Pool runner pulling sockets off the waiting list and pushing them - * through clientConnectionRun. This dies when the I2PTunnel instance - * is closed. - * - */ - private class TunnelConnectionBuilder implements Runnable { - public void run() { - Socket s = null; - while (open) { - try { - synchronized (_waitingSockets) { - if (_waitingSockets.isEmpty()) - _waitingSockets.wait(); - else - s = (Socket)_waitingSockets.remove(0); - } - } catch (InterruptedException ie) {} - - if (s != null) { - long before = System.currentTimeMillis(); - clientConnectionRun(s); - long total = System.currentTimeMillis() - before; - _context.statManager().addRateData("i2ptunnel.client.buildRunTime", total, 0); - } - s = null; - } - } - } - /** * Manage a connection in a separate thread. This only works if * you do not override manageConnection() diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java index e151d733d7d8b55d75bb9e9b8b81fd8b4845ea49..70265a154d2a14fd94169b929921b1a427f00c29 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java @@ -58,7 +58,6 @@ import net.i2p.util.Log; * @author zzz a stripped-down I2PTunnelHTTPClient */ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements Runnable { - private static final Log _log = new Log(I2PTunnelConnectClient.class); private final static byte[] ERR_DESTINATION_UNKNOWN = ("HTTP/1.1 503 Service Unavailable\r\n"+ @@ -340,8 +339,8 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R _requestId = id; } public void run() { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Timeout occured requesting " + _target); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("Timeout occured requesting " + _target); handleConnectClientException(new RuntimeException("Timeout"), _out, _target, _usingProxy, _wwwProxy, _requestId); closeSocket(_socket); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPBidirServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPBidirServer.java index 54b2046cfb14259208453908574b6c25e423249b..ff1f3766188acdfca53cc383cb56bacf0311070f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPBidirServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPBidirServer.java @@ -11,7 +11,6 @@ import net.i2p.util.EventDispatcher; import net.i2p.util.Log; public class I2PTunnelHTTPBidirServer extends I2PTunnelHTTPServer { - private final static Log log = new Log(I2PTunnelHTTPBidirServer.class); public I2PTunnelHTTPBidirServer(InetAddress host, int port, int proxyport, String privData, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super(host, port, privData, spoofHost, l, notifyThis, tunnel); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index 8288e702fda131e109d611b363cf20fe5bf3cc2a..a9b1a82878597bcbfe4121d210768ccef001e2c3 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -61,7 +61,6 @@ import net.i2p.util.Translate; * */ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runnable { - private static final Log _log = new Log(I2PTunnelHTTPClient.class); private HashMap addressHelpers = new HashMap(); @@ -894,15 +893,15 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn _requestId = id; } public void run() { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Timeout occured requesting " + _target); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("Timeout occured requesting " + _target); handleHTTPClientException(new RuntimeException("Timeout"), _out, _target, _usingProxy, _wwwProxy, _requestId); closeSocket(_socket); } } - private static String DEFAULT_JUMP_SERVERS = + public static final String DEFAULT_JUMP_SERVERS = "http://i2host.i2p/cgi-bin/i2hostjump?," + "http://stats.i2p/cgi-bin/jump.cgi?a=," + "http://i2jump.i2p/"; @@ -940,8 +939,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn // Skip jump servers we don't know String jumphost = jurl.substring(7); // "http://" jumphost = jumphost.substring(0, jumphost.indexOf('/')); - Destination dest = I2PAppContext.getGlobalContext().namingService().lookup(jumphost); - if (dest == null) continue; + if (!jumphost.endsWith(".i2p")) + continue; + if (!jumphost.endsWith(".b32.i2p")) { + Destination dest = I2PAppContext.getGlobalContext().namingService().lookup(jumphost); + if (dest == null) continue; + } out.write("<br><a href=\"".getBytes()); out.write(jurl.getBytes()); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java index 131a02dbcadea22ce8716f821dce8d8d417973e1..0f1b35c3295d27805cf4dae23035e45f942501f6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java @@ -25,7 +25,7 @@ import net.i2p.util.Log; * @since 0.8.2 */ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implements Runnable { - private static final Log _log = new Log(I2PTunnelHTTPClientBase.class); + protected final List<String> _proxyList; protected final static byte[] ERR_NO_OUTPROXY = diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index 921d8992bf731d3cba65673e0b0d7f1fc3822e97..d5af6bfd05bd3efd3429d57e05d46e6c6bf357c7 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -31,7 +31,7 @@ import net.i2p.data.Base32; * */ public class I2PTunnelHTTPServer extends I2PTunnelServer { - private final static Log _log = new Log(I2PTunnelHTTPServer.class); + /** what Host: should we seem to be to the webserver? */ private String _spoofHost; private static final String HASH_HEADER = "X-I2P-DestHash"; @@ -40,6 +40,20 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { private static final String[] CLIENT_SKIPHEADERS = {HASH_HEADER, DEST64_HEADER, DEST32_HEADER}; private static final String SERVER_HEADER = "Server"; private static final String[] SERVER_SKIPHEADERS = {SERVER_HEADER}; + private static final long HEADER_TIMEOUT = 60*1000; + + private final static byte[] ERR_UNAVAILABLE = + ("HTTP/1.1 503 Service Unavailable\r\n"+ + "Content-Type: text/html; charset=iso-8859-1\r\n"+ + "Cache-control: no-cache\r\n"+ + "Connection: close\r\n"+ + "Proxy-Connection: close\r\n"+ + "\r\n"+ + "<html><head><title>503 Service Unavailable<title></head>\n"+ + "<body><h2>503 Service Unavailable</h2>\n" + + "<p>This I2P eepsite is unavailable. It may be down or undergoing maintenance.</p>\n" + + "</body></html>") + .getBytes(); public I2PTunnelHTTPServer(InetAddress host, int port, String privData, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super(host, port, privData, l, notifyThis, tunnel); @@ -73,8 +87,9 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { //local is fast, so synchronously. Does not need that many //threads. try { - // give them 5 seconds to send in the HTTP request - socket.setReadTimeout(5*1000); + // The headers _should_ be in the first packet, but + // may not be, depending on the client-side options + socket.setReadTimeout(HEADER_TIMEOUT); InputStream in = socket.getInputStream(); @@ -130,13 +145,24 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { } else { new I2PTunnelRunner(s, socket, slock, null, modifiedHeader.getBytes(), null); } + + long afterHandle = getTunnel().getContext().clock().now(); + long timeToHandle = afterHandle - afterAccept; + getTunnel().getContext().statManager().addRateData("i2ptunnel.httpserver.blockingHandleTime", timeToHandle, 0); + if ( (timeToHandle > 1000) && (_log.shouldLog(Log.WARN)) ) + _log.warn("Took a while to handle the request for " + remoteHost + ':' + remotePort + + " [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]"); } catch (SocketException ex) { + try { + // Send a 503, so the user doesn't get an HTTP Proxy error message + // and blame his router or the network. + socket.getOutputStream().write(ERR_UNAVAILABLE); + } catch (IOException ioe) {} try { socket.close(); - } catch (IOException ioe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error while closing the received i2p con", ex); - } + } catch (IOException ioe) {} + if (_log.shouldLog(Log.ERROR)) + _log.error("Error connecting to HTTP server " + remoteHost + ':' + remotePort, ex); } catch (IOException ex) { try { socket.close(); @@ -150,12 +176,6 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { if (_log.shouldLog(Log.ERROR)) _log.error("OOM in HTTP server", oom); } - - long afterHandle = getTunnel().getContext().clock().now(); - long timeToHandle = afterHandle - afterAccept; - getTunnel().getContext().statManager().addRateData("i2ptunnel.httpserver.blockingHandleTime", timeToHandle, 0); - if ( (timeToHandle > 1000) && (_log.shouldLog(Log.WARN)) ) - _log.warn("Took a while to handle the request [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]"); } private static class CompressedRequestor implements Runnable { @@ -169,6 +189,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { _headers = headers; _ctx = ctx; } + public void run() { if (_log.shouldLog(Log.INFO)) _log.info("Compressed requestor running"); @@ -183,7 +204,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { _log.info("request headers: " + _headers); serverout.write(_headers.getBytes()); browserin = _browser.getInputStream(); - I2PAppThread sender = new I2PAppThread(new Sender(serverout, browserin, "server: browser to server"), Thread.currentThread().getName() + "hcs"); + I2PAppThread sender = new I2PAppThread(new Sender(serverout, browserin, "server: browser to server", _log), Thread.currentThread().getName() + "hcs"); sender.start(); browserout = _browser.getOutputStream(); @@ -233,14 +254,19 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { } private static class Sender implements Runnable { - private OutputStream _out; - private InputStream _in; - private String _name; - public Sender(OutputStream out, InputStream in, String name) { + private final OutputStream _out; + private final InputStream _in; + private final String _name; + // shadows _log in super() + private final Log _log; + + public Sender(OutputStream out, InputStream in, String name, Log log) { _out = out; _in = in; _name = name; + _log = log; } + public void run() { if (_log.shouldLog(Log.INFO)) _log.info(_name + ": Begin sending"); @@ -277,16 +303,16 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { protected boolean shouldCompress() { return true; } @Override protected void finishHeaders() throws IOException { - if (_log.shouldLog(Log.INFO)) - _log.info("Including x-i2p-gzip as the content encoding in the response"); + //if (_log.shouldLog(Log.INFO)) + // _log.info("Including x-i2p-gzip as the content encoding in the response"); out.write("Content-encoding: x-i2p-gzip\r\n".getBytes()); super.finishHeaders(); } @Override protected void beginProcessing() throws IOException { - if (_log.shouldLog(Log.INFO)) - _log.info("Beginning compression processing"); + //if (_log.shouldLog(Log.INFO)) + // _log.info("Beginning compression processing"); //out.flush(); _gzipOut = new InternalGZIPOutputStream(out); out = _gzipOut; diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java index 850a1fedc4650a2463e2b84f9ff0c6b4ddaa60c5..d5b3dda65cd4b1b1aec0521cf74b99d60f51550f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java @@ -20,8 +20,6 @@ import net.i2p.util.Log; */ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable { - private static final Log _log = new Log(I2PTunnelIRCClient.class); - /** used to assign unique IDs to the threads / clients. no logic or functionality */ private static volatile long __clientId = 0; @@ -130,6 +128,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable private Socket local; private I2PSocket remote; private StringBuffer expectedPong; + // shadows _log in super() + private final Log _log = new Log(I2PTunnelIRCClient.class); public IrcInboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong) { local=_local; @@ -207,6 +207,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable private Socket local; private I2PSocket remote; private StringBuffer expectedPong; + // shadows _log in super() + private final Log _log = new Log(I2PTunnelIRCClient.class); public IrcOutboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong) { local=_local; @@ -308,7 +310,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable try { command = field[idx++]; } catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command? { - _log.warn("Dropping defective message: index out of bounds while extracting command."); + //_log.warn("Dropping defective message: index out of bounds while extracting command."); return null; } @@ -431,13 +433,13 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable 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 + "\")"); + //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 + "]"); + //if (_log.shouldLog(Log.WARN)) + // _log.warn("sending ping [" + rv + "], waiting for [" + expectedPong + "] orig was [" + s + "]"); return rv; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java index 1d05103e79ea3fa29e24923a4d86ca86d8444786..4537389bd2ae3443dc922966474b17c41dc0e68a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java @@ -61,9 +61,7 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { public static final String PROP_WEBIRC_SPOOF_IP_DEFAULT="127.0.0.1"; public static final String PROP_HOSTNAME="ircserver.fakeHostname"; public static final String PROP_HOSTNAME_DEFAULT="%f.b32.i2p"; - - private static final Log _log = new Log(I2PTunnelIRCServer.class); - + private static final long HEADER_TIMEOUT = 60*1000; /** * @throws IllegalArgumentException if the I2PTunnel does not contain @@ -108,8 +106,9 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { try { String modifiedRegistration; if(!this.method.equals("webirc")) { - // give them 15 seconds to send in the request - socket.setReadTimeout(15*1000); + // The headers _should_ be in the first packet, but + // may not be, depending on the client-side options + socket.setReadTimeout(HEADER_TIMEOUT); InputStream in = socket.getInputStream(); modifiedRegistration = filterRegistration(in, cloakDest(socket.getPeerDestination())); socket.setReadTimeout(readTimeout); @@ -126,12 +125,12 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { Socket s = new Socket(remoteHost, remotePort); new I2PTunnelRunner(s, socket, slock, null, modifiedRegistration.getBytes(), null); } catch (SocketException ex) { + // TODO send the equivalent of a 503? try { socket.close(); - } catch (IOException ioe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error while closing the received i2p con", ex); - } + } catch (IOException ioe) {} + if (_log.shouldLog(Log.ERROR)) + _log.error("Error connecting to IRC server " + remoteHost + ':' + remotePort, ex); } catch (IOException ex) { try { socket.close(); @@ -181,8 +180,8 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { if (++lineCount > 10) throw new IOException("Too many lines before USER or SERVER, giving up"); s = s.trim(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Got line: " + s); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("Got line: " + s); String field[]=s.split(" ",5); String command; @@ -214,8 +213,8 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { if ("SERVER".equalsIgnoreCase(command)) break; } - if (_log.shouldLog(Log.DEBUG)) - _log.debug("All done, sending: " + buf.toString()); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("All done, sending: " + buf.toString()); return buf.toString(); } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java index fc39e19964cbbd7c2f93b69695c824fd0e3371f0..5427130bc781366df814519bc292a9a3338a0ac6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java @@ -16,6 +16,12 @@ import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.Iterator; import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadFactory; import net.i2p.I2PAppContext; import net.i2p.I2PException; @@ -30,8 +36,7 @@ import net.i2p.util.Log; public class I2PTunnelServer extends I2PTunnelTask implements Runnable { - private final static Log _log = new Log(I2PTunnelServer.class); - + protected final Log _log; protected I2PSocketManager sockMgr; protected I2PServerSocket i2pss; @@ -48,12 +53,17 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { /** default timeout to 3 minutes - override if desired */ protected long readTimeout = DEFAULT_READ_TIMEOUT; - private static final boolean DEFAULT_USE_POOL = false; + /** do we use threads? default true (ignored for standard servers, always false) */ + private static final String PROP_USE_POOL = "i2ptunnel.usePool"; + private static final boolean DEFAULT_USE_POOL = true; protected static volatile long __serverId = 0; + /** max number of threads - this many slowlorisses will DOS this server, but too high could OOM the JVM */ private static final String PROP_HANDLER_COUNT = "i2ptunnel.blockingHandlerCount"; - private static final int DEFAULT_HANDLER_COUNT = 10; - - + private static final int DEFAULT_HANDLER_COUNT = 65; + /** min number of threads */ + private static final int MIN_HANDLERS = 0; + /** how long to wait before dropping an idle thread */ + private static final long HANDLER_KEEPALIVE_MS = 30*1000; protected I2PTunnelTask task = null; protected boolean bidir = false; @@ -67,8 +77,8 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { */ public I2PTunnelServer(InetAddress host, int port, String privData, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super("Server at " + host + ':' + port, notifyThis, tunnel); + _log = tunnel.getContext().logManager().getLog(getClass()); ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(privData)); - SetUsePool(tunnel); init(host, port, bais, privData, l); } @@ -79,7 +89,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { public I2PTunnelServer(InetAddress host, int port, File privkey, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super("Server at " + host + ':' + port, notifyThis, tunnel); - SetUsePool(tunnel); + _log = tunnel.getContext().logManager().getLog(getClass()); FileInputStream fis = null; try { fis = new FileInputStream(privkey); @@ -99,19 +109,10 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { */ public I2PTunnelServer(InetAddress host, int port, InputStream privData, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super("Server at " + host + ':' + port, notifyThis, tunnel); - SetUsePool(tunnel); + _log = tunnel.getContext().logManager().getLog(getClass()); init(host, port, privData, privkeyname, l); } - - private void SetUsePool(I2PTunnel Tunnel) { - String usePool = Tunnel.getClientOptions().getProperty("i2ptunnel.usePool"); - if (usePool != null) - _usePool = "true".equalsIgnoreCase(usePool); - else - _usePool = DEFAULT_USE_POOL; - } - private static final int RETRY_DELAY = 20*1000; private static final int MAX_RETRIES = 4; @@ -143,6 +144,16 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { return; } + // extending classes default to threaded, but for a standard server, we can't get slowlorissed + _usePool = !getClass().equals(I2PTunnelServer.class); + if (_usePool) { + String usePool = getTunnel().getClientOptions().getProperty(PROP_USE_POOL); + if (usePool != null) + _usePool = "true".equalsIgnoreCase(usePool); + else + _usePool = DEFAULT_USE_POOL; + } + // Todo: Can't stop a tunnel from the UI while it's in this loop (no session yet) int retries = 0; while (sockMgr == null) { @@ -199,8 +210,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { * */ public void startRunning() { - Thread t = new I2PAppThread(this); - t.setName("Server " + (++__serverId)); + Thread t = new I2PAppThread(this, "Server " + remoteHost + ':' + remotePort, true); t.start(); } @@ -236,7 +246,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { } return false; } - l.log("Stopping tunnels for server at " + getTunnel().listenHost + ':' + this.remotePort); + l.log("Stopping tunnels for server at " + this.remoteHost + ':' + this.remotePort); try { if (i2pss != null) i2pss.close(); getTunnel().removeSession(sockMgr.getSession()); @@ -259,67 +269,106 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { rv = Integer.parseInt(cnt); if (rv <= 0) rv = DEFAULT_HANDLER_COUNT; - } catch (NumberFormatException nfe) { - rv = DEFAULT_HANDLER_COUNT; - } + } catch (NumberFormatException nfe) {} } return rv; } + /** + * If usePool is set, this starts the executor pool. + * Then, do the accept() loop, and either + * hands each I2P socket to the executor or runs it in-line. + */ public void run() { - if (shouldUsePool()) { - I2PServerSocket i2pS_S = sockMgr.getServerSocket(); - int handlers = getHandlerCount(); - for (int i = 0; i < handlers; i++) { - I2PAppThread handler = new I2PAppThread(new Handler(i2pS_S), "Handle Server " + i); - handler.start(); - } - } else { - I2PServerSocket i2pS_S = sockMgr.getServerSocket(); - while (true) { - try { - final I2PSocket i2ps = i2pS_S.accept(); - if (i2ps == null) throw new I2PException("I2PServerSocket closed"); - new I2PAppThread(new Runnable() { public void run() { blockingHandle(i2ps); } }).start(); - } catch (I2PException ipe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error accepting - KILLING THE TUNNEL SERVER", ipe); - return; - } catch (ConnectException ce) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error accepting", ce); - // not killing the server.. - } catch(SocketTimeoutException ste) { - // ignored, we never set the timeout + I2PServerSocket i2pS_S = sockMgr.getServerSocket(); + ThreadPoolExecutor executor = null; + if (_log.shouldLog(Log.WARN)) { + if (_usePool) + _log.warn("Starting executor with " + getHandlerCount() + " threads max"); + else + _log.warn("Threads disabled, running blockingHandles inline"); + } + if (_usePool) { + executor = new CustomThreadPoolExecutor(getHandlerCount(), "ServerHandler pool " + remoteHost + ':' + remotePort); + } + while (open) { + try { + final I2PSocket i2ps = i2pS_S.accept(); + if (i2ps == null) throw new I2PException("I2PServerSocket closed"); + if (_usePool) { + try { + executor.execute(new Handler(i2ps)); + } catch (RejectedExecutionException ree) { + try { + i2ps.close(); + } catch (IOException ioe) {} + if (open && _log.shouldLog(Log.ERROR)) + _log.error("ServerHandler queue full for " + remoteHost + ':' + remotePort + + "; increase " + PROP_HANDLER_COUNT + '?', ree); + } + } else { + // use only for standard servers that can't get slowlorissed! Not for http or irc + blockingHandle(i2ps); } + } catch (I2PException ipe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error accepting - KILLING THE TUNNEL SERVER", ipe); + return; + } catch (ConnectException ce) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error accepting", ce); + // not killing the server.. + try { + Thread.currentThread().sleep(500); + } catch (InterruptedException ie) {} + } catch(SocketTimeoutException ste) { + // ignored, we never set the timeout } } + if (executor != null) + executor.shutdownNow(); } + /** + * Not really needed for now but in case we want to add some hooks like afterExecute(). + */ + private static class CustomThreadPoolExecutor extends ThreadPoolExecutor { + public CustomThreadPoolExecutor(int max, String name) { + super(MIN_HANDLERS, max, HANDLER_KEEPALIVE_MS, TimeUnit.MILLISECONDS, + new SynchronousQueue(), new CustomThreadFactory(name)); + } + } + + /** just to set the name and set Daemon */ + private static class CustomThreadFactory implements ThreadFactory { + private String _name; + + public CustomThreadFactory(String name) { + _name = name; + } + + public Thread newThread(Runnable r) { + Thread rv = Executors.defaultThreadFactory().newThread(r); + rv.setName(_name); + rv.setDaemon(true); + return rv; + } + } + public boolean shouldUsePool() { return _usePool; } /** - * minor thread pool to pull off the accept() concurrently. there are still lots - * (and lots) of wasted threads within the I2PTunnelRunner, but its a start - * + * Run the blockingHandler. */ private class Handler implements Runnable { - private I2PServerSocket _serverSocket; - public Handler(I2PServerSocket serverSocket) { - _serverSocket = serverSocket; + private I2PSocket _i2ps; + + public Handler(I2PSocket socket) { + _i2ps = socket; } + public void run() { - while (open) { - try { - blockingHandle(_serverSocket.accept()); - } catch (I2PException ex) { - _log.error("Error while waiting for I2PConnections", ex); - return; - } catch (IOException ex) { - _log.error("Error while waiting for I2PConnections", ex); - return; - } - } + blockingHandle(_i2ps); } } @@ -335,20 +384,21 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { Socket s = new Socket(remoteHost, remotePort); afterSocket = I2PAppContext.getGlobalContext().clock().now(); new I2PTunnelRunner(s, socket, slock, null, null); + + long afterHandle = I2PAppContext.getGlobalContext().clock().now(); + long timeToHandle = afterHandle - afterAccept; + if ( (timeToHandle > 1000) && (_log.shouldLog(Log.WARN)) ) + _log.warn("Took a while to handle the request for " + remoteHost + ':' + remotePort + + " [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]"); } catch (SocketException ex) { try { socket.close(); - } catch (IOException ioe) { - _log.error("Error while closing the received i2p con", ex); - } + } 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); } - - long afterHandle = I2PAppContext.getGlobalContext().clock().now(); - long timeToHandle = afterHandle - afterAccept; - if (timeToHandle > 1000) - _log.warn("Took a while to handle the request [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]"); } } 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 01888d8d10481895bb96e0d782c4c3686976acc9..84e66cf7289e9f89d29c88c0b444c97c8235e702 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java @@ -30,7 +30,6 @@ import net.i2p.util.Log; */ public class I2PSOCKSIRCTunnel extends I2PSOCKSTunnel { - private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(I2PSOCKSIRCTunnel.class); private static int __clientId = 0; /** @param pkf private key file name or null for transient key */ diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java index 14cafbdfdc5513079a93386f849e6b81f4f4c62f..10d51fe2e1dbc432990e6d3d794b8f57abff7c66 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java @@ -26,7 +26,6 @@ import net.i2p.util.Log; public class I2PSOCKSTunnel extends I2PTunnelClientBase { - private static final Log _log = new Log(I2PSOCKSTunnel.class); private HashMap<String, List<String>> proxies = null; // port# + "" or "default" -> hostname list protected Destination outProxyDest = null; diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java index 38c82a70c54f4a90d32302cffd683e75521cc049..d9c5fcd03d760992da1c6b4931165533d2be917a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java @@ -45,7 +45,6 @@ import net.i2p.util.Log; */ public abstract class I2PTunnelUDPClientBase extends I2PTunnelTask implements Source, Sink { - private static final Log _log = new Log(I2PTunnelUDPClientBase.class); protected I2PAppContext _context; protected Logging l; diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java index 6ba8379f9453e647fb2485cfd798f7c7542de67a..4d43d53083a8c940919b48953e24f648522442b3 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java @@ -46,7 +46,7 @@ import net.i2p.util.Log; public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sink { - private final static Log _log = new Log(I2PTunnelUDPServerBase.class); + private final Log _log; private final Object lock = new Object(); protected Object slock = new Object(); @@ -73,6 +73,7 @@ public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sin public I2PTunnelUDPServerBase(boolean verify, File privkey, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super("UDPServer <- " + privkeyname, notifyThis, tunnel); + _log = tunnel.getContext().logManager().getLog(I2PTunnelUDPServerBase.class); FileInputStream fis = null; try { fis = new FileInputStream(privkey); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java index b60dc289eb76eabccbe2a53202ecfa751249afad..87beb689cb3022bf97268321896537c59db2cbef 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -18,6 +18,7 @@ import net.i2p.data.Destination; import net.i2p.data.PrivateKeyFile; import net.i2p.data.Signature; import net.i2p.data.SigningPrivateKey; +import net.i2p.i2ptunnel.I2PTunnelHTTPClient; import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase; import net.i2p.i2ptunnel.TunnelController; import net.i2p.i2ptunnel.TunnelControllerGroup; @@ -171,14 +172,23 @@ public class EditBean extends IndexBean { return getProperty(tunnel, "i2cp.leaseSetKey", ""); } - public boolean getAccess(int tunnel) { - return getBooleanProperty(tunnel, "i2cp.enableAccessList"); + public String getAccessMode(int tunnel) { + if (getBooleanProperty(tunnel, PROP_ENABLE_ACCESS_LIST)) + return "1"; + if (getBooleanProperty(tunnel, PROP_ENABLE_BLACKLIST)) + return "2"; + return "0"; } public String getAccessList(int tunnel) { return getProperty(tunnel, "i2cp.accessList", "").replace(",", "\n"); } + public String getJumpList(int tunnel) { + return getProperty(tunnel, I2PTunnelHTTPClient.PROP_JUMP_SERVERS, + I2PTunnelHTTPClient.DEFAULT_JUMP_SERVERS).replace(",", "\n"); + } + public boolean getClose(int tunnel) { return getBooleanProperty(tunnel, "i2cp.closeOnIdle"); } @@ -234,6 +244,35 @@ public class EditBean extends IndexBean { return getProperty(tunnel, I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW, ""); } + /** all of these are @since 0.8.3 */ + public String getLimitMinute(int tunnel) { + return getProperty(tunnel, PROP_MAX_CONNS_MIN, "0"); + } + + public String getLimitHour(int tunnel) { + return getProperty(tunnel, PROP_MAX_CONNS_HOUR, "0"); + } + + public String getLimitDay(int tunnel) { + return getProperty(tunnel, PROP_MAX_CONNS_DAY, "0"); + } + + public String getTotalMinute(int tunnel) { + return getProperty(tunnel, PROP_MAX_TOTAL_CONNS_MIN, "0"); + } + + public String getTotalHour(int tunnel) { + return getProperty(tunnel, PROP_MAX_TOTAL_CONNS_HOUR, "0"); + } + + public String getTotalDay(int tunnel) { + return getProperty(tunnel, PROP_MAX_TOTAL_CONNS_DAY, "0"); + } + + public String getMaxStreams(int tunnel) { + return getProperty(tunnel, PROP_MAX_STREAMS, "0"); + } + private int getProperty(int tunnel, String prop, int def) { TunnelController tun = getController(tunnel); if (tun != null) { @@ -270,7 +309,14 @@ public class EditBean extends IndexBean { return false; } + /** @since 0.8.3 */ + public boolean isRouterContext() { + return _context.isRouterContext(); + } + public String getI2CPHost(int tunnel) { + if (_context.isRouterContext()) + return _("internal"); TunnelController tun = getController(tunnel); if (tun != null) return tun.getI2CPHost(); @@ -279,6 +325,8 @@ public class EditBean extends IndexBean { } public String getI2CPPort(int tunnel) { + if (_context.isRouterContext()) + return _("internal"); TunnelController tun = getController(tunnel); if (tun != null) return tun.getI2CPPort(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 2035f4627963baa237cb09a13ed5a19804b93331..bb1a339c294140da226b3711aa5066c6265eda61 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -24,6 +24,7 @@ import net.i2p.data.Certificate; import net.i2p.data.Destination; import net.i2p.data.PrivateKeyFile; import net.i2p.data.SessionKey; +import net.i2p.i2ptunnel.I2PTunnelHTTPClient; import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase; import net.i2p.i2ptunnel.TunnelController; import net.i2p.i2ptunnel.TunnelControllerGroup; @@ -537,11 +538,11 @@ public class IndexBean { public void setDescription(String description) { _description = (description != null ? description.trim() : null); } - /** I2CP host the router is on */ + /** I2CP host the router is on, ignored when in router context */ public void setClientHost(String host) { _i2cpHost = (host != null ? host.trim() : null); } - /** I2CP port the router is on */ + /** I2CP port the router is on, ignored when in router context */ public void setClientport(String port) { _i2cpPort = (port != null ? port.trim() : null); } @@ -643,9 +644,17 @@ public class IndexBean { public void setEncrypt(String moo) { _booleanOptions.add("i2cp.encryptLeaseSet"); } - public void setAccess(String moo) { - _booleanOptions.add("i2cp.enableAccessList"); + + protected static final String PROP_ENABLE_ACCESS_LIST = "i2cp.enableAccessList"; + protected static final String PROP_ENABLE_BLACKLIST = "i2cp.enableBlackList"; + + public void setAccessMode(String val) { + if ("1".equals(val)) + _booleanOptions.add(PROP_ENABLE_ACCESS_LIST); + else if ("2".equals(val)) + _booleanOptions.add(PROP_ENABLE_BLACKLIST); } + public void setDelayOpen(String moo) { _booleanOptions.add("i2cp.delayOpen"); } @@ -671,10 +680,17 @@ public class IndexBean { if (val != null) _otherOptions.put("i2cp.leaseSetKey", val.trim()); } + public void setAccessList(String val) { if (val != null) _otherOptions.put("i2cp.accessList", val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ",")); } + + public void setJumpList(String val) { + if (val != null) + _otherOptions.put(I2PTunnelHTTPClient.PROP_JUMP_SERVERS, val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ",")); + } + public void setCloseTime(String val) { if (val != null) { try { @@ -712,6 +728,50 @@ public class IndexBean { _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW, s.trim()); } + /** all of these are @since 0.8.3 */ + protected static final String PROP_MAX_CONNS_MIN = "i2p.streaming.maxConnsPerMinute"; + protected static final String PROP_MAX_CONNS_HOUR = "i2p.streaming.maxConnsPerHour"; + protected static final String PROP_MAX_CONNS_DAY = "i2p.streaming.maxConnsPerDay"; + protected static final String PROP_MAX_TOTAL_CONNS_MIN = "i2p.streaming.maxTotalConnsPerMinute"; + protected static final String PROP_MAX_TOTAL_CONNS_HOUR = "i2p.streaming.maxTotalConnsPerHour"; + protected static final String PROP_MAX_TOTAL_CONNS_DAY = "i2p.streaming.maxTotalConnsPerDay"; + protected static final String PROP_MAX_STREAMS = "i2p.streaming.maxConcurrentStreams"; + + public void setLimitMinute(String s) { + if (s != null) + _otherOptions.put(PROP_MAX_CONNS_MIN, s.trim()); + } + + public void setLimitHour(String s) { + if (s != null) + _otherOptions.put(PROP_MAX_CONNS_HOUR, s.trim()); + } + + public void setLimitDay(String s) { + if (s != null) + _otherOptions.put(PROP_MAX_CONNS_DAY, s.trim()); + } + + public void setTotalMinute(String s) { + if (s != null) + _otherOptions.put(PROP_MAX_TOTAL_CONNS_MIN, s.trim()); + } + + public void setTotalHour(String s) { + if (s != null) + _otherOptions.put(PROP_MAX_TOTAL_CONNS_HOUR, s.trim()); + } + + public void setTotalDay(String s) { + if (s != null) + _otherOptions.put(PROP_MAX_TOTAL_CONNS_DAY, s.trim()); + } + + public void setMaxStreams(String s) { + if (s != null) + _otherOptions.put(PROP_MAX_STREAMS, s.trim()); + } + /** params needed for hashcash and dest modification */ public void setEffort(String val) { if (val != null) { @@ -904,16 +964,20 @@ public class IndexBean { I2PTunnelHTTPClientBase.PROP_AUTH, I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH }; private static final String _booleanServerOpts[] = { - "i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", "i2cp.enableAccessList" + "i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", PROP_ENABLE_ACCESS_LIST, PROP_ENABLE_BLACKLIST }; private static final String _otherClientOpts[] = { "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.closeIdleTime", - "proxyUsername", "proxyPassword", "outproxyUsername", "outproxyPassword" + "proxyUsername", "proxyPassword", "outproxyUsername", "outproxyPassword", + I2PTunnelHTTPClient.PROP_JUMP_SERVERS }; private static final String _otherServerOpts[] = { - "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList" + "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList", + PROP_MAX_CONNS_MIN, PROP_MAX_CONNS_HOUR, PROP_MAX_CONNS_DAY, + PROP_MAX_TOTAL_CONNS_MIN, PROP_MAX_TOTAL_CONNS_HOUR, PROP_MAX_TOTAL_CONNS_DAY, + PROP_MAX_STREAMS }; - protected static final Set _noShowSet = new HashSet(); + protected static final Set _noShowSet = new HashSet(64); static { _noShowSet.addAll(Arrays.asList(_noShowOpts)); _noShowSet.addAll(Arrays.asList(_booleanClientOpts)); @@ -929,12 +993,14 @@ public class IndexBean { config.setProperty("name", _name); if (_description != null) config.setProperty("description", _description); - if (_i2cpHost != null) - config.setProperty("i2cpHost", _i2cpHost); - if ( (_i2cpPort != null) && (_i2cpPort.trim().length() > 0) ) { - config.setProperty("i2cpPort", _i2cpPort); - } else { - config.setProperty("i2cpPort", "7654"); + if (!_context.isRouterContext()) { + if (_i2cpHost != null) + config.setProperty("i2cpHost", _i2cpHost); + if ( (_i2cpPort != null) && (_i2cpPort.trim().length() > 0) ) { + config.setProperty("i2cpPort", _i2cpPort); + } else { + config.setProperty("i2cpPort", "7654"); + } } if (_privKeyFile != null) config.setProperty("privKeyFile", _privKeyFile); @@ -1020,7 +1086,7 @@ public class IndexBean { } } - private String _(String key) { + protected String _(String key) { return Messages._(key, _context); } } diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index 2b69440ac1f290e065e42f6e8d40b13e29542540..f2b427542ba1c87d60310e99174a8692fb3634b3 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -286,19 +286,19 @@ <% } // !streamrclient %> <div id="optionsField" class="rowItem"> - <label><%=intl._("I2CP Options")%>:</label> + <label><%=intl._("Router I2CP Address")%>:</label> </div> <div id="optionsHostField" class="rowItem"> <label for="clientHost" accesskey="o"> <%=intl._("Host")%>(<span class="accessKey">o</span>): </label> - <input type="text" id="clientHost" name="clientHost" size="20" title="I2CP Hostname or IP" value="<%=editBean.getI2CPHost(curTunnel)%>" class="freetext" /> + <input type="text" id="clientHost" name="clientHost" size="20" title="I2CP Hostname or IP" value="<%=editBean.getI2CPHost(curTunnel)%>" class="freetext" <% if (editBean.isRouterContext()) { %> readonly="readonly" <% } %> /> </div> <div id="optionsPortField" class="rowItem"> <label for="clientPort" accesskey="r"> <%=intl._("Port")%>(<span class="accessKey">r</span>): </label> - <input type="text" id="port" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" /> + <input type="text" id="clientPort" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" <% if (editBean.isRouterContext()) { %> readonly="readonly" <% } %> /> </div> <% if (!"streamrclient".equals(tunnelType)) { // streamr client sends pings so it will never be idle %> @@ -465,6 +465,18 @@ </div> <% } // httpclient || connect || socks || socksirc %> + <% if ("httpclient".equals(tunnelType)) { %> + <div id="optionsField" class="rowItem"> + <label><%=intl._("Jump URL List")%>:</label> + </div> + <div id="hostField" class="rowItem"> + <textarea rows="2" style="height: 8em;" cols="60" id="hostField" name="jumpList" title="List of helper URLs to offer when a host is not found in your addressbook" wrap="off"><%=editBean.getJumpList(curTunnel)%></textarea> + </div> + <div class="subdivider"> + <hr /> + </div> + <% } // httpclient %> + <div id="customOptionsField" class="rowItem"> <label for="customOptions" accesskey="u"> <%=intl._("Custom options")%>(<span class="accessKey">u</span>): diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index 773d323a2bf9c88f5d683798b2e485e012f84e3b..b0f870fb7aa5797730ea6a1bc6f38ff480b17ac2 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -305,19 +305,19 @@ <% } // !streamrserver %> <div id="optionsField" class="rowItem"> - <label><%=intl._("I2CP Options")%>:</label> + <label><%=intl._("Router I2CP Address")%>:</label> </div> <div id="optionsHostField" class="rowItem"> <label for="clientHost" accesskey="o"> <%=intl._("Host")%>(<span class="accessKey">o</span>): </label> - <input type="text" id="clientHost" name="clientHost" size="20" title="I2CP Hostname or IP" value="<%=editBean.getI2CPHost(curTunnel)%>" class="freetext" /> + <input type="text" id="clientHost" name="clientHost" size="20" title="I2CP Hostname or IP" value="<%=editBean.getI2CPHost(curTunnel)%>" class="freetext" <% if (editBean.isRouterContext()) { %> readonly="readonly" <% } %> /> </div> <div id="optionsPortField" class="rowItem"> <label for="clientPort" accesskey="r"> <%=intl._("Port")%>(<span class="accessKey">r</span>): </label> - <input type="text" id="clientPort" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" /> + <input type="text" id="clientPort" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" <% if (editBean.isRouterContext()) { %> readonly="readonly" <% } %> /> </div> <div class="subdivider"> @@ -333,7 +333,7 @@ <label for="encrypt" accesskey="e"> <%=intl._("Enable")%>: </label> - <input value="1" type="checkbox" id="startOnLoad" name="encrypt" title="Encrypt LeaseSet"<%=(editBean.getEncrypt(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" /> + <input value="1" type="checkbox" id="startOnLoad" name="encrypt" title="ONLY clients with the encryption key will be able to connect"<%=(editBean.getEncrypt(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" /> </div> <div id="portField" class="rowItem"> <label for="encrypt" accesskey="e"> @@ -359,19 +359,64 @@ </label> </div> <div id="portField" class="rowItem"> - <label for="access" accesskey="s"> - <%=intl._("Enable")%>: - </label> - <input value="1" type="checkbox" id="startOnLoad" name="access" title="Enable Access List"<%=(editBean.getAccess(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" /> + <label><%=intl._("Disable")%></label> + <input value="0" type="radio" id="startOnLoad" name="accessMode" title="Allow all clients"<%=(editBean.getAccessMode(curTunnel).equals("0") ? " checked=\"checked\"" : "")%> class="tickbox" /> + <label><%=intl._("Whitelist")%></label> + <input value="1" type="radio" id="startOnLoad" name="accessMode" title="Allow listed clients only"<%=(editBean.getAccessMode(curTunnel).equals("1") ? " checked=\"checked\"" : "")%> class="tickbox" /> + <label><%=intl._("Blacklist")%></label> + <input value="2" type="radio" id="startOnLoad" name="accessMode" title="Reject listed clients"<%=(editBean.getAccessMode(curTunnel).equals("2") ? " checked=\"checked\"" : "")%> class="tickbox" /> </div> <div id="hostField" class="rowItem"> <label for="accessList" accesskey="s"> <%=intl._("Access List")%>: </label> - <textarea rows="2" style="height: 4em;" cols="60" id="hostField" name="accessList" title="Access List" wrap="off"><%=editBean.getAccessList(curTunnel)%></textarea> - <span class="comment"><%=intl._("(Restrict to these clients only)")%></span> + <textarea rows="2" style="height: 8em;" cols="60" id="hostField" name="accessList" title="Access List" wrap="off"><%=editBean.getAccessList(curTunnel)%></textarea> </div> + <div class="subdivider"> + <hr /> + </div> + + <div class="rowItem"> + <div id="optionsField" class="rowItem"> + <label><%=intl._("Inbound connection limits (0 to disable)")%><br><%=intl._("Per client")%>:</label> + </div> + <div id="portField" class="rowItem"> + <label><%=intl._("Per minute")%>:</label> + <input type="text" id="port" name="limitMinute" value="<%=editBean.getLimitMinute(curTunnel)%>" class="freetext" /> + </div> + <div id="portField" class="rowItem"> + <label><%=intl._("Per hour")%>:</label> + <input type="text" id="port" name="limitHour" value="<%=editBean.getLimitHour(curTunnel)%>" class="freetext" /> + </div> + <div id="portField" class="rowItem"> + <label><%=intl._("Per day")%>:</label> + <input type="text" id="port" name="limitDay" value="<%=editBean.getLimitDay(curTunnel)%>" class="freetext" /> + </div> + </div> + <div class="rowItem"> + <div id="optionsField" class="rowItem"> + <label><%=intl._("Total")%>:</label> + </div> + <div id="portField" class="rowItem"> + <input type="text" id="port" name="totalMinute" value="<%=editBean.getTotalMinute(curTunnel)%>" class="freetext" /> + </div> + <div id="portField" class="rowItem"> + <input type="text" id="port" name="totalHour" value="<%=editBean.getTotalHour(curTunnel)%>" class="freetext" /> + </div> + <div id="portField" class="rowItem"> + <input type="text" id="port" name="totalDay" value="<%=editBean.getTotalDay(curTunnel)%>" class="freetext" /> + </div> + </div> + <div class="rowItem"> + <div id="optionsField" class="rowItem"> + <label><%=intl._("Max concurrent connections (0 to disable)")%>:</label> + </div> + <div id="portField" class="rowItem"> + <input type="text" id="port" name="maxStreams" value="<%=editBean.getMaxStreams(curTunnel)%>" class="freetext" /> + </div> + </div> + <div class="subdivider"> <hr /> </div> diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java index d0028fdb81dba2d83e47844d722c4efdad513cff..9f43aa24644b1fb1f8d77c8214b7ca9b4ce4d547 100644 --- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java @@ -19,9 +19,10 @@ public interface I2PServerSocket { /** * Waits for the next socket connecting. If a remote user tried to make a * connection and the local application wasn't .accept()ing new connections, - * they should get refused (if .accept() doesnt occur in some small period) + * they should get refused (if .accept() doesnt occur in some small period). + * Warning - unlike regular ServerSocket, may return null. * - * @return a connected I2PSocket + * @return a connected I2PSocket OR NULL * * @throws I2PException if there is a problem with reading a new socket * from the data available (aka the I2PSession closed, etc) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java index 72acbb5404aa56ba05dca95e94d8c22348c5ff0c..8a525180bc384930124b543760117d26346ba7f4 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java @@ -3,6 +3,7 @@ package net.i2p.router.web; import net.i2p.I2PAppContext; import net.i2p.crypto.TrustedUpdate; import net.i2p.data.DataHelper; +import net.i2p.util.FileUtil; /** * @@ -61,14 +62,10 @@ public class ConfigUpdateHandler extends FormHandler { public static final String DEFAULT_UPDATE_URL; static { - String foo; - try { - Class.forName("java.util.jar.Pack200", false, ClassLoader.getSystemClassLoader()); - foo = PACK200_URLS; - } catch (ClassNotFoundException cnfe) { - foo = NO_PACK200_URLS; - } - DEFAULT_UPDATE_URL = foo; + if (FileUtil.isPack200Supported()) + DEFAULT_UPDATE_URL = PACK200_URLS; + else + DEFAULT_UPDATE_URL = NO_PACK200_URLS; } public static final String PROP_TRUSTED_KEYS = "router.trustedUpdateKeys"; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java index b3ce2fa83237dce542606c94fc40746a6bbb15ac..1b0e043e393067561bcb012b475a84b556ab7de8 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java @@ -59,7 +59,13 @@ public class GraphHelper extends FormHandler { try { _width = Math.min(Integer.parseInt(str), MAX_X); } catch (NumberFormatException nfe) {} } public void setRefreshDelay(String str) { - try { _refreshDelaySeconds = Math.max(Integer.parseInt(str), MIN_REFRESH); } catch (NumberFormatException nfe) {} + try { + int rds = Integer.parseInt(str); + if (rds > 0) + _refreshDelaySeconds = Math.max(rds, MIN_REFRESH); + else + _refreshDelaySeconds = -1; + } catch (NumberFormatException nfe) {} } public String getImages() { @@ -83,7 +89,7 @@ public class GraphHelper extends FormHandler { + "&periodCount=" + (3 * _periodCount ) + "&width=" + (3 * _width) + "&height=" + (3 * _height) - + "\" / target=\"_blank\">"); + + "\" target=\"_blank\">"); String title = _("Combined bandwidth graph"); _out.write("<img class=\"statimage\" width=\"" + (_width + 83) + "\" height=\"" + (_height + 92) @@ -129,6 +135,8 @@ public class GraphHelper extends FormHandler { return ""; } + private static final int[] times = { 60, 2*60, 5*60, 10*60, 30*60, 60*60, -1 }; + public String getForm() { String prev = System.getProperty("net.i2p.router.web.GraphHelper.nonce"); if (prev != null) System.setProperty("net.i2p.router.web.GraphHelper.noncePrev", prev); @@ -145,8 +153,22 @@ public class GraphHelper extends FormHandler { _out.write(_("Image sizes") + ": " + _("width") + ": <input size=\"4\" type=\"text\" name=\"width\" value=\"" + _width + "\"> " + _("pixels") + ", " + _("height") + ": <input size=\"4\" type=\"text\" name=\"height\" value=\"" + _height + "\"> " + _("pixels") + "<br>\n"); - _out.write(_("Refresh delay") + ": <select name=\"refreshDelay\"><option value=\"60\">1 " + _("minute") + "</option><option value=\"120\">2 " + _("minutes") + "</option><option value=\"300\">5 " + _("minutes") + "</option><option value=\"600\">10 " + _("minutes") + "</option><option value=\"1800\">30 " + _("minutes") + "</option><option value=\"3600\">1 " + _("hour") + "</option><option value=\"-1\">" + _("Never") + "</option></select><br>\n"); - _out.write("<hr><div class=\"formaction\"><input type=\"submit\" value=\"" + _("Redraw") + "\"></div></form>"); + _out.write(_("Refresh delay") + ": <select name=\"refreshDelay\">"); + for (int i = 0; i < times.length; i++) { + _out.write("<option value=\""); + _out.write(Integer.toString(times[i])); + _out.write("\""); + if (times[i] == _refreshDelaySeconds) + _out.write(" selected=\"true\""); + _out.write(">"); + if (times[i] > 0) + _out.write(DataHelper.formatDuration2(times[i] * 1000)); + else + _out.write(_("Never")); + _out.write("</option>\n"); + } + _out.write("</select><br>\n" + + "<hr><div class=\"formaction\"><input type=\"submit\" value=\"" + _("Redraw") + "\"></div></form>"); } catch (IOException ioe) { ioe.printStackTrace(); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LocaleWebAppHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/LocaleWebAppHandler.java index 91ede9e5297d0e89808e89b79b5d30c55380be52..ebf5bcf9c436465d7e80b29deb95534d42726001 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/LocaleWebAppHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/LocaleWebAppHandler.java @@ -53,7 +53,8 @@ public class LocaleWebAppHandler extends WebApplicationHandler // home page pathInContext = "/index.jsp"; } else if (pathInContext.indexOf("/", 1) < 0 && - !pathInContext.endsWith(".jsp")) { + (!pathInContext.endsWith(".jsp")) && + (!pathInContext.endsWith(".txt"))) { // add .jsp to pages at top level pathInContext += ".jsp"; } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java index 68533f7ef63c19cfce8b1b4a7c384b4b90f1b309..870f162d4f9b88a719ebe76a6543c2484aaf7b93 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -4,37 +4,53 @@ import java.util.ArrayList; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; +import java.security.KeyStore; import java.util.List; import java.util.Properties; import java.util.StringTokenizer; import net.i2p.I2PAppContext; import net.i2p.apps.systray.SysTray; +import net.i2p.data.Base32; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.SecureDirectory; +import net.i2p.util.SecureFileOutputStream; +import net.i2p.util.ShellCommand; import org.mortbay.http.DigestAuthenticator; import org.mortbay.http.HashUserRealm; import org.mortbay.http.SecurityConstraint; +import org.mortbay.http.SslListener; import org.mortbay.http.handler.SecurityHandler; import org.mortbay.jetty.Server; import org.mortbay.jetty.servlet.WebApplicationContext; import org.mortbay.jetty.servlet.WebApplicationHandler; +import org.mortbay.util.InetAddrPort; public class RouterConsoleRunner { private Server _server; - private String _listenPort = "7657"; - private String _listenHost = "127.0.0.1"; - private String _webAppsDir = "./webapps/"; + private String _listenPort; + private String _listenHost; + private String _sslListenPort; + private String _sslListenHost; + private String _webAppsDir; private static final String PROP_WEBAPP_CONFIG_FILENAME = "router.webappsConfigFile"; private static final String DEFAULT_WEBAPP_CONFIG_FILENAME = "webapps.config"; private static final DigestAuthenticator authenticator = new DigestAuthenticator(); public static final String ROUTERCONSOLE = "routerconsole"; public static final String PREFIX = "webapps."; public static final String ENABLED = ".startOnLoad"; + private static final String PROP_KEYSTORE_PASSWORD = "routerconsole.keystorePassword"; + private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit"; + private static final String PROP_KEY_PASSWORD = "routerconsole.keyPassword"; + private static final String DEFAULT_LISTEN_PORT = "7657"; + private static final String DEFAULT_LISTEN_HOST = "127.0.0.1"; + private static final String DEFAULT_WEBAPPS_DIR = "./webapps/"; + private static final String USAGE = "Bad RouterConsoleRunner arguments, check clientApp.0.args in your clients.config file! " + + "Usage: [[port host[,host]] [-s sslPort [host[,host]]] [webAppsDir]]"; static { System.setProperty("org.mortbay.http.Version.paranoid", "true"); @@ -42,6 +58,27 @@ public class RouterConsoleRunner { } /** + * <pre> + * non-SSL: + * RouterConsoleRunner + * RouterConsoleRunner 7657 + * RouterConsoleRunner 7657 127.0.0.1 + * RouterConsoleRunner 7657 127.0.0.1,::1 + * RouterConsoleRunner 7657 127.0.0.1,::1 ./webapps/ + * + * SSL: + * RouterConsoleRunner -s 7657 + * RouterConsoleRunner -s 7657 127.0.0.1 + * RouterConsoleRunner -s 7657 127.0.0.1,::1 + * RouterConsoleRunner -s 7657 127.0.0.1,::1 ./webapps/ + * + * If using both, non-SSL must be first: + * RouterConsoleRunner 7657 127.0.0.1 -s 7667 + * RouterConsoleRunner 7657 127.0.0.1 -s 7667 127.0.0.1 + * RouterConsoleRunner 7657 127.0.0.1,::1 -s 7667 127.0.0.1,::1 + * RouterConsoleRunner 7657 127.0.0.1,::1 -s 7667 127.0.0.1,::1 ./webapps/ + * </pre> + * * @param args second arg may be a comma-separated list of bind addresses, * for example ::1,127.0.0.1 * On XP, the other order (127.0.0.1,::1) fails the IPV6 bind, @@ -50,10 +87,40 @@ public class RouterConsoleRunner { * So the wise choice is ::1,127.0.0.1 */ public RouterConsoleRunner(String args[]) { - if (args.length == 3) { - _listenPort = args[0].trim(); - _listenHost = args[1].trim(); - _webAppsDir = args[2].trim(); + if (args.length == 0) { + // _listenHost and _webAppsDir are defaulted below + _listenPort = DEFAULT_LISTEN_PORT; + } else { + boolean ssl = false; + for (int i = 0; i < args.length; i++) { + if (args[i].equals("-s")) + ssl = true; + else if ((!ssl) && _listenPort == null) + _listenPort = args[i]; + else if ((!ssl) && _listenHost == null) + _listenHost = args[i]; + else if (ssl && _sslListenPort == null) + _sslListenPort = args[i]; + else if (ssl && _sslListenHost == null) + _sslListenHost = args[i]; + else if (_webAppsDir == null) + _webAppsDir = args[i]; + else { + System.err.println(USAGE); + throw new IllegalArgumentException(USAGE); + } + } + } + if (_listenHost == null) + _listenHost = DEFAULT_LISTEN_HOST; + if (_sslListenHost == null) + _sslListenHost = _listenHost; + if (_webAppsDir == null) + _webAppsDir = DEFAULT_WEBAPPS_DIR; + // _listenPort and _sslListenPort are not defaulted, if one or the other is null, do not enable + if (_listenPort == null && _sslListenPort == null) { + System.err.println(USAGE); + throw new IllegalArgumentException(USAGE); } } @@ -96,22 +163,63 @@ public class RouterConsoleRunner { List<String> notStarted = new ArrayList(); WebApplicationHandler baseHandler = null; try { - StringTokenizer tok = new StringTokenizer(_listenHost, " ,"); int boundAddresses = 0; - while (tok.hasMoreTokens()) { - String host = tok.nextToken().trim(); + + // add standard listeners + if (_listenPort != null) { + StringTokenizer tok = new StringTokenizer(_listenHost, " ,"); + while (tok.hasMoreTokens()) { + String host = tok.nextToken().trim(); + try { + if (host.indexOf(":") >= 0) // IPV6 - requires patched Jetty 5 + _server.addListener('[' + host + "]:" + _listenPort); + else + _server.addListener(host + ':' + _listenPort); + boundAddresses++; + } catch (IOException ioe) { // this doesn't seem to work, exceptions don't happen until start() below + System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + ' ' + ioe); + } + } + } + + // add SSL listeners + int sslPort = 0; + if (_sslListenPort != null) { try { - if (host.indexOf(":") >= 0) // IPV6 - requires patched Jetty 5 - _server.addListener('[' + host + "]:" + _listenPort); - else - _server.addListener(host + ':' + _listenPort); - boundAddresses++; - } catch (IOException ioe) { // this doesn't seem to work, exceptions don't happen until start() below - System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + ' ' + ioe); + sslPort = Integer.parseInt(_sslListenPort); + } catch (NumberFormatException nfe) {} + if (sslPort <= 0) + System.err.println("Bad routerconsole SSL port " + _sslListenPort); + } + if (sslPort > 0) { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + File keyStore = new File(ctx.getConfigDir(), "keystore/console.ks"); + if (verifyKeyStore(keyStore)) { + StringTokenizer tok = new StringTokenizer(_sslListenHost, " ,"); + while (tok.hasMoreTokens()) { + String host = tok.nextToken().trim(); + // doing it this way means we don't have to escape an IPv6 host with [] + InetAddrPort iap = new InetAddrPort(host, sslPort); + try { + SslListener ssll = new SslListener(iap); + // the keystore path and password + ssll.setKeystore(keyStore.getAbsolutePath()); + ssll.setPassword(ctx.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD)); + // the X.509 cert password (if not present, verifyKeyStore() returned false) + ssll.setKeyPassword(ctx.getProperty(PROP_KEY_PASSWORD, "thisWontWork")); + _server.addListener(ssll); + boundAddresses++; + } catch (Exception e) { // probably no exceptions at this point + System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + " for SSL: " + e); + } + } + } else { + System.err.println("Unable to create or access keystore for SSL: " + keyStore.getAbsolutePath()); } } + if (boundAddresses <= 0) { - System.err.println("Unable to bind routerconsole to any address on port " + _listenPort); + System.err.println("Unable to bind routerconsole to any address on port " + _listenPort + (sslPort > 0 ? (" or SSL port " + sslPort) : "")); return; } _server.setRootWebApp(ROUTERCONSOLE); @@ -201,6 +309,90 @@ public class RouterConsoleRunner { } } + /** + * @return success if it exists and we have a password, or it was created successfully. + * @since 0.8.3 + */ + private static boolean verifyKeyStore(File ks) { + if (ks.exists()) { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + boolean rv = ctx.getProperty(PROP_KEY_PASSWORD) != null; + if (!rv) + System.err.println("Console SSL error, must set " + PROP_KEY_PASSWORD + " in " + (new File(ctx.getConfigDir(), "router.config")).getAbsolutePath()); + return rv; + } + File dir = ks.getParentFile(); + if (!dir.exists()) { + File sdir = new SecureDirectory(dir.getAbsolutePath()); + if (!sdir.mkdir()) + return false; + } + return createKeyStore(ks); + } + + + /** + * Call out to keytool to create a new keystore with a keypair in it. + * Trying to do this programatically is a nightmare, requiring either BouncyCastle + * libs or using proprietary Sun libs, and it's a huge mess. + * + * @return success + * @since 0.8.3 + */ + private static boolean createKeyStore(File ks) { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + // make a random 48 character password (30 * 8 / 5) + byte[] rand = new byte[30]; + ctx.random().nextBytes(rand); + String keyPassword = Base32.encode(rand); + // and one for the cname + ctx.random().nextBytes(rand); + String cname = Base32.encode(rand) + ".console.i2p.net"; + + String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath(); + String[] args = new String[] { + keytool, + "-genkey", // -genkeypair preferred in newer keytools, but this works with more + "-storetype", KeyStore.getDefaultType(), + "-keystore", ks.getAbsolutePath(), + "-storepass", DEFAULT_KEYSTORE_PASSWORD, + "-alias", "console", + "-dname", "CN=" + cname + ",OU=Console,O=I2P Anonymous Network,L=XX,ST=XX,C=XX", + "-validity", "3652", // 10 years + "-keyalg", "DSA", + "-keysize", "1024", + "-keypass", keyPassword}; + boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 30); // 30 secs + if (success) { + success = ks.exists(); + if (success) { + SecureFileOutputStream.setPerms(ks); + try { + RouterContext rctx = (RouterContext) ctx; + rctx.router().setConfigSetting(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); + rctx.router().setConfigSetting(PROP_KEY_PASSWORD, keyPassword); + rctx.router().saveConfig(); + } catch (Exception e) {} // class cast exception + } + } + if (success) { + System.err.println("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" + + "The certificate name was generated randomly, and is not associated with your " + + "IP address, host name, router identity, or destination keys."); + } else { + System.err.println("Failed to create console SSL keystore using command line:"); + StringBuilder buf = new StringBuilder(256); + for (int i = 0; i < args.length; i++) { + buf.append('"').append(args[i]).append("\" "); + } + System.err.println(buf.toString()); + System.err.println("This is for the Sun/Oracle keytool, others may be incompatible.\n" + + "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD + + " to " + (new File(ctx.getConfigDir(), "router.config")).getAbsolutePath()); + } + return success; + } + static void initialize(WebApplicationContext context) { String password = getPassword(); if (password != null) { diff --git a/apps/routerconsole/jsp/viewhistory.jsp b/apps/routerconsole/jsp/viewhistory.jsp new file mode 100644 index 0000000000000000000000000000000000000000..6268abd5a2778ef04b7907d6b10059e416e6752d --- /dev/null +++ b/apps/routerconsole/jsp/viewhistory.jsp @@ -0,0 +1,12 @@ +<% +/* + * USE CAUTION WHEN EDITING + * Trailing whitespace OR NEWLINE on the last line will cause + * IllegalStateExceptions !!! + * + * Do not tag this file for translation. + */ +response.setContentType("text/plain"); +String base = net.i2p.I2PAppContext.getGlobalContext().getBaseDir().getAbsolutePath(); +net.i2p.util.FileUtil.readFile("history.txt", base, response.getOutputStream()); +%> \ No newline at end of file diff --git a/apps/routerconsole/jsp/web.xml b/apps/routerconsole/jsp/web.xml index 8679abcb773f94d7aeb6c789304c728ffb47e34d..e5bdbeb32d27599f182980802ce6e27b9f9ebf4c 100644 --- a/apps/routerconsole/jsp/web.xml +++ b/apps/routerconsole/jsp/web.xml @@ -17,6 +17,11 @@ <url-pattern>/javadoc/*</url-pattern> </servlet-mapping> + <servlet-mapping> + <servlet-name>net.i2p.router.web.jsp.viewhistory_jsp</servlet-name> + <url-pattern>/history.txt</url-pattern> + </servlet-mapping> + <session-config> <session-timeout> 30 diff --git a/apps/streaming/java/src/net/i2p/client/streaming/I2PServerSocketFull.java b/apps/streaming/java/src/net/i2p/client/streaming/I2PServerSocketFull.java index 262b496243c65df870062d1df05e1c26a2d2cf65..acb58fe15fc7025f25bdf6882de2981b16706244 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/I2PServerSocketFull.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/I2PServerSocketFull.java @@ -15,8 +15,9 @@ class I2PServerSocketFull implements I2PServerSocket { } /** + * Warning, unlike regular ServerSocket, may return null * - * @return I2PSocket + * @return I2PSocket OR NULL * @throws net.i2p.I2PException * @throws SocketTimeoutException */ diff --git a/apps/streaming/java/src/net/i2p/client/streaming/I2PSocketManagerFull.java b/apps/streaming/java/src/net/i2p/client/streaming/I2PSocketManagerFull.java index 314ff4e44805f77521982177ab0dd26146ccb8f3..03abafdda1163d6b9462696f1179a8c4355426a6 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/I2PSocketManagerFull.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/I2PSocketManagerFull.java @@ -114,7 +114,7 @@ public class I2PSocketManagerFull implements I2PSocketManager { /** * - * @return connected I2PSocket + * @return connected I2PSocket OR NULL * @throws net.i2p.I2PException * @throws java.net.SocketTimeoutException */ diff --git a/build.xml b/build.xml index 7c75e857aeaa5d2e1dbe523463552a89bcafff2a..4e0af31268e5be45278eb0fd7106e8f0d313846e 100644 --- a/build.xml +++ b/build.xml @@ -6,10 +6,8 @@ <!-- <property name="javac.compilerargs" value="-warn:-unchecked,raw,unused,serial" /> --> - <!-- Add Apache Harmony's Pack200 library if you don't have java.util.jar.Pack200 - See core/java/src/net/i2p/util/FileUtil.java for code changes required - to use this library instead of Sun's version. - Or to comment it all out if you don't have either. + <!-- Additional classpath. No longer required; we find pack200 classes at runtime. + See core/java/src/net/i2p/util/FileUtil.java for more info. --> <!-- <property name="javac.classpath" value="/PATH/TO/pack200.jar" /> @@ -239,7 +237,7 @@ splitindex="true" doctitle="I2P Javadocs for Release ${release.number} Build ${build.number}" windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}"> - <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:freenet.support.CPUInformation:org.bouncycastle.crypto:org.bouncycastle.crypto.*:gnu.crypto.*:gnu.gettext:org.xlattice.crypto.filters:com.nettgryppa.security" /> + <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:org.bouncycastle.crypto:org.bouncycastle.crypto.*:gnu.crypto.*:gnu.gettext:org.xlattice.crypto.filters:com.nettgryppa.security" /> <group title="Streaming Library" packages="net.i2p.client.streaming" /> <group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:org.cybergarage.*:org.freenetproject" /> <group title="Router Console" packages="net.i2p.router.web" /> diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java index b497796408cb2b6051d9386c3ed7dd230bf82c31..e6df37e6f467ffb683daa48138fa4c3b866ec44e 100644 --- a/core/java/src/net/i2p/I2PAppContext.java +++ b/core/java/src/net/i2p/I2PAppContext.java @@ -3,6 +3,7 @@ package net.i2p; import java.io.File; import java.util.HashSet; import java.util.Properties; +import java.util.Random; import java.util.Set; import net.i2p.client.naming.NamingService; @@ -21,7 +22,9 @@ import net.i2p.crypto.KeyGenerator; import net.i2p.crypto.SHA256Generator; import net.i2p.crypto.SessionKeyManager; import net.i2p.crypto.TransientSessionKeyManager; +import net.i2p.data.Base64; import net.i2p.data.RoutingKeyGenerator; +import net.i2p.internal.InternalClientManager; import net.i2p.stat.StatManager; import net.i2p.util.Clock; import net.i2p.util.ConcurrentHashSet; @@ -363,10 +366,12 @@ public class I2PAppContext { if (_tmpDir == null) { String d = getProperty("i2p.dir.temp", System.getProperty("java.io.tmpdir")); // our random() probably isn't warmed up yet - String f = "i2p-" + Math.abs((new java.util.Random()).nextInt()) + ".tmp"; + byte[] rand = new byte[6]; + (new Random()).nextBytes(rand); + String f = "i2p-" + Base64.encode(rand) + ".tmp"; _tmpDir = new SecureDirectory(d, f); if (_tmpDir.exists()) { - // good or bad ? + // good or bad ? loop and try again? } else if (_tmpDir.mkdir()) { _tmpDir.deleteOnExit(); } else { @@ -843,4 +848,13 @@ public class I2PAppContext { public boolean isRouterContext() { return false; } + + /** + * Use this to connect to the router in the same JVM. + * @return always null in I2PAppContext, the client manager if in RouterContext + * @since 0.8.3 + */ + public InternalClientManager internalClientManager() { + return null; + } } diff --git a/core/java/src/net/i2p/client/ClientWriterRunner.java b/core/java/src/net/i2p/client/ClientWriterRunner.java index 056208fb5a021470a0766c3205eda71cf8bc8ad6..f69148da3e212210782d41a65e1ec3577b9774bd 100644 --- a/core/java/src/net/i2p/client/ClientWriterRunner.java +++ b/core/java/src/net/i2p/client/ClientWriterRunner.java @@ -9,6 +9,7 @@ import java.util.concurrent.LinkedBlockingQueue; import net.i2p.data.i2cp.I2CPMessage; import net.i2p.data.i2cp.I2CPMessageImpl; import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.internal.PoisonI2CPMessage; import net.i2p.util.I2PAppThread; /** @@ -50,7 +51,7 @@ class ClientWriterRunner implements Runnable { public void stopWriting() { _messagesToWrite.clear(); try { - _messagesToWrite.put(new PoisonMessage()); + _messagesToWrite.put(new PoisonI2CPMessage()); } catch (InterruptedException ie) {} } @@ -62,7 +63,7 @@ class ClientWriterRunner implements Runnable { } catch (InterruptedException ie) { continue; } - if (msg.getType() == PoisonMessage.MESSAGE_TYPE) + if (msg.getType() == PoisonI2CPMessage.MESSAGE_TYPE) break; // only thread, we don't need synchronized try { @@ -80,18 +81,4 @@ class ClientWriterRunner implements Runnable { } _messagesToWrite.clear(); } - - /** - * End-of-stream msg used to stop the concurrent queue - * See http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/BlockingQueue.html - * - */ - private static class PoisonMessage extends I2CPMessageImpl { - public static final int MESSAGE_TYPE = 999999; - public int getType() { - return MESSAGE_TYPE; - } - public void doReadMessage(InputStream buf, int size) throws I2CPMessageException, IOException {} - public byte[] doWriteMessage() throws I2CPMessageException, IOException { return null; } - } } diff --git a/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java b/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..d562388f13fff706ad9ca1c042addc0f12eb684f --- /dev/null +++ b/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java @@ -0,0 +1,183 @@ +package net.i2p.client; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.security.KeyStore; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import net.i2p.I2PAppContext; +import net.i2p.util.Log; + +/** + * Loads trusted ASCII certs from ~/.i2p/certificates/ and $CWD/certificates/. + * Keeps a single static SSLContext for the whole JVM. + * + * @author zzz + * @since 0.8.3 + */ +class I2CPSSLSocketFactory { + + private static final Object _initLock = new Object(); + private static SSLSocketFactory _factory; + private static Log _log; + + private static final String CERT_DIR = "certificates"; + + /** + * Initializes the static SSL Context if required, then returns a socket + * to the host. + * + * @param ctx just for logging + * @throws IOException on init error or usual socket errors + */ + public static Socket createSocket(I2PAppContext ctx, String host, int port) throws IOException { + synchronized(_initLock) { + if (_factory == null) { + _log = ctx.logManager().getLog(I2CPSSLSocketFactory.class); + initSSLContext(ctx); + if (_factory == null) + throw new IOException("Unable to create SSL Context for I2CP Client"); + _log.info("I2CP Client-side SSL Context initialized"); + } + } + return _factory.createSocket(host, port); + } + + /** + * Loads certs from + * the ~/.i2p/certificates/ and $CWD/certificates/ directories. + */ + private static void initSSLContext(I2PAppContext context) { + KeyStore ks; + try { + ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null, "".toCharArray()); + } catch (GeneralSecurityException gse) { + _log.error("Key Store init error", gse); + return; + } catch (IOException ioe) { + _log.error("Key Store init error", ioe); + return; + } + + File dir = new File(context.getConfigDir(), CERT_DIR); + int adds = addCerts(dir, ks); + int totalAdds = adds; + if (adds > 0 && _log.shouldLog(Log.INFO)) + _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath()); + + File dir2 = new File(System.getProperty("user.dir"), CERT_DIR); + if (!dir.getAbsolutePath().equals(dir2.getAbsolutePath())) { + adds = addCerts(dir2, ks); + totalAdds += adds; + if (adds > 0 && _log.shouldLog(Log.INFO)) + _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath()); + } + if (totalAdds > 0) { + if (_log.shouldLog(Log.INFO)) + _log.info("Loaded total of " + totalAdds + " new trusted certificates"); + } else { + _log.error("No trusted certificates loaded (looked in " + + dir.getAbsolutePath() + (dir.getAbsolutePath().equals(dir2.getAbsolutePath()) ? "" : (" and " + dir2.getAbsolutePath())) + + ", I2CP SSL client connections will fail. " + + "Copy the file certificates/i2cp.local.crt from the router to the directory."); + // don't continue, since we didn't load the system keystore, we have nothing. + return; + } + + try { + SSLContext sslc = SSLContext.getInstance("TLS"); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + sslc.init(null, tmf.getTrustManagers(), context.random()); + _factory = sslc.getSocketFactory(); + } catch (GeneralSecurityException gse) { + _log.error("SSL context init error", gse); + } + } + + /** + * Load all X509 Certs from a directory and add them to the + * trusted set of certificates in the key store + * + * @return number successfully added + */ + private static int addCerts(File dir, KeyStore ks) { + if (_log.shouldLog(Log.INFO)) + _log.info("Looking for X509 Certificates in " + dir.getAbsolutePath()); + int added = 0; + if (dir.exists() && dir.isDirectory()) { + File[] files = dir.listFiles(); + if (files != null) { + for (int i = 0; i < files.length; i++) { + File f = files[i]; + if (!f.isFile()) + continue; + // use file name as alias + String alias = f.getName().toLowerCase(); + boolean success = addCert(f, alias, ks); + if (success) + added++; + } + } + } + return added; + } + + /** + * Load an X509 Cert from a file and add it to the + * trusted set of certificates in the key store + * + * @return success + */ + private static boolean addCert(File file, String alias, KeyStore ks) { + InputStream fis = null; + try { + fis = new FileInputStream(file); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate)cf.generateCertificate(fis); + if (_log.shouldLog(Log.INFO)) { + _log.info("Read X509 Certificate from " + file.getAbsolutePath() + + " Issuer: " + cert.getIssuerX500Principal() + + "; Valid From: " + cert.getNotBefore() + + " To: " + cert.getNotAfter()); + } + try { + cert.checkValidity(); + } catch (CertificateExpiredException cee) { + _log.error("Rejecting expired X509 Certificate: " + file.getAbsolutePath(), cee); + return false; + } catch (CertificateNotYetValidException cnyve) { + _log.error("Rejecting X509 Certificate not yet valid: " + file.getAbsolutePath(), cnyve); + return false; + } + ks.setCertificateEntry(alias, cert); + if (_log.shouldLog(Log.INFO)) + _log.info("Now trusting X509 Certificate, Issuer: " + cert.getIssuerX500Principal()); + } catch (GeneralSecurityException gse) { + _log.error("Error reading X509 Certificate: " + file.getAbsolutePath(), gse); + return false; + } catch (IOException ioe) { + _log.error("Error reading X509 Certificate: " + file.getAbsolutePath(), ioe); + return false; + } finally { + try { if (fis != null) fis.close(); } catch (IOException foo) {} + } + return true; + } +} diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index ea75f73ef8ebb91d3a7db1f32296fd79bf2e539f..8090e0eaed88e65144a23f521edc75e650cd672b 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -39,8 +39,10 @@ import net.i2p.data.i2cp.I2CPMessageException; import net.i2p.data.i2cp.I2CPMessageReader; import net.i2p.data.i2cp.MessagePayloadMessage; import net.i2p.data.i2cp.SessionId; -import net.i2p.util.I2PThread; -import net.i2p.util.InternalSocket; +import net.i2p.internal.I2CPMessageQueue; +import net.i2p.internal.InternalClientManager; +import net.i2p.internal.QueuedI2CPMessageReader; +import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; @@ -66,9 +68,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa /** currently granted lease set, or null */ private LeaseSet _leaseSet; - /** hostname of router */ + /** hostname of router - will be null if in RouterContext */ protected String _hostname; - /** port num to router */ + /** port num to router - will be 0 if in RouterContext */ protected int _portNum; /** socket for comm */ protected Socket _socket; @@ -79,6 +81,13 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa /** where we pipe our messages */ protected /* FIXME final FIXME */OutputStream _out; + /** + * Used for internal connections to the router. + * If this is set, _socket, _writer, and _out will be null. + * @since 0.8.3 + */ + protected I2CPMessageQueue _queue; + /** who we send events to */ protected I2PSessionListener _sessionListener; @@ -122,6 +131,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa private long _lastActivity; private boolean _isReduced; + /** SSL interface (only) @since 0.8.3 */ + protected static final String PROP_ENABLE_SSL = "i2cp.SSL"; + void dateUpdated() { _dateReceived = true; synchronized (_dateReceivedLock) { @@ -172,19 +184,24 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa protected void loadConfig(Properties options) { _options = new Properties(); _options.putAll(filter(options)); - _hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1"); - String portNum = _options.getProperty(I2PClient.PROP_TCP_PORT, LISTEN_PORT + ""); - try { - _portNum = Integer.parseInt(portNum); - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn(getPrefix() + "Invalid port number specified, defaulting to " - + LISTEN_PORT, nfe); - _portNum = LISTEN_PORT; + if (_context.isRouterContext()) { + // just for logging + _hostname = "[internal connection]"; + } else { + _hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1"); + String portNum = _options.getProperty(I2PClient.PROP_TCP_PORT, LISTEN_PORT + ""); + try { + _portNum = Integer.parseInt(portNum); + } catch (NumberFormatException nfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn(getPrefix() + "Invalid port number specified, defaulting to " + + LISTEN_PORT, nfe); + _portNum = LISTEN_PORT; + } } - // auto-add auth if required, not set in the options, and we are in the same JVM - if (_context.isRouterContext() && + // auto-add auth if required, not set in the options, and we are not in the same JVM + if ((!_context.isRouterContext()) && Boolean.valueOf(_context.getProperty("i2cp.auth")).booleanValue() && ((!options.containsKey("i2cp.username")) || (!options.containsKey("i2cp.password")))) { String configUser = _context.getProperty("i2cp.username"); @@ -272,10 +289,6 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa setOpening(true); _closed = false; _availabilityNotifier.stopNotifying(); - I2PThread notifier = new I2PThread(_availabilityNotifier); - notifier.setName("Notifier " + _myDestination.calculateHash().toBase64().substring(0,4)); - notifier.setDaemon(true); - notifier.start(); if ( (_options != null) && (I2PClient.PROP_RELIABILITY_GUARANTEED.equals(_options.getProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT))) ) { @@ -285,17 +298,32 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa long startConnect = _context.clock().now(); try { - // If we are in the router JVM, connect using the interal pseudo-socket - _socket = InternalSocket.getSocket(_hostname, _portNum); - // _socket.setSoTimeout(1000000); // Uhmmm we could really-really use a real timeout, and handle it. - _out = _socket.getOutputStream(); - synchronized (_out) { - _out.write(I2PClient.PROTOCOL_BYTE); - _out.flush(); + // If we are in the router JVM, connect using the interal queue + if (_context.isRouterContext()) { + // _socket, _out, and _writer remain null + InternalClientManager mgr = _context.internalClientManager(); + if (mgr == null) + throw new I2PSessionException("Router is not ready for connections"); + // the following may throw an I2PSessionException + _queue = mgr.connect(); + _reader = new QueuedI2CPMessageReader(_queue, this); + } else { + if (Boolean.valueOf(_options.getProperty(PROP_ENABLE_SSL)).booleanValue()) + _socket = I2CPSSLSocketFactory.createSocket(_context, _hostname, _portNum); + else + _socket = new Socket(_hostname, _portNum); + // _socket.setSoTimeout(1000000); // Uhmmm we could really-really use a real timeout, and handle it. + _out = _socket.getOutputStream(); + synchronized (_out) { + _out.write(I2PClient.PROTOCOL_BYTE); + _out.flush(); + } + _writer = new ClientWriterRunner(_out, this); + InputStream in = _socket.getInputStream(); + _reader = new I2CPMessageReader(in, this); } - _writer = new ClientWriterRunner(_out, this); - InputStream in = _socket.getInputStream(); - _reader = new I2CPMessageReader(in, this); + Thread notifier = new I2PAppThread(_availabilityNotifier, "ClientNotifier " + getPrefix(), true); + notifier.start(); if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "before startReading"); _reader.startReading(); if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Before getDate"); @@ -435,6 +463,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } } + /** + * This notifies the client of payload messages. + * Needs work. + */ protected class AvailabilityNotifier implements Runnable { private List _pendingIds; private List _pendingSizes; @@ -497,8 +529,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } /** - * Recieve notification of some I2CP message and handle it if possible - * + * The I2CPMessageEventListener callback. + * Recieve notification of some I2CP message and handle it if possible. + * @param reader unused */ public void messageReceived(I2CPMessageReader reader, I2CPMessage message) { I2CPMessageHandler handler = _handlerMap.getHandler(message.getType()); @@ -515,7 +548,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } /** - * Recieve notifiation of an error reading the I2CP stream + * The I2CPMessageEventListener callback. + * Recieve notifiation of an error reading the I2CP stream. + * @param reader unused * @param error non-null */ public void readError(I2CPMessageReader reader, Exception error) { @@ -567,9 +602,14 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa * @throws I2PSessionException if the message is malformed or there is an error writing it out */ void sendMessage(I2CPMessage message) throws I2PSessionException { - if (isClosed() || _writer == null) + if (isClosed()) throw new I2PSessionException("Already closed"); - _writer.addMessage(message); + else if (_queue != null) + _queue.offer(message); // internal + else if (_writer == null) + throw new I2PSessionException("Already closed"); + else + _writer.addMessage(message); } /** @@ -581,8 +621,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa // Only log as WARN if the router went away int level; String msgpfx; - if ((error instanceof EOFException) || - (error.getMessage() != null && error.getMessage().startsWith("Pipe closed"))) { + if (error instanceof EOFException) { level = Log.WARN; msgpfx = "Router closed connection: "; } else { @@ -631,7 +670,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa _log.warn("Error destroying the session", ipe); } } - _availabilityNotifier.stopNotifying(); + // SimpleSession does not initialize + if (_availabilityNotifier != null) + _availabilityNotifier.stopNotifying(); _closed = true; _closing = false; closeSocket(); @@ -649,6 +690,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa _reader.stopReading(); _reader = null; } + if (_queue != null) { + // internal + _queue.close(); + } if (_writer != null) { _writer.stopWriting(); _writer = null; @@ -666,7 +711,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } /** - * Recieve notification that the I2CP connection was disconnected + * The I2CPMessageEventListener callback. + * Recieve notification that the I2CP connection was disconnected. + * @param reader unused */ public void disconnected(I2CPMessageReader reader) { if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Disconnected", new Exception("Disconnected")); @@ -733,11 +780,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa buf.append(s); else buf.append(getClass().getSimpleName()); - buf.append(" #"); if (_sessionId != null) - buf.append(_sessionId.getSessionId()); - else - buf.append("n/a"); + buf.append(" #").append(_sessionId.getSessionId()); buf.append("]: "); return buf.toString(); } diff --git a/core/java/src/net/i2p/client/I2PSimpleSession.java b/core/java/src/net/i2p/client/I2PSimpleSession.java index f4bc3e812ca6d42f8f4c58be9985ec049ffdf56b..ed9ec5cc369e1a492bc4e0c80b0cb1d6c60f0b2b 100644 --- a/core/java/src/net/i2p/client/I2PSimpleSession.java +++ b/core/java/src/net/i2p/client/I2PSimpleSession.java @@ -19,8 +19,10 @@ import net.i2p.data.i2cp.DestLookupMessage; import net.i2p.data.i2cp.DestReplyMessage; import net.i2p.data.i2cp.GetBandwidthLimitsMessage; import net.i2p.data.i2cp.I2CPMessageReader; -import net.i2p.util.I2PThread; -import net.i2p.util.InternalSocket; +import net.i2p.internal.I2CPMessageQueue; +import net.i2p.internal.InternalClientManager; +import net.i2p.internal.QueuedI2CPMessageReader; +import net.i2p.util.I2PAppThread; /** * Create a new session for doing naming and bandwidth queries only. Do not create a Destination. @@ -44,12 +46,12 @@ class I2PSimpleSession extends I2PSessionImpl2 { * @throws I2PSessionException if there is a problem */ public I2PSimpleSession(I2PAppContext context, Properties options) throws I2PSessionException { + // Warning, does not call super() _context = context; _log = context.logManager().getLog(I2PSimpleSession.class); _handlerMap = new SimpleMessageHandlerMap(context); _closed = true; _closing = false; - _availabilityNotifier = new AvailabilityNotifier(); if (options == null) options = System.getProperties(); loadConfig(options); @@ -65,23 +67,32 @@ class I2PSimpleSession extends I2PSessionImpl2 { @Override public void connect() throws I2PSessionException { _closed = false; - _availabilityNotifier.stopNotifying(); - I2PThread notifier = new I2PThread(_availabilityNotifier); - notifier.setName("Simple Notifier"); - notifier.setDaemon(true); - notifier.start(); try { - // If we are in the router JVM, connect using the interal pseudo-socket - _socket = InternalSocket.getSocket(_hostname, _portNum); - _out = _socket.getOutputStream(); - synchronized (_out) { - _out.write(I2PClient.PROTOCOL_BYTE); - _out.flush(); + // If we are in the router JVM, connect using the interal queue + if (_context.isRouterContext()) { + // _socket, _out, and _writer remain null + InternalClientManager mgr = _context.internalClientManager(); + if (mgr == null) + throw new I2PSessionException("Router is not ready for connections"); + // the following may throw an I2PSessionException + _queue = mgr.connect(); + _reader = new QueuedI2CPMessageReader(_queue, this); + } else { + if (Boolean.valueOf(getOptions().getProperty(PROP_ENABLE_SSL)).booleanValue()) + _socket = I2CPSSLSocketFactory.createSocket(_context, _hostname, _portNum); + else + _socket = new Socket(_hostname, _portNum); + _out = _socket.getOutputStream(); + synchronized (_out) { + _out.write(I2PClient.PROTOCOL_BYTE); + _out.flush(); + } + _writer = new ClientWriterRunner(_out, this); + InputStream in = _socket.getInputStream(); + _reader = new I2CPMessageReader(in, this); } - _writer = new ClientWriterRunner(_out, this); - InputStream in = _socket.getInputStream(); - _reader = new I2CPMessageReader(in, this); + // we do not receive payload messages, so we do not need an AvailabilityNotifier _reader.startReading(); } catch (UnknownHostException uhe) { diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java index 057892b65fde0b8bf9976c7ade0d3044a0b36f0c..39633881e861456d1a22e232e1380d28b4400a4c 100644 --- a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java @@ -27,11 +27,11 @@ import net.i2p.util.Log; public class I2CPMessageReader { private final static Log _log = new Log(I2CPMessageReader.class); private InputStream _stream; - private I2CPMessageEventListener _listener; - private I2CPMessageReaderRunner _reader; - private Thread _readerThread; + protected I2CPMessageEventListener _listener; + protected I2CPMessageReaderRunner _reader; + protected Thread _readerThread; - private static volatile long __readerId = 0; + protected static volatile long __readerId = 0; public I2CPMessageReader(InputStream stream, I2CPMessageEventListener lsnr) { _stream = stream; @@ -42,6 +42,14 @@ public class I2CPMessageReader { _readerThread.setName("I2CP Reader " + (++__readerId)); } + /** + * For internal extension only. No stream. + * @since 0.8.3 + */ + protected I2CPMessageReader(I2CPMessageEventListener lsnr) { + setListener(lsnr); + } + public void setListener(I2CPMessageEventListener lsnr) { _listener = lsnr; } @@ -114,9 +122,9 @@ public class I2CPMessageReader { public void disconnected(I2CPMessageReader reader); } - private class I2CPMessageReaderRunner implements Runnable { - private volatile boolean _doRun; - private volatile boolean _stayAlive; + protected class I2CPMessageReaderRunner implements Runnable { + protected volatile boolean _doRun; + protected volatile boolean _stayAlive; public I2CPMessageReaderRunner() { _doRun = true; @@ -175,7 +183,8 @@ public class I2CPMessageReader { cancelRunner(); } } - if (!_doRun) { + // ??? unused + if (_stayAlive && !_doRun) { // pause .5 secs when we're paused try { Thread.sleep(500); diff --git a/core/java/src/net/i2p/internal/I2CPMessageQueue.java b/core/java/src/net/i2p/internal/I2CPMessageQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..93bea3a3f29b55ece440d048f2fdd31e21975de8 --- /dev/null +++ b/core/java/src/net/i2p/internal/I2CPMessageQueue.java @@ -0,0 +1,51 @@ +package net.i2p.internal; + +import net.i2p.data.i2cp.I2CPMessage; + +/** + * Contains the methods to talk to a router or client via I2CP, + * when both are in the same JVM. + * This interface contains methods to access two queues, + * one for transmission and one for receiving. + * The methods are identical to those in java.util.concurrent.BlockingQueue. + * + * Reading may be done in a thread using the QueuedI2CPMessageReader class. + * Non-blocking writing may be done directly with offer(). + * + * @author zzz + * @since 0.8.3 + */ +public abstract class I2CPMessageQueue { + + /** + * Send a message, nonblocking. + * @return success (false if no space available) + */ + public abstract boolean offer(I2CPMessage msg); + + /** + * Receive a message, nonblocking. + * Unused for now. + * @return message or null if none available + */ + public abstract I2CPMessage poll(); + + /** + * Send a message, blocking until space is available. + * Unused for now. + */ + public abstract void put(I2CPMessage msg) throws InterruptedException; + + /** + * Receive a message, blocking until one is available. + * @return message + */ + public abstract I2CPMessage take() throws InterruptedException; + + /** + * == offer(new PoisonI2CPMessage()); + */ + public void close() { + offer(new PoisonI2CPMessage()); + } +} diff --git a/core/java/src/net/i2p/internal/InternalClientManager.java b/core/java/src/net/i2p/internal/InternalClientManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a923fb9f706ac0069356f2df2516d65ed11f69c0 --- /dev/null +++ b/core/java/src/net/i2p/internal/InternalClientManager.java @@ -0,0 +1,19 @@ +package net.i2p.internal; + +import net.i2p.client.I2PSessionException; +import net.i2p.data.i2cp.I2CPMessage; + +/** + * A manager for the in-JVM I2CP message interface + * + * @author zzz + * @since 0.8.3 + */ +public interface InternalClientManager { + + /** + * Connect to the router, receiving a message queue to talk to the router with. + * @throws I2PSessionException if the router isn't ready + */ + public I2CPMessageQueue connect() throws I2PSessionException; +} diff --git a/core/java/src/net/i2p/internal/PoisonI2CPMessage.java b/core/java/src/net/i2p/internal/PoisonI2CPMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..dda1301ee620d281da3f63b2101c7d350a558140 --- /dev/null +++ b/core/java/src/net/i2p/internal/PoisonI2CPMessage.java @@ -0,0 +1,56 @@ +package net.i2p.internal; + +import java.io.InputStream; + +import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.data.i2cp.I2CPMessageImpl; + +/** + * For marking end-of-queues in a standard manner. + * Don't actually send it. + * + * @author zzz + * @since 0.8.3 + */ +public class PoisonI2CPMessage extends I2CPMessageImpl { + public final static int MESSAGE_TYPE = 999999; + + public PoisonI2CPMessage() { + super(); + } + + /** + * @deprecated don't do this + * @throws I2CPMessageException always + */ + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException { + throw new I2CPMessageException("Don't do this"); + } + + /** + * @deprecated don't do this + * @throws I2CPMessageException always + */ + protected byte[] doWriteMessage() throws I2CPMessageException { + throw new I2CPMessageException("Don't do this"); + } + + public int getType() { + return MESSAGE_TYPE; + } + + /* FIXME missing hashCode() method FIXME */ + @Override + public boolean equals(Object object) { + if ((object != null) && (object instanceof PoisonI2CPMessage)) { + return true; + } + + return false; + } + + @Override + public String toString() { + return "[PoisonMessage]"; + } +} diff --git a/core/java/src/net/i2p/internal/QueuedI2CPMessageReader.java b/core/java/src/net/i2p/internal/QueuedI2CPMessageReader.java new file mode 100644 index 0000000000000000000000000000000000000000..d713b678d4463a2ff617f54638a9e9500c8c421f --- /dev/null +++ b/core/java/src/net/i2p/internal/QueuedI2CPMessageReader.java @@ -0,0 +1,62 @@ +package net.i2p.internal; + +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.I2CPMessageReader; +import net.i2p.util.I2PThread; + +/** + * Get messages off an In-JVM queue, zero-copy + * + * @author zzz + * @since 0.8.3 + */ +public class QueuedI2CPMessageReader extends I2CPMessageReader { + private final I2CPMessageQueue in; + + public QueuedI2CPMessageReader(I2CPMessageQueue in, I2CPMessageEventListener lsnr) { + super(lsnr); + this.in = in; + _reader = new QueuedI2CPMessageReaderRunner(); + _readerThread = new I2PThread(_reader, "I2CP Internal Reader " + (++__readerId), true); + } + + protected class QueuedI2CPMessageReaderRunner extends I2CPMessageReaderRunner implements Runnable { + + public QueuedI2CPMessageReaderRunner() { + super(); + } + + @Override + public void cancelRunner() { + super.cancelRunner(); + _readerThread.interrupt(); + } + + @Override + public void run() { + while (_stayAlive) { + while (_doRun) { + // do read + I2CPMessage msg = null; + try { + msg = in.take(); + if (msg.getType() == PoisonI2CPMessage.MESSAGE_TYPE) + cancelRunner(); + else + _listener.messageReceived(QueuedI2CPMessageReader.this, msg); + } catch (InterruptedException ie) {} + } + // ??? unused + if (_stayAlive && !_doRun) { + // pause .5 secs when we're paused + try { + Thread.sleep(500); + } catch (InterruptedException ie) { + _listener.disconnected(QueuedI2CPMessageReader.this); + cancelRunner(); + } + } + } + } + } +} diff --git a/core/java/src/net/i2p/internal/package.html b/core/java/src/net/i2p/internal/package.html new file mode 100644 index 0000000000000000000000000000000000000000..edac509f01e1887342b677e561225ba20a3de00f --- /dev/null +++ b/core/java/src/net/i2p/internal/package.html @@ -0,0 +1,7 @@ +<html><body> +<p> +Interface and classes for a router and client +within the same JVM to directly pass I2CP messages using Queues +instead of serialized messages over socket streams. +</p> +</body></html> diff --git a/core/java/src/net/i2p/util/FileUtil.java b/core/java/src/net/i2p/util/FileUtil.java index b56f197ab96c47142296125dc7b43196507e7d65..b5bb48a4353fef584bc6088f0408df34d97f5568 100644 --- a/core/java/src/net/i2p/util/FileUtil.java +++ b/core/java/src/net/i2p/util/FileUtil.java @@ -9,6 +9,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; @@ -16,13 +18,11 @@ import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -// Pack200 import -// you must also uncomment the correct line in unpack() below -// For gcj, gij, etc., comment both out +// Pack200 now loaded dynamically in unpack() below // // For Sun, OpenJDK, IcedTea, etc, use this -import java.util.jar.Pack200; - +//import java.util.jar.Pack200; +// // For Apache Harmony or if you put its pack200.jar in your library directory use this //import org.apache.harmony.unpack200.Archive; @@ -231,37 +231,79 @@ public class FileUtil { } /** - * This won't work right if one of the two options in unpack() is commented out. + * Public since 0.8.3 * @since 0.8.1 */ - private static boolean isPack200Supported() { + public static boolean isPack200Supported() { try { Class.forName("java.util.jar.Pack200", false, ClassLoader.getSystemClassLoader()); return true; } catch (Exception e) {} try { - Class.forName("org.apache.harmony.pack200.Archive", false, ClassLoader.getSystemClassLoader()); + Class.forName("org.apache.harmony.unpack200.Archive", false, ClassLoader.getSystemClassLoader()); return true; } catch (Exception e) {} return false; } + private static boolean _failedOracle; + private static boolean _failedApache; + /** + * Unpack using either Oracle or Apache's unpack200 library, + * with the classes discovered at runtime so neither is required at compile time. + * * Caller must close streams + * @throws IOException on unpack error or if neither library is available. + * Will not throw ClassNotFoundException. + * @throws org.apache.harmony.pack200.Pack200Exception which is not an IOException * @since 0.8.1 */ private static void unpack(InputStream in, JarOutputStream out) throws Exception { // For Sun, OpenJDK, IcedTea, etc, use this - Pack200.newUnpacker().unpack(in, out); + //Pack200.newUnpacker().unpack(in, out); + if (!_failedOracle) { + try { + Class p200 = Class.forName("java.util.jar.Pack200", true, ClassLoader.getSystemClassLoader()); + Method newUnpacker = p200.getMethod("newUnpacker", (Class[]) null); + Object unpacker = newUnpacker.invoke(null,(Object[]) null); + Method unpack = unpacker.getClass().getMethod("unpack", new Class[] {InputStream.class, JarOutputStream.class}); + // throws IOException + unpack.invoke(unpacker, new Object[] {in, out}); + return; + } catch (ClassNotFoundException e) { + _failedOracle = true; + //e.printStackTrace(); + } catch (NoSuchMethodException e) { + _failedOracle = true; + //e.printStackTrace(); + } + } // ------------------ // For Apache Harmony or if you put its pack200.jar in your library directory use this //(new Archive(in, out)).unpack(); - + if (!_failedApache) { + try { + Class p200 = Class.forName("org.apache.harmony.unpack200.Archive", true, ClassLoader.getSystemClassLoader()); + Constructor newUnpacker = p200.getConstructor(new Class[] {InputStream.class, JarOutputStream.class}); + Object unpacker = newUnpacker.newInstance(new Object[] {in, out}); + Method unpack = unpacker.getClass().getMethod("unpack", (Class[]) null); + // throws IOException or Pack200Exception + unpack.invoke(unpacker, (Object[]) null); + return; + } catch (ClassNotFoundException e) { + _failedApache = true; + //e.printStackTrace(); + } catch (NoSuchMethodException e) { + _failedApache = true; + //e.printStackTrace(); + } + } // ------------------ // For gcj, gij, etc., use this - //throw new IOException("Pack200 not supported"); + throw new IOException("Unpack200 not supported"); } /** @@ -378,12 +420,13 @@ public class FileUtil { } /** - * Usage: FileUtil (delete path | copy source dest) + * Usage: FileUtil (delete path | copy source dest | unzip path.zip) * */ public static void main(String args[]) { if ( (args == null) || (args.length < 2) ) { - testRmdir(); + System.err.println("Usage: delete path | copy source dest | unzip path.zip"); + //testRmdir(); } else if ("delete".equals(args[0])) { boolean deleted = FileUtil.rmdir(args[1], false); if (!deleted) @@ -407,6 +450,7 @@ public class FileUtil { } } + /***** private static void testRmdir() { File t = new File("rmdirTest/test/subdir/blah"); boolean created = t.mkdirs(); @@ -417,4 +461,5 @@ public class FileUtil { else System.out.println("PASS: rmdirTest deleted"); } + *****/ } diff --git a/core/java/src/net/i2p/util/I2PThread.java b/core/java/src/net/i2p/util/I2PThread.java index c21c66f6bebbacd73094b8cdc45118185d261852..9b76b8fc9c92c9261c1e2338e3c5a05931f4e9fa 100644 --- a/core/java/src/net/i2p/util/I2PThread.java +++ b/core/java/src/net/i2p/util/I2PThread.java @@ -61,8 +61,8 @@ public class I2PThread extends Thread { _createdBy = new Exception("Created by"); } - private void log(int level, String msg) { log(level, msg, null); } - private void log(int level, String msg, Throwable t) { + private static void log(int level, String msg) { log(level, msg, null); } + private static void log(int level, String msg, Throwable t) { // we cant assume log is created if (_log == null) _log = new Log(I2PThread.class); if (_log.shouldLog(level)) @@ -72,12 +72,12 @@ public class I2PThread extends Thread { @Override public void run() { _name = Thread.currentThread().getName(); - log(Log.DEBUG, "New thread started: " + _name, _createdBy); + log(Log.INFO, "New thread started" + (isDaemon() ? " (daemon): " : ": ") + _name, _createdBy); try { super.run(); } catch (Throwable t) { try { - log(Log.CRIT, "Killing thread " + getName(), t); + log(Log.CRIT, "Thread terminated unexpectedly: " + getName(), t); } catch (Throwable woof) { System.err.println("Died within the OOM itself"); t.printStackTrace(); @@ -85,12 +85,12 @@ public class I2PThread extends Thread { if (t instanceof OutOfMemoryError) fireOOM((OutOfMemoryError)t); } - log(Log.DEBUG, "Thread finished gracefully: " + _name); + log(Log.INFO, "Thread finished normally: " + _name); } @Override protected void finalize() throws Throwable { - log(Log.DEBUG, "Thread finalized: " + _name); + //log(Log.DEBUG, "Thread finalized: " + _name); super.finalize(); } diff --git a/core/java/src/net/i2p/util/ShellCommand.java b/core/java/src/net/i2p/util/ShellCommand.java index d6fce002192ce4cbac1e67973ebe693971202b27..12b668f67ee7b9345b4d3c301e3969b182c7f96c 100644 --- a/core/java/src/net/i2p/util/ShellCommand.java +++ b/core/java/src/net/i2p/util/ShellCommand.java @@ -51,17 +51,18 @@ public class ShellCommand { */ private class CommandThread extends Thread { - final Object caller; - boolean consumeOutput; - String shellCommand; - - CommandThread(Object caller, String shellCommand, boolean consumeOutput) { + private final Object caller; + private final boolean consumeOutput; + private final Object shellCommand; + + /** + * @param shellCommand either a String or a String[] (since 0.8.3) + */ + CommandThread(Object caller, Object shellCommand, boolean consumeOutput) { super("CommandThread"); this.caller = caller; this.shellCommand = shellCommand; this.consumeOutput = consumeOutput; - _commandSuccessful = false; - _commandCompleted = false; } @Override @@ -200,6 +201,9 @@ public class ShellCommand { * {@link #getErrorStream()}, respectively. Input can be passed to the * <code>STDIN</code> of the shell process via {@link #getInputStream()}. * + * Warning, no good way to quote or escape spaces in arguments with this method. + * @deprecated unused + * * @param shellCommand The command for the shell to execute. */ public void execute(String shellCommand) { @@ -215,6 +219,9 @@ public class ShellCommand { * Input can be passed to the <code>STDIN</code> of the shell process via * {@link #getInputStream()}. * + * Warning, no good way to quote or escape spaces in arguments with this method. + * @deprecated unused + * * @param shellCommand The command for the shell to execute. * @return <code>true</code> if the spawned shell process * returns an exit status of 0 (indicating success), @@ -237,6 +244,9 @@ public class ShellCommand { * {@link #getErrorStream()}, respectively. Input can be passed to the * <code>STDIN</code> of the shell process via {@link #getInputStream()}. * + * Warning, no good way to quote or escape spaces in arguments with this method. + * @deprecated unused + * * @param shellCommand The command for the shell to execute. * @param seconds The method will return <code>true</code> if this * number of seconds elapses without the process @@ -276,6 +286,9 @@ public class ShellCommand { * without waiting for an exit status. Any output produced by the executed * command will not be displayed. * + * Warning, no good way to quote or escape spaces in arguments with this method. + * @deprecated unused + * * @param shellCommand The command for the shell to execute. * @throws IOException */ @@ -288,6 +301,8 @@ public class ShellCommand { * all of the command's resulting shell processes have completed. Any output * produced by the executed command will not be displayed. * + * Warning, no good way to quote or escape spaces in arguments with this method. + * * @param shellCommand The command for the shell to execute. * @return <code>true</code> if the spawned shell process * returns an exit status of 0 (indicating success), @@ -307,7 +322,12 @@ public class ShellCommand { * specified number of seconds has elapsed first. Any output produced by the * executed command will not be displayed. * - * @param shellCommand The command for the shell to execute. + * Warning, no good way to quote or escape spaces in arguments when shellCommand is a String. + * Use a String array for best results, especially on Windows. + * + * @param shellCommand The command for the shell to execute, as a String. + * You can't quote arguments successfully. + * See Runtime.exec(String) for more info. * @param seconds The method will return <code>true</code> if this * number of seconds elapses without the process * returning an exit status. A value of <code>0</code> @@ -317,7 +337,33 @@ public class ShellCommand { * else <code>false</code>. */ public synchronized boolean executeSilentAndWaitTimed(String shellCommand, int seconds) { + return executeSAWT(shellCommand, seconds); + } + /** + * Passes a command to the shell for execution. This method blocks until + * all of the command's resulting shell processes have completed unless a + * specified number of seconds has elapsed first. Any output produced by the + * executed command will not be displayed. + * + * @param commandArray The command for the shell to execute, + * as a String[]. + * See Runtime.exec(String[]) for more info. + * @param seconds The method will return <code>true</code> if this + * number of seconds elapses without the process + * returning an exit status. A value of <code>0</code> + * here disables waiting. + * @return <code>true</code> if the spawned shell process + * returns an exit status of 0 (indicating success), + * else <code>false</code>. + * @since 0.8.3 + */ + public synchronized boolean executeSilentAndWaitTimed(String[] commandArray, int seconds) { + return executeSAWT(commandArray, seconds); + } + + /** @since 0.8.3 */ + private boolean executeSAWT(Object shellCommand, int seconds) { _commandThread = new CommandThread(this, shellCommand, CONSUME_OUTPUT); _commandThread.start(); try { @@ -364,7 +410,10 @@ public class ShellCommand { return; } - private boolean execute(String shellCommand, boolean consumeOutput, boolean waitForExitStatus) { + /** + * @param shellCommand either a String or a String[] (since 0.8.3) - quick hack + */ + private boolean execute(Object shellCommand, boolean consumeOutput, boolean waitForExitStatus) { StreamConsumer processStderrConsumer; StreamConsumer processStdoutConsumer; @@ -374,7 +423,13 @@ public class ShellCommand { StreamReader processStdoutReader; try { - _process = Runtime.getRuntime().exec(shellCommand, null); + // easy way so we don't have to copy this whole method + if (shellCommand instanceof String) + _process = Runtime.getRuntime().exec((String)shellCommand); + else if (shellCommand instanceof String[]) + _process = Runtime.getRuntime().exec((String[])shellCommand); + else + throw new ClassCastException("shell command must be a String or a String[]"); if (consumeOutput) { processStderrConsumer = new StreamConsumer(_process.getErrorStream()); processStderrConsumer.start(); diff --git a/core/java/src/net/i2p/util/SimpleScheduler.java b/core/java/src/net/i2p/util/SimpleScheduler.java index ee7d36e99a609d33555ef874adb5f0b1a6f4b1af..f764debe99f6cad3d730c64bae342b9679011d13 100644 --- a/core/java/src/net/i2p/util/SimpleScheduler.java +++ b/core/java/src/net/i2p/util/SimpleScheduler.java @@ -28,12 +28,14 @@ import net.i2p.I2PAppContext; public class SimpleScheduler { private static final SimpleScheduler _instance = new SimpleScheduler(); public static SimpleScheduler getInstance() { return _instance; } - private static final int THREADS = 4; + private static final int MIN_THREADS = 2; + private static final int MAX_THREADS = 4; private I2PAppContext _context; private Log _log; private ScheduledThreadPoolExecutor _executor; private String _name; private int _count; + private final int _threads; protected SimpleScheduler() { this("SimpleScheduler"); } protected SimpleScheduler(String name) { @@ -41,7 +43,9 @@ public class SimpleScheduler { _log = _context.logManager().getLog(SimpleScheduler.class); _name = name; _count = 0; - _executor = new ScheduledThreadPoolExecutor(THREADS, new CustomThreadFactory()); + long maxMemory = Runtime.getRuntime().maxMemory(); + _threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024)))); + _executor = new ScheduledThreadPoolExecutor(_threads, new CustomThreadFactory()); _executor.prestartAllCoreThreads(); } @@ -65,6 +69,13 @@ public class SimpleScheduler { re.schedule(); } + /** + * Queue up the given event to be fired after timeoutMs and every + * timeoutMs thereafter. The TimedEvent must not do its own rescheduling. + * As all Exceptions are caught in run(), these will not prevent + * subsequent executions (unlike SimpleTimer, where the TimedEvent does + * its own rescheduling). + */ public void addPeriodicEvent(SimpleTimer.TimedEvent event, long timeoutMs) { addPeriodicEvent(event, timeoutMs, timeoutMs); } @@ -90,7 +101,7 @@ public class SimpleScheduler { private class CustomThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread rv = Executors.defaultThreadFactory().newThread(r); - rv.setName(_name + ' ' + (++_count) + '/' + THREADS); + rv.setName(_name + ' ' + (++_count) + '/' + _threads); // Uncomment this to test threadgrouping, but we should be all safe now that the constructor preallocates! // String name = rv.getThreadGroup().getName(); // if(!name.equals("main")) { diff --git a/core/java/src/net/i2p/util/SimpleTimer.java b/core/java/src/net/i2p/util/SimpleTimer.java index 6a8b855e305f3cfc892c78c4306de871c1346e73..0b543071199669c8ea3e802ead0b4aae82c7b254 100644 --- a/core/java/src/net/i2p/util/SimpleTimer.java +++ b/core/java/src/net/i2p/util/SimpleTimer.java @@ -18,14 +18,16 @@ import net.i2p.I2PAppContext; public class SimpleTimer { private static final SimpleTimer _instance = new SimpleTimer(); public static SimpleTimer getInstance() { return _instance; } - private I2PAppContext _context; - private Log _log; + private final I2PAppContext _context; + private final Log _log; /** event time (Long) to event (TimedEvent) mapping */ private final TreeMap _events; /** event (TimedEvent) to event time (Long) mapping */ private Map _eventTimes; private final List _readyEvents; private SimpleStore runn; + private static final int MIN_THREADS = 2; + private static final int MAX_THREADS = 4; protected SimpleTimer() { this("SimpleTimer"); } protected SimpleTimer(String name) { @@ -39,9 +41,11 @@ public class SimpleTimer { runner.setName(name); runner.setDaemon(true); runner.start(); - for (int i = 0; i < 3; i++) { + long maxMemory = Runtime.getRuntime().maxMemory(); + int threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024)))); + for (int i = 1; i <= threads ; i++) { I2PThread executor = new I2PThread(new Executor(_context, _log, _readyEvents, runn)); - executor.setName(name + "Executor " + i); + executor.setName(name + "Executor " + i + '/' + threads); executor.setDaemon(true); executor.start(); } diff --git a/core/java/src/net/i2p/util/SimpleTimer2.java b/core/java/src/net/i2p/util/SimpleTimer2.java index b2af33cf2be15bd04606b720e0adce550bf426bf..bda41e6211b5a58c8ebd483d4118094eaa9e4e60 100644 --- a/core/java/src/net/i2p/util/SimpleTimer2.java +++ b/core/java/src/net/i2p/util/SimpleTimer2.java @@ -27,12 +27,14 @@ import net.i2p.I2PAppContext; public class SimpleTimer2 { private static final SimpleTimer2 _instance = new SimpleTimer2(); public static SimpleTimer2 getInstance() { return _instance; } - private static final int THREADS = 4; + private static final int MIN_THREADS = 2; + private static final int MAX_THREADS = 4; private I2PAppContext _context; private static Log _log; // static so TimedEvent can use it private ScheduledThreadPoolExecutor _executor; private String _name; private int _count; + private final int _threads; protected SimpleTimer2() { this("SimpleTimer2"); } protected SimpleTimer2(String name) { @@ -40,7 +42,9 @@ public class SimpleTimer2 { _log = _context.logManager().getLog(SimpleTimer2.class); _name = name; _count = 0; - _executor = new CustomScheduledThreadPoolExecutor(THREADS, new CustomThreadFactory()); + long maxMemory = Runtime.getRuntime().maxMemory(); + _threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024)))); + _executor = new CustomScheduledThreadPoolExecutor(_threads, new CustomThreadFactory()); _executor.prestartAllCoreThreads(); } @@ -67,7 +71,7 @@ public class SimpleTimer2 { private class CustomThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread rv = Executors.defaultThreadFactory().newThread(r); - rv.setName(_name + ' ' + (++_count) + '/' + THREADS); + rv.setName(_name + ' ' + (++_count) + '/' + _threads); // Uncomment this to test threadgrouping, but we should be all safe now that the constructor preallocates! // String name = rv.getThreadGroup().getName(); // if(!name.equals("main")) { diff --git a/installer/resources/clients.config b/installer/resources/clients.config index f82aec52687e5d872b266deecefba6e48c7d335e..08c6c62ba88e0e01d1d90b64e04f48f29e59ed30 100644 --- a/installer/resources/clients.config +++ b/installer/resources/clients.config @@ -6,6 +6,21 @@ # # fire up the web console +## There are several choices, here are some examples: +## non-SSL, bind to local IPv4 only +#clientApp.0.args=7657 127.0.0.1 ./webapps/ +## non-SSL, bind to local IPv6 only +#clientApp.0.args=7657 ::1 ./webapps/ +## non-SSL, bind to all IPv4 addresses +#clientApp.0.args=7657 0.0.0.0 ./webapps/ +## non-SSL, bind to all IPv6 addresses +#clientApp.0.args=7657 :: ./webapps/ +## For SSL only, change clientApp.4.args below to https:// +## SSL only +#clientApp.0.args=-s 7657 ::1,127.0.0.1 ./webapps/ +## non-SSL and SSL +#clientApp.0.args=7657 ::1,127.0.0.1 -s 7667 ::1,127.0.0.1 ./webapps/ +## non-SSL only, both IPv6 and IPv4 local interfaces clientApp.0.args=7657 ::1,127.0.0.1 ./webapps/ clientApp.0.main=net.i2p.router.web.RouterConsoleRunner clientApp.0.name=I2P Router Console diff --git a/installer/resources/jetty.xml b/installer/resources/jetty.xml index d82cf5580f0a5c7cfee11978d46cb99eb57d6c23..29900cb6b07975af1d9488ce9a82721971bc9c84 100644 --- a/installer/resources/jetty.xml +++ b/installer/resources/jetty.xml @@ -71,17 +71,29 @@ <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> - <!-- Add a HTTPS SSL listener on port 8443 --> + <!-- Add a HTTPS SSL listener on port 8443 --> + <!-- --> + <!-- In the unlikely event you would want SSL support for your eepsite. --> + <!-- You would need to generate a selfsigned certificate in a keystore --> + <!-- in ~/.i2p/eepsite/keystore.ks, for example with the command line: --> + <!-- + keytool -genkey -storetype JKS -keystore ~/.i2p/eepsite/keystore.ks -storepass changeit -alias console -dname CN=xyz123.eepsite.i2p.net,OU=Eepsite,O=I2P Anonymous Network,L=XX,ST=XX,C=XX -validity 3650 -keyalg DSA -keysize 1024 -keypass myKeyPassword + --> + <!-- Change the CN and key password in the example, of course. --> + <!-- You wouldn't want to open this up to the regular internet, --> + <!-- would you?? Untested and not recommended. --> <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> <!-- UNCOMMENT TO ACTIVATE <Call name="addListener"> <Arg> - <New class="org.mortbay.http.SunJsseListener"> + <New class="org.mortbay.http.SslListener"> <Set name="Port">8443</Set> <Set name="PoolName">main</Set> - <Set name="Keystore"><SystemProperty name="jetty.home" default="."/>/etc/demokeystore</Set> - <Set name="Password">OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4</Set> - <Set name="KeyPassword">OBF:1u2u1wml1z7s1z7a1wnl1u2g</Set> + <Set name="Keystore">./eepsite/keystore.ks</Set> + <!-- the keystore password --> + <Set name="Password">changeit</Set> + <!-- the X.509 certificate password --> + <Set name="KeyPassword">myKeyPassword</Set> <Set name="NonPersistentUserAgent">MSIE 5</Set> </New> </Arg> diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java b/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java index fd3e7063bd290ded9ab4cfcaac8d752fb6138f09..74b93b34f4f4d31862a659b6f781f2d294d23a87 100644 --- a/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java +++ b/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java @@ -161,7 +161,8 @@ public class I2NPMessageReader { cancelRunner(); } } - if (!_doRun) { + // ??? unused + if (_stayAlive && !_doRun) { // pause .5 secs when we're paused try { Thread.sleep(500); } catch (InterruptedException ie) {} } diff --git a/router/java/src/net/i2p/router/JobQueue.java b/router/java/src/net/i2p/router/JobQueue.java index a8b5395b00c6c5cb114ad2b4776aed8ac02fe0fb..42ddadf5ab7e8704d1d9ff041b613311508a4d01 100644 --- a/router/java/src/net/i2p/router/JobQueue.java +++ b/router/java/src/net/i2p/router/JobQueue.java @@ -395,10 +395,8 @@ public class JobQueue { for (int i = _queueRunners.size(); i < numThreads; i++) { JobQueueRunner runner = new JobQueueRunner(_context, i); _queueRunners.put(Integer.valueOf(i), runner); - Thread t = new I2PThread(runner); - t.setName("JobQueue"+(_runnerId++)); + Thread t = new I2PThread(runner, "JobQueue " + (++_runnerId) + '/' + numThreads, false); //t.setPriority(I2PThread.MAX_PRIORITY-1); - t.setDaemon(false); t.start(); } } else if (_queueRunners.size() == numThreads) { diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 7f8b2788ea70983f9fcee6ce57226034e13b4c6f..5904d04ea0b6bc0412086b16ff3e98d1a7844696 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -1281,11 +1281,7 @@ public class Router { */ private void beginMarkingLiveliness() { File f = getPingFile(); - // not an I2PThread for context creation issues - Thread t = new Thread(new MarkLiveliness(_context, this, f)); - t.setName("Mark router liveliness"); - t.setDaemon(true); - t.start(); + SimpleScheduler.getInstance().addPeriodicEvent(new MarkLiveliness(this, f), 0, LIVELINESS_DELAY); } public static final String PROP_BANDWIDTH_SHARE_PERCENTAGE = "router.sharePercentage"; @@ -1523,22 +1519,24 @@ private static class UpdateRoutingKeyModifierJob extends JobImpl { } } -private static class MarkLiveliness implements Runnable { - private RouterContext _context; +/** + * Write a timestamp to the ping file where the wrapper can see it + */ +private static class MarkLiveliness implements SimpleTimer.TimedEvent { private Router _router; private File _pingFile; - public MarkLiveliness(RouterContext ctx, Router router, File pingFile) { - _context = ctx; + + public MarkLiveliness(Router router, File pingFile) { _router = router; _pingFile = pingFile; - } - public void run() { _pingFile.deleteOnExit(); - do { + } + + public void timeReached() { + if (_router.isAlive()) ping(); - try { Thread.sleep(Router.LIVELINESS_DELAY); } catch (InterruptedException ie) {} - } while (_router.isAlive()); - _pingFile.delete(); + else + _pingFile.delete(); } private void ping() { diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 3d5ed609edb65cb900a9ebc68bad1cf9af8b93ce..cb3c6366251378c0cf50972d197c446bc6d960f3 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -6,6 +6,7 @@ import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.data.Hash; +import net.i2p.internal.InternalClientManager; import net.i2p.router.client.ClientManagerFacadeImpl; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.router.peermanager.Calculator; @@ -34,7 +35,7 @@ import net.i2p.util.KeyRing; */ public class RouterContext extends I2PAppContext { private Router _router; - private ClientManagerFacade _clientManagerFacade; + private ClientManagerFacadeImpl _clientManagerFacade; private ClientMessagePool _clientMessagePool; private JobQueue _jobQueue; private InNetMessagePool _inNetMessagePool; @@ -106,10 +107,12 @@ public class RouterContext extends I2PAppContext { } public void initAll() { - if ("false".equals(getProperty("i2p.dummyClientFacade", "false"))) - _clientManagerFacade = new ClientManagerFacadeImpl(this); - else - _clientManagerFacade = new DummyClientManagerFacade(this); + if (getBooleanProperty("i2p.dummyClientFacade")) + System.err.println("i2p.dummpClientFacade currently unsupported"); + _clientManagerFacade = new ClientManagerFacadeImpl(this); + // removed since it doesn't implement InternalClientManager for now + //else + // _clientManagerFacade = new DummyClientManagerFacade(this); _clientMessagePool = new ClientMessagePool(this); _jobQueue = new JobQueue(this); _inNetMessagePool = new InNetMessagePool(this); @@ -395,4 +398,13 @@ public class RouterContext extends I2PAppContext { public boolean isRouterContext() { return true; } + + /** + * Use this to connect to the router in the same JVM. + * @return the client manager + * @since 0.8.3 + */ + public InternalClientManager internalClientManager() { + return _clientManagerFacade; + } } diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java index b3468e4e0be51c575d0998569cdea4de1fa83165..8bef2776d5a5a3687da21a6fe72eb35049adcddb 100644 --- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java +++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java @@ -50,9 +50,9 @@ import net.i2p.util.SimpleTimer; * * @author jrandom */ -public class ClientConnectionRunner { +class ClientConnectionRunner { private Log _log; - private RouterContext _context; + protected final RouterContext _context; private ClientManager _manager; /** socket for this particular peer connection */ private Socket _socket; @@ -71,7 +71,7 @@ public class ClientConnectionRunner { /** set of messageIds created but not yet ACCEPTED */ private Set<MessageId> _acceptedPending; /** thingy that does stuff */ - private I2CPMessageReader _reader; + protected I2CPMessageReader _reader; /** just for this destination */ private SessionKeyManager _sessionKeyManager; /** @@ -109,7 +109,7 @@ public class ClientConnectionRunner { */ public void startRunning() { try { - _reader = new I2CPMessageReader(_socket.getInputStream(), new ClientMessageEventListener(_context, this)); + _reader = new I2CPMessageReader(_socket.getInputStream(), new ClientMessageEventListener(_context, this, true)); _writer = new ClientWriterRunner(_context, this); I2PThread t = new I2PThread(_writer); t.setName("I2CP Writer " + ++__id); @@ -469,18 +469,8 @@ public class ClientConnectionRunner { _log.warn("Error sending I2CP message - client went away", eofe); stopRunning(); } catch (IOException ioe) { - // only warn if client went away - int level; - String emsg; - if (ioe.getMessage() != null && ioe.getMessage().startsWith("Pipe closed")) { - level = Log.WARN; - emsg = "Error sending I2CP message - client went away"; - } else { - level = Log.ERROR; - emsg = "IO Error sending I2CP message to client"; - } - if (_log.shouldLog(level)) - _log.log(level, emsg, ioe); + if (_log.shouldLog(Log.ERROR)) + _log.error("IO Error sending I2CP message to client", ioe); stopRunning(); } catch (Throwable t) { _log.log(Log.CRIT, "Unhandled exception sending I2CP message to client", t); diff --git a/router/java/src/net/i2p/router/client/ClientListenerRunner.java b/router/java/src/net/i2p/router/client/ClientListenerRunner.java index 7a7d448ea99d10bf1ec51ee2953142ca17c915f4..5dc5c650686d47b7425e403614f3e2f4d0c7e848 100644 --- a/router/java/src/net/i2p/router/client/ClientListenerRunner.java +++ b/router/java/src/net/i2p/router/client/ClientListenerRunner.java @@ -24,13 +24,13 @@ import net.i2p.util.Log; * * @author jrandom */ -public class ClientListenerRunner implements Runnable { - protected Log _log; - protected RouterContext _context; - protected ClientManager _manager; +class ClientListenerRunner implements Runnable { + protected final Log _log; + protected final RouterContext _context; + protected final ClientManager _manager; protected ServerSocket _socket; - protected int _port; - private boolean _bindAllInterfaces; + protected final int _port; + protected final boolean _bindAllInterfaces; protected boolean _running; protected boolean _listening; @@ -38,18 +38,33 @@ public class ClientListenerRunner implements Runnable { public ClientListenerRunner(RouterContext context, ClientManager manager, int port) { _context = context; - _log = _context.logManager().getLog(ClientListenerRunner.class); + _log = _context.logManager().getLog(getClass()); _manager = manager; _port = port; - - String val = context.getProperty(BIND_ALL_INTERFACES); - _bindAllInterfaces = Boolean.valueOf(val).booleanValue(); + _bindAllInterfaces = context.getBooleanProperty(BIND_ALL_INTERFACES); } - public void setPort(int port) { _port = port; } - public int getPort() { return _port; } public boolean isListening() { return _running && _listening; } + /** + * Get a ServerSocket. + * Split out so it can be overridden for SSL. + * @since 0.8.3 + */ + protected ServerSocket getServerSocket() throws IOException { + if (_bindAllInterfaces) { + if (_log.shouldLog(Log.INFO)) + _log.info("Listening on port " + _port + " on all interfaces"); + return new ServerSocket(_port); + } else { + String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST, + ClientManagerFacadeImpl.DEFAULT_HOST); + if (_log.shouldLog(Log.INFO)) + _log.info("Listening on port " + _port + " of the specific interface: " + listenInterface); + return new ServerSocket(_port, 0, InetAddress.getByName(listenInterface)); + } + } + /** * Start up the socket listener, listens for connections, and * fires those connections off via {@link #runConnection runConnection}. @@ -62,18 +77,7 @@ public class ClientListenerRunner implements Runnable { int curDelay = 1000; while (_running) { try { - if (_bindAllInterfaces) { - if (_log.shouldLog(Log.INFO)) - _log.info("Listening on port " + _port + " on all interfaces"); - _socket = new ServerSocket(_port); - } else { - String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST, - ClientManagerFacadeImpl.DEFAULT_HOST); - if (_log.shouldLog(Log.INFO)) - _log.info("Listening on port " + _port + " of the specific interface: " + listenInterface); - _socket = new ServerSocket(_port, 0, InetAddress.getByName(listenInterface)); - } - + _socket = getServerSocket(); if (_log.shouldLog(Log.DEBUG)) _log.debug("ServerSocket created, before accept: " + _socket); @@ -131,7 +135,8 @@ public class ClientListenerRunner implements Runnable { } /** give the i2cp client 5 seconds to show that they're really i2cp clients */ - private final static int CONNECT_TIMEOUT = 5*1000; + protected final static int CONNECT_TIMEOUT = 5*1000; + private final static int LOOP_DELAY = 250; /** * Verify the first byte. @@ -141,16 +146,17 @@ public class ClientListenerRunner implements Runnable { protected boolean validate(Socket socket) { try { InputStream is = socket.getInputStream(); - for (int i = 0; i < 20; i++) { + for (int i = 0; i < CONNECT_TIMEOUT / LOOP_DELAY; i++) { if (is.available() > 0) return is.read() == I2PClient.PROTOCOL_BYTE; - try { Thread.sleep(250); } catch (InterruptedException ie) {} + try { Thread.sleep(LOOP_DELAY); } catch (InterruptedException ie) {} } } catch (IOException ioe) {} if (_log.shouldLog(Log.WARN)) _log.warn("Peer did not authenticate themselves as I2CP quickly enough, dropping"); return false; } + /** * Handle the connection by passing it off to a {@link ClientConnectionRunner ClientConnectionRunner} * diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java index 7d866ab0b650e961ecd763be04d41434721291b4..a534bdfb19627fb9b9acab93d67a061167159959 100644 --- a/router/java/src/net/i2p/router/client/ClientManager.java +++ b/router/java/src/net/i2p/router/client/ClientManager.java @@ -15,7 +15,9 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; +import net.i2p.client.I2PSessionException; import net.i2p.crypto.SessionKeyManager; import net.i2p.data.DataHelper; import net.i2p.data.Destination; @@ -23,8 +25,10 @@ import net.i2p.data.Hash; import net.i2p.data.LeaseSet; import net.i2p.data.Payload; import net.i2p.data.TunnelId; +import net.i2p.data.i2cp.I2CPMessage; import net.i2p.data.i2cp.MessageId; import net.i2p.data.i2cp.SessionConfig; +import net.i2p.internal.I2CPMessageQueue; import net.i2p.router.ClientManagerFacade; import net.i2p.router.ClientMessage; import net.i2p.router.Job; @@ -39,13 +43,18 @@ import net.i2p.util.Log; * * @author jrandom */ -public class ClientManager { - private Log _log; +class ClientManager { + private final Log _log; private ClientListenerRunner _listener; - private ClientListenerRunner _internalListener; private final HashMap<Destination, ClientConnectionRunner> _runners; // Destination --> ClientConnectionRunner private final Set<ClientConnectionRunner> _pendingRunners; // ClientConnectionRunner for clients w/out a Dest yet - private RouterContext _ctx; + private final RouterContext _ctx; + private boolean _isStarted; + + /** Disable external interface, allow internal clients only @since 0.8.3 */ + private static final String PROP_DISABLE_EXTERNAL = "i2cp.disableInterface"; + /** SSL interface (only) @since 0.8.3 */ + private static final String PROP_ENABLE_SSL = "i2cp.SSL"; /** ms to wait before rechecking for inbound messages to deliver to clients */ private final static int INBOUND_POLL_INTERVAL = 300; @@ -53,10 +62,10 @@ public class ClientManager { public ClientManager(RouterContext context, int port) { _ctx = context; _log = context.logManager().getLog(ClientManager.class); - _ctx.statManager().createRateStat("client.receiveMessageSize", - "How large are messages received by the client?", - "ClientMessages", - new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + //_ctx.statManager().createRateStat("client.receiveMessageSize", + // "How large are messages received by the client?", + // "ClientMessages", + // new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); _runners = new HashMap(); _pendingRunners = new HashSet(); startListeners(port); @@ -64,16 +73,16 @@ public class ClientManager { /** Todo: Start a 3rd listener for IPV6? */ private void startListeners(int port) { - _listener = new ClientListenerRunner(_ctx, this, port); - Thread t = new I2PThread(_listener); - t.setName("ClientListener:" + port); - t.setDaemon(true); - t.start(); - _internalListener = new InternalClientListenerRunner(_ctx, this, port); - t = new I2PThread(_internalListener); - t.setName("ClientListener:" + port + "-i"); - t.setDaemon(true); - t.start(); + if (!_ctx.getBooleanProperty(PROP_DISABLE_EXTERNAL)) { + // there's no option to start both an SSL and non-SSL listener + if (_ctx.getBooleanProperty(PROP_ENABLE_SSL)) + _listener = new SSLClientListenerRunner(_ctx, this, port); + else + _listener = new ClientListenerRunner(_ctx, this, port); + Thread t = new I2PThread(_listener, "ClientListener:" + port, true); + t.start(); + } + _isStarted = true; } public void restart() { @@ -95,9 +104,10 @@ public class ClientManager { } public void shutdown() { + _isStarted = false; _log.info("Shutting down the ClientManager"); - _listener.stopListening(); - _internalListener.stopListening(); + if (_listener != null) + _listener.stopListening(); Set<ClientConnectionRunner> runners = new HashSet(); synchronized (_runners) { for (Iterator<ClientConnectionRunner> iter = _runners.values().iterator(); iter.hasNext();) { @@ -117,7 +127,28 @@ public class ClientManager { } } - public boolean isAlive() { return _listener.isListening(); } + /** + * The InternalClientManager interface. + * Connects to the router, receiving a message queue to talk to the router with. + * @throws I2PSessionException if the router isn't ready + * @since 0.8.3 + */ + public I2CPMessageQueue internalConnect() throws I2PSessionException { + if (!_isStarted) + throw new I2PSessionException("Router client manager is shut down"); + // for now we make these unlimited size + LinkedBlockingQueue<I2CPMessage> in = new LinkedBlockingQueue(); + LinkedBlockingQueue<I2CPMessage> out = new LinkedBlockingQueue(); + I2CPMessageQueue myQueue = new I2CPMessageQueueImpl(in, out); + I2CPMessageQueue hisQueue = new I2CPMessageQueueImpl(out, in); + ClientConnectionRunner runner = new QueuedClientConnectionRunner(_ctx, this, myQueue); + registerConnection(runner); + return hisQueue; + } + + public boolean isAlive() { + return _isStarted && (_listener == null || _listener.isListening()); + } public void registerConnection(ClientConnectionRunner runner) { synchronized (_pendingRunners) { @@ -469,8 +500,8 @@ public class ClientManager { runner = getRunner(_msg.getDestinationHash()); if (runner != null) { - _ctx.statManager().addRateData("client.receiveMessageSize", - _msg.getPayload().getSize(), 0); + //_ctx.statManager().addRateData("client.receiveMessageSize", + // _msg.getPayload().getSize(), 0); runner.receiveMessage(_msg.getDestination(), null, _msg.getPayload()); } else { // no client connection... diff --git a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java index 066d6cc354223c3ee1e53b56e31c78fc4f82e4ab..5fd0bbc28baf0c4a7326efb7539f3fa35448cb1d 100644 --- a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java +++ b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.Set; +import net.i2p.client.I2PSessionException; import net.i2p.crypto.SessionKeyManager; import net.i2p.data.DataHelper; import net.i2p.data.Destination; @@ -21,6 +22,8 @@ import net.i2p.data.Hash; import net.i2p.data.LeaseSet; import net.i2p.data.i2cp.MessageId; import net.i2p.data.i2cp.SessionConfig; +import net.i2p.internal.I2CPMessageQueue; +import net.i2p.internal.InternalClientManager; import net.i2p.router.ClientManagerFacade; import net.i2p.router.ClientMessage; import net.i2p.router.Job; @@ -32,7 +35,7 @@ import net.i2p.util.Log; * * @author jrandom */ -public class ClientManagerFacadeImpl extends ClientManagerFacade { +public class ClientManagerFacadeImpl extends ClientManagerFacade implements InternalClientManager { private final static Log _log = new Log(ClientManagerFacadeImpl.class); private ClientManager _manager; private RouterContext _context; @@ -220,4 +223,16 @@ public class ClientManagerFacadeImpl extends ClientManagerFacade { else return Collections.EMPTY_SET; } + + /** + * The InternalClientManager interface. + * Connect to the router, receiving a message queue to talk to the router with. + * @throws I2PSessionException if the router isn't ready + * @since 0.8.3 + */ + public I2CPMessageQueue connect() throws I2PSessionException { + if (_manager != null) + return _manager.internalConnect(); + throw new I2PSessionException("No manager yet"); + } } diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index edaefc599005388b314a057ef42ded213487f2ec..d45df2cdb0245e7e779b29ec770f6832bc19da7a 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -42,14 +42,19 @@ import net.i2p.util.RandomSource; * */ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventListener { - private Log _log; - private RouterContext _context; - private ClientConnectionRunner _runner; + private final Log _log; + private final RouterContext _context; + private final ClientConnectionRunner _runner; + private final boolean _enforceAuth; - public ClientMessageEventListener(RouterContext context, ClientConnectionRunner runner) { + /** + * @param enforceAuth set false for in-JVM, true for socket access + */ + public ClientMessageEventListener(RouterContext context, ClientConnectionRunner runner, boolean enforceAuth) { _context = context; _log = _context.logManager().getLog(ClientMessageEventListener.class); _runner = runner; + _enforceAuth = enforceAuth; _context.statManager().createRateStat("client.distributeTime", "How long it took to inject the client message into the router", "ClientMessages", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); } @@ -153,10 +158,7 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi } // Auth, since 0.8.2 - // In-JVM accesses have access to the same context properties, so - // they will be set on the client side... therefore we don't need to pass in - // some indication of (socket instanceof InternalSocket) - if (Boolean.valueOf(_context.getProperty("i2cp.auth")).booleanValue()) { + if (_enforceAuth && Boolean.valueOf(_context.getProperty("i2cp.auth")).booleanValue()) { String configUser = _context.getProperty("i2cp.username"); String configPW = _context.getProperty("i2cp.password"); if (configUser != null && configPW != null) { diff --git a/router/java/src/net/i2p/router/client/ClientWriterRunner.java b/router/java/src/net/i2p/router/client/ClientWriterRunner.java index 49fcddcc208962b78097acc645c6b1215a5b7f77..b93a4e5f447e2102a66202280ea667b4baa9c135 100644 --- a/router/java/src/net/i2p/router/client/ClientWriterRunner.java +++ b/router/java/src/net/i2p/router/client/ClientWriterRunner.java @@ -8,6 +8,7 @@ import java.util.concurrent.LinkedBlockingQueue; import net.i2p.data.i2cp.I2CPMessage; import net.i2p.data.i2cp.I2CPMessageImpl; import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.internal.PoisonI2CPMessage; import net.i2p.router.RouterContext; import net.i2p.util.Log; @@ -52,7 +53,7 @@ class ClientWriterRunner implements Runnable { public void stopWriting() { _messagesToWrite.clear(); try { - _messagesToWrite.put(new PoisonMessage()); + _messagesToWrite.put(new PoisonI2CPMessage()); } catch (InterruptedException ie) {} } @@ -64,23 +65,9 @@ class ClientWriterRunner implements Runnable { } catch (InterruptedException ie) { continue; } - if (msg.getType() == PoisonMessage.MESSAGE_TYPE) + if (msg.getType() == PoisonI2CPMessage.MESSAGE_TYPE) break; _runner.writeMessage(msg); } } - - /** - * End-of-stream msg used to stop the concurrent queue - * See http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/BlockingQueue.html - * - */ - private static class PoisonMessage extends I2CPMessageImpl { - public static final int MESSAGE_TYPE = 999999; - public int getType() { - return MESSAGE_TYPE; - } - public void doReadMessage(InputStream buf, int size) throws I2CPMessageException, IOException {} - public byte[] doWriteMessage() throws I2CPMessageException, IOException { return null; } - } } diff --git a/router/java/src/net/i2p/router/client/I2CPMessageQueueImpl.java b/router/java/src/net/i2p/router/client/I2CPMessageQueueImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..f65b061766a8bf9982e09601ebf775de0b03cd7e --- /dev/null +++ b/router/java/src/net/i2p/router/client/I2CPMessageQueueImpl.java @@ -0,0 +1,57 @@ +package net.i2p.router.client; + +import java.util.concurrent.BlockingQueue; + +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.internal.I2CPMessageQueue; + +/** + * Contains the methods to talk to a router or client via I2CP, + * when both are in the same JVM. + * This interface contains methods to access two queues, + * one for transmission and one for receiving. + * The methods are identical to those in java.util.concurrent.BlockingQueue + * + * @author zzz + * @since 0.8.3 + */ +class I2CPMessageQueueImpl extends I2CPMessageQueue { + private final BlockingQueue<I2CPMessage> _in; + private final BlockingQueue<I2CPMessage> _out; + + public I2CPMessageQueueImpl(BlockingQueue<I2CPMessage> in, BlockingQueue<I2CPMessage> out) { + _in = in; + _out = out; + } + + /** + * Send a message, nonblocking + * @return success (false if no space available) + */ + public boolean offer(I2CPMessage msg) { + return _out.offer(msg); + } + + /** + * Receive a message, nonblocking + * @return message or null if none available + */ + public I2CPMessage poll() { + return _in.poll(); + } + + /** + * Send a message, blocking until space is available + */ + public void put(I2CPMessage msg) throws InterruptedException { + _out.put(msg); + } + + /** + * Receive a message, blocking until one is available + * @return message + */ + public I2CPMessage take() throws InterruptedException { + return _in.take(); + } +} diff --git a/router/java/src/net/i2p/router/client/InternalClientListenerRunner.java b/router/java/src/net/i2p/router/client/InternalClientListenerRunner.java deleted file mode 100644 index 995c69400f5d17988f7ccd0f9d47008e6968956b..0000000000000000000000000000000000000000 --- a/router/java/src/net/i2p/router/client/InternalClientListenerRunner.java +++ /dev/null @@ -1,89 +0,0 @@ -package net.i2p.router.client; -/* - * free (adj.): unencumbered; not under the control of others - * Written by jrandom in 2003 and released into the public domain - * with no warranty of any kind, either expressed or implied. - * It probably won't make your computer catch on fire, or eat - * your children, but it might. Use at your own risk. - * - */ - -import java.io.IOException; -import java.net.Socket; - -import net.i2p.router.RouterContext; -import net.i2p.util.Log; -import net.i2p.util.InternalServerSocket; - -/** - * Listen for in-JVM connections on the internal "socket" - * - * @author zzz - * @since 0.7.9 - */ -public class InternalClientListenerRunner extends ClientListenerRunner { - - public InternalClientListenerRunner(RouterContext context, ClientManager manager, int port) { - super(context, manager, port); - _log = _context.logManager().getLog(InternalClientListenerRunner.class); - } - - /** - * Start up the socket listener, listens for connections, and - * fires those connections off via {@link #runConnection runConnection}. - * This only returns if the socket cannot be opened or there is a catastrophic - * failure. - * - */ - public void runServer() { - try { - if (_log.shouldLog(Log.INFO)) - _log.info("Listening on internal port " + _port); - _socket = new InternalServerSocket(_port); - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("InternalServerSocket created, before accept: " + _socket); - - _listening = true; - _running = true; - while (_running) { - try { - Socket socket = _socket.accept(); - if (validate(socket)) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Internal connection received"); - runConnection(socket); - } else { - if (_log.shouldLog(Log.WARN)) - _log.warn("Refused connection from " + socket.getInetAddress()); - try { - socket.close(); - } catch (IOException ioe) {} - } - } catch (IOException ioe) { - if (_context.router().isAlive()) - _log.error("Server error accepting", ioe); - } catch (Throwable t) { - if (_context.router().isAlive()) - _log.error("Fatal error running client listener - killing the thread!", t); - _listening = false; - return; - } - } - } catch (IOException ioe) { - if (_context.router().isAlive()) - _log.error("Error listening on internal port " + _port, ioe); - } - - _listening = false; - if (_socket != null) { - try { _socket.close(); } catch (IOException ioe) {} - _socket = null; - } - - - if (_context.router().isAlive()) - _log.error("CANCELING I2CP LISTEN", new Exception("I2CP Listen cancelled!!!")); - _running = false; - } -} diff --git a/router/java/src/net/i2p/router/client/QueuedClientConnectionRunner.java b/router/java/src/net/i2p/router/client/QueuedClientConnectionRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..758e8221e76e1956f1ed9596f3be895513e27414 --- /dev/null +++ b/router/java/src/net/i2p/router/client/QueuedClientConnectionRunner.java @@ -0,0 +1,76 @@ +package net.i2p.router.client; + +import java.io.IOException; + +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.internal.I2CPMessageQueue; +import net.i2p.internal.QueuedI2CPMessageReader; +import net.i2p.router.RouterContext; +import net.i2p.util.Log; + +/** + * Zero-copy in-JVM. + * While super() starts both a reader and a writer thread, we only need a reader thread here. + * + * @author zzz + * @since 0.8.3 + */ +class QueuedClientConnectionRunner extends ClientConnectionRunner { + private final I2CPMessageQueue queue; + + /** + * Create a new runner with the given queues + * + */ + public QueuedClientConnectionRunner(RouterContext context, ClientManager manager, I2CPMessageQueue queue) { + super(context, manager, null); + this.queue = queue; + } + + + + /** + * Starts the reader thread. Does not call super(). + */ + @Override + public void startRunning() { + _reader = new QueuedI2CPMessageReader(this.queue, new ClientMessageEventListener(_context, this, false)); + _reader.startReading(); + } + + /** + * Calls super() to stop the reader, and sends a poison message to the client. + */ + @Override + void stopRunning() { + super.stopRunning(); + queue.close(); + } + + /** + * In super(), doSend queues it to the writer thread and + * the writer thread calls writeMessage() to write to the output stream. + * Since we have no writer thread this shouldn't happen. + */ + @Override + void writeMessage(I2CPMessage msg) { + throw new RuntimeException("huh?"); + } + + /** + * Actually send the I2CPMessage to the client. + * Nonblocking. + */ + @Override + void doSend(I2CPMessage msg) throws I2CPMessageException { + // This will never fail, for now, as the router uses unbounded queues + // Perhaps in the future we may want to use bounded queues, + // with non-blocking writes for the router + // and blocking writes for the client? + boolean success = queue.offer(msg); + if (!success) + throw new I2CPMessageException("I2CP write to queue failed"); + } + +} diff --git a/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java b/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..0dc053a3361e3b57d2d6d7931712cbe10d307b8a --- /dev/null +++ b/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java @@ -0,0 +1,282 @@ +package net.i2p.router.client; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; +import java.net.ServerSocket; +import java.security.KeyStore; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.Arrays; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLContext; + +import net.i2p.client.I2PClient; +import net.i2p.data.Base32; +import net.i2p.data.Base64; +import net.i2p.router.RouterContext; +import net.i2p.util.Log; +import net.i2p.util.SecureDirectory; +import net.i2p.util.SecureFileOutputStream; +import net.i2p.util.ShellCommand; + +/** + * SSL version of ClientListenerRunner + * + * @since 0.8.3 + * @author zzz + */ +class SSLClientListenerRunner extends ClientListenerRunner { + + private SSLServerSocketFactory _factory; + + private static final String PROP_KEYSTORE_PASSWORD = "i2cp.keystorePassword"; + private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit"; + private static final String PROP_KEY_PASSWORD = "i2cp.keyPassword"; + private static final String KEY_ALIAS = "i2cp"; + private static final String ASCII_KEYFILE = "i2cp.local.crt"; + + public SSLClientListenerRunner(RouterContext context, ClientManager manager, int port) { + super(context, manager, port); + } + + /** + * @return success if it exists and we have a password, or it was created successfully. + */ + private boolean verifyKeyStore(File ks) { + if (ks.exists()) { + boolean rv = _context.getProperty(PROP_KEY_PASSWORD) != null; + if (!rv) + _log.error("I2CP SSL error, must set " + PROP_KEY_PASSWORD + " in " + + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath()); + return rv; + } + File dir = ks.getParentFile(); + if (!dir.exists()) { + File sdir = new SecureDirectory(dir.getAbsolutePath()); + if (!sdir.mkdir()) + return false; + } + boolean rv = createKeyStore(ks); + + // Now read it back out of the new keystore and save it in ascii form + // where the clients can get to it. + // Failure of this part is not fatal. + if (rv) + exportCert(ks); + return rv; + } + + + /** + * Call out to keytool to create a new keystore with a keypair in it. + * Trying to do this programatically is a nightmare, requiring either BouncyCastle + * libs or using proprietary Sun libs, and it's a huge mess. + * If successful, stores the keystore password and key password in router.config. + * + * @return success + */ + private boolean createKeyStore(File ks) { + // make a random 48 character password (30 * 8 / 5) + byte[] rand = new byte[30]; + _context.random().nextBytes(rand); + String keyPassword = Base32.encode(rand); + // and one for the cname + _context.random().nextBytes(rand); + String cname = Base32.encode(rand) + ".i2cp.i2p.net"; + + String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath(); + String[] args = new String[] { + keytool, + "-genkey", // -genkeypair preferred in newer keytools, but this works with more + "-storetype", KeyStore.getDefaultType(), + "-keystore", ks.getAbsolutePath(), + "-storepass", DEFAULT_KEYSTORE_PASSWORD, + "-alias", KEY_ALIAS, + "-dname", "CN=" + cname + ",OU=I2CP,O=I2P Anonymous Network,L=XX,ST=XX,C=XX", + "-validity", "3652", // 10 years + "-keyalg", "DSA", + "-keysize", "1024", + "-keypass", keyPassword}; + boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 30); // 30 secs + if (success) { + success = ks.exists(); + if (success) { + SecureFileOutputStream.setPerms(ks); + _context.router().setConfigSetting(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); + _context.router().setConfigSetting(PROP_KEY_PASSWORD, keyPassword); + _context.router().saveConfig(); + } + } + if (success) { + _log.logAlways(Log.INFO, "Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" + + "The certificate name was generated randomly, and is not associated with your " + + "IP address, host name, router identity, or destination keys."); + } else { + _log.error("Failed to create I2CP SSL keystore using command line:"); + StringBuilder buf = new StringBuilder(256); + for (int i = 0; i < args.length; i++) { + buf.append('"').append(args[i]).append("\" "); + } + _log.error(buf.toString()); + _log.error("This is for the Sun/Oracle keytool, others may be incompatible.\n" + + "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD + + " to " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath()); + } + return success; + } + + /** + * Pull the cert back OUT of the keystore and save it as ascii + * so the clients can get to it. + */ + private void exportCert(File ks) { + File sdir = new SecureDirectory(_context.getConfigDir(), "certificates"); + if (sdir.exists() || sdir.mkdir()) { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream fis = new FileInputStream(ks); + String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); + keyStore.load(fis, ksPass.toCharArray()); + fis.close(); + Certificate cert = keyStore.getCertificate(KEY_ALIAS); + if (cert != null) { + File certFile = new File(sdir, ASCII_KEYFILE); + saveCert(cert, certFile); + } else { + _log.error("Error getting SSL cert to save as ASCII"); + } + } catch (GeneralSecurityException gse) { + _log.error("Error saving ASCII SSL keys", gse); + } catch (IOException ioe) { + _log.error("Error saving ASCII SSL keys", ioe); + } + } else { + _log.error("Error saving ASCII SSL keys"); + } + } + + private static final int LINE_LENGTH = 64; + + /** + * Modified from: + * http://www.exampledepot.com/egs/java.security.cert/ExportCert.html + * + * Write a certificate to a file in base64 format. + */ + private void saveCert(Certificate cert, File file) { + OutputStream os = null; + try { + // Get the encoded form which is suitable for exporting + byte[] buf = cert.getEncoded(); + os = new SecureFileOutputStream(file); + PrintWriter wr = new PrintWriter(os); + wr.println("-----BEGIN CERTIFICATE-----"); + String b64 = Base64.encode(buf, true); // true = use standard alphabet + for (int i = 0; i < b64.length(); i += LINE_LENGTH) { + wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length()))); + } + wr.println("-----END CERTIFICATE-----"); + wr.flush(); + } catch (CertificateEncodingException cee) { + _log.error("Error writing X509 Certificate " + file.getAbsolutePath(), cee); + } catch (IOException ioe) { + _log.error("Error writing X509 Certificate " + file.getAbsolutePath(), ioe); + } finally { + try { if (os != null) os.close(); } catch (IOException foo) {} + } + } + + /** + * Sets up the SSLContext and sets the socket factory. + * @return success + */ + private boolean initializeFactory(File ks) { + String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); + String keyPass = _context.getProperty(PROP_KEY_PASSWORD); + if (keyPass == null) { + _log.error("No key password, set " + PROP_KEY_PASSWORD + + " in " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath()); + return false; + } + try { + SSLContext sslc = SSLContext.getInstance("TLS"); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream fis = new FileInputStream(ks); + keyStore.load(fis, ksPass.toCharArray()); + fis.close(); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, keyPass.toCharArray()); + sslc.init(kmf.getKeyManagers(), null, _context.random()); + _factory = sslc.getServerSocketFactory(); + return true; + } catch (GeneralSecurityException gse) { + _log.error("Error loading SSL keys", gse); + } catch (IOException ioe) { + _log.error("Error loading SSL keys", ioe); + } + return false; + } + + /** + * Get a SSLServerSocket. + */ + @Override + protected ServerSocket getServerSocket() throws IOException { + ServerSocket rv; + if (_bindAllInterfaces) { + if (_log.shouldLog(Log.INFO)) + _log.info("Listening on port " + _port + " on all interfaces"); + rv = _factory.createServerSocket(_port); + } else { + String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST, + ClientManagerFacadeImpl.DEFAULT_HOST); + if (_log.shouldLog(Log.INFO)) + _log.info("Listening on port " + _port + " of the specific interface: " + listenInterface); + rv = _factory.createServerSocket(_port, 0, InetAddress.getByName(listenInterface)); + } + return rv; + } + + /** + * Create (if necessary) and load the key store, then run. + */ + @Override + public void runServer() { + File keyStore = new File(_context.getConfigDir(), "keystore/i2cp.ks"); + if (verifyKeyStore(keyStore) && initializeFactory(keyStore)) { + super.runServer(); + } else { + _log.error("SSL I2CP server error - Failed to create or open key store"); + } + } + + /** + * Overridden because SSL handshake may need more time, + * and available() in super doesn't work. + * The handshake doesn't start until a read(). + */ + @Override + protected boolean validate(Socket socket) { + try { + InputStream is = socket.getInputStream(); + int oldTimeout = socket.getSoTimeout(); + socket.setSoTimeout(4 * CONNECT_TIMEOUT); + boolean rv = is.read() == I2PClient.PROTOCOL_BYTE; + socket.setSoTimeout(oldTimeout); + return rv; + } catch (IOException ioe) {} + if (_log.shouldLog(Log.WARN)) + _log.warn("Peer did not authenticate themselves as I2CP quickly enough, dropping"); + return false; + } +} diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java index 39856f509f95bc1d2daf41fcb088ad1db7b85e66..094f526c79913e433bfa53e178bee6d2f0b22725 100644 --- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java +++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java @@ -45,6 +45,7 @@ public class CommSystemFacadeImpl extends CommSystemFacade { _context = context; _log = _context.logManager().getLog(CommSystemFacadeImpl.class); _manager = null; + _context.statManager().createRateStat("transport.getBidsJobTime", "How long does it take?", "Transport", new long[] { 10*60*1000l }); startGeoIP(); } @@ -131,7 +132,9 @@ public class CommSystemFacadeImpl extends CommSystemFacade { public void processMessage(OutNetMessage msg) { //GetBidsJob j = new GetBidsJob(_context, this, msg); //j.runJob(); + long before = _context.clock().now(); GetBidsJob.getBids(_context, this, msg); + _context.statManager().addRateData("transport.getBidsJobTime", _context.clock().now() - before, 0); } @Override diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java index fd5cf1ac91c7445f7cdec059c058571a5d0a5c30..374c7a5ba0dbf89fca890c991106a7209377476d 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java @@ -24,22 +24,27 @@ import net.i2p.util.Log; * @author zzz */ public class NTCPSendFinisher { - private static final int THREADS = 4; + private static final int MIN_THREADS = 1; + private static final int MAX_THREADS = 4; private final I2PAppContext _context; private final NTCPTransport _transport; private final Log _log; - private int _count; + private static int _count; private ThreadPoolExecutor _executor; + private static int _threads; public NTCPSendFinisher(I2PAppContext context, NTCPTransport transport) { _context = context; _log = _context.logManager().getLog(NTCPSendFinisher.class); _transport = transport; + _context.statManager().createRateStat("ntcp.sendFinishTime", "How long to queue and excecute msg.afterSend()", "ntcp", new long[] {5*1000}); } public void start() { _count = 0; - _executor = new CustomThreadPoolExecutor(); + long maxMemory = Runtime.getRuntime().maxMemory(); + _threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024)))); + _executor = new CustomThreadPoolExecutor(_threads); } public void stop() { @@ -57,18 +62,18 @@ public class NTCPSendFinisher { } // not really needed for now but in case we want to add some hooks like afterExecute() - private class CustomThreadPoolExecutor extends ThreadPoolExecutor { - public CustomThreadPoolExecutor() { + private static class CustomThreadPoolExecutor extends ThreadPoolExecutor { + public CustomThreadPoolExecutor(int num) { // use unbounded queue, so maximumPoolSize and keepAliveTime have no effect - super(THREADS, THREADS, 1000, TimeUnit.MILLISECONDS, + super(num, num, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), new CustomThreadFactory()); } } - private class CustomThreadFactory implements ThreadFactory { + private static class CustomThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread rv = Executors.defaultThreadFactory().newThread(r); - rv.setName("NTCPSendFinisher " + (++_count) + '/' + THREADS); + rv.setName("NTCPSendFinisher " + (++_count) + '/' + _threads); rv.setDaemon(true); return rv; } @@ -78,15 +83,18 @@ public class NTCPSendFinisher { * Call afterSend() for the message */ private class RunnableEvent implements Runnable { - private OutNetMessage _msg; + private final OutNetMessage _msg; + private final long _queued; public RunnableEvent(OutNetMessage msg) { _msg = msg; + _queued = _context.clock().now(); } public void run() { try { _transport.afterSend(_msg, true, false, _msg.getSendTime()); + _context.statManager().addRateData("ntcp.sendFinishTime", _context.clock().now() - _queued, 0); } catch (Throwable t) { _log.log(Log.CRIT, " wtf, afterSend borked", t); } diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java index 3d6d91f5167f1bb3e2509418b44ecf4494289f7e..31060ba411f41edcbb969a5f12dba21a5ebca97b 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java @@ -433,8 +433,10 @@ public class NTCPTransport extends TransportImpl { return skews; } - private static final int NUM_CONCURRENT_READERS = 3; - private static final int NUM_CONCURRENT_WRITERS = 3; + private static final int MIN_CONCURRENT_READERS = 2; // unless < 32MB + private static final int MIN_CONCURRENT_WRITERS = 2; // unless < 32MB + private static final int MAX_CONCURRENT_READERS = 4; + private static final int MAX_CONCURRENT_WRITERS = 4; /** * Called by TransportManager. @@ -449,12 +451,8 @@ public class NTCPTransport extends TransportImpl { if (_pumper.isAlive()) return _myAddress != null ? _myAddress.toRouterAddress() : null; if (_log.shouldLog(Log.WARN)) _log.warn("Starting ntcp transport listening"); - _finisher.start(); - _pumper.startPumping(); - - _reader.startReading(NUM_CONCURRENT_READERS); - _writer.startWriting(NUM_CONCURRENT_WRITERS); + startIt(); configureLocalAddress(); return bindAddress(); } @@ -471,12 +469,8 @@ public class NTCPTransport extends TransportImpl { if (_pumper.isAlive()) return _myAddress != null ? _myAddress.toRouterAddress() : null; if (_log.shouldLog(Log.WARN)) _log.warn("Restarting ntcp transport listening"); - _finisher.start(); - _pumper.startPumping(); - - _reader.startReading(NUM_CONCURRENT_READERS); - _writer.startWriting(NUM_CONCURRENT_WRITERS); + startIt(); if (addr == null) _myAddress = null; else @@ -484,6 +478,28 @@ public class NTCPTransport extends TransportImpl { return bindAddress(); } + /** + * Start up. Caller must synchronize. + * @since 0.8.3 + */ + private void startIt() { + _finisher.start(); + _pumper.startPumping(); + + long maxMemory = Runtime.getRuntime().maxMemory(); + int nr, nw; + if (maxMemory < 32*1024*1024) { + nr = nw = 1; + } else if (maxMemory < 64*1024*1024) { + nr = nw = 2; + } else { + nr = Math.max(MIN_CONCURRENT_READERS, Math.min(MAX_CONCURRENT_READERS, _context.bandwidthLimiter().getInboundKBytesPerSecond() / 20)); + nw = Math.max(MIN_CONCURRENT_WRITERS, Math.min(MAX_CONCURRENT_WRITERS, _context.bandwidthLimiter().getOutboundKBytesPerSecond() / 20)); + } + _reader.startReading(nr); + _writer.startWriting(nw); + } + public boolean isAlive() { return _pumper.isAlive(); } diff --git a/router/java/src/net/i2p/router/transport/ntcp/Reader.java b/router/java/src/net/i2p/router/transport/ntcp/Reader.java index c1029b26e5031ac2cd6b59f76c557b5e97a29545..96948154536224b8c42532431a7aafc2613d517c 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/Reader.java +++ b/router/java/src/net/i2p/router/transport/ntcp/Reader.java @@ -15,13 +15,13 @@ import net.i2p.util.Log; * */ class Reader { - private RouterContext _context; - private Log _log; + private final RouterContext _context; + private final Log _log; // TODO change to LBQ ?? private final List<NTCPConnection> _pendingConnections; - private List<NTCPConnection> _liveReads; - private List<NTCPConnection> _readAfterLive; - private List<Runner> _runners; + private final List<NTCPConnection> _liveReads; + private final List<NTCPConnection> _readAfterLive; + private final List<Runner> _runners; public Reader(RouterContext ctx) { _context = ctx; @@ -33,9 +33,9 @@ class Reader { } public void startReading(int numReaders) { - for (int i = 0; i < numReaders; i++) { + for (int i = 1; i <= numReaders; i++) { Runner r = new Runner(); - I2PThread t = new I2PThread(r, "NTCP read " + i, true); + I2PThread t = new I2PThread(r, "NTCP reader " + i + '/' + numReaders, true); _runners.add(r); t.start(); } diff --git a/router/java/src/net/i2p/router/transport/ntcp/Writer.java b/router/java/src/net/i2p/router/transport/ntcp/Writer.java index ca676c572ce5c4dc7a6572f964d8ec918338ee34..260569df7554799ba6deabdd8c479c824ad74162 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/Writer.java +++ b/router/java/src/net/i2p/router/transport/ntcp/Writer.java @@ -14,12 +14,12 @@ import net.i2p.util.Log; * */ class Writer { - private RouterContext _context; - private Log _log; + private final RouterContext _context; + private final Log _log; private final List<NTCPConnection> _pendingConnections; - private List<NTCPConnection> _liveWrites; - private List<NTCPConnection> _writeAfterLive; - private List<Runner> _runners; + private final List<NTCPConnection> _liveWrites; + private final List<NTCPConnection> _writeAfterLive; + private final List<Runner> _runners; public Writer(RouterContext ctx) { _context = ctx; @@ -31,9 +31,9 @@ class Writer { } public void startWriting(int numWriters) { - for (int i = 0; i < numWriters; i++) { + for (int i = 1; i <=numWriters; i++) { Runner r = new Runner(); - I2PThread t = new I2PThread(r, "NTCP write " + i, true); + I2PThread t = new I2PThread(r, "NTCP writer " + i + '/' + numWriters, true); _runners.add(r); t.start(); } diff --git a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java index 08b6088c4ffbd01295121adbed5559037ca30ca9..4988a06851a463b3b9abd9367c623741e185e96c 100644 --- a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java +++ b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java @@ -27,7 +27,9 @@ class MessageReceiver { private final BlockingQueue<InboundMessageState> _completeMessages; private boolean _alive; //private ByteCache _cache; - private static final int THREADS = 5; + private static final int MIN_THREADS = 2; // unless < 32MB + private static final int MAX_THREADS = 5; + private final int _threadCount; private static final long POISON_IMS = -99999999999l; public MessageReceiver(RouterContext ctx, UDPTransport transport) { @@ -35,10 +37,19 @@ class MessageReceiver { _log = ctx.logManager().getLog(MessageReceiver.class); _transport = transport; _completeMessages = new LinkedBlockingQueue(); + + long maxMemory = Runtime.getRuntime().maxMemory(); + if (maxMemory < 32*1024*1024) + _threadCount = 1; + else if (maxMemory < 64*1024*1024) + _threadCount = 2; + else + _threadCount = Math.max(MIN_THREADS, Math.min(MAX_THREADS, ctx.bandwidthLimiter().getInboundKBytesPerSecond() / 20)); + // the runners run forever, no need to have a cache //_cache = ByteCache.getInstance(64, I2NPMessage.MAX_SIZE); _context.statManager().createRateStat("udp.inboundExpired", "How many messages were expired before reception?", "udp", UDPTransport.RATES); - _context.statManager().createRateStat("udp.inboundRemaining", "How many messages were remaining when a message is pulled off the complete queue?", "udp", UDPTransport.RATES); + //_context.statManager().createRateStat("udp.inboundRemaining", "How many messages were remaining when a message is pulled off the complete queue?", "udp", UDPTransport.RATES); _context.statManager().createRateStat("udp.inboundReady", "How many messages were ready when a message is added to the complete queue?", "udp", UDPTransport.RATES); _context.statManager().createRateStat("udp.inboundReadTime", "How long it takes to parse in the completed fragments into a message?", "udp", UDPTransport.RATES); _context.statManager().createRateStat("udp.inboundReceiveProcessTime", "How long it takes to add the message to the transport?", "udp", UDPTransport.RATES); @@ -49,8 +60,8 @@ class MessageReceiver { public void startup() { _alive = true; - for (int i = 0; i < THREADS; i++) { - I2PThread t = new I2PThread(new Runner(), "UDP message receiver " + i + '/' + THREADS, true); + for (int i = 0; i < _threadCount; i++) { + I2PThread t = new I2PThread(new Runner(), "UDP message receiver " + (i+1) + '/' + _threadCount, true); t.start(); } } @@ -64,7 +75,7 @@ class MessageReceiver { public void shutdown() { _alive = false; _completeMessages.clear(); - for (int i = 0; i < THREADS; i++) { + for (int i = 0; i < _threadCount; i++) { InboundMessageState ims = new InboundMessageState(_context, POISON_IMS, null); _completeMessages.offer(ims); } @@ -119,8 +130,8 @@ class MessageReceiver { if (message != null) { long before = System.currentTimeMillis(); - if (remaining > 0) - _context.statManager().addRateData("udp.inboundRemaining", remaining, 0); + //if (remaining > 0) + // _context.statManager().addRateData("udp.inboundRemaining", remaining, 0); int size = message.getCompleteSize(); if (_log.shouldLog(Log.INFO)) _log.info("Full message received (" + message.getMessageId() + ") after " + message.getLifetime()); diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java index d130a4c8382c886b9c427bc0137f79d59f194fb1..2c0138228d728d69a854573718659232a03526bc 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java @@ -31,11 +31,13 @@ class PacketHandler { private boolean _keepReading; private final Handler[] _handlers; - private static final int NUM_HANDLERS = 5; + private static final int MIN_NUM_HANDLERS = 2; // unless < 32MB + private static final int MAX_NUM_HANDLERS = 5; /** let packets be up to 30s slow */ private static final long GRACE_PERIOD = Router.CLOCK_FUDGE_FACTOR + 30*1000; - PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher, InboundMessageFragments inbound, PeerTestManager testManager, IntroductionManager introManager) {// LINT -- Exporting non-public type through public API + PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher, + InboundMessageFragments inbound, PeerTestManager testManager, IntroductionManager introManager) { _context = ctx; _log = ctx.logManager().getLog(PacketHandler.class); _transport = transport; @@ -44,10 +46,20 @@ class PacketHandler { _inbound = inbound; _testManager = testManager; _introManager = introManager; - _handlers = new Handler[NUM_HANDLERS]; - for (int i = 0; i < NUM_HANDLERS; i++) { + + long maxMemory = Runtime.getRuntime().maxMemory(); + int num_handlers; + if (maxMemory < 32*1024*1024) + num_handlers = 1; + else if (maxMemory < 64*1024*1024) + num_handlers = 2; + else + num_handlers = Math.max(MIN_NUM_HANDLERS, Math.min(MAX_NUM_HANDLERS, ctx.bandwidthLimiter().getInboundKBytesPerSecond() / 20)); + _handlers = new Handler[num_handlers]; + for (int i = 0; i < num_handlers; i++) { _handlers[i] = new Handler(); } + _context.statManager().createRateStat("udp.handleTime", "How long it takes to handle a received packet after its been pulled off the queue", "udp", UDPTransport.RATES); _context.statManager().createRateStat("udp.queueTime", "How long after a packet is received can we begin handling it", "udp", UDPTransport.RATES); _context.statManager().createRateStat("udp.receivePacketSkew", "How long ago after the packet was sent did we receive it", "udp", UDPTransport.RATES); @@ -79,8 +91,8 @@ class PacketHandler { public void startup() { _keepReading = true; - for (int i = 0; i < NUM_HANDLERS; i++) { - I2PThread t = new I2PThread(_handlers[i], "UDP Packet handler " + i + '/' + NUM_HANDLERS, true); + for (int i = 0; i < _handlers.length; i++) { + I2PThread t = new I2PThread(_handlers[i], "UDP Packet handler " + (i+1) + '/' + _handlers.length, true); t.start(); } } @@ -91,8 +103,8 @@ class PacketHandler { String getHandlerStatus() { StringBuilder rv = new StringBuilder(); - rv.append("Handlers: ").append(NUM_HANDLERS); - for (int i = 0; i < NUM_HANDLERS; i++) { + rv.append("Handlers: ").append(_handlers.length); + for (int i = 0; i < _handlers.length; i++) { Handler handler = _handlers[i]; rv.append(" handler ").append(i).append(" state: ").append(handler._state); } diff --git a/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java b/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java index 05db0b0ce4a9c96b39661f32d371a200d52a642c..7f29f5743d09b94aaf127dcba64c73bd030393f4 100644 --- a/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java +++ b/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java @@ -16,22 +16,26 @@ public class TunnelGatewayPumper implements Runnable { private RouterContext _context; private final BlockingQueue<PumpedTunnelGateway> _wantsPumping; private boolean _stop; - private static final int PUMPERS = 4; + private static final int MIN_PUMPERS = 1; + private static final int MAX_PUMPERS = 4; + private final int _pumpers; /** Creates a new instance of TunnelGatewayPumper */ public TunnelGatewayPumper(RouterContext ctx) { _context = ctx; _wantsPumping = new LinkedBlockingQueue(); _stop = false; - for (int i = 0; i < PUMPERS; i++) - new I2PThread(this, "Tunnel GW pumper " + i + '/' + PUMPERS, true).start(); + long maxMemory = Runtime.getRuntime().maxMemory(); + _pumpers = (int) Math.max(MIN_PUMPERS, Math.min(MAX_PUMPERS, 1 + (maxMemory / (32*1024*1024)))); + for (int i = 0; i < _pumpers; i++) + new I2PThread(this, "Tunnel GW pumper " + (i+1) + '/' + _pumpers, true).start(); } public void stopPumping() { _stop=true; _wantsPumping.clear(); PumpedTunnelGateway poison = new PoisonPTG(_context); - for (int i = 0; i < PUMPERS; i++) + for (int i = 0; i < _pumpers; i++) _wantsPumping.offer(poison); for (int i = 1; i <= 5 && !_wantsPumping.isEmpty(); i++) { try {