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.net.InetAddress;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
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.app.ClientAppManager;
import net.i2p.app.ClientAppState;
import static net.i2p.app.ClientAppState.*;
import net.i2p.apps.systray.SysTray;
import net.i2p.crypto.KeyStoreUtil;
import net.i2p.data.Base32;
import net.i2p.data.DataHelper;
import net.i2p.jetty.I2PLogger;
import net.i2p.router.RouterContext;
import net.i2p.router.update.ConsoleUpdateManager;
import net.i2p.router.app.RouterApp;
import net.i2p.util.Addresses;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
import net.i2p.util.PortMapper;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SystemVersion;
import net.i2p.util.VersionComparator;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.NCSARequestLog;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.bio.SocketConnector;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.server.ssl.SslSocketConnector;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.eclipse.jetty.util.security.Credential.MD5;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
/**
* Start the router console.
*/
public class RouterConsoleRunner implements RouterApp {
static {
// To take effect, must be set before any Jetty classes are loaded
try {
Log.setLog(new I2PLogger());
} 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
// This causes an NPE in AbstractLifeCycle
// http://dev.eclipse.org/mhonarc/lists/jetty-users/msg02587.html
//System.setProperty("org.eclipse.jetty.util.log.class", "net.i2p.jetty.I2PLogger");
}
private final RouterContext _context;
private final ClientAppManager _mgr;
private volatile ClientAppState _state = UNINITIALIZED;
private static Server _server;
private String _listenPort;
private String _listenHost;
private String _sslListenPort;
private String _sslListenHost;
private String _webAppsDir;
private static final String DEFAULT_WEBAPP_CONFIG_FILENAME = "webapps.config";
// Jetty Auth
private static final DigestAuthenticator authenticator = new DigestAuthenticator();
static {
// default changed from 0 (forever) in Jetty 6 to 60*1000 ms in Jetty 7
authenticator.setMaxNonceAge(7*24*60*60*1000L);
}
public static final String JETTY_REALM = "i2prouter";
private static final String JETTY_ROLE = "routerAdmin";
public static final String PROP_CONSOLE_PW = "routerconsole.auth." + JETTY_REALM;
public static final String PROP_PW_ENABLE = "routerconsole.auth.enable";
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";
public static final int 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";
/**
*
* 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(RouterContext ctx, ClientAppManager mgr, String args[]) {
_context = ctx;
_mgr = mgr;
if (args.length == 0) {
// _listenHost and _webAppsDir are defaulted below
_listenPort = Integer.toString(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);
}
_state = INITIALIZED;
}
public static void main(String args[]) {
List contexts = RouterContext.listContexts();
if (contexts == null || contexts.isEmpty())
throw new IllegalStateException("no router context");
RouterConsoleRunner runner = new RouterConsoleRunner(contexts.get(0), null, args);
runner.startup();
}
/////// ClientApp methods
/** @since 0.9.4 */
public synchronized void startup() {
changeState(STARTING);
startTrayApp(_context);
startConsole();
}
/** @since 0.9.4 */
public synchronized void shutdown(String[] args) {
if (_state == STOPPED)
return;
changeState(STOPPING);
if (PluginStarter.pluginsEnabled(_context))
(new I2PAppThread(new PluginStopper(_context), "PluginStopper")).start();
try {
_server.stop();
} catch (Exception ie) {}
PortMapper portMapper = _context.portMapper();
portMapper.unregister(PortMapper.SVC_CONSOLE);
portMapper.unregister(PortMapper.SVC_HTTPS_CONSOLE);
changeState(STOPPED);
}
/** @since 0.9.4 */
public ClientAppState getState() {
return _state;
}
/** @since 0.9.4 */
public String getName() {
return "console";
}
/** @since 0.9.4 */
public String getDisplayName() {
return "Router Console";
}
/////// end ClientApp methods
private synchronized void changeState(ClientAppState state) {
_state = state;
if (_mgr != null)
_mgr.notify(this, state, null, null);
}
/**
* 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(I2PAppContext ctx) {
try {
//TODO: move away from routerconsole into a separate application.
//ApplicationManager?
boolean recentJava = SystemVersion.isJava6();
// default false for now
boolean desktopguiEnabled = ctx.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");
// this check is in SysTray but do it here too
if (SystemVersion.isWindows() && (!Boolean.getBoolean("systray.disable")) && (!SystemVersion.is64Bit()))
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(_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");
// 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();
// Gone in Jetty 7
//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();
// gone in Jetty 7
//_server.addHandler(hColl);
_server.setHandler(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 += '/';
HandlerWrapper rootWebApp = null;
ServletHandler rootServletHandler = null;
List 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
lsnr.setAcceptors(1); // default changed to 2 somewhere in Jetty 7?
//_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)) {
// the keystore path and password
SslContextFactory sslFactory = new SslContextFactory(keyStore.getAbsolutePath());
sslFactory.setKeyStorePassword(_context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD));
// the X.509 cert password (if not present, verifyKeyStore() returned false)
sslFactory.setKeyManagerPassword(_context.getProperty(PROP_KEY_PASSWORD, "thisWontWork"));
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
AbstractConnector ssll;
if (SystemVersion.isJava6() && !SystemVersion.isGNU()) {
SslSelectChannelConnector sssll = new SslSelectChannelConnector(sslFactory);
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(sslFactory);
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
ssll.setAcceptors(1); // default changed to 2 somewhere in Jetty 7?
//_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;
}
File tmpdir = new SecureDirectory(workDir, ROUTERCONSOLE + "-" +
(_listenPort != null ? _listenPort : _sslListenPort));
tmpdir.mkdir();
rootServletHandler = new ServletHandler();
rootWebApp = new LocaleWebAppHandler(_context,
"/", _webAppsDir + ROUTERCONSOLE + ".war",
tmpdir, rootServletHandler);
initialize(_context, (WebAppContext)(rootWebApp.getHandler()));
chColl.addHandler(rootWebApp);
} catch (Exception ioe) {
ioe.printStackTrace();
}
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=364936
// WARN:oejw.WebAppContext:Failed startup of context o.e.j.w.WebAppContext{/,jar:file:/.../webapps/routerconsole.war!/},/.../webapps/routerconsole.war
// java.lang.IllegalStateException: zip file closed
Resource.setDefaultUseCaches(false);
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 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);
if (_mgr != null)
_mgr.register(this);
}
} 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();
}
// stat summarizer registers its own hook
// RouterAppManager registers its own hook
if (_mgr == null)
_context.addShutdownTask(new ServerShutdown());
ConfigServiceHandler.registerSignalHandler(_context);
}
/**
* @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;
}
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 boolean createKeyStore(File ks) {
// make a random 48 character password (30 * 8 / 5)
String keyPassword = KeyStoreUtil.randomString();
// and one for the cname
String cname = KeyStoreUtil.randomString() + ".console.i2p.net";
boolean success = KeyStoreUtil.createKeys(ks, "console", cname, "Console", keyPassword);
if (success) {
success = ks.exists();
if (success) {
try {
Map 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.\n" +
"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;
}
/**
* Set up basic security constraints for the webapp.
* Add all users and passwords.
*/
static void initialize(RouterContext ctx, WebAppContext context) {
ConstraintSecurityHandler sec = new ConstraintSecurityHandler();
List constraints = new ArrayList(4);
ConsolePasswordManager mgr = new ConsolePasswordManager(ctx);
boolean enable = ctx.getBooleanProperty(PROP_PW_ENABLE);
if (enable) {
Map userpw = mgr.getMD5(PROP_CONSOLE_PW);
if (userpw.isEmpty()) {
enable = false;
ctx.router().saveConfig(PROP_CONSOLE_PW, "false");
} else {
HashLoginService realm = new HashLoginService(JETTY_REALM);
sec.setLoginService(realm);
sec.setAuthenticator(authenticator);
for (Map.Entry e : userpw.entrySet()) {
String user = e.getKey();
String pw = e.getValue();
realm.putUser(user, Credential.getCredential(MD5.__TYPE + pw), new String[] {JETTY_ROLE});
Constraint constraint = new Constraint(user, JETTY_ROLE);
constraint.setAuthenticate(true);
ConstraintMapping cm = new ConstraintMapping();
cm.setConstraint(constraint);
cm.setPathSpec("/");
constraints.add(cm);
}
}
}
// This forces a '403 Forbidden' response for TRACE and OPTIONS unless the
// WAC handler handles it.
// (LocaleWebAppHandler returns a '405 Method Not Allowed')
// TRACE and OPTIONS aren't really security issues...
// TRACE doesn't echo stuff unless you call setTrace(true)
// But it might bug some people
// The other strange methods - PUT, DELETE, MOVE - are disabled by default
// See also:
// http://old.nabble.com/Disable-HTTP-TRACE-in-Jetty-5.x-td12412607.html
Constraint sc = new Constraint();
sc.setName("No trace");
ConstraintMapping cm = new ConstraintMapping();
cm.setMethod("TRACE");
cm.setConstraint(sc);
cm.setPathSpec("/");
constraints.add(cm);
sc = new Constraint();
sc.setName("No options");
cm = new ConstraintMapping();
cm.setMethod("OPTIONS");
cm.setConstraint(sc);
cm.setPathSpec("/");
constraints.add(cm);
ConstraintMapping cmarr[] = constraints.toArray(new ConstraintMapping[constraints.size()]);
sec.setConstraintMappings(cmarr);
context.setSecurityHandler(sec);
}
/** @since 0.8.8 */
private class ServerShutdown implements Runnable {
public void run() {
shutdown(null);
}
}
private Properties webAppProperties() {
return webAppProperties(_context.getConfigDir().getAbsolutePath());
}
/** @since 0.9.4 */
public static Properties webAppProperties(I2PAppContext ctx) {
return webAppProperties(ctx.getConfigDir().getAbsolutePath());
}
public static Properties webAppProperties(String dir) {
Properties rv = new Properties();
// String webappConfigFile = _context.getProperty(PROP_WEBAPP_CONFIG_FILENAME, DEFAULT_WEBAPP_CONFIG_FILENAME);
String webappConfigFile = DEFAULT_WEBAPP_CONFIG_FILENAME;
File cfgFile = new File(dir, webappConfigFile);
try {
DataHelper.loadProps(rv, cfgFile);
} catch (IOException ioe) {
// _log.warn("Error loading the client app properties from " + cfgFile.getName(), ioe);
}
return rv;
}
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);
}
}
static class WarFilenameFilter implements FilenameFilter {
private static final WarFilenameFilter _filter = new WarFilenameFilter();
public static WarFilenameFilter instance() { return _filter; }
public boolean accept(File dir, String name) {
return (name != null) && (name.endsWith(".war") && !name.equals(ROUTERCONSOLE + ".war"));
}
}
/**
* Just to set the name and set Daemon
* @since Jetty 6
*/
private static class CustomThreadPoolExecutor extends ExecutorThreadPool {
public CustomThreadPoolExecutor() {
super(new ThreadPoolExecutor(
MIN_THREADS, MAX_THREADS, MAX_IDLE_TIME, TimeUnit.MILLISECONDS,
new SynchronousQueue(),
new CustomThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy())
);
}
}
/**
* Just to set the name and set Daemon
* @since Jetty 6
*/
private static class CustomThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread rv = Executors.defaultThreadFactory().newThread(r);
rv.setName(THREAD_NAME);
rv.setDaemon(true);
return rv;
}
}
}