Add host name/address to automatically generated whitelist, improve handling
of these regexes.

Patch by: jat
Review by: scottb


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5018 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHostChecker.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHostChecker.java
index 89f755e..eb7b32e 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHostChecker.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHostChecker.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
@@ -31,7 +33,7 @@
   /**
    * The set of always allowed URLs, which are immune to blacklisting.
    */
-  private static final Set<String> alwaysValidHttpHosts = new HashSet<String>();
+  private static final Pattern alwaysValidHttpHosts;
 
   /**
    * The set of blacklisted URLs.
@@ -48,19 +50,49 @@
   private static final Set<String> validHttpHosts = new HashSet<String>();
 
   static {
-    alwaysValidHttpHosts.add("^https?://localhost");
-    alwaysValidHttpHosts.add("^file:");
-    alwaysValidHttpHosts.add("^about:");
-    alwaysValidHttpHosts.add("^res:");
-    alwaysValidHttpHosts.add("^javascript:");
-    alwaysValidHttpHosts.add("^([a-zA-Z][:])[/\\\\]");
+    Set<String> regexes = new HashSet<String>();
+    // Regular URLs may or may not have a port, and must either end with
+    // the host+port, or be followed by a slash (to avoid attacks like
+    // localhost.evildomain.org).
+    String portSuffix = "(:\\d+)?(/.*)?";
+    regexes.add("https?://localhost" + portSuffix);
+    regexes.add("https?://localhost[.]localdomain" + portSuffix);
+    regexes.add("https?://127[.]0[.]0[.]1" + portSuffix);
+    String hostName;
+    try {
+      hostName = InetAddress.getLocalHost().getHostName();
+      if (hostName != null) {
+        hostName = hostName.replace(".", "[.]");
+        regexes.add("https?://" + hostName + portSuffix);
+      }
+    } catch (UnknownHostException e) {
+      // Ignore
+    }
+    String addr;
+    try {
+      addr = InetAddress.getLocalHost().getHostAddress();
+      if (addr != null) {
+        addr = addr.replace(".", "[.]");
+        regexes.add("https?://" + addr + portSuffix);
+      }
+    } catch (UnknownHostException e) {
+      // Ignore
+    }
+    regexes.add("file:.*");
+    regexes.add("about:.*");
+    regexes.add("res:.*");
+    regexes.add("javascript:.*");
+    regexes.add("([a-z][:])[/\\\\].*");
     // matches c:\ and c:/
-    alwaysValidHttpHosts.add("^https?://localhost/");
-    alwaysValidHttpHosts.add("^https?://localhost[.]localdomain/");
-    alwaysValidHttpHosts.add("^https?://127[.]0[.]0[.]1/");
-    alwaysValidHttpHosts.add("^https?://localhost$");
-    alwaysValidHttpHosts.add("^https?://localhost[.]localdomain$");
-    alwaysValidHttpHosts.add("^https?://127[.]0[.]0[.]1$");
+    StringBuilder buf = new StringBuilder();
+    String prefix = "(";
+    for (String regex : regexes) {
+      buf.append(prefix).append('(').append(regex).append(')');
+      prefix = "|";
+    }
+    buf.append(")");
+    alwaysValidHttpHosts = Pattern.compile(buf.toString(),
+        Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
   }
 
   /**
@@ -90,9 +122,10 @@
    * @return true if the host matches
    */
   public static String checkHost(String hostUnderConsideration, Set<String> hosts) {
+    // TODO(jat): build a single regex instead of looping
     hostUnderConsideration = hostUnderConsideration.toLowerCase();
-    for (Iterator<String> i = hosts.iterator(); i.hasNext();) {
-      String rule = i.next().toString().toLowerCase();
+    for (String rule : hosts) {
+      rule = rule.toLowerCase();
       // match on lowercased regex
       if (hostUnderConsideration.matches(".*" + rule + ".*")) {
         return rule;
@@ -149,20 +182,18 @@
   }
 
   /**
-   * This method returns true if the host is always admissable, regardless of
+   * This method returns true if the host is always admissible, regardless of
    * the blacklist.
    * 
    * @param url the URL to be verified
-   * @return returns true if the host is always admissable
+   * @return returns true if the host is always admissible
    */
   public static boolean isAlwaysWhitelisted(String url) {
-    String whitelistRuleFound;
-    whitelistRuleFound = checkHost(url, alwaysValidHttpHosts);
-    return whitelistRuleFound != null;
+    return alwaysValidHttpHosts.matcher(url).matches();
   }
 
   /**
-   * This method returns true if the host is forbidden.
+   * This method returns non-null if the host is forbidden.
    * 
    * @param url the URL to be verified
    * @return returns the regex that specified the host matches the blacklist
@@ -173,7 +204,7 @@
   }
 
   /**
-   * This method returns true if the host is admissable, provided it is not on
+   * This method returns null if the host is admissible, provided it is not on
    * the blacklist.
    * 
    * @param url the URL to be verified
diff --git a/dev/core/test/com/google/gwt/dev/GWTShellTest.java b/dev/core/test/com/google/gwt/dev/GWTShellTest.java
index a50967e..29ab059 100644
--- a/dev/core/test/com/google/gwt/dev/GWTShellTest.java
+++ b/dev/core/test/com/google/gwt/dev/GWTShellTest.java
@@ -44,6 +44,21 @@
 
     assertNotNull(BrowserWidgetHostChecker.matchWhitelisted("white"));
     assertNotNull(BrowserWidgetHostChecker.matchBlacklisted("black"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1.40"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1.40:88"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1.40:88/"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1.40:88/foo"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org:88"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org:88/"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org:88/foo"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org/"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org/foo"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://www.evildomain.org/foo?http://localhost"));
+    assertTrue(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1"));
+    assertTrue(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1:88"));
+    assertTrue(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1:88/"));
+    assertTrue(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1:88/foo"));
 
     assertEquals(new File("myGen").getAbsoluteFile(),
         options.getGenDir().getAbsoluteFile());
diff --git a/dev/core/test/com/google/gwt/dev/HostedModeTest.java b/dev/core/test/com/google/gwt/dev/HostedModeTest.java
index 74b5761..86316ee 100644
--- a/dev/core/test/com/google/gwt/dev/HostedModeTest.java
+++ b/dev/core/test/com/google/gwt/dev/HostedModeTest.java
@@ -31,7 +31,9 @@
 public class HostedModeTest extends ArgProcessorTestBase {
 
   public static class MySCL extends ServletContainerLauncher {
-    public ServletContainer start(TreeLogger logger, int port, File appRootDir)
+    @Override
+    public ServletContainer start(TreeLogger logger, String bindAddr, int port,
+        File appRootDir)
         throws BindException, Exception {
       throw new UnsupportedOperationException();
     }
@@ -55,6 +57,21 @@
 
     assertNotNull(BrowserWidgetHostChecker.matchWhitelisted("white"));
     assertNotNull(BrowserWidgetHostChecker.matchBlacklisted("black"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1.40"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1.40:88"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1.40:88/"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1.40:88/foo"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org:88"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org:88/"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org:88/foo"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org/"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://localhost.evildomain.org/foo"));
+    assertFalse(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://www.evildomain.org/foo?http://localhost"));
+    assertTrue(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1"));
+    assertTrue(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1:88"));
+    assertTrue(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1:88/"));
+    assertTrue(BrowserWidgetHostChecker.isAlwaysWhitelisted("http://127.0.0.1:88/foo"));
 
     assertEquals(new File("myGen").getAbsoluteFile(),
         options.getGenDir().getAbsoluteFile());