Browse Source

[git-bridge] add CORS support (#23892)

GitOrigin-RevId: 9e3d85e479746affd047434ec9ce2588b60ca76c
Jakob Ackermann 1 year ago
parent
commit
14d9874b68

+ 1 - 0
services/git-bridge/conf/envsubst_template.json

@@ -3,6 +3,7 @@
   "bindIp": "${GIT_BRIDGE_BIND_IP:-0.0.0.0}",
   "idleTimeout": ${GIT_BRIDGE_IDLE_TIMEOUT:-30000},
   "rootGitDirectory": "${GIT_BRIDGE_ROOT_DIR:-/tmp/wlgb}",
+  "allowedCorsOrigins": "${GIT_BRIDGE_ALLOWED_CORS_ORIGINS:-https://localhost}",
   "apiBaseUrl": "${GIT_BRIDGE_API_BASE_URL:-https://localhost/api/v0}",
   "postbackBaseUrl": "${GIT_BRIDGE_POSTBACK_BASE_URL:-https://localhost}",
   "serviceName": "${GIT_BRIDGE_SERVICE_NAME:-Overleaf}",

+ 1 - 0
services/git-bridge/conf/example_config.json

@@ -3,6 +3,7 @@
     "bindIp": "127.0.0.1",
     "idleTimeout": 30000,
     "rootGitDirectory": "/tmp/wlgb",
+    "allowedCorsOrigins": "https://localhost",
     "apiBaseUrl": "https://localhost/api/v0",
     "postbackBaseUrl": "https://localhost",
     "serviceName": "Overleaf",

+ 1 - 0
services/git-bridge/conf/local.json

@@ -3,6 +3,7 @@
   "bindIp": "0.0.0.0",
   "idleTimeout": 30000,
   "rootGitDirectory": "/tmp/wlgb",
+  "allowedCorsOrigins": "http://v2.overleaf.test",
   "apiBaseUrl": "http://v2.overleaf.test:3000/api/v0",
   "postbackBaseUrl": "http://git-bridge:8000",
   "serviceName": "Overleaf",

+ 15 - 0
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java

@@ -26,6 +26,7 @@ public class Config implements JSONSource {
         config.bindIp,
         config.idleTimeout,
         config.rootGitDirectory,
+        config.allowedCorsOrigins,
         config.apiBaseURL,
         config.postbackURL,
         config.serviceName,
@@ -41,6 +42,7 @@ public class Config implements JSONSource {
   private String bindIp;
   private int idleTimeout;
   private String rootGitDirectory;
+  private String[] allowedCorsOrigins;
   private String apiBaseURL;
   private String postbackURL;
   private String serviceName;
@@ -64,6 +66,7 @@ public class Config implements JSONSource {
       String bindIp,
       int idleTimeout,
       String rootGitDirectory,
+      String[] allowedCorsOrigins,
       String apiBaseURL,
       String postbackURL,
       String serviceName,
@@ -77,6 +80,7 @@ public class Config implements JSONSource {
     this.bindIp = bindIp;
     this.idleTimeout = idleTimeout;
     this.rootGitDirectory = rootGitDirectory;
+    this.allowedCorsOrigins = allowedCorsOrigins;
     this.apiBaseURL = apiBaseURL;
     this.postbackURL = postbackURL;
     this.serviceName = serviceName;
@@ -101,6 +105,13 @@ public class Config implements JSONSource {
     }
     this.apiBaseURL = apiBaseURL;
     serviceName = getElement(configObject, "serviceName").getAsString();
+    final String rawAllowedCorsOrigins =
+        getOptionalString(configObject, "allowedCorsOrigins").trim();
+    if (rawAllowedCorsOrigins.isEmpty()) {
+      allowedCorsOrigins = new String[] {};
+    } else {
+      allowedCorsOrigins = rawAllowedCorsOrigins.split(",");
+    }
     postbackURL = getElement(configObject, "postbackBaseUrl").getAsString();
     if (!postbackURL.endsWith("/")) {
       postbackURL += "/";
@@ -139,6 +150,10 @@ public class Config implements JSONSource {
     return this.sqliteHeapLimitBytes;
   }
 
+  public String[] getAllowedCorsOrigins() {
+    return allowedCorsOrigins;
+  }
+
   public String getAPIBaseURL() {
     return apiBaseURL;
   }

+ 47 - 0
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/CORSHandler.java

@@ -0,0 +1,47 @@
+package uk.ac.ic.wlgitbridge.server;
+
+import java.io.IOException;
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import uk.ac.ic.wlgitbridge.util.Log;
+
+public class CORSHandler extends AbstractHandler {
+  private final Set<String> allowedCorsOrigins;
+
+  public CORSHandler(String[] allowedCorsOrigins) {
+    this.allowedCorsOrigins = Set.of(allowedCorsOrigins);
+  }
+
+  @Override
+  public void handle(
+      String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+
+    String origin = request.getHeader("Origin");
+    if (origin == null) {
+      return; // Not a CORS request
+    }
+
+    final boolean ok = allowedCorsOrigins.contains(origin);
+    if (ok) {
+      response.setHeader("Access-Control-Allow-Origin", origin);
+      response.setHeader("Access-Control-Allow-Credentials", "true");
+      response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE");
+      response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
+      response.setHeader("Access-Control-Max-Age", "86400"); // cache for 24h
+    }
+    String method = baseRequest.getMethod();
+    if ("OPTIONS".equals(method)) {
+      Log.debug("OPTIONS <- {}", target);
+      baseRequest.setHandled(true);
+      if (ok) {
+        response.setStatus(200);
+      } else {
+        response.setStatus(403);
+      }
+    }
+  }
+}

+ 1 - 0
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/GitBridgeServer.java

@@ -110,6 +110,7 @@ public class GitBridgeServer {
     this.jettyServer.addConnector(connector);
 
     HandlerCollection handlers = new HandlerList();
+    handlers.addHandler(new CORSHandler(config.getAllowedCorsOrigins()));
     handlers.addHandler(initApiHandler());
     handlers.addHandler(initBaseHandler());
     handlers.addHandler(initGitHandler(config, repoStore, snapshotApi));

+ 81 - 2
services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java

@@ -465,8 +465,12 @@ public class WLGitBridgeIntegrationTest {
 
   @After
   public void tearDown() {
-    server.stop();
-    wlgb.stop();
+    if (server != null) {
+      server.stop();
+    }
+    if (wlgb != null) {
+      wlgb.stop();
+    }
   }
 
   private void gitConfig(File dir) throws IOException, InterruptedException {
@@ -1391,6 +1395,80 @@ public class WLGitBridgeIntegrationTest {
     assertTrue(f.exists());
   }
 
+  @Test
+  public void noCors() throws IOException, ExecutionException, InterruptedException {
+
+    int gitBridgePort = 33893;
+    int mockServerPort = 3893;
+
+    server = new MockSnapshotServer(mockServerPort, getResource("/canServePushedFiles").toFile());
+    server.start();
+    server.setState(states.get("canServePushedFiles").get("state"));
+
+    wlgb = new GitBridgeApp(new String[] {makeConfigFile(gitBridgePort, mockServerPort)});
+    wlgb.run();
+
+    String url = "http://127.0.0.1:" + gitBridgePort + "/status";
+    Response response = asyncHttpClient().prepareGet(url).execute().get();
+    assertEquals(200, response.getStatusCode());
+    assertEquals("ok\n", response.getResponseBody());
+    assertNull(response.getHeader("Access-Control-Allow-Origin"));
+  }
+
+  @Test
+  public void cors() throws IOException, ExecutionException, InterruptedException {
+
+    int gitBridgePort = 33894;
+    int mockServerPort = 3894;
+
+    server = new MockSnapshotServer(mockServerPort, getResource("/canServePushedFiles").toFile());
+    server.start();
+    server.setState(states.get("canServePushedFiles").get("state"));
+
+    wlgb = new GitBridgeApp(new String[] {makeConfigFile(gitBridgePort, mockServerPort)});
+    wlgb.run();
+
+    String url = "http://127.0.0.1:" + gitBridgePort + "/status";
+
+    // Success
+    Response response =
+        asyncHttpClient()
+            .prepareOptions(url)
+            .setHeader("Origin", "https://localhost")
+            .execute()
+            .get();
+    assertEquals(200, response.getStatusCode());
+    assertEquals("", response.getResponseBody());
+    assertEquals("https://localhost", response.getHeader("Access-Control-Allow-Origin"));
+
+    response =
+        asyncHttpClient().prepareGet(url).setHeader("Origin", "https://localhost").execute().get();
+    assertEquals(200, response.getStatusCode());
+    assertEquals("ok\n", response.getResponseBody());
+    assertEquals("https://localhost", response.getHeader("Access-Control-Allow-Origin"));
+
+    // Deny
+    response =
+        asyncHttpClient()
+            .prepareOptions(url)
+            .setHeader("Origin", "https://not-localhost")
+            .execute()
+            .get();
+    assertEquals(403, response.getStatusCode());
+    assertEquals("", response.getResponseBody());
+    assertNull(response.getHeader("Access-Control-Allow-Origin"));
+
+    response =
+        asyncHttpClient()
+            .prepareGet(url)
+            .setHeader("Origin", "https://not-localhost")
+            .execute()
+            .get();
+    assertEquals(200, response.getStatusCode());
+    assertEquals("ok\n", response.getResponseBody());
+    assertNull(response.getHeader("Access-Control-Allow-Origin"));
+  }
+
   private String makeConfigFile(int port, int apiPort) throws IOException {
     return makeConfigFile(port, apiPort, null);
   }
@@ -1409,6 +1487,7 @@ public class WLGitBridgeIntegrationTest {
             + "    \"rootGitDirectory\": \""
             + wlgb.getAbsolutePath()
             + "\",\n"
+            + "    \"allowedCorsOrigins\": \"https://localhost\",\n"
             + "    \"apiBaseUrl\": \"http://127.0.0.1:"
             + apiPort
             + "/api/v0\",\n"

+ 1 - 0
services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/config/ConfigTest.java

@@ -90,6 +90,7 @@ public class ConfigTest {
             + "  \"bindIp\": \"127.0.0.1\",\n"
             + "  \"idleTimeout\": 30000,\n"
             + "  \"rootGitDirectory\": \"/var/wlgb/git\",\n"
+            + "  \"allowedCorsOrigins\": [],\n"
             + "  \"apiBaseURL\": \"http://127.0.0.1:60000/api/v0/\",\n"
             + "  \"postbackURL\": \"http://127.0.0.1/\",\n"
             + "  \"serviceName\": \"Overleaf\",\n"

+ 1 - 1
services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/bridge/BridgeTest.java

@@ -50,7 +50,7 @@ public class BridgeTest {
     gcJob = mock(GcJob.class);
     bridge =
         new Bridge(
-            new Config(0, "", 0, "", "", "", "", null, false, null, null, null, 0),
+            new Config(0, "", 0, "", null, "", "", "", null, false, null, null, null, 0),
             lock,
             repoStore,
             dbStore,