forked from I2P_Developers/i2p.i2p
Disable webapp by default Add link in Services section of sidebar Add definition in PortMapper Add stub controller for socket implementation, WIP Javadocs
1217 lines
55 KiB
Java
1217 lines
55 KiB
Java
package net.i2p.router.web;
|
|
|
|
import java.awt.GraphicsEnvironment;
|
|
import java.io.File;
|
|
import java.io.FileFilter;
|
|
import java.io.IOException;
|
|
import java.io.Serializable;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.net.InetAddress;
|
|
import java.net.Inet4Address;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.ServerSocket;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Properties;
|
|
import java.util.Set;
|
|
import java.util.SortedSet;
|
|
import java.util.StringTokenizer;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
|
|
import net.i2p.I2PAppContext;
|
|
import net.i2p.app.ClientApp;
|
|
import net.i2p.app.ClientAppManager;
|
|
import net.i2p.app.ClientAppState;
|
|
import static net.i2p.app.ClientAppState.*;
|
|
import net.i2p.crypto.KeyStoreUtil;
|
|
import net.i2p.data.DataHelper;
|
|
import net.i2p.jetty.I2PLogger;
|
|
import net.i2p.router.RouterContext;
|
|
import net.i2p.router.app.RouterApp;
|
|
import net.i2p.router.news.NewsManager;
|
|
import net.i2p.router.sybil.Analysis;
|
|
import net.i2p.router.update.ConsoleUpdateManager;
|
|
import net.i2p.util.Addresses;
|
|
import net.i2p.util.FileSuffixFilter;
|
|
import net.i2p.util.FileUtil;
|
|
import net.i2p.util.I2PAppThread;
|
|
import net.i2p.util.PortMapper;
|
|
import net.i2p.util.SecureDirectory;
|
|
import net.i2p.util.I2PSSLSocketFactory;
|
|
import net.i2p.util.SystemVersion;
|
|
|
|
import org.eclipse.jetty.security.HashLoginService;
|
|
import org.eclipse.jetty.security.ConstraintMapping;
|
|
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
|
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
|
|
import org.eclipse.jetty.server.AbstractConnector;
|
|
import org.eclipse.jetty.server.ConnectionFactory;
|
|
import org.eclipse.jetty.server.Connector;
|
|
import org.eclipse.jetty.server.HttpConfiguration;
|
|
import org.eclipse.jetty.server.HttpConnectionFactory;
|
|
import org.eclipse.jetty.server.NCSARequestLog;
|
|
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
|
import org.eclipse.jetty.server.Server;
|
|
import org.eclipse.jetty.server.UserIdentity;
|
|
import org.eclipse.jetty.server.ServerConnector;
|
|
import org.eclipse.jetty.server.SslConnectionFactory;
|
|
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.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.ScheduledExecutorScheduler;
|
|
import org.eclipse.jetty.util.thread.ThreadPool;
|
|
|
|
import org.tanukisoftware.wrapper.WrapperManager;
|
|
|
|
/**
|
|
* 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 Server _server;
|
|
private static ScheduledExecutorScheduler _jettyTimer;
|
|
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);
|
|
}
|
|
private static final String NAME = "console";
|
|
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";
|
|
/** from Jetty Credential.java */
|
|
private static final String MD5_CREDENTIAL_TYPE = "MD5:";
|
|
|
|
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 PROP_KEY_PASSWORD = "routerconsole.keyPassword";
|
|
public static final int DEFAULT_LISTEN_PORT = PortMapper.DEFAULT_CONSOLE_PORT;
|
|
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]]";
|
|
|
|
/** this is for the handlers only. We will adjust for the connectors and acceptors below. */
|
|
private static final int MIN_THREADS = 1;
|
|
/** this is for the handlers only. We will adjust for the connectors and acceptors below. */
|
|
private static final int MAX_THREADS = 24;
|
|
private static final int MAX_IDLE_TIME = 90*1000;
|
|
private static final String THREAD_NAME = "RouterConsole Jetty";
|
|
public static final String PROP_DTG_ENABLED = "desktopgui.enabled";
|
|
static final String PROP_ALLOWED_HOSTS = "routerconsole.allowedHosts";
|
|
/** @since 0.9.34 */
|
|
static final FileFilter WAR_FILTER = new WarFilenameFilter();
|
|
|
|
/**
|
|
* <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,
|
|
* 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 = PortMapper.DEFAULT_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<RouterContext> 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);
|
|
checkJavaVersion();
|
|
startTrayApp();
|
|
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();
|
|
stopAllWebApps();
|
|
try {
|
|
_server.stop();
|
|
} catch (Exception ie) {}
|
|
PortMapper portMapper = _context.portMapper();
|
|
portMapper.unregister(PortMapper.SVC_CONSOLE);
|
|
portMapper.unregister(PortMapper.SVC_HTTPS_CONSOLE);
|
|
synchronized(RouterConsoleRunner.class) {
|
|
if (_jettyTimer != null) {
|
|
try {
|
|
_jettyTimer.stop();
|
|
} catch (Exception e) {}
|
|
_jettyTimer = null;
|
|
}
|
|
}
|
|
changeState(STOPPED);
|
|
}
|
|
|
|
/** @since 0.9.4 */
|
|
public ClientAppState getState() {
|
|
return _state;
|
|
}
|
|
|
|
/** @since 0.9.4 */
|
|
public String getName() {
|
|
return NAME;
|
|
}
|
|
|
|
/** @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);
|
|
}
|
|
|
|
/**
|
|
* To get to Jetty
|
|
* @return may be null or stopped perhaps
|
|
* @since Jetty 6 since it doesn't have Server.getServers()
|
|
*/
|
|
synchronized Server getConsoleServer() {
|
|
return _server;
|
|
}
|
|
|
|
/**
|
|
* To get to Jetty
|
|
* @return may be null or stopped perhaps
|
|
* @since 0.9.38
|
|
*/
|
|
static Server getConsoleServer(I2PAppContext ctx) {
|
|
ClientApp app = ctx.clientAppManager().getRegisteredApp(NAME);
|
|
return (app != null) ? ((RouterConsoleRunner)app).getConsoleServer() : null;
|
|
}
|
|
|
|
|
|
/** @since 0.8.13, moved from LogsHelper in 0.9.33 */
|
|
public static String jettyVersion() {
|
|
return Server.getVersion();
|
|
}
|
|
|
|
private void startTrayApp() {
|
|
// if no permissions, don't even try
|
|
// isLaunchedAsService() always returns true on Linux
|
|
if (GraphicsEnvironment.isHeadless() || SystemVersion.isLinuxService() ||
|
|
(SystemVersion.isWindows() && _context.hasWrapper() && WrapperManager.isLaunchedAsService())) {
|
|
// required true for jrobin to work
|
|
System.setProperty("java.awt.headless", "true");
|
|
return;
|
|
}
|
|
try {
|
|
// default false for now, except on OSX and non-service windows
|
|
String sdtg = _context.getProperty(PROP_DTG_ENABLED);
|
|
boolean desktopguiEnabled = Boolean.parseBoolean(sdtg) ||
|
|
(sdtg == null && (SystemVersion.isWindows() || SystemVersion.isMac()));
|
|
if (desktopguiEnabled) {
|
|
System.setProperty("java.awt.headless", "false");
|
|
net.i2p.desktopgui.Main dtg = new net.i2p.desktopgui.Main(_context, _mgr, null);
|
|
dtg.startup();
|
|
} 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();
|
|
}
|
|
}
|
|
|
|
/** @since 0.9.17 */
|
|
private void checkJavaVersion() {
|
|
boolean noJava7 = !SystemVersion.isJava7();
|
|
boolean noPack200 = (PluginStarter.pluginsEnabled(_context) || !NewsHelper.isUpdateDisabled(_context)) &&
|
|
!FileUtil.isPack200Supported();
|
|
boolean openARM = SystemVersion.isARM() && SystemVersion.isOpenJDK() && !SystemVersion.isJava9();
|
|
boolean isZero = SystemVersion.isZeroVM();
|
|
boolean isJava11 = false; // SystemVersion.isJava11();
|
|
if (noJava7 || noPack200 || openARM || isZero || isJava11) {
|
|
String s = "Java version: " + System.getProperty("java.version") +
|
|
" OS: " + System.getProperty("os.name") + ' ' +
|
|
System.getProperty("os.arch") + ' ' +
|
|
System.getProperty("os.version");
|
|
net.i2p.util.Log log = _context.logManager().getLog(RouterConsoleRunner.class);
|
|
log.logAlways(net.i2p.util.Log.WARN, s);
|
|
System.out.println("Warning: " + s);
|
|
if (noJava7) {
|
|
s = "Java 7 is now required, please upgrade";
|
|
log.logAlways(net.i2p.util.Log.WARN, s);
|
|
System.out.println("Warning: " + s);
|
|
}
|
|
if (noPack200) {
|
|
s = "Pack200 is required for plugins and automatic updates, please upgrade Java";
|
|
log.logAlways(net.i2p.util.Log.WARN, s);
|
|
System.out.println("Warning: " + s);
|
|
}
|
|
if (openARM) {
|
|
s = "OpenJDK 7/8 are not recommended for ARM. Use OpenJDK 9 (or higher) or Oracle Java 8 (or higher)";
|
|
log.logAlways(net.i2p.util.Log.WARN, s);
|
|
System.out.println("Warning: " + s);
|
|
}
|
|
if (isZero) {
|
|
s = "OpenJDK Zero is a very slow interpreter-only JVM. Not recommended for use with I2P. Please use a faster JVM if possible.";
|
|
log.logAlways(net.i2p.util.Log.WARN, s);
|
|
System.out.println("Warning: " + s);
|
|
}
|
|
//if (isJava11) {
|
|
// s = "Java 11+ support is beta, and not recommended for general use";
|
|
// log.logAlways(net.i2p.util.Log.WARN, s);
|
|
// System.out.println("Warning: " + s);
|
|
//}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* http://irc.codehaus.org/display/JETTY/Porting+to+jetty6
|
|
*
|
|
*<pre>
|
|
* Server
|
|
* HandlerCollection
|
|
* HostCheckHandler (extends GzipHandler)
|
|
* ContextHandlerCollection
|
|
* LocaleWebAppHandler (routerconsole)
|
|
* SessionHandler
|
|
* SecurityHandler
|
|
* ServletHandler
|
|
* servlets...
|
|
* WebAppContext (i2psnark)
|
|
* WebAppContext (i2ptunnel)
|
|
* WebAppContext (imagegen)
|
|
* WebAppContext (susidns)
|
|
* WebAppContext (susimail)
|
|
* WebAppContext (for each plugin with a .war)
|
|
* DefaultHandler
|
|
* RequestLogHandler (opt)
|
|
*</pre>
|
|
*
|
|
* Porting to Jetty 9:
|
|
*
|
|
* http://dev.eclipse.org/mhonarc/lists/jetty-dev/msg01952.html
|
|
* You are missing a few facts about Jetty 9.1 ...
|
|
* First, there are no longer any blocking connectors.
|
|
* Its all async / nio connectors now. (mainly because that's the direction that the servlet api 3.1 is taking)
|
|
*
|
|
* Next, there is only 1 connector. The ServerConnector.
|
|
* However, it takes 1 or more ConnectionFactory implementations to know how to handle the incoming connection.
|
|
* We have factories for HTTP (0.9 thru 1.1), SPDY, SSL-http, and SSL-npn so far.
|
|
* This list of factories will expand as the future of connectivity to web servers is ever growing (think HTTP/2)
|
|
*
|
|
* Use the embedded examples for help understanding this.
|
|
* http://git.eclipse.org/c/jetty/org.eclipse.jetty.project.git/tree/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyConnectors.java?id=jetty-9.1.0.RC0
|
|
*
|
|
*/
|
|
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");
|
|
// FIXME
|
|
// http://dev.eclipse.org/mhonarc/lists/jetty-users/msg03487.html
|
|
//_server.setGracefulShutdown(1000);
|
|
|
|
// In Jetty 6, QTP was not concurrent, so we switched to
|
|
// ThreadPoolExecutor with a fixed-size queue, a set maxThreads,
|
|
// and a RejectedExecutionPolicy of CallerRuns.
|
|
// Unfortunately, CallerRuns causes lockups in Jetty NIO (ticket #1395)
|
|
// In addition, no flavor of TPE gives us what QTP does:
|
|
// - TPE direct handoff (which we were using) never queues.
|
|
// This doesn't provide any burst management when maxThreads is reached.
|
|
// CallerRuns was an attempt to work around that.
|
|
// - TPE unbounded queue does not adjust the number of threads.
|
|
// This doesn't provide automatic resource management.
|
|
// - TPE bounded queue does not add threads until the queue is full.
|
|
// This doesn't provide good responsiveness to even small bursts.
|
|
// QTP adds threads as soon as the queue is non-empty.
|
|
// QTP as of Jetty 7 uses concurrent.
|
|
// QTP unbounded queue is the default in Jetty.
|
|
// So switch back to QTP with a bounded queue.
|
|
//
|
|
// ref:
|
|
// http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ThreadPoolExecutor.html
|
|
// https://wiki.eclipse.org/Jetty/Howto/High_Load
|
|
//
|
|
//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");
|
|
LinkedBlockingQueue<Runnable> lbq = new LinkedBlockingQueue<Runnable>(4*MAX_THREADS);
|
|
// min and max threads will be reset below
|
|
QueuedThreadPool qtp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, MAX_IDLE_TIME, lbq);
|
|
qtp.setName(THREAD_NAME);
|
|
qtp.setDaemon(true);
|
|
_server = new Server(qtp);
|
|
//}
|
|
|
|
HandlerCollection hColl = new HandlerCollection();
|
|
ContextHandlerCollection chColl = new ContextHandlerCollection();
|
|
HostCheckHandler chCollWrapper = new HostCheckHandler(_context);
|
|
chCollWrapper.setHandler(chColl);
|
|
// gone in Jetty 7
|
|
//_server.addHandler(hColl);
|
|
_server.setHandler(hColl);
|
|
hColl.addHandler(chCollWrapper);
|
|
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 += '/';
|
|
|
|
Set<String> listenHosts = new HashSet<String>(8);
|
|
HandlerWrapper rootWebApp = null;
|
|
ServletHandler rootServletHandler = null;
|
|
List<Connector> connectors = new ArrayList<Connector>(4);
|
|
try {
|
|
int boundAddresses = 0;
|
|
SortedSet<String> 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) {
|
|
List<String> hosts = new ArrayList<String>(2);
|
|
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
|
|
if ((!hasIPV6) && Addresses.isIPv6Address(host))
|
|
throw new IOException("IPv6 addresses unsupported");
|
|
if ((!hasIPV4) && Addresses.isIPv4Address(host))
|
|
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
|
|
// 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) {}
|
|
}
|
|
HttpConfiguration httpConfig = new HttpConfiguration();
|
|
// number of acceptors, (default) number of selectors
|
|
ServerConnector lsnr = new ServerConnector(_server, 1, 0,
|
|
new HttpConnectionFactory(httpConfig));
|
|
//lsnr.setUseDirectBuffers(false); // default true seems to be leaky
|
|
lsnr.setHost(host);
|
|
lsnr.setPort(lport);
|
|
lsnr.setIdleTimeout(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++;
|
|
hosts.add(host);
|
|
} 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);
|
|
}
|
|
}
|
|
if (hosts.isEmpty()) {
|
|
_context.portMapper().register(PortMapper.SVC_CONSOLE, lport);
|
|
} else {
|
|
// put IPv4 first
|
|
Collections.sort(hosts, new HostComparator());
|
|
_context.portMapper().register(PortMapper.SVC_CONSOLE, hosts.get(0), lport);
|
|
// note that we could still fail in connector.start() below
|
|
listenHosts.addAll(hosts);
|
|
}
|
|
}
|
|
|
|
// 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");
|
|
// Put the list of hosts together early, so we can put it in the selfsigned cert.
|
|
StringTokenizer tok = new StringTokenizer(_sslListenHost, " ,");
|
|
Set<String> altNames = new HashSet<String>(4);
|
|
while (tok.hasMoreTokens()) {
|
|
String s = tok.nextToken().trim();
|
|
if (!s.equals("0.0.0.0") && !s.equals("::") &&
|
|
!s.equals("0:0:0:0:0:0:0:0"))
|
|
altNames.add(s);
|
|
}
|
|
String allowed = _context.getProperty(PROP_ALLOWED_HOSTS);
|
|
if (allowed != null) {
|
|
tok = new StringTokenizer(allowed, " ,");
|
|
while (tok.hasMoreTokens()) {
|
|
altNames.add(tok.nextToken().trim());
|
|
}
|
|
}
|
|
if (verifyKeyStore(keyStore, altNames)) {
|
|
// the keystore path and password
|
|
SslContextFactory sslFactory = new SslContextFactory(keyStore.getAbsolutePath());
|
|
sslFactory.setKeyStorePassword(_context.getProperty(PROP_KEYSTORE_PASSWORD, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD));
|
|
// the X.509 cert password (if not present, verifyKeyStore() returned false)
|
|
sslFactory.setKeyManagerPassword(_context.getProperty(PROP_KEY_PASSWORD, "thisWontWork"));
|
|
sslFactory.addExcludeProtocols(I2PSSLSocketFactory.EXCLUDE_PROTOCOLS.toArray(
|
|
new String[I2PSSLSocketFactory.EXCLUDE_PROTOCOLS.size()]));
|
|
sslFactory.addExcludeCipherSuites(I2PSSLSocketFactory.EXCLUDE_CIPHERS.toArray(
|
|
new String[I2PSSLSocketFactory.EXCLUDE_CIPHERS.size()]));
|
|
List<String> hosts = new ArrayList<String>(2);
|
|
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
|
|
if ((!hasIPV6) && Addresses.isIPv6Address(host))
|
|
throw new IOException("IPv6 addresses unsupported");
|
|
if ((!hasIPV4) && Addresses.isIPv4Address(host))
|
|
throw new IOException("IPv4 addresses unsupported");
|
|
ServerSocket testSock = null;
|
|
try {
|
|
// see comments above
|
|
testSock = new ServerSocket();
|
|
InetSocketAddress isa = new InetSocketAddress(host, 0);
|
|
testSock.bind(isa);
|
|
} finally {
|
|
if (testSock != null) try { testSock.close(); } catch (IOException ioe) {}
|
|
}
|
|
HttpConfiguration httpConfig = new HttpConfiguration();
|
|
httpConfig.setSecureScheme("https");
|
|
httpConfig.setSecurePort(sslPort);
|
|
httpConfig.addCustomizer(new SecureRequestCustomizer());
|
|
// number of acceptors, (default) number of selectors
|
|
ServerConnector ssll = new ServerConnector(_server, 1, 0,
|
|
new SslConnectionFactory(sslFactory, "http/1.1"),
|
|
new HttpConnectionFactory(httpConfig));
|
|
//sssll.setUseDirectBuffers(false); // default true seems to be leaky
|
|
ssll.setHost(host);
|
|
ssll.setPort(sslPort);
|
|
ssll.setIdleTimeout(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++;
|
|
hosts.add(host);
|
|
} 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);
|
|
}
|
|
}
|
|
if (hosts.isEmpty()) {
|
|
_context.portMapper().register(PortMapper.SVC_HTTPS_CONSOLE, sslPort);
|
|
} else {
|
|
// put IPv4 first
|
|
Collections.sort(hosts, new HostComparator());
|
|
_context.portMapper().register(PortMapper.SVC_HTTPS_CONSOLE, hosts.get(0), sslPort);
|
|
// note that we could still fail in connector.start() below
|
|
listenHosts.addAll(hosts);
|
|
}
|
|
} 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;
|
|
}
|
|
// Each address spawns a Connector and an Acceptor thread
|
|
// If the min is less than this, we have no thread for the handlers or the expiration thread.
|
|
qtp.setMinThreads(MIN_THREADS + (2 * boundAddresses));
|
|
qtp.setMaxThreads(MAX_THREADS + (2 * boundAddresses));
|
|
|
|
File tmpdir = new SecureDirectory(workDir, ROUTERCONSOLE + "-" +
|
|
(_listenPort != null ? _listenPort : _sslListenPort));
|
|
tmpdir.mkdir();
|
|
rootServletHandler = new ServletHandler();
|
|
rootWebApp = new LocaleWebAppHandler(_context,
|
|
"/", _webAppsDir + ROUTERCONSOLE + ".war",
|
|
tmpdir, rootServletHandler);
|
|
try {
|
|
// Not sure who is supposed to call this, but unless we do,
|
|
// all the jsps die NPE, because JspFactory.getDefaultContext() returns null.
|
|
// We probably have to do this because we don't bundle the Jetty annotations jar and scanner.
|
|
// This is only with Tomcat 8, not with the Jetty (Eclipse) jsp impl.
|
|
// Got a clue from this ancient post for Tomcat 6:
|
|
// https://bz.apache.org/bugzilla/show_bug.cgi?id=39804
|
|
// see also apps/jetty/build.xml
|
|
Class.forName("org.eclipse.jetty.apache.jsp.JettyJasperInitializer");
|
|
} catch (ClassNotFoundException cnfe) {
|
|
System.err.println("Warning: JettyJasperInitializer not found");
|
|
}
|
|
WebAppContext wac = (WebAppContext)(rootWebApp.getHandler());
|
|
initialize(_context, wac);
|
|
WebAppStarter.setWebAppConfiguration(wac);
|
|
chColl.addHandler(rootWebApp);
|
|
|
|
} catch (Exception ioe) {
|
|
ioe.printStackTrace();
|
|
}
|
|
|
|
// fix up the allowed hosts set (see HostCheckHandler)
|
|
if (listenHosts.contains("0.0.0.0") ||
|
|
listenHosts.contains("::") ||
|
|
listenHosts.contains("0:0:0:0:0:0:0:0")) {
|
|
// empty set says all are valid
|
|
listenHosts.clear();
|
|
} else {
|
|
listenHosts.add("localhost");
|
|
listenHosts.add("127.0.0.1");
|
|
listenHosts.add("::1");
|
|
listenHosts.add("0:0:0:0:0:0:0:1");
|
|
String allowed = _context.getProperty(PROP_ALLOWED_HOSTS);
|
|
if (allowed != null) {
|
|
StringTokenizer tok = new StringTokenizer(allowed, " ,");
|
|
while (tok.hasMoreTokens()) {
|
|
listenHosts.add(tok.nextToken());
|
|
}
|
|
}
|
|
}
|
|
chCollWrapper.setListenHosts(listenHosts);
|
|
|
|
// 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) {
|
|
String port = (_listenPort != null) ? _listenPort : ((_sslListenPort != null) ? _sslListenPort : Integer.toString(DEFAULT_LISTEN_PORT));
|
|
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:" + port + "/,\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<String>();
|
|
if (_server.isRunning()) {
|
|
File dir = new File(_webAppsDir);
|
|
File files[] = dir.listFiles(WAR_FILTER);
|
|
if (files != null) {
|
|
for (int i = 0; i < files.length; i++) {
|
|
String appName = files[i].getName();
|
|
appName = appName.substring(0, appName.lastIndexOf(".war"));
|
|
String enabled = props.getProperty(PREFIX + appName + ENABLED);
|
|
if (appName.equals("addressbook")) {
|
|
// addressbook.war is now empty, thread is started by SusiDNS
|
|
if (enabled != null) {
|
|
props.remove(PREFIX + "addressbook" + ENABLED);
|
|
rewrite = true;
|
|
}
|
|
} else if (appName.equals("jsonrpc") && enabled == null) {
|
|
// jsonrpc (i2pcontrol) webapp default is false
|
|
props.setProperty(PREFIX + "jsonrpc" + ENABLED, "false");
|
|
rewrite = true;
|
|
} else if (! "false".equals(enabled)) {
|
|
try {
|
|
String path = files[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(_context), "StatSummarizer", true);
|
|
t.setPriority(Thread.NORM_PRIORITY - 1);
|
|
t.start();
|
|
|
|
ConsoleUpdateManager um = new ConsoleUpdateManager(_context, _mgr, null);
|
|
um.start();
|
|
NewsManager nm = new NewsManager(_context, _mgr, null);
|
|
nm.startup();
|
|
|
|
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);
|
|
|
|
if (_mgr != null &&
|
|
_context.getBooleanProperty(HelperBase.PROP_ADVANCED) &&
|
|
_context.getProperty(Analysis.PROP_FREQUENCY, 0L) > 0) {
|
|
// registers and starts itself
|
|
Analysis.getInstance(_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, Set<String> altNames) {
|
|
if (ks.exists()) {
|
|
String ksPW = _context.getProperty(PROP_KEYSTORE_PASSWORD, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD);
|
|
KeyStoreUtil.logCertExpiration(ks, ksPW, 180*24*60*60*1000L);
|
|
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, altNames);
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a new keystore with a keypair in it.
|
|
*
|
|
* @return success
|
|
* @since 0.8.3
|
|
*/
|
|
private boolean createKeyStore(File ks, Set<String> altNames) {
|
|
// make a random 48 character password (30 * 8 / 5)
|
|
String keyPassword = KeyStoreUtil.randomString();
|
|
String cname = "localhost";
|
|
boolean success = KeyStoreUtil.createKeys(ks, "console", cname, altNames, "Console", keyPassword);
|
|
if (success) {
|
|
success = ks.exists();
|
|
if (success) {
|
|
try {
|
|
Map<String, String> changes = new HashMap<String, String>();
|
|
changes.put(PROP_KEYSTORE_PASSWORD, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD);
|
|
changes.put(PROP_KEY_PASSWORD, keyPassword);
|
|
_context.router().saveConfig(changes, null);
|
|
} catch (Exception e) {} // class cast exception
|
|
// export cert, fails silently
|
|
File dir = new SecureDirectory(_context.getConfigDir(), "certificates");
|
|
dir.mkdir();
|
|
dir = new SecureDirectory(dir, "console");
|
|
dir.mkdir();
|
|
File certFile = new File(dir, "console.local.crt");
|
|
KeyStoreUtil.exportCert(ks, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD, "console", certFile);
|
|
}
|
|
}
|
|
if (success) {
|
|
System.err.println("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" +
|
|
"The certificate was generated randomly.\n" +
|
|
"Unless you have changed the default settings, the certificate 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<ConstraintMapping> constraints = new ArrayList<ConstraintMapping>(4);
|
|
ConsolePasswordManager mgr = new ConsolePasswordManager(ctx);
|
|
boolean enable = ctx.getBooleanProperty(PROP_PW_ENABLE);
|
|
if (enable) {
|
|
Map<String, String> userpw = mgr.getMD5(PROP_CONSOLE_PW);
|
|
if (userpw.isEmpty()) {
|
|
enable = false;
|
|
ctx.router().saveConfig(PROP_PW_ENABLE, "false");
|
|
} else {
|
|
HashLoginService realm = new CustomHashLoginService(JETTY_REALM, context.getContextPath(),
|
|
ctx.logManager().getLog(RouterConsoleRunner.class));
|
|
sec.setLoginService(realm);
|
|
sec.setAuthenticator(authenticator);
|
|
String[] role = new String[] {JETTY_ROLE};
|
|
for (Map.Entry<String, String> e : userpw.entrySet()) {
|
|
String user = e.getKey();
|
|
String pw = e.getValue();
|
|
Credential cred = Credential.getCredential(MD5_CREDENTIAL_TYPE + pw);
|
|
realm.putUser(user, cred, role);
|
|
Constraint constraint = new Constraint(user, JETTY_ROLE);
|
|
constraint.setAuthenticate(true);
|
|
ConstraintMapping cm = new ConstraintMapping();
|
|
cm.setConstraint(constraint);
|
|
cm.setPathSpec("/");
|
|
constraints.add(cm);
|
|
// Jetty does auth checking only with ISO-8859-1,
|
|
// so register a 2nd and 3rd user with different encodings if necessary.
|
|
// Might work, might not...
|
|
// There's no standard and browser behavior varies.
|
|
// Chrome sends UTF-8. Firefox doesn't send anything.
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=41489
|
|
// see also RFC 7616/7617 (late 2015) and PasswordManager.md5Hex()
|
|
byte[] b1 = DataHelper.getUTF8(user);
|
|
byte[] b2 = DataHelper.getASCII(user);
|
|
if (!DataHelper.eq(b1, b2)) {
|
|
try {
|
|
// each char truncated to 8 bytes
|
|
String user2 = new String(b2, "ISO-8859-1");
|
|
realm.putUser(user2, cred, role);
|
|
constraint = new Constraint(user2, JETTY_ROLE);
|
|
constraint.setAuthenticate(true);
|
|
cm = new ConstraintMapping();
|
|
cm.setConstraint(constraint);
|
|
cm.setPathSpec("/");
|
|
constraints.add(cm);
|
|
|
|
// each UTF-8 byte as a char
|
|
// this is what chrome does
|
|
String user3 = new String(b1, "ISO-8859-1");
|
|
realm.putUser(user3, cred, role);
|
|
constraint = new Constraint(user3, JETTY_ROLE);
|
|
constraint.setAuthenticate(true);
|
|
cm = new ConstraintMapping();
|
|
cm.setConstraint(constraint);
|
|
cm.setPathSpec("/");
|
|
constraints.add(cm);
|
|
} catch (UnsupportedEncodingException uee) {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
// No, we can't share the ConstraintSecurityHandler across all webapps
|
|
// But we can force all webapps to use a single Timer thread
|
|
// see HashSessionManager javadoc
|
|
synchronized(RouterConsoleRunner.class) {
|
|
if (_jettyTimer == null) {
|
|
_jettyTimer = new ScheduledExecutorScheduler("Console HashSessionScavenger", true);
|
|
try {
|
|
_jettyTimer.start();
|
|
} catch (Exception e) {
|
|
System.err.println("Warning: ScheduledExecutorScheduler start failed: " + e);
|
|
}
|
|
}
|
|
context.getServletContext().setAttribute("org.eclipse.jetty.server.session.timer", _jettyTimer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For logging authentication failures
|
|
* @since 0.9.28
|
|
*/
|
|
private static class CustomHashLoginService extends HashLoginService {
|
|
private final String _webapp;
|
|
private final net.i2p.util.Log _log;
|
|
|
|
public CustomHashLoginService(String realm, String webapp, net.i2p.util.Log log) {
|
|
super(realm);
|
|
_webapp = webapp;
|
|
_log = log;
|
|
}
|
|
|
|
@Override
|
|
public UserIdentity login(String username, Object credentials) {
|
|
UserIdentity rv = super.login(username, credentials);
|
|
if (rv == null)
|
|
//_log.logAlways(net.i2p.util.Log.WARN, "Console authentication failed, webapp: " + _webapp + ", user: " + username);
|
|
_log.logAlways(net.i2p.util.Log.WARN, "Console authentication failed, user: " + username);
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
/** @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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops all but the root webapp (routerconsole.war)
|
|
* In Jetty 9, stopping the server doesn't stop the non-root webapps,
|
|
* so we must do it here.
|
|
* There should be a better way to do this, possibly by
|
|
* making the webapps "managed".
|
|
* @since 0.9.30
|
|
*/
|
|
private void stopAllWebApps() {
|
|
Properties props = webAppProperties(_context);
|
|
Set<String> keys = props.stringPropertyNames();
|
|
for (String name : keys) {
|
|
if (name.startsWith(PREFIX) && name.endsWith(ENABLED)) {
|
|
String app = name.substring(PREFIX.length(), name.lastIndexOf(ENABLED));
|
|
if (ROUTERCONSOLE.equals(app))
|
|
continue;
|
|
if (WebAppStarter.isWebAppRunning(_context, app)) {
|
|
try {
|
|
WebAppStarter.stopWebApp(_context, app);
|
|
} catch (Throwable t) { t.printStackTrace(); }
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private static class WarFilenameFilter extends FileSuffixFilter {
|
|
private static final String RCWAR = ROUTERCONSOLE + ".war";
|
|
|
|
public WarFilenameFilter() { super(".war"); }
|
|
|
|
public boolean accept(File file) {
|
|
return super.accept(file) && !file.getName().equals(RCWAR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Put IPv4 first
|
|
* @since 0.9.24
|
|
*/
|
|
private static class HostComparator implements Comparator<String>, Serializable {
|
|
public int compare(String l, String r) {
|
|
boolean l4 = l.contains(".");
|
|
boolean r4 = r.contains(".");
|
|
if (l4 && !r4)
|
|
return -1;
|
|
if (r4 && !l4)
|
|
return 1;
|
|
return l.compareTo(r);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<Runnable>(),
|
|
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;
|
|
}
|
|
}
|
|
*****/
|
|
|
|
}
|