Add CORS to default plugin requests handling

Allow to request static or Documentation content with respect to CORS.
This change was inspired by [1] and [2]. It sets
Access-Control-Allow-Origin header to origin of the client if the
client's domain matches a regular expression defined in
'site.allowOriginRegex' or when 'site.allowOriginRegex' is empty
(assumption is that access to documentation is not restricted).

[1] https://gerrit-review.googlesource.com/c/gerrit/+/84191
[2] https://gerrit-review.googlesource.com/c/gitiles/+/84151

Change-Id: I0343ac1cdce9da10fea9bc207a4114e1596fbfab
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 9730032..e5b3d86 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
+import static com.google.common.net.HttpHeaders.ORIGIN;
+import static com.google.common.net.HttpHeaders.VARY;
 import static com.google.gerrit.common.FileUtil.lastModified;
 import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CHARACTER_ENCODING;
 import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CONTENT_TYPE;
@@ -21,6 +26,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
@@ -34,6 +40,7 @@
 import com.google.gerrit.httpd.resources.SmallResource;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.documentation.MarkdownFormatter;
 import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.plugins.Plugin;
@@ -80,6 +87,7 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.slf4j.Logger;
@@ -101,6 +109,7 @@
   private List<Plugin> pending = new ArrayList<>();
   private ContextMapper wrapper;
   private final ConcurrentMap<String, PluginHolder> plugins = Maps.newConcurrentMap();
+  private final Pattern allowOrigin;
 
   @Inject
   HttpPluginServlet(
@@ -109,7 +118,8 @@
       @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
       SshInfo sshInfo,
       RestApiServlet.Globals globals,
-      PluginsCollection plugins) {
+      PluginsCollection plugins,
+      @GerritServerConfig Config cfg) {
     this.mimeUtil = mimeUtil;
     this.webUrl = webUrl;
     this.resourceCache = cache;
@@ -130,6 +140,7 @@
     }
     this.sshHost = sshHost;
     this.sshPort = sshPort;
+    this.allowOrigin = makeAllowOrigin(cfg);
   }
 
   @Override
@@ -262,6 +273,8 @@
       return;
     }
 
+    checkCors(req, res);
+
     String file = pathInfo.substring(1);
     PluginResourceKey key = PluginResourceKey.create(holder.plugin, file);
     Resource rsc = resourceCache.getIfPresent(key);
@@ -333,6 +346,32 @@
     }
   }
 
+  private static Pattern makeAllowOrigin(Config cfg) {
+    String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
+    if (allow.length > 0) {
+      return Pattern.compile(Joiner.on('|').join(allow));
+    }
+    return null;
+  }
+
+  private void checkCors(HttpServletRequest req, HttpServletResponse res) {
+    String origin = req.getHeader(ORIGIN);
+    if (!Strings.isNullOrEmpty(origin) && isOriginAllowed(origin)) {
+      res.addHeader(VARY, ORIGIN);
+      setCorsHeaders(res, origin);
+    }
+  }
+
+  private void setCorsHeaders(HttpServletResponse res, String origin) {
+    res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+    res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
+    res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, HEAD");
+  }
+
+  private boolean isOriginAllowed(String origin) {
+    return allowOrigin == null || allowOrigin.matcher(origin).matches();
+  }
+
   private boolean hasUpToDateCachedResource(Resource cachedResource, long lastUpdateTime) {
     return cachedResource != null && cachedResource.isUnchanged(lastUpdateTime);
   }