From a6c99c273e2ac88494486667cdebf90c4805377c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 29 Feb 2016 12:25:24 +0100 Subject: [PATCH] some Windows WebDAV compatibility fixes --- .../frontend/webdav/WebDavServer.java | 8 +-- .../webdav/WebDavServletContextFactory.java | 2 + .../webdav/WindowsCompatibilityServlet.java | 39 +++++++++++ .../webdav/filters/LoopbackFilter.java | 37 ++++++++++ .../WindowsCompatibilityServletTest.java | 49 +++++++++++++ .../webdav/filters/LoopbackFilterTest.java | 70 +++++++++++++++++++ 6 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServlet.java create mode 100644 main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/LoopbackFilter.java create mode 100644 main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServletTest.java create mode 100644 main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/filters/LoopbackFilterTest.java diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java index 6839e7506..1b9076ea3 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServer.java @@ -16,7 +16,6 @@ import java.util.concurrent.LinkedBlockingQueue; import javax.inject.Inject; import javax.inject.Singleton; -import org.apache.commons.lang3.SystemUtils; import org.cryptomator.filesystem.Folder; import org.cryptomator.frontend.Frontend; import org.cryptomator.frontend.FrontendCreationFailedException; @@ -36,7 +35,6 @@ import org.slf4j.LoggerFactory; public class WebDavServer implements FrontendFactory { private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class); - private static final String LOCALHOST = SystemUtils.IS_OS_WINDOWS ? "::1" : "localhost"; private static final int MAX_PENDING_REQUESTS = 200; private static final int MAX_THREADS = 200; private static final int MIN_THREADS = 4; @@ -57,8 +55,8 @@ public class WebDavServer implements FrontendFactory { this.servletCollection = new ContextHandlerCollection(); this.servletContextFactory = servletContextFactory; this.webdavMounterProvider = webdavMounterProvider; - - localConnector.setHost(LOCALHOST); + + servletCollection.addHandler(WindowsCompatibilityServlet.createServletContextHandler()); server.setConnectors(new Connector[] {localConnector}); server.setHandler(servletCollection); } @@ -111,7 +109,7 @@ public class WebDavServer implements FrontendFactory { } final URI uri; try { - uri = new URI("http", null, LOCALHOST, getPort(), contextPath, null, null); + uri = new URI("http", null, "localhost", getPort(), contextPath, null, null); } catch (URISyntaxException e) { throw new IllegalStateException(e); } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java index 8021556da..3fc0454c8 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.filesystem.Folder; import org.cryptomator.frontend.webdav.filters.AcceptRangeFilter; +import org.cryptomator.frontend.webdav.filters.LoopbackFilter; import org.cryptomator.frontend.webdav.filters.MacChunkedPutCompatibilityFilter; import org.cryptomator.frontend.webdav.filters.MkcolComplianceFilter; import org.cryptomator.frontend.webdav.filters.UriNormalizationFilter; @@ -65,6 +66,7 @@ class WebDavServletContextFactory { final ServletContextHandler servletContext = new ServletContextHandler(null, contextPath, ServletContextHandler.SESSIONS); final ServletHolder servletHolder = new ServletHolder(contextPath, new WebDavServlet(contextRoot, root)); servletContext.addServlet(servletHolder, WILDCARD); + servletContext.addFilter(LoopbackFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(MkcolComplianceFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(AcceptRangeFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(new FilterHolder(new UriNormalizationFilter(resourceTypeChecker)), WILDCARD, EnumSet.of(DispatcherType.REQUEST)); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServlet.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServlet.java new file mode 100644 index 000000000..4a81620d5 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServlet.java @@ -0,0 +1,39 @@ +package org.cryptomator.frontend.webdav; + +import java.io.IOException; +import java.util.EnumSet; + +import javax.servlet.DispatcherType; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.cryptomator.frontend.webdav.filters.LoopbackFilter; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +/** + * The server needs to respond to requests to the root resource, because Windows is stupid. + */ +public class WindowsCompatibilityServlet extends HttpServlet { + + private static final String ROOT_PATH = "/"; + + @Override + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.addHeader("DAV", "1, 2"); + resp.addHeader("MS-Author-Via", "DAV"); + // resp.addHeader("Allow", "OPTIONS, GET, HEAD, POST, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, PUT, DELETE, MOVE, LOCK, UNLOCK"); + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + } + + public static ServletContextHandler createServletContextHandler() { + final ServletContextHandler servletContext = new ServletContextHandler(null, ROOT_PATH, ServletContextHandler.NO_SESSIONS); + final ServletHolder servletHolder = new ServletHolder(ROOT_PATH, WindowsCompatibilityServlet.class); + servletContext.addServlet(servletHolder, ROOT_PATH); + servletContext.addFilter(LoopbackFilter.class, ROOT_PATH, EnumSet.of(DispatcherType.REQUEST)); + return servletContext; + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/LoopbackFilter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/LoopbackFilter.java new file mode 100644 index 000000000..4fa6774c3 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/filters/LoopbackFilter.java @@ -0,0 +1,37 @@ +package org.cryptomator.frontend.webdav.filters; + +import java.io.IOException; +import java.net.InetAddress; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Blocks all requests from external hosts. + */ +public class LoopbackFilter implements HttpFilter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // no-op + } + + @Override + public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (InetAddress.getByName(request.getRemoteAddr()).isLoopbackAddress()) { + chain.doFilter(request, response); + } else { + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Can only access drive from localhost."); + } + } + + @Override + public void destroy() { + // no-op + } + +} diff --git a/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServletTest.java b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServletTest.java new file mode 100644 index 000000000..4a5feaf9d --- /dev/null +++ b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WindowsCompatibilityServletTest.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.frontend.webdav; + +import java.io.IOException; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + + +public class WindowsCompatibilityServletTest { + + @Test + public void testFactory() throws ServletException { + ServletHolder[] holders = WindowsCompatibilityServlet.createServletContextHandler().getServletHandler().getServlets(); + Assert.assertEquals(1, holders.length); + ServletHolder holder = holders[0]; + + Servlet servlet = holder.getServlet(); + Assert.assertTrue(servlet instanceof WindowsCompatibilityServlet); + } + + @Test + public void testResponse() throws IOException, ServletException { + final WindowsCompatibilityServlet servlet = new WindowsCompatibilityServlet(); + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + servlet.doOptions(request, response); + + Mockito.verify(response).addHeader("MS-Author-Via", "DAV"); + Mockito.verify(response).addHeader("DAV", "1, 2"); + Mockito.verify(response).setStatus(204); + } + +} diff --git a/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/filters/LoopbackFilterTest.java b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/filters/LoopbackFilterTest.java new file mode 100644 index 000000000..bb6e77cbf --- /dev/null +++ b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/filters/LoopbackFilterTest.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.frontend.webdav.filters; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Arrays; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +@RunWith(Theories.class) +public class LoopbackFilterTest { + + @DataPoints + public static final Iterable HOST_NAMES = Arrays.asList("127.0.0.1", "0::1", "1.2.3.4", "google.com"); + + private LoopbackFilter filter; + private FilterChain chain; + private HttpServletRequest request; + private HttpServletResponse response; + + @Before + public void setup() { + filter = new LoopbackFilter(); + chain = Mockito.mock(FilterChain.class); + request = Mockito.mock(HttpServletRequest.class); + response = Mockito.mock(HttpServletResponse.class); + } + + @Theory + public void testWithLoopbackAddress(String hostname) throws IOException, ServletException { + Assume.assumeTrue(InetAddress.getByName(hostname).isLoopbackAddress()); + Mockito.when(request.getRemoteAddr()).thenReturn(hostname); + + filter.doFilter(request, response, chain); + Mockito.verify(chain).doFilter(request, response); + } + + @Theory + public void testWithExternalAddress(String hostname) throws IOException, ServletException { + Assume.assumeFalse(InetAddress.getByName(hostname).isLoopbackAddress()); + Mockito.when(request.getRemoteAddr()).thenReturn(hostname); + + filter.doFilter(request, response, chain); + + ArgumentCaptor statusCode = ArgumentCaptor.forClass(Integer.class); + Mockito.verify(response).sendError(statusCode.capture(), Mockito.anyString()); + Assert.assertEquals(405, statusCode.getValue().intValue()); + } + +}