Add SSL support to DevMode.
Issue: 1806
Patch by: jat
Review by: conroy, tobyr
Review at http://gwt-code-reviews.appspot.com/1324801
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9628 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java b/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
index c599507..cd47771 100644
--- a/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
+++ b/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
@@ -58,6 +58,18 @@
public String getName() {
return "Web Server";
}
+ /**
+ * Return true if this servlet container launcher is configured for secure
+ * operation (ie, HTTPS). This value is only queried after arguments, if any,
+ * have been processed.
+ *
+ * The default implementation just returns false.
+ *
+ * @return true if HTTPS is in use
+ */
+ public boolean isSecure() {
+ return false;
+ }
/**
* Process any supplied arguments.
diff --git a/dev/core/src/com/google/gwt/dev/DevMode.java b/dev/core/src/com/google/gwt/dev/DevMode.java
index db3e200..7d186b2 100644
--- a/dev/core/src/com/google/gwt/dev/DevMode.java
+++ b/dev/core/src/com/google/gwt/dev/DevMode.java
@@ -62,6 +62,9 @@
* Handles the -server command line flag.
*/
protected static class ArgHandlerServer extends ArgHandlerString {
+
+ private static final String DEFAULT_SCL = JettyLauncher.class.getName();
+
private HostedModeOptions options;
public ArgHandlerServer(HostedModeOptions options) {
@@ -73,7 +76,7 @@
if (options.isNoServer()) {
return null;
} else {
- return new String[]{getTag(), JettyLauncher.class.getName()};
+ return new String[]{getTag(), DEFAULT_SCL};
}
}
@@ -106,6 +109,9 @@
sclArgs = null;
sclClassName = arg;
}
+ if (sclClassName.length() == 0) {
+ sclClassName = DEFAULT_SCL;
+ }
Throwable t;
try {
Class<?> clazz = Class.forName(sclClassName, true,
@@ -479,6 +485,13 @@
}
}
+ isHttps = scl.isSecure();
+
+ // Tell the UI if the web server is secure
+ if (isHttps) {
+ ui.setWebServerSecure(serverLogger);
+ }
+
/*
* TODO: This is a hack to pass the base log level to the SCL. We'll have
* to figure out a better way to do this for SCLs in general.
diff --git a/dev/core/src/com/google/gwt/dev/DevModeBase.java b/dev/core/src/com/google/gwt/dev/DevModeBase.java
index 2bb3092..1cfd74a 100644
--- a/dev/core/src/com/google/gwt/dev/DevModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/DevModeBase.java
@@ -676,8 +676,9 @@
private static final Random RNG = new Random();
- public static String normalizeURL(String unknownUrlText, int port, String host) {
- if (unknownUrlText.indexOf(":") != -1) {
+ public static String normalizeURL(String unknownUrlText, boolean isHttps,
+ int port, String host) {
+ if (unknownUrlText.contains("://")) {
// Assume it's a full url.
return unknownUrlText;
}
@@ -687,14 +688,18 @@
unknownUrlText = unknownUrlText.substring(1);
}
- if (port != 80) {
- // CHECKSTYLE_OFF: Not really an assembled error message, so no space
- // after ':'.
- return "http://" + host + ":" + port + "/" + unknownUrlText;
- // CHECKSTYLE_ON
- } else {
- return "http://" + host + "/" + unknownUrlText;
+ String protocol = "http";
+ String portString = ":" + port;
+ if (isHttps) {
+ protocol += "s";
+ if (port == 443) {
+ portString = "";
+ }
+ } else if (port == 80) {
+ portString = "";
}
+
+ return protocol + "://" + host + portString + "/" + unknownUrlText;
}
/**
@@ -728,6 +733,8 @@
protected String connectAddress;
+ protected boolean isHttps;
+
protected BrowserListener listener;
protected final HostedModeBaseOptions options;
@@ -1203,7 +1210,8 @@
ensureCodeServerListener();
Map<String, URL> startupUrls = new HashMap<String, URL>();
for (String prenormalized : options.getStartupURLs()) {
- String startupURL = normalizeURL(prenormalized, getPort(), getHost());
+ String startupURL = normalizeURL(prenormalized, isHttps, getPort(),
+ getHost());
logger.log(TreeLogger.DEBUG, "URL " + prenormalized + " normalized as "
+ startupURL, null);
try {
diff --git a/dev/core/src/com/google/gwt/dev/RunWebApp.java b/dev/core/src/com/google/gwt/dev/RunWebApp.java
index d4987b2..d5604ce 100644
--- a/dev/core/src/com/google/gwt/dev/RunWebApp.java
+++ b/dev/core/src/com/google/gwt/dev/RunWebApp.java
@@ -140,7 +140,8 @@
options.addStartupURL("/");
}
for (String startupUrl : options.getStartupURLs()) {
- startupUrl = DevModeBase.normalizeURL(startupUrl, port, "localhost");
+ startupUrl = DevModeBase.normalizeURL(startupUrl, false, port,
+ "localhost");
try {
BrowserLauncher.browse(startupUrl);
} catch (IOException e) {
diff --git a/dev/core/src/com/google/gwt/dev/SwingUI.java b/dev/core/src/com/google/gwt/dev/SwingUI.java
index ca66ef5..fbffa35 100644
--- a/dev/core/src/com/google/gwt/dev/SwingUI.java
+++ b/dev/core/src/com/google/gwt/dev/SwingUI.java
@@ -290,6 +290,20 @@
}
});
}
+
+ @Override
+ public void setWebServerSecure(TreeLogger serverLogger) {
+ if (webServerLog != null && serverLogger == webServerLog.getLogger()) {
+ EventQueue.invokeLater(new Runnable() {
+ public void run() {
+ // TODO(jat): if the web server has an icon, should combine with the
+ // secure icon or perhaps switch to a different one.
+ ImageIcon secureIcon = loadImageIcon("secure24.png");
+ tabs.setIconAt(1, secureIcon);
+ }
+ });
+ }
+ }
protected int getNextSessionCounter(File logdir) {
synchronized (sessionCounterLock) {
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
index 1034031..42ee9e5 100644
--- a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
@@ -21,6 +21,7 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.dev.util.InstalledHelpInfo;
+import com.google.gwt.dev.util.Util;
import org.mortbay.component.AbstractLifeCycle;
import org.mortbay.jetty.AbstractConnector;
@@ -31,6 +32,7 @@
import org.mortbay.jetty.HttpFields.Field;
import org.mortbay.jetty.handler.RequestLogHandler;
import org.mortbay.jetty.nio.SelectChannelConnector;
+import org.mortbay.jetty.security.SslSocketConnector;
import org.mortbay.jetty.webapp.WebAppClassLoader;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.log.Log;
@@ -475,6 +477,15 @@
}
/**
+ * Represents the type of SSL client certificate authentication desired.
+ */
+ private enum ClientAuth {
+ NONE,
+ WANT,
+ REQUIRE,
+ }
+
+ /**
* System property to suppress warnings about loading web app classes from the
* system classpath.
*/
@@ -495,18 +506,118 @@
System.setProperty("build.compiler", antJavaC);
}
+ /**
+ * Setup a connector for the bind address/port.
+ *
+ * @param connector
+ * @param bindAddress
+ * @param port
+ */
+ private static void setupConnector(AbstractConnector connector,
+ String bindAddress, int port) {
+ if (bindAddress != null) {
+ connector.setHost(bindAddress.toString());
+ }
+ connector.setPort(port);
+
+ // Don't share ports with an existing process.
+ connector.setReuseAddress(false);
+
+ // Linux keeps the port blocked after shutdown if we don't disable this.
+ connector.setSoLingerTime(0);
+ }
+
// default value used if setBaseLogLevel isn't called
private TreeLogger.Type baseLogLevel = TreeLogger.INFO;
private String bindAddress = null;
+ private ClientAuth clientAuth;
+
+ private String keyStore;
+
+ private String keyStorePassword;
+
private final Object privateInstanceLock = new Object();
+ private boolean useSsl;
+
@Override
public String getName() {
return "Jetty";
}
+ @Override
+ public boolean isSecure() {
+ return useSsl;
+ }
+
+ @Override
+ public boolean processArguments(TreeLogger logger, String arguments) {
+ if (arguments != null && arguments.length() > 0) {
+ // TODO(jat): better parsing of the args
+ for (String arg : arguments.split(",")) {
+ int equals = arg.indexOf('=');
+ String tag;
+ String value = null;
+ if (equals < 0) {
+ tag = arg;
+ } else {
+ tag = arg.substring(0, equals);
+ value = arg.substring(equals + 1);
+ }
+ if ("ssl".equals(tag)) {
+ useSsl = true;
+ URL keyStoreUrl = getClass().getResource("localhost.keystore");
+ if (keyStoreUrl == null) {
+ logger.log(TreeLogger.ERROR, "Default GWT keystore not found");
+ return false;
+ }
+ keyStore = keyStoreUrl.toExternalForm();
+ keyStorePassword = "localhost";
+ } else if ("keystore".equals(tag)) {
+ useSsl = true;
+ keyStore = value;
+ } else if ("password".equals(tag)) {
+ useSsl = true;
+ keyStorePassword = value;
+ } else if ("pwfile".equals(tag)) {
+ useSsl = true;
+ keyStorePassword = Util.readFileAsString(new File(value)).trim();
+ if (keyStorePassword == null) {
+ logger.log(TreeLogger.ERROR,
+ "Unable to read keystore password from '" + value + "'");
+ return false;
+ }
+ } else if ("clientAuth".equals(tag)) {
+ useSsl = true;
+ try {
+ clientAuth = ClientAuth.valueOf(value);
+ } catch (IllegalArgumentException e) {
+ logger.log(TreeLogger.WARN, "Ignoring invalid clientAuth of '"
+ + value + "'");
+ }
+ } else {
+ logger.log(TreeLogger.ERROR, "Unexpected argument to "
+ + JettyLauncher.class.getSimpleName() + ": " + arg);
+ return false;
+ }
+ }
+ if (useSsl) {
+ if (keyStore == null) {
+ logger.log(TreeLogger.ERROR, "A keystore is required to use SSL");
+ return false;
+ }
+ if (keyStorePassword == null) {
+ logger.log(TreeLogger.ERROR,
+ "A keystore password is required to use SSL");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
/*
* TODO: This is a hack to pass the base log level to the SCL. We'll have to
* figure out a better way to do this for SCLs in general. Please do not
@@ -540,19 +651,10 @@
// Turn off XML validation.
System.setProperty("org.mortbay.xml.XmlParser.Validating", "false");
- AbstractConnector connector = getConnector();
- if (bindAddress != null) {
- connector.setHost(bindAddress.toString());
- }
- connector.setPort(port);
-
- // Don't share ports with an existing process.
- connector.setReuseAddress(false);
-
- // Linux keeps the port blocked after shutdown if we don't disable this.
- connector.setSoLingerTime(0);
-
Server server = new Server();
+
+ AbstractConnector connector = getConnector(logger);
+ setupConnector(connector, bindAddress, port);
server.addConnector(connector);
// Create a new web app in the war directory.
@@ -582,7 +684,36 @@
"/");
}
- protected AbstractConnector getConnector() {
+ protected AbstractConnector getConnector(TreeLogger logger) {
+ if (useSsl) {
+ TreeLogger sslLogger = logger.branch(TreeLogger.INFO,
+ "Listening for SSL connections");
+ sslLogger.log(TreeLogger.TRACE, "Using keystore " + keyStore);
+ SslSocketConnector conn = new SslSocketConnector();
+ if (clientAuth != null) {
+ switch (clientAuth) {
+ case NONE:
+ conn.setWantClientAuth(false);
+ conn.setNeedClientAuth(false);
+ break;
+ case WANT:
+ sslLogger.log(TreeLogger.TRACE, "Requesting client certificates");
+ conn.setWantClientAuth(true);
+ conn.setNeedClientAuth(false);
+ break;
+ case REQUIRE:
+ sslLogger.log(TreeLogger.TRACE, "Requiring client certificates");
+ conn.setWantClientAuth(true);
+ conn.setNeedClientAuth(true);
+ break;
+ }
+ }
+ conn.setKeystore(keyStore);
+ conn.setTruststore(keyStore);
+ conn.setKeyPassword(keyStorePassword);
+ conn.setTrustPassword(keyStorePassword);
+ return conn;
+ }
return new SelectChannelConnector();
}
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/README-SSL.txt b/dev/core/src/com/google/gwt/dev/shell/jetty/README-SSL.txt
new file mode 100644
index 0000000..a257373
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/README-SSL.txt
@@ -0,0 +1,35 @@
+To use SSL, you will need a certificate. A self-signed certificate for
+localhost is included, but if you want to use a different address you will
+need to create another. To generate a self-signed certificate for an arbitrary
+host name, you can use (for example):
+
+ keytool -keystore keystore -alias jetty -genkey -keyalg RSA -validity 365
+
+Be sure and give the CN as the name that will be used with -bindAddress (which
+will be 127.0.0.1 if not provided).
+
+Note that self-signed certificates will cause the browser to prompt the user
+to accept the certificate -- this should be fine for development, but if not
+you can purchase a real web server certificate from a trusted CA and convert
+it to keystore format using openssl and keytool.
+
+You can use your own keystore like this:
+ -server :keystore=/path/to/keystore,password=password
+OR
+ -server :keystore=/path/to/keystore,pwfile=/path/to/password/file
+
+Using the password option exposes the password to other users on your system,
+so the pwfile option is recommended instead if you care about keeping the
+password secret.
+
+You can also set the clientAuth parameter to request or require client
+certificates (which must have a suitable certificate chain in the keystore),
+like this:
+ -server :keystore=/path/to/keystore,password=password,clientAuth=WANT
+OR
+ -server :keystore=/path/to/keystore,password=password,clientAuth=REQUIRE
+
+You can use a default localhost-only self-signed certificate by just using
+ -server :ssl
+
+
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/localhost.keystore b/dev/core/src/com/google/gwt/dev/shell/jetty/localhost.keystore
new file mode 100644
index 0000000..928d8ac
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/localhost.keystore
Binary files differ
diff --git a/dev/core/src/com/google/gwt/dev/shell/secure24.png b/dev/core/src/com/google/gwt/dev/shell/secure24.png
new file mode 100644
index 0000000..7fda062
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/secure24.png
Binary files differ
diff --git a/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java b/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java
index f08ea6c..f538a1e 100644
--- a/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java
+++ b/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java
@@ -157,6 +157,17 @@
}
/**
+ * Show in the UI that the web server, identified by the logger returned from
+ * {@link #getWebServerLogger(String, byte[])}, is operating in a secure
+ * fashion.
+ *
+ * @param serverLogger
+ */
+ public void setWebServerSecure(TreeLogger serverLogger) {
+ // do nothing by default
+ }
+
+ /**
* Call callback for a given event.
*
* @param eventType type of event
diff --git a/eclipse/samples/Showcase/Showcase-SSL.launch b/eclipse/samples/Showcase/Showcase-SSL.launch
new file mode 100644
index 0000000..c2a9d50
--- /dev/null
+++ b/eclipse/samples/Showcase/Showcase-SSL.launch
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gwt-dev/core/src/com/google/gwt/dev/DevMode.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
+<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry containerPath="org.eclipse.jdt.launching.JRE_CONTAINER" javaProject="Showcase" path="1" type="4"/> "/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry internalArchive="/Showcase/core/src" path="3" type="2"/> "/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry internalArchive="/gwt-user/core/src" path="3" type="2"/> "/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry internalArchive="/gwt-user/core/super" path="3" type="2"/> "/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry internalArchive="/gwt-dev/core/super" path="3" type="2"/> "/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry id="org.eclipse.jdt.launching.classpathentry.defaultClasspath"> <memento exportedEntriesOnly="false" project="Showcase"/> </runtimeClasspathEntry> "/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.DevMode"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-port auto -codeServerPort auto -server :ssl -startupUrl Showcase.html com.google.gwt.sample.showcase.Showcase"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="Showcase"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea -Xmx512M -Dgwt.devjar=${gwt_devjar}"/>
+</launchConfiguration>