Add support for detecting CSP violations during unit tests in GWT.

This CL defines and implements a config property
"strictCspTestingEnabled" that can be set in .gwt.xml
config files. When running unit tests using a module where
this option is enabled, the GWT test runner enforce a strict
(https://csp.withgoogle.com/docs/strict-csp.html) CSP policy on the
running page.

Change-Id: I387de41fb2019086c741d9c81077f54b0e8595ea
Review-Link: https://gwt-review.googlesource.com/#/c/19080/
diff --git a/user/src/com/google/gwt/junit/JUnit.gwt.xml b/user/src/com/google/gwt/junit/JUnit.gwt.xml
index e7fc0f5..54f1ba4 100644
--- a/user/src/com/google/gwt/junit/JUnit.gwt.xml
+++ b/user/src/com/google/gwt/junit/JUnit.gwt.xml
@@ -55,4 +55,12 @@
   <servlet path="/junithost/*" class="com.google.gwt.junit.server.JUnitHostImpl"/>
   <servlet path="/remote_logging" class="com.google.gwt.junit.server.JUnitHostImpl"/>
 
+  <!--
+    gwt.cspTestingEnabled enables checking for CSP violations during unit tests.
+    When this is turned on, a nonce-based policy employing 'strict-dynamic' will
+    be enforced on the hosting page. If code run during a unit test violates
+    this policy, an error will be signaled.
+  -->
+  <define-property name="gwt.strictCspTestingEnabled" values="true, false"/>
+  <set-property name="gwt.strictCspTestingEnabled" value="false"/>
 </module>
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index e8838c0..0a161fb 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -81,9 +81,11 @@
 import junit.framework.TestResult;
 
 import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.FilterHolder;
 import org.eclipse.jetty.webapp.WebAppContext;
 
 import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -100,7 +102,15 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
 import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
 
 /**
  * This class is responsible for hosting JUnit test case execution. There are
@@ -1129,6 +1139,13 @@
         }
       }
     }
+    BindingProperty strictCspTestingEnabledProperty =
+        module.getProperties().findBindingProp("gwt.strictCspTestingEnabled");
+
+    if (strictCspTestingEnabledProperty != null &&
+        "true".equals(strictCspTestingEnabledProperty.getConstrainedValue())) {
+      addCspFilter("/" + module.getName() + "/*");
+    }
     if (developmentMode) {
       // BACKWARDS COMPATIBILITY: many linkers currently fail in dev mode.
       try {
@@ -1151,6 +1168,32 @@
     }
   }
 
+  /**
+   * Adds a filter to the server that automatically adds Content-Security-Policy HTTP headers to
+   * responses on the given path.
+   */
+  private void addCspFilter(String path) {
+    wac.addFilter(new FilterHolder(new Filter() {
+      @Override
+      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+          throws IOException, ServletException {
+
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        httpResponse.addHeader("Content-Security-Policy",
+            "script-src 'nonce-gwt-nonce' 'unsafe-inline' 'strict-dynamic' https: http: " +
+            "'unsafe-eval' 'report-sample'");
+
+        chain.doFilter(request, response);
+      }
+
+      @Override
+      public void init(FilterConfig arg0) throws ServletException { }
+
+      @Override
+      public void destroy() { }
+    }), path, EnumSet.of(DispatcherType.REQUEST));
+  }
+
   private void checkArgs() {
     if (runStyle.getTries() > 1
         && !(batchingStrategy instanceof NoBatchingStrategy)) {
diff --git a/user/src/com/google/gwt/junit/public/junit.html b/user/src/com/google/gwt/junit/public/junit.html
index 33cdeca..2978fa5 100644
--- a/user/src/com/google/gwt/junit/public/junit.html
+++ b/user/src/com/google/gwt/junit/public/junit.html
@@ -21,7 +21,7 @@
 <meta name='gwt:onPropertyErrorFn' content='junitOnPropertyErrorFn'>
 </head>
 <body>
-<script>
+<script nonce="gwt-nonce">
 function junitOnLoadErrorFn(moduleName, e) {
   junitLaunchError('Failed to load module ' + moduleName + ': ' + e);
 }
@@ -57,7 +57,7 @@
   moduleName = moduleName.substr(0, pos);
   pos = moduleName.lastIndexOf('/');
   moduleName = moduleName.substr(pos + 1);
-  document.write('<script language="javascript" src="' + encodeURIComponent(moduleName) + '.nocache.js"><\/script>');
+  document.write('<script language="javascript" nonce="gwt-nonce" src="' + encodeURIComponent(moduleName) + '.nocache.js"><\/script>');
 }
 loadSelectionScript();
 </script>
diff --git a/user/test/com/google/gwt/dom/DOMTest.gwt.xml b/user/test/com/google/gwt/dom/DOMTest.gwt.xml
index 85f4cc8..1deafa7 100644
--- a/user/test/com/google/gwt/dom/DOMTest.gwt.xml
+++ b/user/test/com/google/gwt/dom/DOMTest.gwt.xml
@@ -14,4 +14,6 @@
 <module>
   <inherits name="com.google.gwt.dom.DOM"/>
   <public path="public-test"/>
+  <inherits name="com.google.gwt.junit.JUnit"/>
+  <set-property name="gwt.strictCspTestingEnabled" value="true"/>
 </module>
diff --git a/user/test/com/google/gwt/junit/JUnitSuite.java b/user/test/com/google/gwt/junit/JUnitSuite.java
index 877657f..c5d28ce 100644
--- a/user/test/com/google/gwt/junit/JUnitSuite.java
+++ b/user/test/com/google/gwt/junit/JUnitSuite.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.junit;
 
+import com.google.gwt.junit.client.CspTest;
 import com.google.gwt.junit.client.DevModeOnCompiledScriptTest;
 import com.google.gwt.junit.client.GWTTestCaseAsyncTest;
 import com.google.gwt.junit.client.GWTTestCaseInheritanceTest;
@@ -68,6 +69,8 @@
     suite.addTestSuite(JUnitMessageQueueTest.class);
     suite.addTestSuite(JUnitShellTest.class);
 
+    suite.addTestSuite(CspTest.class);
+
     return suite;
   }
 }
diff --git a/user/test/com/google/gwt/junit/JUnitTest.gwt.xml b/user/test/com/google/gwt/junit/JUnitTest.gwt.xml
index b9cd010..653d53b 100644
--- a/user/test/com/google/gwt/junit/JUnitTest.gwt.xml
+++ b/user/test/com/google/gwt/junit/JUnitTest.gwt.xml
@@ -14,4 +14,6 @@
 
 <module>
   <inherits name="com.google.gwt.user.User"/>
+  <inherits name="com.google.gwt.junit.JUnit"/>
+  <set-property name="gwt.strictCspTestingEnabled" value="true"/>
 </module>
diff --git a/user/test/com/google/gwt/junit/client/CspTest.java b/user/test/com/google/gwt/junit/client/CspTest.java
new file mode 100644
index 0000000..f078ee1
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/CspTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.junit.client;
+
+import com.google.gwt.dom.client.ButtonElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * This test case checks that the test runner correctly sets up CSP when enabled.
+ */
+public class CspTest extends GWTTestCase {
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.junit.JUnitTest";
+  }
+
+  @DoNotRunWith(Platform.HtmlUnitLayout)
+  public void testCsp() {
+    initClickDidFire();
+
+    ButtonElement btn = Document.get().createPushButtonElement();
+    btn.setAttribute("onclick", "clickDidFire = true");
+    btn.click();
+
+    assertFalse(getClickDidFire());
+  }
+
+  private native void initClickDidFire() /*-{
+    $wnd.clickDidFire = false;
+  }-*/;
+
+  private native boolean getClickDidFire() /*-{
+    return $wnd.clickDidFire;
+  }-*/;
+
+}
diff --git a/user/test/com/google/gwt/user/UserTest.gwt.xml b/user/test/com/google/gwt/user/UserTest.gwt.xml
index 3656da2..b81506c 100644
--- a/user/test/com/google/gwt/user/UserTest.gwt.xml
+++ b/user/test/com/google/gwt/user/UserTest.gwt.xml
@@ -13,6 +13,9 @@
 <!-- limitations under the License.                                         -->
 
 <module>
-	<inherits name="com.google.gwt.user.User"/>
-	<public path="public-test"/>
+  <inherits name="com.google.gwt.user.User"/>
+  <public path="public-test"/>
+  <set-property name="gwt.cspCompatModeEnabled" value="true"/>
+  <inherits name="com.google.gwt.junit.JUnit"/>
+  <set-property name="gwt.strictCspTestingEnabled" value="true"/>
 </module>