diff --git a/services/git-bridge/conf/envsubst_template.json b/services/git-bridge/conf/envsubst_template.json index 1f52ffbaef..6aa91be700 100644 --- a/services/git-bridge/conf/envsubst_template.json +++ b/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}", diff --git a/services/git-bridge/conf/example_config.json b/services/git-bridge/conf/example_config.json index bfad73f461..1e5b95e5a6 100644 --- a/services/git-bridge/conf/example_config.json +++ b/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", diff --git a/services/git-bridge/conf/local.json b/services/git-bridge/conf/local.json index 03ce4febe4..69eb31ab2f 100644 --- a/services/git-bridge/conf/local.json +++ b/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", diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java index cf36916600..492302721b 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java +++ b/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; } diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/CORSHandler.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/CORSHandler.java new file mode 100644 index 0000000000..10d978c352 --- /dev/null +++ b/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 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); + } + } + } +} diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/GitBridgeServer.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/GitBridgeServer.java index 30c5039212..c576e2e9d8 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/GitBridgeServer.java +++ b/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)); diff --git a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java index e250798652..8491aa8055 100644 --- a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java +++ b/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" diff --git a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/config/ConfigTest.java b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/config/ConfigTest.java index ddafc621d6..cbb4265d5b 100644 --- a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/config/ConfigTest.java +++ b/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" diff --git a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/bridge/BridgeTest.java b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/bridge/BridgeTest.java index f749dea357..e27c3488c0 100644 --- a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/bridge/BridgeTest.java +++ b/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,