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="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER&quot; javaProject=&quot;Showcase&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/Showcase/core/src&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwt-user/core/src&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwt-user/core/super&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwt-dev/core/super&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#10;&lt;runtimeClasspathEntry id=&quot;org.eclipse.jdt.launching.classpathentry.defaultClasspath&quot;&gt;&#10;&lt;memento exportedEntriesOnly=&quot;false&quot; project=&quot;Showcase&quot;/&gt;&#10;&lt;/runtimeClasspathEntry&gt;&#10;"/>
+</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&#10;-codeServerPort auto&#10;-server :ssl&#10;-startupUrl Showcase.html&#13;&#10;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&#13;&#10;-Xmx512M&#13;&#10;-Dgwt.devjar=${gwt_devjar}"/>
+</launchConfiguration>