diff --git a/backend/src/main/java/org/cryptomator/hub/VueHistoryModeFilter.java b/backend/src/main/java/org/cryptomator/hub/filters/VueHistoryModeFilter.java similarity index 59% rename from backend/src/main/java/org/cryptomator/hub/VueHistoryModeFilter.java rename to backend/src/main/java/org/cryptomator/hub/filters/VueHistoryModeFilter.java index ea5a0686..9fe7db16 100644 --- a/backend/src/main/java/org/cryptomator/hub/VueHistoryModeFilter.java +++ b/backend/src/main/java/org/cryptomator/hub/filters/VueHistoryModeFilter.java @@ -1,4 +1,4 @@ -package org.cryptomator.hub; +package org.cryptomator.hub.filters; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -11,7 +11,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.regex.Pattern; /** * A global http filter which redirects all 404-responses to the frontend app root. Necessary for using history mode in the vue router @@ -21,8 +20,6 @@ @WebFilter(urlPatterns = "/*") public class VueHistoryModeFilter extends HttpFilter { - private static final Pattern FILE_NAME_PATTERN = Pattern.compile(".*[.][a-zA-Z\\d]+"); - @ConfigProperty(name = "quarkus.resteasy.path") String apiPathPrefix; @@ -31,19 +28,14 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) HttpServletResponse response = (HttpServletResponse) res; chain.doFilter(request, response); - if (response.getStatus() == 404) { - String path = request.getRequestURI().substring( - request.getContextPath().length()).replaceAll("[/]+$", ""); //delete all "/" at end of string - if (!path.startsWith(apiPathPrefix) && !FILE_NAME_PATTERN.matcher(path).matches()) { //TODO: possibly exclude even more prefixes (e.g. keycloak) - // We could not find the resource, i.e. it is not anything known to the server (i.e. it is not a REST - // endpoint or a servlet), and does not look like a file so try handling it in the front-end routes - // and reset the response status code to 200. - try { - response.setStatus(200); - request.getRequestDispatcher("/").forward(request, response); - } finally { - response.getOutputStream().close(); - } + // exclude requests to the ReST API from filtering: + String contextRelativePath = request.getRequestURI().substring(request.getContextPath().length()); + if (response.getStatus() == 404 && !contextRelativePath.startsWith(apiPathPrefix)) { + try { + response.setStatus(200); + request.getRequestDispatcher("/").forward(request, response); + } finally { + response.getOutputStream().close(); } } } diff --git a/backend/src/test/java/org/cryptomator/hub/filters/VueHistoryModeFilterTest.java b/backend/src/test/java/org/cryptomator/hub/filters/VueHistoryModeFilterTest.java new file mode 100644 index 00000000..18afeb89 --- /dev/null +++ b/backend/src/test/java/org/cryptomator/hub/filters/VueHistoryModeFilterTest.java @@ -0,0 +1,84 @@ +package org.cryptomator.hub.filters; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +import javax.servlet.FilterChain; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class VueHistoryModeFilterTest { + + private HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + private HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + private FilterChain chain = Mockito.mock(FilterChain.class); + + private VueHistoryModeFilter filter = new VueHistoryModeFilter(); + + @BeforeEach + public void setup() { + filter.apiPathPrefix = "/api"; + } + + @ParameterizedTest(name = "path = {0}") + @ValueSource(strings = {"/ctx/api", "/ctx/api/foo?k=v", "/ctx/api/foo/bar/", "/ctx/api/"}) + @DisplayName("don't filter requests to /ctx/api/*") + public void testDoNotFilterApi(String reqUri) throws ServletException, IOException { + Mockito.doReturn(reqUri).when(req).getRequestURI(); + Mockito.doReturn("/ctx").when(req).getContextPath(); + Mockito.doReturn(404).when(res).getStatus(); + + filter.doFilter(req, res, chain); + + Mockito.verify(chain).doFilter(req, res); + Mockito.verify(req).getRequestURI(); + Mockito.verify(req).getContextPath(); + Mockito.verify(res).getStatus(); + Mockito.verifyNoMoreInteractions(res, req, chain); + } + + @ParameterizedTest(name = "statuscode = {0}") + @ValueSource(ints = {200, 201, 301, 401, 403}) + @DisplayName("don't filter non-404 responses") + public void testDoNotFilterNon404(int status) throws ServletException, IOException { + Mockito.doReturn("/ctx/foo").when(req).getRequestURI(); + Mockito.doReturn("/ctx").when(req).getContextPath(); + Mockito.doReturn(status).when(res).getStatus(); + + filter.doFilter(req, res, chain); + + Mockito.verify(chain).doFilter(req, res); + Mockito.verify(req).getRequestURI(); + Mockito.verify(req).getContextPath(); + Mockito.verify(res).getStatus(); + Mockito.verifyNoMoreInteractions(res, req, chain); + } + + @ParameterizedTest(name = "path = {0}") + @ValueSource(strings = {"/ctx", "/ctx/foo?k=v", "/ctx/foo/bar/"}) + @DisplayName("filter 404 response to non-api resources") + public void testDoFilterNonApi(String reqUri) throws ServletException, IOException { + var dispatcher = Mockito.mock(RequestDispatcher.class); + var out = Mockito.mock(ServletOutputStream.class); + Mockito.doReturn(reqUri).when(req).getRequestURI(); + Mockito.doReturn("/ctx").when(req).getContextPath(); + Mockito.doReturn(404).when(res).getStatus(); + Mockito.doReturn(dispatcher).when(req).getRequestDispatcher("/"); + Mockito.doReturn(out).when(res).getOutputStream(); + + filter.doFilter(req, res, chain); + + Mockito.verify(res).setStatus(200); + Mockito.verify(req).getRequestDispatcher("/"); + Mockito.verify(dispatcher).forward(req, res); + Mockito.verify(out).close(); + } + +} \ No newline at end of file