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>