Ejemplo n.º 1
0
 /**
  * Deferred deletion of plugins that we failed to delete before.
  *
  * @since 0.9.13
  */
 private static void deferredDeletePlugins(RouterContext ctx) {
   Log log = ctx.logManager().getLog(PluginStarter.class);
   boolean changed = false;
   Properties props = pluginProperties();
   for (Iterator<Map.Entry<Object, Object>> iter = props.entrySet().iterator(); iter.hasNext(); ) {
     Map.Entry<Object, Object> e = iter.next();
     String name = (String) e.getKey();
     if (name.startsWith(PREFIX) && name.endsWith(ENABLED)) {
       // deferred deletion of a plugin
       if (e.getValue().equals(DELETED)) {
         String app = name.substring(PREFIX.length(), name.lastIndexOf(ENABLED));
         // shouldn't happen, this is run early
         if (isPluginRunning(app, ctx)) continue;
         File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + app);
         boolean deleted = FileUtil.rmdir(pluginDir, false);
         if (deleted) {
           log.logAlways(Log.WARN, "Deferred deletion of " + pluginDir + " successful");
           iter.remove();
           changed = true;
         } else {
           if (log.shouldLog(Log.WARN)) log.warn("Deferred deletion of " + pluginDir + " failed");
         }
       }
     }
   }
   if (changed) storePluginProperties(props);
 }
Ejemplo n.º 2
0
  /**
   * @return true on success
   * @throws just about anything, caller would be wise to catch Throwable
   */
  public static boolean stopPlugin(RouterContext ctx, String appName) throws Exception {
    Log log = ctx.logManager().getLog(PluginStarter.class);
    File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
    if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
      log.error("Cannot stop nonexistent plugin: " + appName);
      return false;
    }

    // stop things in clients.config
    File clientConfig = new File(pluginDir, "clients.config");
    if (clientConfig.exists()) {
      Properties props = new Properties();
      DataHelper.loadProps(props, clientConfig);
      List<ClientAppConfig> clients = ClientAppConfig.getClientApps(clientConfig);
      runClientApps(ctx, pluginDir, clients, "stop");
    }

    // stop console webapps in console/webapps
    // ContextHandlerCollection server = WebAppStarter.getConsoleServer();
    // if (server != null) {
    /*
        File consoleDir = new File(pluginDir, "console");
        Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath());
        File webappDir = new File(consoleDir, "webapps");
        String fileNames[] = webappDir.list(RouterConsoleRunner.WarFilenameFilter.instance());
        if (fileNames != null) {
            for (int i = 0; i < fileNames.length; i++) {
                String warName = fileNames[i].substring(0, fileNames[i].lastIndexOf(".war"));
                if (Arrays.asList(STANDARD_WEBAPPS).contains(warName)) {
                    continue;
                }
                WebAppStarter.stopWebApp(server, warName);
            }
        }
    */
    if (pluginWars.containsKey(appName)) {
      Iterator<String> wars = pluginWars.get(appName).iterator();
      while (wars.hasNext()) {
        String warName = wars.next();
        WebAppStarter.stopWebApp(warName);
      }
      pluginWars.get(appName).clear();
    }
    // }

    // remove summary bar link
    Properties props = pluginProperties(ctx, appName);
    String name =
        ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(ctx));
    if (name == null) name = ConfigClientsHelper.stripHTML(props, "consoleLinkName");
    if (name != null && name.length() > 0) NavHelper.unregisterApp(name);

    if (log.shouldLog(Log.WARN)) log.warn("Stopping plugin: " + appName);
    return true;
  }
Ejemplo n.º 3
0
  public static void storeWebAppProperties(RouterContext ctx, Properties props) {
    // String webappConfigFile = _context.getProperty(PROP_WEBAPP_CONFIG_FILENAME,
    // DEFAULT_WEBAPP_CONFIG_FILENAME);
    String webappConfigFile = DEFAULT_WEBAPP_CONFIG_FILENAME;
    File cfgFile = new File(ctx.getConfigDir(), webappConfigFile);

    try {
      DataHelper.storeProps(props, cfgFile);
    } catch (IOException ioe) {
      // _log.warn("Error loading the client app properties from " + cfgFile.getName(), ioe);
    }
  }
Ejemplo n.º 4
0
  /** @return true on success - caller should call stopPlugin() first */
  static boolean deletePlugin(RouterContext ctx, String appName) throws Exception {
    Log log = ctx.logManager().getLog(PluginStarter.class);
    File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
    if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
      log.error("Cannot delete nonexistent plugin: " + appName);
      return false;
    }
    // uninstall things in clients.config
    File clientConfig = new File(pluginDir, "clients.config");
    if (clientConfig.exists()) {
      Properties props = new Properties();
      DataHelper.loadProps(props, clientConfig);
      List<ClientAppConfig> clients = ClientAppConfig.getClientApps(clientConfig);
      runClientApps(ctx, pluginDir, clients, "uninstall");
    }

    // unregister themes, and switch to default if we are unregistering the current theme
    File dir = new File(pluginDir, "console/themes");
    File[] tfiles = dir.listFiles();
    if (tfiles != null) {
      String current = ctx.getProperty(CSSHelper.PROP_THEME_NAME);
      Map<String, String> changes = new HashMap<String, String>();
      List<String> removes = new ArrayList<String>();
      for (int i = 0; i < tfiles.length; i++) {
        String name = tfiles[i].getName();
        if (tfiles[i].isDirectory() && (!Arrays.asList(STANDARD_THEMES).contains(tfiles[i]))) {
          removes.add(ConfigUIHelper.PROP_THEME_PFX + name);
          if (name.equals(current)) changes.put(CSSHelper.PROP_THEME_NAME, CSSHelper.DEFAULT_THEME);
        }
      }
      ctx.router().saveConfig(changes, removes);
    }

    boolean deleted = FileUtil.rmdir(pluginDir, false);
    Properties props = pluginProperties();
    for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) {
      String name = (String) iter.next();
      if (name.startsWith(PREFIX + appName + '.')) iter.remove();
    }
    if (!deleted) {
      // This happens on Windows when there are plugin jars in classpath
      // Mark it as deleted, we will try again after restart
      log.logAlways(Log.WARN, "Deletion of " + pluginDir + " failed, will try again at restart");
      props.setProperty(PREFIX + appName + ENABLED, DELETED);
    }
    storePluginProperties(props);
    return true;
  }
Ejemplo n.º 5
0
 /**
  * @return success if it exists and we have a password, or it was created successfully.
  * @since 0.8.3
  */
 private boolean verifyKeyStore(File ks) {
   if (ks.exists()) {
     boolean rv = _context.getProperty(PROP_KEY_PASSWORD) != null;
     if (!rv)
       System.err.println(
           "Console 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;
   }
   return createKeyStore(ks);
 }
Ejemplo n.º 6
0
  /**
   * @param action "start" or "stop" or "uninstall"
   * @throws just about anything if an app has a delay less than zero, caller would be wise to catch
   *     Throwable If no apps have a delay less than zero, it shouldn't throw anything
   */
  private static void runClientApps(
      RouterContext ctx, File pluginDir, List<ClientAppConfig> apps, String action)
      throws Exception {
    Log log = ctx.logManager().getLog(PluginStarter.class);

    // initialize pluginThreadGroup and _pendingPluginClients
    String pluginName = pluginDir.getName();
    if (!pluginThreadGroups.containsKey(pluginName))
      pluginThreadGroups.put(pluginName, new ThreadGroup(pluginName));
    ThreadGroup pluginThreadGroup = pluginThreadGroups.get(pluginName);
    if (action.equals("start"))
      _pendingPluginClients.put(pluginName, new ConcurrentHashSet<SimpleTimer2.TimedEvent>());

    for (ClientAppConfig app : apps) {
      // If the client is a running ClientApp that we want to stop,
      // bypass all the logic below.
      if (action.equals("stop")) {
        String[] argVal = LoadClientAppsJob.parseArgs(app.args);
        // We must do all the substitution just as when started, so the
        // argument array comparison in getClientApp() works.
        // Do this after parsing so we don't need to worry about quoting
        for (int i = 0; i < argVal.length; i++) {
          if (argVal[i].indexOf("$") >= 0) {
            argVal[i] = argVal[i].replace("$I2P", ctx.getBaseDir().getAbsolutePath());
            argVal[i] = argVal[i].replace("$CONFIG", ctx.getConfigDir().getAbsolutePath());
            argVal[i] = argVal[i].replace("$PLUGIN", pluginDir.getAbsolutePath());
          }
        }
        ClientApp ca = ctx.routerAppManager().getClientApp(app.className, argVal);
        if (ca != null) {
          // even if (ca.getState() != ClientAppState.RUNNING), we do this, we don't want to fall
          // thru
          try {
            ca.shutdown(LoadClientAppsJob.parseArgs(app.stopargs));
          } catch (Throwable t) {
            throw new Exception(t);
          }
          continue;
        }
      }

      if (action.equals("start") && app.disabled) continue;
      String argVal[];
      if (action.equals("start")) {
        // start
        argVal = LoadClientAppsJob.parseArgs(app.args);
      } else {
        String args;
        if (action.equals("stop")) args = app.stopargs;
        else if (action.equals("uninstall")) args = app.uninstallargs;
        else throw new IllegalArgumentException("bad action");
        // args must be present
        if (args == null || args.length() <= 0) continue;
        argVal = LoadClientAppsJob.parseArgs(args);
      }
      // do this after parsing so we don't need to worry about quoting
      for (int i = 0; i < argVal.length; i++) {
        if (argVal[i].indexOf("$") >= 0) {
          argVal[i] = argVal[i].replace("$I2P", ctx.getBaseDir().getAbsolutePath());
          argVal[i] = argVal[i].replace("$CONFIG", ctx.getConfigDir().getAbsolutePath());
          argVal[i] = argVal[i].replace("$PLUGIN", pluginDir.getAbsolutePath());
        }
      }

      ClassLoader cl = null;
      if (app.classpath != null) {
        String cp = app.classpath;
        if (cp.indexOf("$") >= 0) {
          cp = cp.replace("$I2P", ctx.getBaseDir().getAbsolutePath());
          cp = cp.replace("$CONFIG", ctx.getConfigDir().getAbsolutePath());
          cp = cp.replace("$PLUGIN", pluginDir.getAbsolutePath());
        }

        // Old way - add for the whole JVM
        // addToClasspath(cp, app.clientName, log);

        // New way - add only for this client
        // We cache the ClassLoader we start the client with, so
        // we can reuse it for stopping and uninstalling.
        // If we don't, the client won't be able to find its
        // static members.
        String clCacheKey = pluginName + app.className + app.args;
        if (!action.equals("start")) cl = _clCache.get(clCacheKey);
        if (cl == null) {
          URL[] urls = classpathToURLArray(cp, app.clientName, log);
          if (urls != null) {
            cl = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());
            if (action.equals("start")) _clCache.put(clCacheKey, cl);
          }
        }
      }

      if (app.delay < 0 && action.equals("start")) {
        // this will throw exceptions
        LoadClientAppsJob.runClientInline(app.className, app.clientName, argVal, log, cl);
      } else if (app.delay == 0 || !action.equals("start")) {
        // quick check, will throw ClassNotFoundException on error
        LoadClientAppsJob.testClient(app.className, cl);
        // run this guy now
        LoadClientAppsJob.runClient(
            app.className, app.clientName, argVal, ctx, log, pluginThreadGroup, cl);
      } else {
        // If there is some delay, there may be a really good reason for it.
        // Loading a class would be one of them!
        // So we do a quick check first, If it bombs out, we delay and try again.
        // If it bombs after that, then we throw the ClassNotFoundException.
        try {
          // quick check
          LoadClientAppsJob.testClient(app.className, cl);
        } catch (ClassNotFoundException ex) {
          // Try again 1 or 2 seconds later.
          // This should be enough time. Although it is a lousy hack
          // it should work for most cases.
          // Perhaps it may be even better to delay a percentage
          // if > 1, and reduce the delay time.
          // Under normal circumstances there will be no delay at all.
          try {
            if (app.delay > 1) {
              Thread.sleep(2000);
            } else {
              Thread.sleep(1000);
            }
          } catch (InterruptedException ie) {
          }
          // quick check, will throw ClassNotFoundException on error
          LoadClientAppsJob.testClient(app.className, cl);
        }
        // wait before firing it up
        SimpleTimer2.TimedEvent evt =
            new TrackedDelayedClient(
                pluginName,
                ctx.simpleTimer2(),
                ctx,
                app.className,
                app.clientName,
                argVal,
                pluginThreadGroup,
                cl);
        evt.schedule(app.delay);
      }
    }
  }
Ejemplo n.º 7
0
  /**
   * @return true on success
   * @throws just about anything, caller would be wise to catch Throwable
   */
  public static boolean startPlugin(RouterContext ctx, String appName) throws Exception {
    Log log = ctx.logManager().getLog(PluginStarter.class);
    File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
    String iconfile = null;
    if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
      log.error("Cannot start nonexistent plugin: " + appName);
      disablePlugin(appName);
      return false;
    }

    // Do we need to extract an update?
    File pluginUpdate = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName + "/app.xpi2p.zip");
    if (pluginUpdate.exists()) {
      // Compare the start time of the router with the plugin.
      if (ctx.router().getWhenStarted() > pluginUpdate.lastModified()) {
        if (!FileUtil.extractZip(pluginUpdate, pluginDir)) {
          pluginUpdate.delete();
          String foo =
              "Plugin '"
                  + appName
                  + "' failed to update! File '"
                  + pluginUpdate
                  + "' deleted. You may need to remove and install the plugin again.";
          log.error(foo);
          disablePlugin(appName);
          throw new Exception(foo);
        } else {
          pluginUpdate.delete();
          // Need to always log this, and  log.logAlways() did not work for me.
          System.err.println("INFO: Plugin updated: " + appName);
        }
      } // silently fail to update, because we have not restarted.
    }

    Properties props = pluginProperties(ctx, appName);

    String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version");
    if (minVersion != null && VersionComparator.comp(CoreVersion.VERSION, minVersion) < 0) {
      String foo = "Plugin " + appName + " requires I2P version " + minVersion + " or higher";
      log.error(foo);
      disablePlugin(appName);
      throw new Exception(foo);
    }

    minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version");
    if (minVersion != null
        && VersionComparator.comp(System.getProperty("java.version"), minVersion) < 0) {
      String foo = "Plugin " + appName + " requires Java version " + minVersion + " or higher";
      log.error(foo);
      disablePlugin(appName);
      throw new Exception(foo);
    }

    String jVersion = LogsHelper.jettyVersion();
    minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version");
    if (minVersion != null && VersionComparator.comp(minVersion, jVersion) > 0) {
      String foo = "Plugin " + appName + " requires Jetty version " + minVersion + " or higher";
      log.error(foo);
      disablePlugin(appName);
      throw new Exception(foo);
    }

    String maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version");
    if (maxVersion != null && VersionComparator.comp(maxVersion, jVersion) < 0) {
      String foo = "Plugin " + appName + " requires Jetty version " + maxVersion + " or lower";
      log.error(foo);
      disablePlugin(appName);
      throw new Exception(foo);
    }

    if (log.shouldLog(Log.INFO)) log.info("Starting plugin: " + appName);

    // register themes
    File dir = new File(pluginDir, "console/themes");
    File[] tfiles = dir.listFiles();
    if (tfiles != null) {
      for (int i = 0; i < tfiles.length; i++) {
        String name = tfiles[i].getName();
        if (tfiles[i].isDirectory() && (!Arrays.asList(STANDARD_THEMES).contains(tfiles[i])))
          ctx.router()
              .setConfigSetting(ConfigUIHelper.PROP_THEME_PFX + name, tfiles[i].getAbsolutePath());
        // we don't need to save
      }
    }

    // load and start things in clients.config
    File clientConfig = new File(pluginDir, "clients.config");
    if (clientConfig.exists()) {
      Properties cprops = new Properties();
      DataHelper.loadProps(cprops, clientConfig);
      List<ClientAppConfig> clients = ClientAppConfig.getClientApps(clientConfig);
      runClientApps(ctx, pluginDir, clients, "start");
    }

    // start console webapps in console/webapps
    ContextHandlerCollection server = WebAppStarter.getConsoleServer();
    if (server != null) {
      File consoleDir = new File(pluginDir, "console");
      Properties wprops = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath());
      File webappDir = new File(consoleDir, "webapps");
      String fileNames[] = webappDir.list(RouterConsoleRunner.WarFilenameFilter.instance());
      if (fileNames != null) {
        if (!pluginWars.containsKey(appName))
          pluginWars.put(appName, new ConcurrentHashSet<String>());
        for (int i = 0; i < fileNames.length; i++) {
          try {
            String warName = fileNames[i].substring(0, fileNames[i].lastIndexOf(".war"));
            // log.error("Found webapp: " + warName);
            // check for duplicates in $I2P
            if (Arrays.asList(STANDARD_WEBAPPS).contains(warName)) {
              log.error("Skipping duplicate webapp " + warName + " in plugin " + appName);
              continue;
            }
            String enabled = wprops.getProperty(RouterConsoleRunner.PREFIX + warName + ENABLED);
            if (!"false".equals(enabled)) {
              if (log.shouldLog(Log.INFO)) log.info("Starting webapp: " + warName);
              String path = new File(webappDir, fileNames[i]).getCanonicalPath();
              WebAppStarter.startWebApp(ctx, server, warName, path);
              pluginWars.get(appName).add(warName);
            }
          } catch (IOException ioe) {
            log.error("Error resolving '" + fileNames[i] + "' in '" + webappDir, ioe);
          }
        }
        // Check for iconfile in plugin.properties
        String icfile = ConfigClientsHelper.stripHTML(props, "console-icon");
        if (icfile != null && !icfile.contains("..")) {
          StringBuilder buf = new StringBuilder(32);
          buf.append('/').append(appName);
          if (!icfile.startsWith("/")) buf.append('/');
          buf.append(icfile);
          iconfile = buf.toString();
        }
      }
    } else {
      log.error("No console web server to start plugins?");
    }

    // add translation jars in console/locale
    // These will not override existing resource bundles since we are adding them
    // later in the classpath.
    File localeDir = new File(pluginDir, "console/locale");
    if (localeDir.exists() && localeDir.isDirectory()) {
      File[] files = localeDir.listFiles();
      if (files != null) {
        boolean added = false;
        for (int i = 0; i < files.length; i++) {
          File f = files[i];
          if (f.getName().endsWith(".jar")) {
            try {
              addPath(f.toURI().toURL());
              log.error("INFO: Adding translation plugin to classpath: " + f);
              added = true;
            } catch (Exception e) {
              log.error("Plugin " + appName + " bad classpath element: " + f, e);
            }
          }
        }
        if (added) Translate.clearCache();
      }
    }
    // add summary bar link
    String name =
        ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(ctx));
    if (name == null) name = ConfigClientsHelper.stripHTML(props, "consoleLinkName");
    String url = ConfigClientsHelper.stripHTML(props, "consoleLinkURL");
    if (name != null && url != null && name.length() > 0 && url.length() > 0) {
      String tip =
          ConfigClientsHelper.stripHTML(props, "consoleLinkTooltip_" + Messages.getLanguage(ctx));
      if (tip == null) tip = ConfigClientsHelper.stripHTML(props, "consoleLinkTooltip");
      NavHelper.registerApp(name, url, tip, iconfile);
    }

    return true;
  }
Ejemplo n.º 8
0
 private Properties webAppProperties() {
   return webAppProperties(_context.getConfigDir().getAbsolutePath());
 }
Ejemplo n.º 9
0
  /**
   * 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 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) + ".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 {
          Map<String, String> changes = new HashMap();
          changes.put(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
          changes.put(PROP_KEY_PASSWORD, keyPassword);
          _context.router().saveConfig(changes, null);
        } 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(_context.getConfigDir(), "router.config")).getAbsolutePath());
    }
    return success;
  }
Ejemplo n.º 10
0
  /**
   * http://irc.codehaus.org/display/JETTY/Porting+to+jetty6
   *
   * <pre>
   * Server
   * 	HandlerCollection
   * 		ContextHandlerCollection
   * 			WebAppContext (i.e. ContextHandler)
   * 				SessionHandler
   * 				SecurityHandler
   * 				ServletHandler
   * 					servlets...
   * 			WebAppContext
   * 			...
   * 		DefaultHandler
   * 		RequestLogHandler (opt)
   * </pre>
   */
  public void startConsole() {
    File workDir = new SecureDirectory(_context.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(_context));
    // } 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", _context.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 = _context.getProperty("routerconsole.log");
    if (log != null) {
      File logFile = new File(log);
      if (!logFile.isAbsolute()) logFile = new File(_context.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(_context.getBaseDir(), _webAppsDir);
      try {
        _webAppsDir = app.getCanonicalPath();
      } catch (IOException ioe) {
      }
    }
    if (!_webAppsDir.endsWith("/")) _webAppsDir += '/';

    WebAppContext rootWebApp = null;
    ServletHandler rootServletHandler = null;
    List<Connector> connectors = new ArrayList(4);
    try {
      int boundAddresses = 0;
      Set addresses = Addresses.getAllAddresses();
      boolean hasIPV4 = addresses.contains("0.0.0.0");
      boolean hasIPV6 = addresses.contains("0:0:0:0:0:0:0:0");

      // add standard listeners
      int lport = 0;
      if (_listenPort != null) {
        try {
          lport = Integer.parseInt(_listenPort);
        } catch (NumberFormatException nfe) {
        }
        if (lport <= 0) System.err.println("Bad routerconsole port " + _listenPort);
      }
      if (lport > 0) {
        StringTokenizer tok = new StringTokenizer(_listenHost, " ,");
        while (tok.hasMoreTokens()) {
          String host = tok.nextToken().trim();
          try {
            // Test before we add the connector, because Jetty 6 won't start if any of the
            // connectors are bad
            InetAddress test = InetAddress.getByName(host);
            if ((!hasIPV6) && (!(test instanceof Inet4Address)))
              throw new IOException("IPv6 addresses unsupported");
            if ((!hasIPV4) && (test instanceof Inet4Address))
              throw new IOException("IPv4 addresses unsupported");
            ServerSocket testSock = null;
            try {
              // On Windows, this was passing and Jetty was still failing,
              // possibly due to %scope_id ???
              // https://issues.apache.org/jira/browse/ZOOKEEPER-667
              // testSock = new ServerSocket(0, 0, test);
              // so do exactly what Jetty does in SelectChannelConnector.open()
              testSock = new ServerSocket();
              InetSocketAddress isa = new InetSocketAddress(host, 0);
              testSock.bind(isa);
            } finally {
              if (testSock != null)
                try {
                  testSock.close();
                } catch (IOException ioe) {
                }
            }
            // if (host.indexOf(":") >= 0) // IPV6 - requires patched Jetty 5
            //    _server.addListener('[' + host + "]:" + _listenPort);
            // else
            //    _server.addListener(host + ':' + _listenPort);
            AbstractConnector lsnr;
            if (SystemVersion.isJava6() && !SystemVersion.isGNU()) {
              SelectChannelConnector slsnr = new SelectChannelConnector();
              slsnr.setUseDirectBuffers(false); // default true seems to be leaky
              lsnr = slsnr;
            } else {
              // Jetty 6 and NIO on Java 5 don't get along that well
              // Also: http://jira.codehaus.org/browse/JETTY-1238
              // "Do not use GCJ with Jetty, it will not work."
              // Actually it does if you don't use NIO
              lsnr = new SocketConnector();
            }
            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);
            connectors.add(lsnr);
            boundAddresses++;
          } catch (Exception ioe) {
            System.err.println(
                "Unable to bind routerconsole to " + host + " port " + _listenPort + ": " + ioe);
            System.err.println(
                "You may ignore this warning if the console is still available at http://localhost:"
                    + _listenPort);
          }
        }
        // XXX: what if listenhosts do not include 127.0.0.1? (Should that ever even happen?)
        _context.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) {
        File keyStore = new File(_context.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 {
              // Test before we add the connector, because Jetty 6 won't start if any of the
              // connectors are bad
              InetAddress test = InetAddress.getByName(host);
              if ((!hasIPV6) && (!(test instanceof Inet4Address)))
                throw new IOException("IPv6 addresses unsupported");
              if ((!hasIPV4) && (test instanceof Inet4Address))
                throw new IOException("IPv4 addresses unsupported");
              ServerSocket testSock = null;
              try {
                // see comments above
                // testSock = new ServerSocket(0, 0, test);
                testSock = new ServerSocket();
                InetSocketAddress isa = new InetSocketAddress(host, 0);
                testSock.bind(isa);
              } finally {
                if (testSock != null)
                  try {
                    testSock.close();
                  } catch (IOException ioe) {
                  }
              }
              // TODO if class not found use SslChannelConnector
              // Sadly there's no common base class with the ssl methods in it
              AbstractConnector ssll;
              if (SystemVersion.isJava6() && !SystemVersion.isGNU()) {
                SslSelectChannelConnector sssll = new SslSelectChannelConnector();
                // the keystore path and password
                sssll.setKeystore(keyStore.getAbsolutePath());
                sssll.setPassword(
                    _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD));
                // the X.509 cert password (if not present, verifyKeyStore() returned false)
                sssll.setKeyPassword(_context.getProperty(PROP_KEY_PASSWORD, "thisWontWork"));
                sssll.setUseDirectBuffers(false); // default true seems to be leaky
                ssll = sssll;
              } else {
                // Jetty 6 and NIO on Java 5 don't get along that well
                SslSocketConnector sssll = new SslSocketConnector();
                // the keystore path and password
                sssll.setKeystore(keyStore.getAbsolutePath());
                sssll.setPassword(
                    _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD));
                // the X.509 cert password (if not present, verifyKeyStore() returned false)
                sssll.setKeyPassword(_context.getProperty(PROP_KEY_PASSWORD, "thisWontWork"));
                ssll = sssll;
              }
              ssll.setHost(host);
              ssll.setPort(sslPort);
              ssll.setMaxIdleTime(90 * 1000); // default 10 sec
              ssll.setName("ConsoleSocket"); // all with same name will use the same thread pool
              // _server.addConnector(ssll);
              connectors.add(ssll);
              boundAddresses++;
            } catch (Exception e) {
              System.err.println(
                  "Unable to bind routerconsole to "
                      + host
                      + " port "
                      + sslPort
                      + " for SSL: "
                      + e);
              if (SystemVersion.isGNU())
                System.err.println("Probably because GNU classpath does not support Sun keystores");
              System.err.println(
                  "You may ignore this warning if the console is still available at https://localhost:"
                      + sslPort);
            }
          }
          _context.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(_context, "/", _webAppsDir + ROUTERCONSOLE + ".war");
      File tmpdir =
          new SecureDirectory(
              workDir, ROUTERCONSOLE + "-" + (_listenPort != null ? _listenPort : _sslListenPort));
      tmpdir.mkdir();
      rootWebApp.setTempDirectory(tmpdir);
      rootWebApp.setExtractWAR(false);
      rootWebApp.setSessionHandler(new SessionHandler());
      rootServletHandler = new ServletHandler();
      rootWebApp.setServletHandler(rootServletHandler);
      initialize(_context, 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("Error starting the Router Console server: " + me);
      me.printStackTrace();
    }

    if (_server.isRunning()) {
      // Add and start the connectors one-by-one
      boolean error = false;
      for (Connector conn : connectors) {
        try {
          _server.addConnector(conn);
          // start after adding so it gets the right thread pool
          conn.start();
        } catch (Throwable me) {
          try {
            _server.removeConnector(conn);
          } catch (Throwable t) {
            t.printStackTrace();
          }
          System.err.println("WARNING: Error starting " + conn + ": " + me);
          me.printStackTrace();
          error = true;
        }
      }
      if (error) {
        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:"
                + _listenPort
                + "/,\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.");
      }
    }

    // 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<String> notStarted = new ArrayList();
    if (_server.isRunning()) {
      File dir = new File(_webAppsDir);
      String fileNames[] = dir.list(WarFilenameFilter.instance());
      if (fileNames != null) {
        for (int i = 0; i < fileNames.length; i++) {
          String appName = fileNames[i].substring(0, fileNames[i].lastIndexOf(".war"));
          String enabled = props.getProperty(PREFIX + appName + ENABLED);
          if (!"false".equals(enabled)) {
            try {
              String path = new File(dir, fileNames[i]).getCanonicalPath();
              WebAppStarter.startWebApp(_context, chColl, appName, path);
              if (enabled == null) {
                // do this so configclients.jsp knows about all apps from reading the config
                props.setProperty(PREFIX + appName + ENABLED, "true");
                rewrite = true;
              }
            } catch (Throwable t) {
              System.err.println("ERROR: Failed to start " + appName + ' ' + t);
              t.printStackTrace();
              notStarted.add(appName);
            }
          } else {
            notStarted.add(appName);
          }
        }
        changeState(RUNNING);
      }
    } else {
      System.err.println("ERROR: Router console did not start, not starting webapps");
      changeState(START_FAILED);
    }

    if (rewrite) storeWebAppProperties(_context, props);

    if (rootServletHandler != null && notStarted.size() > 0) {
      // map each not-started webapp to the error page
      ServletHolder noWebApp = rootServletHandler.getServlet("net.i2p.router.web.jsp.nowebapp_jsp");
      for (int i = 0; i < notStarted.size(); i++) {
        // we want a new handler for each one since if the webapp is started we remove the
        // handler???
        try {
          if (noWebApp != null) {
            String path = '/' + notStarted.get(i);
            // LocaleWebAppsHandler adds a .jsp
            rootServletHandler.addServletWithMapping(noWebApp, path + ".jsp");
            rootServletHandler.addServletWithMapping(noWebApp, path + "/*");
          } else {
            System.err.println("Can't find nowebapp.jsp?");
          }
        } catch (Throwable me) {
          System.err.println(me);
          me.printStackTrace();
        }
      }
    }

    Thread t = new I2PAppThread(new StatSummarizer(), "StatSummarizer", true);
    t.setPriority(Thread.NORM_PRIORITY - 1);
    t.start();

    ConsoleUpdateManager um = new ConsoleUpdateManager(_context);
    um.start();

    if (PluginStarter.pluginsEnabled(_context)) {
      t = new I2PAppThread(new PluginStarter(_context), "PluginStarter", true);
      t.setPriority(Thread.NORM_PRIORITY - 1);
      t.start();
      _context.addShutdownTask(new PluginStopper(_context));
    }
    // stat summarizer registers its own hook
    _context.addShutdownTask(new ServerShutdown());
    ConfigServiceHandler.registerSignalHandler(_context);
  }