Revision: 9628
Author: gwt.mirror...@gmail.com
Date: Wed Jan 26 10:35:35 2011
Log: Add SSL support to DevMode.

Issue: 1806
Patch by: jat
Review by: conroy, tobyr

Review at http://gwt-code-reviews.appspot.com/1324801

http://code.google.com/p/google-web-toolkit/source/detail?r=9628

Added:
 /trunk/dev/core/src/com/google/gwt/dev/shell/jetty/README-SSL.txt
 /trunk/dev/core/src/com/google/gwt/dev/shell/jetty/localhost.keystore
 /trunk/dev/core/src/com/google/gwt/dev/shell/secure24.png
 /trunk/eclipse/samples/Showcase/Showcase-SSL.launch
Modified:
 /trunk/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java
 /trunk/dev/core/src/com/google/gwt/dev/DevMode.java
 /trunk/dev/core/src/com/google/gwt/dev/DevModeBase.java
 /trunk/dev/core/src/com/google/gwt/dev/RunWebApp.java
 /trunk/dev/core/src/com/google/gwt/dev/SwingUI.java
 /trunk/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
 /trunk/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java

=======================================
--- /dev/null
+++ /trunk/dev/core/src/com/google/gwt/dev/shell/jetty/README-SSL.txt Wed Jan 26 10:35:35 2011
@@ -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
+
+
=======================================
--- /dev/null   
+++ /trunk/dev/core/src/com/google/gwt/dev/shell/jetty/localhost.keystore Wed Jan 26 10:35:35 2011
Binary file, no diff available.
=======================================
--- /dev/null   
+++ /trunk/dev/core/src/com/google/gwt/dev/shell/secure24.png Wed Jan 26 10:35:35 2011
Binary file, no diff available.
=======================================
--- /dev/null
+++ /trunk/eclipse/samples/Showcase/Showcase-SSL.launch Wed Jan 26 10:35:35 2011
@@ -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>
=======================================
--- /trunk/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java Fri Jun 18 12:28:31 2010 +++ /trunk/dev/core/src/com/google/gwt/core/ext/ServletContainerLauncher.java Wed Jan 26 10:35:35 2011
@@ -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.
=======================================
--- /trunk/dev/core/src/com/google/gwt/dev/DevMode.java Fri Jan 7 11:13:03 2011 +++ /trunk/dev/core/src/com/google/gwt/dev/DevMode.java Wed Jan 26 10:35:35 2011
@@ -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,
@@ -478,6 +484,13 @@
           return -1;
         }
       }
+
+      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
=======================================
--- /trunk/dev/core/src/com/google/gwt/dev/DevModeBase.java Mon Dec 20 10:49:11 2010 +++ /trunk/dev/core/src/com/google/gwt/dev/DevModeBase.java Wed Jan 26 10:35:35 2011
@@ -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 {
=======================================
--- /trunk/dev/core/src/com/google/gwt/dev/RunWebApp.java Wed Dec 9 16:06:06 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/RunWebApp.java Wed Jan 26 10:35:35 2011
@@ -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) {
=======================================
--- /trunk/dev/core/src/com/google/gwt/dev/SwingUI.java Tue Nov 24 15:01:58 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/SwingUI.java Wed Jan 26 10:35:35 2011
@@ -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) {
=======================================
--- /trunk/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java Wed Nov 10 09:24:08 2010 +++ /trunk/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java Wed Jan 26 10:35:35 2011
@@ -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;
@@ -473,6 +475,15 @@
       classLoader.destroy();
     }
   }
+
+  /**
+   * 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
@@ -494,18 +505,118 @@
         "org.eclipse.jdt.core.JDTCompilerAdapter");
     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
@@ -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();
   }

=======================================
--- /trunk/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java Tue Nov 24 15:01:58 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java Wed Jan 26 10:35:35 2011
@@ -155,6 +155,17 @@
   public void setStartupUrls(Map<String, URL> urls) {
     // do nothing by default
   }
+
+  /**
+ * 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.

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to