package net.i2p.router.web; import java.util.ArrayList; import java.awt.GraphicsEnvironment; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.security.KeyStore; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import java.util.concurrent.Executors; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import net.i2p.I2PAppContext; import net.i2p.apps.systray.SysTray; import net.i2p.data.Base32; import net.i2p.data.DataHelper; import net.i2p.desktopgui.Main; import net.i2p.jetty.I2PLogger; import net.i2p.router.RouterContext; import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.PortMapper; import net.i2p.util.SecureDirectory; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.ShellCommand; import net.i2p.util.VersionComparator; import org.mortbay.jetty.AbstractConnector; import org.mortbay.jetty.Connector; import org.mortbay.jetty.Handler; import org.mortbay.jetty.NCSARequestLog; import org.mortbay.jetty.Server; import org.mortbay.jetty.handler.ContextHandlerCollection; import org.mortbay.jetty.handler.DefaultHandler; import org.mortbay.jetty.handler.HandlerCollection; import org.mortbay.jetty.handler.RequestLogHandler; import org.mortbay.jetty.nio.SelectChannelConnector; import org.mortbay.jetty.security.DigestAuthenticator; import org.mortbay.jetty.security.HashUserRealm; import org.mortbay.jetty.security.Constraint; import org.mortbay.jetty.security.ConstraintMapping; import org.mortbay.jetty.security.SecurityHandler; import org.mortbay.jetty.security.SslSelectChannelConnector; import org.mortbay.jetty.servlet.ServletHandler; import org.mortbay.jetty.servlet.ServletHolder; import org.mortbay.jetty.servlet.SessionHandler; import org.mortbay.jetty.webapp.WebAppContext; import org.mortbay.log.Log; import org.mortbay.thread.QueuedThreadPool; import org.mortbay.thread.concurrent.ThreadPool; /** * Start the router console. */ public class RouterConsoleRunner { private static Server _server; 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]]"; private static final int MIN_THREADS = 1; private static final int MAX_THREADS = 24; private static final int MAX_IDLE_TIME = 90*1000; private static final String THREAD_NAME = "RouterConsole Jetty"; static { System.setProperty("org.mortbay.http.Version.paranoid", "true"); } /** *
* 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/ ** * @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, * because 127.0.0.1 will bind ::1 also. But even though it's bound * to both, we can't connect to [::1]:7657 for some reason. * So the wise choice is ::1,127.0.0.1 */ public RouterConsoleRunner(String args[]) { 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); } } public static void main(String args[]) { startTrayApp(); RouterConsoleRunner runner = new RouterConsoleRunner(args); runner.startConsole(); } /** * SInce _server is now static * @return may be null or stopped perhaps * @since Jetty 6 since it doesn't have Server.getServers() */ static Server getConsoleServer() { return _server; } private static void startTrayApp() { try { //TODO: move away from routerconsole into a separate application. //ApplicationManager? VersionComparator v = new VersionComparator(); boolean recentJava = v.compare(System.getProperty("java.runtime.version"), "1.6") >= 0; // default false for now boolean desktopguiEnabled = I2PAppContext.getGlobalContext().getBooleanProperty("desktopgui.enabled"); if (recentJava && desktopguiEnabled) { //Check if we are in a headless environment, set properties accordingly System.setProperty("java.awt.headless", Boolean.toString(GraphicsEnvironment.isHeadless())); String[] args = new String[0]; net.i2p.desktopgui.Main.beginStartup(args); } else { // required true for jrobin to work System.setProperty("java.awt.headless", "true"); SysTray.getInstance(); } } catch (Throwable t) { t.printStackTrace(); } } /** * http://irc.codehaus.org/display/JETTY/Porting+to+jetty6 * *
* Server * HandlerCollection * ContextHandlerCollection * WebAppContext (i.e. ContextHandler) * SessionHandler * SecurityHandler * ServletHandler * servlets... * WebAppContext * ... * DefaultHandler * RequestLogHandler (opt) **/ public void startConsole() { File workDir = new SecureDirectory(I2PAppContext.getGlobalContext().getTempDir(), "jetty-work"); boolean workDirRemoved = FileUtil.rmdir(workDir, false); if (!workDirRemoved) System.err.println("ERROR: Unable to remove Jetty temporary work directory"); boolean workDirCreated = workDir.mkdirs(); if (!workDirCreated) System.err.println("ERROR: Unable to create Jetty temporary work directory"); //try { // Log.setLog(new I2PLogger(I2PAppContext.getGlobalContext())); //} catch (Throwable t) { // System.err.println("INFO: I2P Jetty logging class not found, logging to wrapper log"); //} // This way it doesn't try to load Slf4jLog first System.setProperty("org.mortbay.log.class", "net.i2p.jetty.I2PLogger"); // so Jetty can find WebAppConfiguration System.setProperty("jetty.class.path", I2PAppContext.getGlobalContext().getBaseDir() + "/lib/routerconsole.jar"); _server = new Server(); _server.setGracefulShutdown(1000); try { ThreadPool ctp = new CustomThreadPoolExecutor(); ctp.prestartAllCoreThreads(); _server.setThreadPool(ctp); } catch (Throwable t) { // class not found... System.out.println("INFO: Jetty concurrent ThreadPool unavailable, using QueuedThreadPool"); QueuedThreadPool qtp = new QueuedThreadPool(MAX_THREADS); qtp.setMinThreads(MIN_THREADS); qtp.setMaxIdleTimeMs(MAX_IDLE_TIME); _server.setThreadPool(qtp); } HandlerCollection hColl = new HandlerCollection(); ContextHandlerCollection chColl = new ContextHandlerCollection(); _server.addHandler(hColl); hColl.addHandler(chColl); hColl.addHandler(new DefaultHandler()); String log = I2PAppContext.getGlobalContext().getProperty("routerconsole.log"); if (log != null) { File logFile = new File(log); if (!logFile.isAbsolute()) logFile = new File(I2PAppContext.getGlobalContext().getLogDir(), "logs/" + log); try { RequestLogHandler rhl = new RequestLogHandler(); rhl.setRequestLog(new NCSARequestLog(logFile.getAbsolutePath())); hColl.addHandler(rhl); } catch (Exception ioe) { System.err.println("ERROR: Unable to create Jetty log: " + ioe); } } boolean rewrite = false; Properties props = webAppProperties(); if (props.isEmpty()) { props.setProperty(PREFIX + ROUTERCONSOLE + ENABLED, "true"); rewrite = true; } // Get an absolute path with a trailing slash for the webapps dir // We assume relative to the base install dir for backward compatibility File app = new File(_webAppsDir); if (!app.isAbsolute()) { app = new File(I2PAppContext.getGlobalContext().getBaseDir(), _webAppsDir); try { _webAppsDir = app.getCanonicalPath(); } catch (IOException ioe) {} } if (!_webAppsDir.endsWith("/")) _webAppsDir += '/'; WebAppContext rootWebApp = null; ServletHandler rootServletHandler = null; try { int boundAddresses = 0; // add standard listeners if (_listenPort != null) { Integer lport = Integer.parseInt(_listenPort); 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); // Use AbstractConnector instead of Connector so we can do setName() AbstractConnector lsnr = new SelectChannelConnector(); lsnr.setHost(host); lsnr.setPort(lport); lsnr.setMaxIdleTime(90*1000); // default 10 sec lsnr.setName("ConsoleSocket"); // all with same name will use the same thread pool _server.addConnector(lsnr); boundAddresses++; } catch (NumberFormatException nfe) { System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + ' ' + nfe); } catch (Exception 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); } } // XXX: what if listenhosts do not include 127.0.0.1? (Should that ever even happen?) I2PAppContext.getGlobalContext().portMapper().register(PortMapper.SVC_CONSOLE,lport); } // add SSL listeners int sslPort = 0; if (_sslListenPort != null) { try { 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 [] try { // TODO if class not found use SslChannelConnector // Sadly there's no common base class with the ssl methods in it SslSelectChannelConnector ssll = new SslSelectChannelConnector(); ssll.setHost(host); ssll.setPort(sslPort); // 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")); ssll.setMaxIdleTime(90*1000); // default 10 sec ssll.setName("ConsoleSocket"); // all with same name will use the same thread pool _server.addConnector(ssll); boundAddresses++; } catch (Exception e) { // probably no exceptions at this point System.err.println("Unable to bind routerconsole to " + host + " port " + sslPort + " for SSL: " + e); } } I2PAppContext.getGlobalContext().portMapper().register(PortMapper.SVC_HTTPS_CONSOLE,sslPort); } 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 + (sslPort > 0 ? (" or SSL port " + sslPort) : "")); return; } rootWebApp = new LocaleWebAppHandler(I2PAppContext.getGlobalContext(), "/", _webAppsDir + ROUTERCONSOLE + ".war"); File tmpdir = new SecureDirectory(workDir, ROUTERCONSOLE + "-" + (_listenPort != null ? _listenPort : _sslListenPort)); tmpdir.mkdir(); rootWebApp.setTempDirectory(tmpdir); rootWebApp.setSessionHandler(new SessionHandler()); rootServletHandler = new ServletHandler(); rootWebApp.setServletHandler(rootServletHandler); initialize(rootWebApp); chColl.addHandler(rootWebApp); } catch (Exception ioe) { ioe.printStackTrace(); } try { // start does a mapContexts() _server.start(); } catch (Throwable me) { // NoClassFoundDefError from a webapp is a throwable, not an exception System.err.println("WARNING: Error starting one or more listeners of the Router Console server.\n" + "If your console is still accessible at http://127.0.0.1:7657/,\n" + "this may be a problem only with binding to the IPV6 address ::1.\n" + "If so, you may ignore this error, or remove the\n" + "\"::1,\" in the \"clientApp.0.args\" line of the clients.config file.\n" + "Exception: " + me); me.printStackTrace(); } // Start all the other webapps after the server is up, // so things start faster. // Jetty 6 starts the connector before the router console is ready // This also prevents one webapp from breaking the whole thing List