Parcourir la source

some Windows WebDAV compatibility fixes

Sebastian Stenzel il y a 9 ans
Parent
commit
a6c99c273e

+ 3 - 5
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);
 		}

+ 2 - 0
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));

+ 39 - 0
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;
+	}
+
+}

+ 37 - 0
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
+	}
+
+}

+ 49 - 0
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);
+	}
+
+}

+ 70 - 0
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<String> 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<Integer> statusCode = ArgumentCaptor.forClass(Integer.class);
+		Mockito.verify(response).sendError(statusCode.capture(), Mockito.anyString());
+		Assert.assertEquals(405, statusCode.getValue().intValue());
+	}
+
+}