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

GitOrigin-RevId: 9e3d85e479746affd047434ec9ce2588b60ca76c
This commit is contained in:
Jakob Ackermann 2025-02-28 08:02:51 +00:00 committed by Copybot
parent 18fb4effb1
commit 14d9874b68
9 changed files with 149 additions and 3 deletions

View file

@ -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}",

View file

@ -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",

View file

@ -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",

View file

@ -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;
}

View file

@ -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);
}
}
}
}

View file

@ -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));

View file

@ -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"

View file

@ -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"

View file

@ -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,