diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java index c7c27564f2..d9448ab0bb 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java @@ -35,6 +35,7 @@ import uk.ac.ic.wlgitbridge.git.handler.hook.WriteLatexPutHook; import uk.ac.ic.wlgitbridge.server.FileHandler; import uk.ac.ic.wlgitbridge.server.PostbackContents; import uk.ac.ic.wlgitbridge.server.PostbackHandler; +import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException; import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException; import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotAttachment; import uk.ac.ic.wlgitbridge.snapshot.push.PostbackManager; @@ -383,6 +384,7 @@ public class Bridge { * @param hostname * @throws SnapshotPostException * @throws IOException + * @throws MissingRepositoryException * @throws ForbiddenException */ public void push( @@ -391,7 +393,7 @@ public class Bridge { RawDirectory directoryContents, RawDirectory oldDirectoryContents, String hostname - ) throws SnapshotPostException, IOException, ForbiddenException { + ) throws SnapshotPostException, IOException, MissingRepositoryException, ForbiddenException { try (LockGuard __ = lock.lockGuard(projectName)) { pushCritical( oauth2, @@ -460,6 +462,7 @@ public class Bridge { * @param directoryContents * @param oldDirectoryContents * @throws IOException + * @throws MissingRepositoryException * @throws ForbiddenException * @throws SnapshotPostException */ @@ -468,7 +471,7 @@ public class Bridge { String projectName, RawDirectory directoryContents, RawDirectory oldDirectoryContents - ) throws IOException, ForbiddenException, SnapshotPostException { + ) throws IOException, MissingRepositoryException, ForbiddenException, SnapshotPostException { Log.info("[{}] Pushing", projectName); String postbackKey = postbackManager.makeKeyForProject(projectName); Log.info( diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApi.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApi.java index d4edf22189..0358db7834 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApi.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApi.java @@ -2,6 +2,7 @@ package uk.ac.ic.wlgitbridge.bridge.snapshot; import com.google.api.client.auth.oauth2.Credential; import uk.ac.ic.wlgitbridge.data.CandidateSnapshot; +import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException; import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException; import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException; import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult; @@ -33,13 +34,14 @@ public interface SnapshotApi { String postbackKey); static T getResult(CompletableFuture result) - throws FailedConnectionException, ForbiddenException { + throws MissingRepositoryException, FailedConnectionException, ForbiddenException { try { return result.join(); } catch (CompletionException e) { try { throw e.getCause(); - } catch (FailedConnectionException + } catch (MissingRepositoryException + | FailedConnectionException | ForbiddenException | RuntimeException r) { throw r; diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApiFacade.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApiFacade.java index ea4d388101..f9085febcb 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApiFacade.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApiFacade.java @@ -4,6 +4,7 @@ import com.google.api.client.auth.oauth2.Credential; import uk.ac.ic.wlgitbridge.data.CandidateSnapshot; import uk.ac.ic.wlgitbridge.data.model.Snapshot; import uk.ac.ic.wlgitbridge.git.exception.GitUserException; +import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException; import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException; import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException; import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult; @@ -65,7 +66,7 @@ public class SnapshotApiFacade { Optional oauth2, CandidateSnapshot candidateSnapshot, String postbackKey - ) throws FailedConnectionException, ForbiddenException { + ) throws MissingRepositoryException, FailedConnectionException, ForbiddenException { return SnapshotApi.getResult(api.push( oauth2, candidateSnapshot, postbackKey)); } diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java index e33294f75e..5478d68e43 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java @@ -6,6 +6,7 @@ import org.apache.commons.codec.binary.Base64; import org.eclipse.jetty.server.Request; import uk.ac.ic.wlgitbridge.application.config.Oauth2; import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApi; +import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException; import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException; import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocRequest; import uk.ac.ic.wlgitbridge.util.Instance; @@ -76,6 +77,8 @@ public class Oauth2Filter implements Filter { filterChain ); return; + } catch (MissingRepositoryException e) { + handleMissingRepository(project, e, (HttpServletResponse) servletResponse); } Log.info("[{}] Auth not needed", project); filterChain.doFilter(servletRequest, servletResponse); @@ -130,7 +133,7 @@ public class Oauth2Filter implements Filter { ) ).execute().getAccessToken(); } catch (TokenResponseException e) { - unauthorized(projectName, capturedUsername, e.getStatusCode(), request, response); + handleNeedAuthorization(projectName, capturedUsername, e.getStatusCode(), request, response); return; } final Credential cred = new Credential.Builder( @@ -145,7 +148,7 @@ public class Oauth2Filter implements Filter { servletResponse ); } else { - unauthorized(projectName, capturedUsername, 0, request, response); + handleNeedAuthorization(projectName, capturedUsername, 0, request, response); } } catch (UnsupportedEncodingException e) { throw new Error("Couldn't retrieve authentication", e); @@ -153,14 +156,14 @@ public class Oauth2Filter implements Filter { } } } else { - unauthorized(projectName, capturedUsername, 0, request, response); + handleNeedAuthorization(projectName, capturedUsername, 0, request, response); } } @Override public void destroy() {} - private void unauthorized( + private void handleNeedAuthorization( String projectName, String userName, int statusCode, @@ -200,4 +203,23 @@ public class Oauth2Filter implements Filter { w.close(); } + private void handleMissingRepository( + String projectName, + MissingRepositoryException e, + HttpServletResponse response + ) throws IOException { + Log.info("[{}] Project missing.", projectName); + + response.setContentType("text/plain"); + + // git special-cases 404 to give "repository '%s' not found", + // rather than displaying the raw status code. + response.setStatus(404); + + PrintWriter w = response.getWriter(); + for (String line : e.getDescriptionLines()) { + w.println(line); + } + w.close(); + } } diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/MissingRepositoryException.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/MissingRepositoryException.java new file mode 100644 index 0000000000..c39f6c2481 --- /dev/null +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/MissingRepositoryException.java @@ -0,0 +1,47 @@ +package uk.ac.ic.wlgitbridge.snapshot.base; + +import com.google.gson.JsonElement; +import uk.ac.ic.wlgitbridge.git.exception.SnapshotAPIException; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; + +public class MissingRepositoryException extends SnapshotAPIException { + + public static final List GENERIC_REASON = Arrays.asList( + "This Overleaf project currently has no git access.", + "", + "If this problem persists, please contact us." + ); + + public static final List EXPORTED_TO_V2 = Arrays.asList( + "This Overleaf project has been moved to Overleaf v2, and git access is temporarily unsupported.", + "", + "See https://www.overleaf.com/help/342 for more information." + ); + + private List descriptionLines; + + public MissingRepositoryException() { + descriptionLines = new ArrayList(); + } + + public MissingRepositoryException(List descriptionLines) { + this.descriptionLines = descriptionLines; + } + + @Override + public void fromJSON(JsonElement json) {} + + @Override + public String getMessage() { + return "no git access"; + } + + @Override + public List getDescriptionLines() { + return this.descriptionLines; + } + +} diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/Request.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/Request.java index 58491fb736..9316eb667b 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/Request.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/Request.java @@ -2,6 +2,7 @@ package uk.ac.ic.wlgitbridge.snapshot.base; import com.google.api.client.http.*; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.ning.http.client.AsyncHttpClient; import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException; import uk.ac.ic.wlgitbridge.util.Instance; @@ -50,7 +51,7 @@ public abstract class Request { return ret; } - private T getResult() throws FailedConnectionException, ForbiddenException { + private T getResult() throws MissingRepositoryException, FailedConnectionException, ForbiddenException { try { HttpResponse response = future.get(); Log.info( @@ -68,12 +69,30 @@ public abstract class Request { throw new FailedConnectionException(); } catch (ExecutionException e) { Throwable cause = e.getCause(); - if (cause instanceof HttpResponseException && - (((HttpResponseException) cause).getStatusCode() == - HttpServletResponse.SC_UNAUTHORIZED || - ((HttpResponseException) cause).getStatusCode() == - HttpServletResponse.SC_FORBIDDEN)) { - throw new ForbiddenException(); + if (cause instanceof HttpResponseException) { + HttpResponseException httpCause = (HttpResponseException) cause; + int sc = httpCause.getStatusCode(); + if (sc == HttpServletResponse.SC_UNAUTHORIZED || sc == HttpServletResponse.SC_FORBIDDEN) { + throw new ForbiddenException(); + } else if (sc == HttpServletResponse.SC_NOT_FOUND) { + try { + JsonObject json = Instance.gson.fromJson(httpCause.getContent(), JsonObject.class); + String message = json.get("message").getAsString(); + + if ("Exported to v2".equals(message)) { + throw new MissingRepositoryException(MissingRepositoryException.EXPORTED_TO_V2); + } + } catch (IllegalStateException + | ClassCastException + | NullPointerException _) { + // disregard any errors that arose while handling the JSON + } + + throw new MissingRepositoryException(); + } else if (sc >= 400 && sc < 500) { + throw new MissingRepositoryException(MissingRepositoryException.GENERIC_REASON); + } + throw new FailedConnectionException(cause); } else { throw new FailedConnectionException(cause); } 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 20b1fb85ed..cd103fca5b 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 @@ -42,8 +42,14 @@ public class WLGitBridgeIntegrationTest { put("canCloneMultipleRepositories", new HashMap() {{ put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/canCloneMultipleRepositories/state/state.json")).build()); }}); - put("cannotCloneAProtectedProject", new HashMap() {{ - put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAProtectedProject/state/state.json")).build()); + put("cannotCloneAProtectedProjectWithoutAuthentication", new HashMap() {{ + put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAProtectedProjectWithoutAuthentication/state/state.json")).build()); + }}); + put("cannotCloneA4xxProject", new HashMap() {{ + put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneA4xxProject/state/state.json")).build()); + }}); + put("cannotCloneAMissingProject", new HashMap() {{ + put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAMissingProject/state/state.json")).build()); }}); put("canPullAModifiedTexFile", new HashMap() {{ put("base", new SnapshotAPIStateBuilder(getResourceAsStream("/canPullAModifiedTexFile/base/state.json")).build()); @@ -727,6 +733,60 @@ public class WLGitBridgeIntegrationTest { wlgb.stop(); } + @Test + public void cannotCloneAProtectedProjectWithoutAuthentication() throws IOException, GitAPIException, InterruptedException { + int gitBridgePort = 33883; + int mockServerPort = 3883; + + MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneAProtectedProjectWithoutAuthentication").toFile()); + server.start(); + server.setState(states.get("cannotCloneAProtectedProjectWithoutAuthentication").get("state")); + GitBridgeApp wlgb = new GitBridgeApp(new String[] { + makeConfigFile(gitBridgePort, mockServerPort) + }); + + wlgb.run(); + Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir); + wlgb.stop(); + assertNotEquals(0, gitProcess.waitFor()); + } + + @Test + public void cannotCloneA4xxProject() throws IOException, GitAPIException, InterruptedException { + int gitBridgePort = 33879; + int mockServerPort = 3879; + + MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneA4xxProject").toFile()); + server.start(); + server.setState(states.get("cannotCloneA4xxProject").get("state")); + GitBridgeApp wlgb = new GitBridgeApp(new String[] { + makeConfigFile(gitBridgePort, mockServerPort) + }); + + wlgb.run(); + Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir); + wlgb.stop(); + assertNotEquals(0, gitProcess.waitFor()); + } + + @Test + public void cannotCloneAMissingProject() throws IOException, GitAPIException, InterruptedException { + int gitBridgePort = 33880; + int mockServerPort = 3880; + + MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneAMissingProject").toFile()); + server.start(); + server.setState(states.get("cannotCloneAMissingProject").get("state")); + GitBridgeApp wlgb = new GitBridgeApp(new String[] { + makeConfigFile(gitBridgePort, mockServerPort) + }); + + wlgb.run(); + Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir); + wlgb.stop(); + assertNotEquals(0, gitProcess.waitFor()); + } + private String makeConfigFile( int port, int apiPort diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneA4xxProject/state/state.json b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneA4xxProject/state/state.json new file mode 100644 index 0000000000..f8c76c1dec --- /dev/null +++ b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneA4xxProject/state/state.json @@ -0,0 +1,18 @@ +[ + { + "project": "gone", + "getDoc": { + "error": 410, + "versionID": 1, + "createdAt": "2018-02-05T15:30:00Z", + "email": "michael.walker@overleaf.com", + "name": "msw" + }, + "getSavedVers": [], + "getForVers": [], + "push": "success", + "postback": { + "type": "outOfDate" + } + } +] diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAMissingProject/state/state.json b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAMissingProject/state/state.json new file mode 100644 index 0000000000..1db62b00b7 --- /dev/null +++ b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAMissingProject/state/state.json @@ -0,0 +1,18 @@ +[ + { + "project": "missing", + "getDoc": { + "error": 404, + "versionID": 1, + "createdAt": "2018-02-06T13:29:00Z", + "email": "michael.walker@overleaf.com", + "name": "msw" + }, + "getSavedVers": [], + "getForVers": [], + "push": "success", + "postback": { + "type": "outOfDate" + } + } +] diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAProtectedProject/state/state.json b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAProtectedProjectWithoutAuthentication/state/state.json similarity index 100% rename from services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAProtectedProject/state/state.json rename to services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAProtectedProjectWithoutAuthentication/state/state.json