* Console:

- Add SSL support - To enable, change clients.config. Examples:
            ## Change to SSL only - just add a '-s'
            clientApp.0.args=-s 7657 ::1,127.0.0.1 ./webapps/
            ## Use both non-SSL and SSL - add '-s port interface'
            clientApp.0.args=7657 ::1,127.0.0.1 -s 7667 ::1,127.0.0.1 ./webapps/
            ## ...and change URLLauncher args further down for the browser to open https:// at startup if you like.
This commit is contained in:
zzz
2010-12-05 19:04:33 +00:00
parent 86de251691
commit 4a9f7b740c
3 changed files with 289 additions and 28 deletions

View File

@ -10,31 +10,46 @@ import java.util.StringTokenizer;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.apps.systray.SysTray; import net.i2p.apps.systray.SysTray;
import net.i2p.data.Base32;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.util.FileUtil; import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread; import net.i2p.util.I2PAppThread;
import net.i2p.util.SecureDirectory; import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.ShellCommand;
import org.mortbay.http.DigestAuthenticator; import org.mortbay.http.DigestAuthenticator;
import org.mortbay.http.HashUserRealm; import org.mortbay.http.HashUserRealm;
import org.mortbay.http.SecurityConstraint; import org.mortbay.http.SecurityConstraint;
import org.mortbay.http.SslListener;
import org.mortbay.http.handler.SecurityHandler; import org.mortbay.http.handler.SecurityHandler;
import org.mortbay.jetty.Server; import org.mortbay.jetty.Server;
import org.mortbay.jetty.servlet.WebApplicationContext; import org.mortbay.jetty.servlet.WebApplicationContext;
import org.mortbay.jetty.servlet.WebApplicationHandler; import org.mortbay.jetty.servlet.WebApplicationHandler;
import org.mortbay.util.InetAddrPort;
public class RouterConsoleRunner { public class RouterConsoleRunner {
private Server _server; private Server _server;
private String _listenPort = "7657"; private String _listenPort;
private String _listenHost = "127.0.0.1"; private String _listenHost;
private String _webAppsDir = "./webapps/"; private String _sslListenPort;
private String _sslListenHost;
private String _webAppsDir;
private static final String PROP_WEBAPP_CONFIG_FILENAME = "router.webappsConfigFile"; private static final String PROP_WEBAPP_CONFIG_FILENAME = "router.webappsConfigFile";
private static final String DEFAULT_WEBAPP_CONFIG_FILENAME = "webapps.config"; private static final String DEFAULT_WEBAPP_CONFIG_FILENAME = "webapps.config";
private static final DigestAuthenticator authenticator = new DigestAuthenticator(); private static final DigestAuthenticator authenticator = new DigestAuthenticator();
public static final String ROUTERCONSOLE = "routerconsole"; public static final String ROUTERCONSOLE = "routerconsole";
public static final String PREFIX = "webapps."; public static final String PREFIX = "webapps.";
public static final String ENABLED = ".startOnLoad"; public static final String ENABLED = ".startOnLoad";
private static final String PROP_KEYSTORE_PASSWORD = "routerconsole.keystorePassword";
private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
private static final String PROP_KEY_PASSWORD = "routerconsole.keyPassword";
private static final String DEFAULT_LISTEN_PORT = "7657";
private static final String DEFAULT_LISTEN_HOST = "127.0.0.1";
private static final String DEFAULT_WEBAPPS_DIR = "./webapps/";
private static final String USAGE = "Bad RouterConsoleRunner arguments, check clientApp.0.args in your clients.config file! " +
"Usage: [[port host[,host]] [-s sslPort [host[,host]]] [webAppsDir]]";
static { static {
System.setProperty("org.mortbay.http.Version.paranoid", "true"); System.setProperty("org.mortbay.http.Version.paranoid", "true");
@ -42,6 +57,27 @@ public class RouterConsoleRunner {
} }
/** /**
* <pre>
* non-SSL:
* RouterConsoleRunner
* RouterConsoleRunner 7657
* RouterConsoleRunner 7657 127.0.0.1
* RouterConsoleRunner 7657 127.0.0.1,::1
* RouterConsoleRunner 7657 127.0.0.1,::1 ./webapps/
*
* SSL:
* RouterConsoleRunner -s 7657
* RouterConsoleRunner -s 7657 127.0.0.1
* RouterConsoleRunner -s 7657 127.0.0.1,::1
* RouterConsoleRunner -s 7657 127.0.0.1,::1 ./webapps/
*
* If using both, non-SSL must be first:
* RouterConsoleRunner 7657 127.0.0.1 -s 7667
* RouterConsoleRunner 7657 127.0.0.1 -s 7667 127.0.0.1
* RouterConsoleRunner 7657 127.0.0.1,::1 -s 7667 127.0.0.1,::1
* RouterConsoleRunner 7657 127.0.0.1,::1 -s 7667 127.0.0.1,::1 ./webapps/
* </pre>
*
* @param args second arg may be a comma-separated list of bind addresses, * @param args second arg may be a comma-separated list of bind addresses,
* for example ::1,127.0.0.1 * for example ::1,127.0.0.1
* On XP, the other order (127.0.0.1,::1) fails the IPV6 bind, * On XP, the other order (127.0.0.1,::1) fails the IPV6 bind,
@ -50,10 +86,40 @@ public class RouterConsoleRunner {
* So the wise choice is ::1,127.0.0.1 * So the wise choice is ::1,127.0.0.1
*/ */
public RouterConsoleRunner(String args[]) { public RouterConsoleRunner(String args[]) {
if (args.length == 3) { if (args.length == 0) {
_listenPort = args[0].trim(); // _listenHost and _webAppsDir are defaulted above
_listenHost = args[1].trim(); _listenPort = DEFAULT_LISTEN_PORT;
_webAppsDir = args[2].trim(); } else {
boolean ssl = false;
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-s"))
ssl = true;
else if ((!ssl) && _listenPort == null)
_listenPort = args[i];
else if ((!ssl) && _listenHost == null)
_listenHost = args[i];
else if (ssl && _sslListenPort == null)
_sslListenPort = args[i];
else if (ssl && _sslListenHost == null)
_sslListenHost = args[i];
else if (_webAppsDir == null)
_webAppsDir = args[i];
else {
System.err.println(USAGE);
throw new IllegalArgumentException(USAGE);
}
}
}
if (_listenHost == null)
_listenHost = DEFAULT_LISTEN_HOST;
if (_sslListenHost == null)
_sslListenHost = _listenHost;
if (_webAppsDir == null)
_webAppsDir = DEFAULT_WEBAPPS_DIR;
// _listenPort and _sslListenPort are not defaulted, if one or the other is null, do not enable
if (_listenPort == null && _sslListenPort == null) {
System.err.println(USAGE);
throw new IllegalArgumentException(USAGE);
} }
} }
@ -96,8 +162,11 @@ public class RouterConsoleRunner {
List<String> notStarted = new ArrayList(); List<String> notStarted = new ArrayList();
WebApplicationHandler baseHandler = null; WebApplicationHandler baseHandler = null;
try { try {
StringTokenizer tok = new StringTokenizer(_listenHost, " ,");
int boundAddresses = 0; int boundAddresses = 0;
// add standard listeners
if (_listenPort != null) {
StringTokenizer tok = new StringTokenizer(_listenHost, " ,");
while (tok.hasMoreTokens()) { while (tok.hasMoreTokens()) {
String host = tok.nextToken().trim(); String host = tok.nextToken().trim();
try { try {
@ -110,8 +179,46 @@ public class RouterConsoleRunner {
System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + ' ' + ioe); System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + ' ' + ioe);
} }
} }
}
// add SSL listeners
int sslPort = 0;
if (_sslListenPort != null) {
try {
sslPort = Integer.parseInt(_sslListenPort);
} catch (NumberFormatException nfe) {}
if (sslPort <= 0)
System.err.println("Bad routerconsole SSL port " + _sslListenPort);
}
if (sslPort > 0) {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
File keyStore = new File(ctx.getConfigDir(), "keystore/console.ks");
if (verifyKeyStore(keyStore)) {
StringTokenizer tok = new StringTokenizer(_sslListenHost, " ,");
while (tok.hasMoreTokens()) {
String host = tok.nextToken().trim();
// doing it this way means we don't have to escape an IPv6 host with []
InetAddrPort iap = new InetAddrPort(host, sslPort);
try {
SslListener ssll = new SslListener(iap);
// the keystore path and password
ssll.setKeystore(keyStore.getAbsolutePath());
ssll.setPassword(ctx.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD));
// the X.509 cert password (if not present, verifyKeyStore() returned false)
ssll.setKeyPassword(ctx.getProperty(PROP_KEY_PASSWORD, "thisWontWork"));
_server.addListener(ssll);
boundAddresses++;
} catch (Exception e) { // probably no exceptions at this point
System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + " for SSL: " + e);
}
}
} else {
System.err.println("Unable to create or access keystore for SSL: " + keyStore.getAbsolutePath());
}
}
if (boundAddresses <= 0) { if (boundAddresses <= 0) {
System.err.println("Unable to bind routerconsole to any address on port " + _listenPort); System.err.println("Unable to bind routerconsole to any address on port " + _listenPort + (sslPort > 0 ? (" or SSL port " + sslPort) : ""));
return; return;
} }
_server.setRootWebApp(ROUTERCONSOLE); _server.setRootWebApp(ROUTERCONSOLE);
@ -201,6 +308,90 @@ public class RouterConsoleRunner {
} }
} }
/**
* @return success if it exists and we have a password, or it was created successfully.
* @since 0.8.3
*/
private static boolean verifyKeyStore(File ks) {
if (ks.exists()) {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
boolean rv = ctx.getProperty(PROP_KEY_PASSWORD) != null;
if (!rv)
System.err.println("Console SSL error, must set " + PROP_KEY_PASSWORD + " in " + (new File(ctx.getConfigDir(), "router.config")).getAbsolutePath());
return rv;
}
File dir = ks.getParentFile();
if (!dir.exists()) {
File sdir = new SecureDirectory(dir.getAbsolutePath());
if (!sdir.mkdir())
return false;
}
return createKeyStore(ks);
}
/**
* Call out to keytool to create a new keystore with a keypair in it.
* Trying to do this programatically is a nightmare, requiring either BouncyCastle
* libs or using proprietary Sun libs, and it's a huge mess.
*
* @return success
* @since 0.8.3
*/
private static boolean createKeyStore(File ks) {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
// make a random 48 character password (30 * 8 / 5)
byte[] rand = new byte[30];
ctx.random().nextBytes(rand);
String keyPassword = Base32.encode(rand);
// and one for the cname
ctx.random().nextBytes(rand);
String cname = Base32.encode(rand) + ".console.i2p.net";
String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath();
String[] args = new String[] {
keytool,
"-genkey", // -genkeypair preferred in newer keytools, but this works with more
"-storetype", "JKS",
"-keystore", ks.getAbsolutePath(),
"-storepass", DEFAULT_KEYSTORE_PASSWORD,
"-alias", "console",
"-dname", "CN=" + cname + ",OU=Console,O=I2P Anonymous Network,L=XX,ST=XX,C=XX",
"-validity", "3652", // 10 years
"-keyalg", "DSA",
"-keysize", "1024",
"-keypass", keyPassword};
boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 30); // 30 secs
if (success) {
success = ks.exists();
if (success) {
SecureFileOutputStream.setPerms(ks);
try {
RouterContext rctx = (RouterContext) ctx;
rctx.router().setConfigSetting(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
rctx.router().setConfigSetting(PROP_KEY_PASSWORD, keyPassword);
rctx.router().saveConfig();
} catch (Exception e) {} // class cast exception
}
}
if (success) {
System.err.println("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" +
"The certificate name was generated randomly, and is not associated with your " +
"IP address, host name, router identity, or destination keys.");
} else {
System.err.println("Failed to create console SSL keystore using command line:");
StringBuilder buf = new StringBuilder(256);
for (int i = 0; i < args.length; i++) {
buf.append('"').append(args[i]).append("\" ");
}
System.err.println(buf.toString());
System.err.println("This is for the Sun/Oracle keytool, others may be incompatible.\n" +
"If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD +
" to " + (new File(ctx.getConfigDir(), "router.config")).getAbsolutePath());
}
return success;
}
static void initialize(WebApplicationContext context) { static void initialize(WebApplicationContext context) {
String password = getPassword(); String password = getPassword();
if (password != null) { if (password != null) {

View File

@ -51,17 +51,18 @@ public class ShellCommand {
*/ */
private class CommandThread extends Thread { private class CommandThread extends Thread {
final Object caller; private final Object caller;
boolean consumeOutput; private final boolean consumeOutput;
String shellCommand; private final Object shellCommand;
CommandThread(Object caller, String shellCommand, boolean consumeOutput) { /**
* @param shellCommand either a String or a String[] (since 0.8.3)
*/
CommandThread(Object caller, Object shellCommand, boolean consumeOutput) {
super("CommandThread"); super("CommandThread");
this.caller = caller; this.caller = caller;
this.shellCommand = shellCommand; this.shellCommand = shellCommand;
this.consumeOutput = consumeOutput; this.consumeOutput = consumeOutput;
_commandSuccessful = false;
_commandCompleted = false;
} }
@Override @Override
@ -200,6 +201,9 @@ public class ShellCommand {
* {@link #getErrorStream()}, respectively. Input can be passed to the * {@link #getErrorStream()}, respectively. Input can be passed to the
* <code>STDIN</code> of the shell process via {@link #getInputStream()}. * <code>STDIN</code> of the shell process via {@link #getInputStream()}.
* *
* Warning, no good way to quote or escape spaces in arguments with this method.
* @deprecated unused
*
* @param shellCommand The command for the shell to execute. * @param shellCommand The command for the shell to execute.
*/ */
public void execute(String shellCommand) { public void execute(String shellCommand) {
@ -215,6 +219,9 @@ public class ShellCommand {
* Input can be passed to the <code>STDIN</code> of the shell process via * Input can be passed to the <code>STDIN</code> of the shell process via
* {@link #getInputStream()}. * {@link #getInputStream()}.
* *
* Warning, no good way to quote or escape spaces in arguments with this method.
* @deprecated unused
*
* @param shellCommand The command for the shell to execute. * @param shellCommand The command for the shell to execute.
* @return <code>true</code> if the spawned shell process * @return <code>true</code> if the spawned shell process
* returns an exit status of 0 (indicating success), * returns an exit status of 0 (indicating success),
@ -237,6 +244,9 @@ public class ShellCommand {
* {@link #getErrorStream()}, respectively. Input can be passed to the * {@link #getErrorStream()}, respectively. Input can be passed to the
* <code>STDIN</code> of the shell process via {@link #getInputStream()}. * <code>STDIN</code> of the shell process via {@link #getInputStream()}.
* *
* Warning, no good way to quote or escape spaces in arguments with this method.
* @deprecated unused
*
* @param shellCommand The command for the shell to execute. * @param shellCommand The command for the shell to execute.
* @param seconds The method will return <code>true</code> if this * @param seconds The method will return <code>true</code> if this
* number of seconds elapses without the process * number of seconds elapses without the process
@ -276,6 +286,9 @@ public class ShellCommand {
* without waiting for an exit status. Any output produced by the executed * without waiting for an exit status. Any output produced by the executed
* command will not be displayed. * command will not be displayed.
* *
* Warning, no good way to quote or escape spaces in arguments with this method.
* @deprecated unused
*
* @param shellCommand The command for the shell to execute. * @param shellCommand The command for the shell to execute.
* @throws IOException * @throws IOException
*/ */
@ -288,6 +301,8 @@ public class ShellCommand {
* all of the command's resulting shell processes have completed. Any output * all of the command's resulting shell processes have completed. Any output
* produced by the executed command will not be displayed. * produced by the executed command will not be displayed.
* *
* Warning, no good way to quote or escape spaces in arguments with this method.
*
* @param shellCommand The command for the shell to execute. * @param shellCommand The command for the shell to execute.
* @return <code>true</code> if the spawned shell process * @return <code>true</code> if the spawned shell process
* returns an exit status of 0 (indicating success), * returns an exit status of 0 (indicating success),
@ -307,7 +322,12 @@ public class ShellCommand {
* specified number of seconds has elapsed first. Any output produced by the * specified number of seconds has elapsed first. Any output produced by the
* executed command will not be displayed. * executed command will not be displayed.
* *
* @param shellCommand The command for the shell to execute. * Warning, no good way to quote or escape spaces in arguments when shellCommand is a String.
* Use a String array for best results, especially on Windows.
*
* @param shellCommand The command for the shell to execute, as a String.
* You can't quote arguments successfully.
* See Runtime.exec(String) for more info.
* @param seconds The method will return <code>true</code> if this * @param seconds The method will return <code>true</code> if this
* number of seconds elapses without the process * number of seconds elapses without the process
* returning an exit status. A value of <code>0</code> * returning an exit status. A value of <code>0</code>
@ -317,7 +337,33 @@ public class ShellCommand {
* else <code>false</code>. * else <code>false</code>.
*/ */
public synchronized boolean executeSilentAndWaitTimed(String shellCommand, int seconds) { public synchronized boolean executeSilentAndWaitTimed(String shellCommand, int seconds) {
return executeSAWT(shellCommand, seconds);
}
/**
* Passes a command to the shell for execution. This method blocks until
* all of the command's resulting shell processes have completed unless a
* specified number of seconds has elapsed first. Any output produced by the
* executed command will not be displayed.
*
* @param commandArray The command for the shell to execute,
* as a String[].
* See Runtime.exec(String[]) for more info.
* @param seconds The method will return <code>true</code> if this
* number of seconds elapses without the process
* returning an exit status. A value of <code>0</code>
* here disables waiting.
* @return <code>true</code> if the spawned shell process
* returns an exit status of 0 (indicating success),
* else <code>false</code>.
* @since 0.8.3
*/
public synchronized boolean executeSilentAndWaitTimed(String[] commandArray, int seconds) {
return executeSAWT(commandArray, seconds);
}
/** @since 0.8.3 */
private boolean executeSAWT(Object shellCommand, int seconds) {
_commandThread = new CommandThread(this, shellCommand, CONSUME_OUTPUT); _commandThread = new CommandThread(this, shellCommand, CONSUME_OUTPUT);
_commandThread.start(); _commandThread.start();
try { try {
@ -364,7 +410,10 @@ public class ShellCommand {
return; return;
} }
private boolean execute(String shellCommand, boolean consumeOutput, boolean waitForExitStatus) { /**
* @param shellCommand either a String or a String[] (since 0.8.3) - quick hack
*/
private boolean execute(Object shellCommand, boolean consumeOutput, boolean waitForExitStatus) {
StreamConsumer processStderrConsumer; StreamConsumer processStderrConsumer;
StreamConsumer processStdoutConsumer; StreamConsumer processStdoutConsumer;
@ -374,7 +423,13 @@ public class ShellCommand {
StreamReader processStdoutReader; StreamReader processStdoutReader;
try { try {
_process = Runtime.getRuntime().exec(shellCommand, null); // easy way so we don't have to copy this whole method
if (shellCommand instanceof String)
_process = Runtime.getRuntime().exec((String)shellCommand);
else if (shellCommand instanceof String[])
_process = Runtime.getRuntime().exec((String[])shellCommand);
else
throw new ClassCastException("shell command must be a String or a String[]");
if (consumeOutput) { if (consumeOutput) {
processStderrConsumer = new StreamConsumer(_process.getErrorStream()); processStderrConsumer = new StreamConsumer(_process.getErrorStream());
processStderrConsumer.start(); processStderrConsumer.start();

View File

@ -6,6 +6,21 @@
# #
# fire up the web console # fire up the web console
## There are several choices, here are some examples:
## non-SSL, bind to local IPv4 only
#clientApp.0.args=7657 127.0.0.1 ./webapps/
## non-SSL, bind to local IPv6 only
#clientApp.0.args=7657 ::1 ./webapps/
## non-SSL, bind to all IPv4 addresses
#clientApp.0.args=7657 0.0.0.0 ./webapps/
## non-SSL, bind to all IPv6 addresses
#clientApp.0.args=7657 :: ./webapps/
## For SSL only, change clientApp.4.args below to https://
## SSL only
#clientApp.0.args=-s 7657 ::1,127.0.0.1 ./webapps/
## non-SSL and SSL
#clientApp.0.args=7657 ::1,127.0.0.1 -s 7667 ::1,127.0.0.1 ./webapps/
## non-SSL only, both IPv6 and IPv4 local interfaces
clientApp.0.args=7657 ::1,127.0.0.1 ./webapps/ clientApp.0.args=7657 ::1,127.0.0.1 ./webapps/
clientApp.0.main=net.i2p.router.web.RouterConsoleRunner clientApp.0.main=net.i2p.router.web.RouterConsoleRunner
clientApp.0.name=I2P Router Console clientApp.0.name=I2P Router Console