Support for CORS in gitiles.

Gitiles now sets the Access-Control-Allow-Origin header to the
HTTP origin of the client iff the client's domain matches a regular
expression defined in gitiles.config.

This should allow XMLHttpRequest to work with Gitiles REST API.

Change-Id: Ica74e3ff3f6af9afd11132a9e3ba1d6f0d53065b
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
index 70c48f1..824339e 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
@@ -25,6 +25,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
@@ -39,6 +40,8 @@
 import java.io.Writer;
 import java.lang.reflect.Type;
 import java.util.Map;
+import java.util.List;
+import java.util.regex.Pattern;
 import java.util.zip.GZIPOutputStream;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
@@ -261,7 +264,7 @@
   protected void renderJson(
       HttpServletRequest req, HttpServletResponse res, Object src, Type typeOfSrc)
       throws IOException {
-    setApiHeaders(res, JSON);
+    setApiHeaders(req, res, JSON);
     res.setStatus(SC_OK);
     try (Writer writer = newWriter(req, res)) {
       newGsonBuilder(req).create().toJson(src, typeOfSrc, writer);
@@ -286,7 +289,7 @@
    */
   protected Writer startRenderText(
       HttpServletRequest req, HttpServletResponse res, String contentType) throws IOException {
-    setApiHeaders(res, contentType);
+    setApiHeaders(req, res, contentType);
     return newWriter(req, res);
   }
 
@@ -320,7 +323,7 @@
       HttpServletRequest req, HttpServletResponse res, int statusCode, String message)
       throws IOException {
     res.setStatus(statusCode);
-    setApiHeaders(res, TEXT);
+    setApiHeaders(req, res, TEXT);
     setCacheHeaders(res);
     try (Writer out = newWriter(req, res)) {
       out.write(message);
@@ -340,18 +343,33 @@
     setNotCacheable(res);
   }
 
-  protected void setApiHeaders(HttpServletResponse res, String contentType) {
+  protected void setApiHeaders(
+       HttpServletRequest req, HttpServletResponse res, String contentType) throws IOException {
     if (!Strings.isNullOrEmpty(contentType)) {
       res.setContentType(contentType);
     }
     res.setCharacterEncoding(UTF_8.name());
     res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment");
-    res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
+
+    GitilesAccess access = getAccess(req);
+    String[] allowOrigin = access.getConfig().getStringList("gitiles", null, "allowOriginRegex");
+
+    if (allowOrigin.length > 0) {
+      String origin = req.getHeader(HttpHeaders.ORIGIN);
+      Pattern allowOriginPattern = Pattern.compile(Joiner.on("|").join(allowOrigin));
+
+      if (!Strings.isNullOrEmpty(origin) && allowOriginPattern.matcher(origin).matches()) {
+        res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+      }
+    } else {
+      res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
+    }
     setCacheHeaders(res);
   }
 
-  protected void setApiHeaders(HttpServletResponse res, FormatType type) {
-    setApiHeaders(res, type.getMimeType());
+  protected void setApiHeaders(HttpServletRequest req, HttpServletResponse res, FormatType type)
+      throws IOException {
+    setApiHeaders(req, res, type.getMimeType());
   }
 
   protected void setDownloadHeaders(HttpServletResponse res, String filename, String contentType) {
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/FakeHttpServletRequest.java b/gitiles-servlet/src/test/java/com/google/gitiles/FakeHttpServletRequest.java
index f113d94..51cb9ae 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/FakeHttpServletRequest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/FakeHttpServletRequest.java
@@ -276,6 +276,10 @@
     return Iterables.getFirst(headers.get(name), null);
   }
 
+  public boolean setHeader(String name, String value) {
+    return headers.put(name, value);
+  }
+
   @Override
   public Enumeration<String> getHeaderNames() {
     return Collections.enumeration(headers.keySet());
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
index b492ed3..a32567e 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
@@ -16,8 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
 
 import com.google.common.io.BaseEncoding;
+import com.google.common.net.HttpHeaders;
 import com.google.gitiles.TreeJsonData.Tree;
 import com.google.template.soy.data.SoyListData;
 import com.google.template.soy.data.restricted.StringData;
@@ -331,6 +333,25 @@
     assertThat(tree.entries.get(0).name).isEqualTo("bar");
   }
 
+  @Test
+  public void allowOrigin() throws Exception {
+    repo.branch("master").commit().add("foo", "contents").create();
+    FakeHttpServletResponse res = buildText("/repo/+/master/foo");
+    assertThat(res.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN))
+        .isEqualTo("http://localhost");
+  }
+
+  @Test
+  public void rejectOrigin() throws Exception {
+    repo.branch("master").commit().add("foo", "contents").create();
+    FakeHttpServletResponse res = buildResponse(
+        "/repo/+/master/foo", "format=text", SC_OK, "http://notlocalhost");
+    assertThat(res.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("text/plain");
+    assertThat(res.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN))
+        .isEqualTo(null);
+  }
+
+
   private Map<String, ?> getBlobData(Map<String, ?> data) {
     return ((Map<String, Map<String, ?>>) data).get("data");
   }
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/ServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/ServletTest.java
index 24c551c..4e08714 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/ServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/ServletTest.java
@@ -59,8 +59,9 @@
   }
 
   protected FakeHttpServletResponse buildResponse(
-      String path, String queryString, int expectedStatus) throws Exception {
+      String path, String queryString, int expectedStatus, String origin) throws Exception {
     FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setHeader(HttpHeaders.ORIGIN, origin);
     req.setPathInfo(path);
     if (queryString != null) {
       req.setQueryString(queryString);
@@ -71,6 +72,11 @@
     return res;
   }
 
+  protected FakeHttpServletResponse buildResponse(
+      String path, String queryString, int expectedStatus) throws Exception {
+    return buildResponse(path, queryString, expectedStatus, "http://localhost");
+  }
+
   protected FakeHttpServletResponse build(String path) throws Exception {
     return buildResponse(path, null, SC_OK);
   }
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java b/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java
index 3571270..9046e0c 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.util.Collections;
 import java.util.Map;
@@ -76,7 +77,10 @@
 
       @Override
       public Config getConfig() {
-        return new Config();
+        Config config = new Config();
+        config.setStringList(
+            "gitiles", null, "allowOriginRegex", ImmutableList.of("http://localhost"));
+        return config;
       }
     };
   }