diff --git a/main/jackrabbit-filesystem-adapter/pom.xml b/main/jackrabbit-filesystem-adapter/pom.xml
index e361a17fc..aabb12c69 100644
--- a/main/jackrabbit-filesystem-adapter/pom.xml
+++ b/main/jackrabbit-filesystem-adapter/pom.xml
@@ -49,7 +49,11 @@
org.apache.commons
commons-lang3
-
+
+ commons-io
+ commons-io
+
+
org.cryptomator
diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/MacChunkedPutCompatibilityFilter.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/MacChunkedPutCompatibilityFilter.java
new file mode 100644
index 000000000..b77e0434e
--- /dev/null
+++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/MacChunkedPutCompatibilityFilter.java
@@ -0,0 +1,146 @@
+package org.cryptomator.webdav.filters;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.input.BoundedInputStream;
+
+/**
+ * If a PUT request with chunked transfer encoding and a X-Expected-Entity-Length header field is sent,
+ * the input stream will return EOF after the number of bytes stated in this header has been read.
+ *
+ * This filter ensures compatibility of the Mac OS X WebDAV client, as Macs don't terminate chunked transfers normally (by sending a 0-byte-chunk).
+ */
+public class MacChunkedPutCompatibilityFilter implements HttpFilter {
+
+ private static final String METHOD_PUT = "PUT";
+ private static final String HEADER_TRANSFER_ENCODING = "Transfer-Encoding";
+ private static final String HEADER_X_EXPECTED_ENTITIY_LENGTH = "X-Expected-Entity-Length";
+ private static final String HEADER_TRANSFER_ENCODING_CHUNKED = "chunked";
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ // no-op
+ }
+
+ @Override
+ public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+ final String expectedEntitiyLengthHeader = request.getHeader(HEADER_X_EXPECTED_ENTITIY_LENGTH);
+ if (METHOD_PUT.equalsIgnoreCase(request.getMethod()) //
+ && HEADER_TRANSFER_ENCODING_CHUNKED.equalsIgnoreCase(request.getHeader(HEADER_TRANSFER_ENCODING)) //
+ && expectedEntitiyLengthHeader != null) {
+ long expectedEntitiyLength;
+ try {
+ expectedEntitiyLength = Long.valueOf(expectedEntitiyLengthHeader);
+ } catch (NumberFormatException e) {
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid X-Expected-Entity-Length");
+ return;
+ }
+ chain.doFilter(new PutRequestWithBoundedInputStream(request, expectedEntitiyLength), response);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ // no-op
+ }
+
+ private static class PutRequestWithBoundedInputStream extends HttpServletRequestWrapper {
+
+ private final long inputStreamLimit;
+
+ public PutRequestWithBoundedInputStream(HttpServletRequest request, long inputStreamLimit) {
+ super(request);
+ this.inputStreamLimit = inputStreamLimit;
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ return new BoundedServletInputStream(super.getInputStream(), inputStreamLimit);
+ }
+
+ }
+
+ /**
+ * Like {@link BoundedInputStream}, but as a ServletInputStream.
+ */
+ private static class BoundedServletInputStream extends ServletInputStream {
+
+ private final BoundedInputStream boundedIn;
+ private final ServletInputStream servletIn;
+ private boolean reachedEof = false;
+ private ReadListener readListener;
+
+ public BoundedServletInputStream(ServletInputStream delegate, long limit) {
+ this.boundedIn = new BoundedInputStream(delegate, limit);
+ this.servletIn = delegate;
+ }
+
+ private void reachedEof() throws IOException {
+ reachedEof = true;
+ if (readListener != null) {
+ readListener.onAllDataRead();
+ }
+ }
+
+ /* BoundedInputStream */
+
+ @Override
+ public long skip(long n) throws IOException {
+ return boundedIn.skip(n);
+ }
+
+ @Override
+ public int available() throws IOException {
+ return boundedIn.available();
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int read = boundedIn.read(b, off, len);
+ if (read == -1) {
+ reachedEof();
+ }
+ return read;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int aByte = boundedIn.read();
+ if (aByte == -1) {
+ reachedEof();
+ }
+ return aByte;
+ }
+
+ /* ServletInputStream */
+
+ @Override
+ public boolean isFinished() {
+ return reachedEof || servletIn.isFinished();
+ }
+
+ @Override
+ public boolean isReady() {
+ return !reachedEof && servletIn.isReady();
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+ servletIn.setReadListener(readListener);
+ this.readListener = readListener;
+ }
+
+ }
+
+}
diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/PutIdleTimeoutFilter.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/PutIdleTimeoutFilter.java
deleted file mode 100644
index 39b13b420..000000000
--- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/PutIdleTimeoutFilter.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package org.cryptomator.webdav.filters;
-
-import java.io.IOException;
-import java.io.InterruptedIOException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ReadListener;
-import javax.servlet.ServletException;
-import javax.servlet.ServletInputStream;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Wrapps the {@link ServletInputStream} into a timeout-aware stream, that returns EOF after a certain timeout.
- * Wrapping is done only for chunked PUT requests, as some WebDAV clients are too stupid to send a EOF-chunk (0-byte-chunk).
- */
-public class PutIdleTimeoutFilter implements HttpFilter {
-
- private static final long TIMEOUT = 100;
- private static final TimeUnit TIMEOUT_UNIT = TimeUnit.MILLISECONDS;
- private static final String METHOD_PUT = "PUT";
- private static final String HEADER_TRANSFER_ENCODING = "Transfer-Encoding";
- private static final String HEADER_TRANSFER_ENCODING_CHUNKED = "chunked";
-
- private final ExecutorService executor = Executors.newSingleThreadExecutor();
-
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
- // no-op
- }
-
- @Override
- public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
- if (METHOD_PUT.equalsIgnoreCase(request.getMethod()) && HEADER_TRANSFER_ENCODING_CHUNKED.equalsIgnoreCase(request.getHeader(HEADER_TRANSFER_ENCODING))) {
- chain.doFilter(new PutRequestWithIdleTimeout(request), response);
- } else {
- chain.doFilter(request, response);
- }
- }
-
- @Override
- public void destroy() {
- executor.shutdownNow();
- }
-
- private class PutRequestWithIdleTimeout extends HttpServletRequestWrapper {
-
- public PutRequestWithIdleTimeout(HttpServletRequest request) {
- super(request);
- }
-
- @Override
- public ServletInputStream getInputStream() throws IOException {
- return new IdleTimeoutServletInputStream(super.getInputStream());
- }
-
- }
-
- private class IdleTimeoutServletInputStream extends ServletInputStream {
-
- private final ServletInputStream delegate;
- private boolean timedOut = false;
-
- public IdleTimeoutServletInputStream(ServletInputStream delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public boolean isFinished() {
- return timedOut || delegate.isFinished();
- }
-
- @Override
- public boolean isReady() {
- return !timedOut && delegate.isReady();
- }
-
- @Override
- public void setReadListener(ReadListener readListener) {
- delegate.setReadListener(readListener);
- }
-
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- try {
- Future readTask = executor.submit(() -> {
- return delegate.read(b, off, len);
- });
- return readTask.get(TIMEOUT, TIMEOUT_UNIT);
- } catch (InterruptedException e) {
- throw new InterruptedIOException();
- } catch (ExecutionException e) {
- throw new IOException("Exception during read", e);
- } catch (TimeoutException e) {
- timedOut = true;
- return -1;
- }
- }
-
- @Override
- public int read() throws IOException {
- try {
- Future readTask = executor.submit(() -> {
- return delegate.read();
- });
- return readTask.get(TIMEOUT, TIMEOUT_UNIT);
- } catch (InterruptedException e) {
- throw new InterruptedIOException();
- } catch (ExecutionException e) {
- throw new IOException("Exception during read", e);
- } catch (TimeoutException e) {
- // throw new InterruptedByTimeoutException();
- timedOut = true;
- return -1;
- }
- }
-
- }
-
-}
diff --git a/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/MacChunkedPutCompatibilityFilterTest.java b/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/MacChunkedPutCompatibilityFilterTest.java
new file mode 100644
index 000000000..05e0c44cb
--- /dev/null
+++ b/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/MacChunkedPutCompatibilityFilterTest.java
@@ -0,0 +1,122 @@
+package org.cryptomator.webdav.filters;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+public class MacChunkedPutCompatibilityFilterTest {
+
+ private MacChunkedPutCompatibilityFilter filter;
+ private FilterChain chain;
+ private HttpServletRequest request;
+ private HttpServletResponse response;
+
+ @Before
+ public void setup() {
+ filter = new MacChunkedPutCompatibilityFilter();
+ chain = Mockito.mock(FilterChain.class);
+ request = Mockito.mock(HttpServletRequest.class);
+ response = Mockito.mock(HttpServletResponse.class);
+ }
+
+ @Test
+ public void testUnfilteredGetRequest() throws IOException, ServletException {
+ Mockito.when(request.getMethod()).thenReturn("GET");
+ filter.doFilter(request, response, chain);
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertSame(request, wrappedReq.getValue());
+ }
+
+ @Test
+ public void testUnfilteredPutRequest1() throws IOException, ServletException {
+ Mockito.when(request.getMethod()).thenReturn("PUT");
+ Mockito.when(request.getHeader("Transfer-Encoding")).thenReturn(null);
+ filter.doFilter(request, response, chain);
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertSame(request, wrappedReq.getValue());
+ }
+
+ @Test
+ public void testUnfilteredPutRequest2() throws IOException, ServletException {
+ Mockito.when(request.getMethod()).thenReturn("PUT");
+ Mockito.when(request.getHeader("Transfer-Encoding")).thenReturn("chunked");
+ Mockito.when(request.getHeader("X-Expected-Entity-Length")).thenReturn(null);
+ filter.doFilter(request, response, chain);
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertSame(request, wrappedReq.getValue());
+ }
+
+ @Test
+ public void testMalformedXExpectedEntityLengthHeader() throws IOException, ServletException {
+ Mockito.when(request.getMethod()).thenReturn("PUT");
+ Mockito.when(request.getHeader("Transfer-Encoding")).thenReturn("chunked");
+ Mockito.when(request.getHeader("X-Expected-Entity-Length")).thenReturn("NaN");
+ filter.doFilter(request, response, chain);
+
+ Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse.SC_BAD_REQUEST), Mockito.anyString());
+ Mockito.verifyNoMoreInteractions(chain);
+ }
+
+ /* actual input stream testing */
+
+ @Test
+ public void testBoundedInputStream() throws IOException, ServletException {
+ ServletInputStream in = Mockito.mock(ServletInputStream.class);
+
+ Mockito.when(request.getMethod()).thenReturn("PUT");
+ Mockito.when(request.getHeader("Transfer-Encoding")).thenReturn("chunked");
+ Mockito.when(request.getHeader("X-Expected-Entity-Length")).thenReturn("5");
+ Mockito.when(request.getInputStream()).thenReturn(in);
+ filter.doFilter(request, response, chain);
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ ServletInputStream wrappedIn = wrappedReq.getValue().getInputStream();
+
+ Mockito.when(in.isFinished()).thenReturn(false);
+ Assert.assertFalse(wrappedIn.isFinished());
+
+ Mockito.when(in.isReady()).thenReturn(true);
+ Assert.assertTrue(wrappedIn.isReady());
+
+ Mockito.when(in.read()).thenReturn(0xFF);
+ Assert.assertEquals(0xFF, wrappedIn.read());
+
+ Mockito.when(in.available()).thenReturn(100);
+ Assert.assertEquals(100, wrappedIn.available());
+
+ Mockito.when(in.skip(2)).thenReturn(2l);
+ Assert.assertEquals(2, wrappedIn.skip(2));
+
+ Mockito.when(in.read(Mockito.any(), Mockito.eq(0), Mockito.eq(100))).thenReturn(100);
+ Mockito.when(in.read(Mockito.any(), Mockito.eq(0), Mockito.eq(2))).thenReturn(2);
+ Assert.assertEquals(2, wrappedIn.read(new byte[100], 0, 100));
+
+ Mockito.when(in.read()).thenReturn(0xFF);
+ Assert.assertEquals(-1, wrappedIn.read());
+
+ Mockito.when(in.isFinished()).thenReturn(false);
+ Assert.assertTrue(wrappedIn.isFinished());
+
+ Mockito.when(in.isReady()).thenReturn(true);
+ Assert.assertFalse(wrappedIn.isReady());
+ }
+
+}
diff --git a/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/UriNormalizationFilterTest.java b/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/UriNormalizationFilterTest.java
new file mode 100644
index 000000000..ad19108d5
--- /dev/null
+++ b/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/UriNormalizationFilterTest.java
@@ -0,0 +1,162 @@
+package org.cryptomator.webdav.filters;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cryptomator.webdav.filters.UriNormalizationFilter.ResourceTypeChecker;
+import org.cryptomator.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+public class UriNormalizationFilterTest {
+
+ private ResourceTypeChecker resourceTypeChecker;
+ private UriNormalizationFilter filter;
+ private FilterChain chain;
+ private HttpServletRequest request;
+ private HttpServletResponse response;
+
+ @Before
+ public void setup() {
+ resourceTypeChecker = Mockito.mock(ResourceTypeChecker.class);
+ filter = new UriNormalizationFilter(resourceTypeChecker);
+ chain = Mockito.mock(FilterChain.class);
+ request = Mockito.mock(HttpServletRequest.class);
+ response = Mockito.mock(HttpServletResponse.class);
+
+ Mockito.when(resourceTypeChecker.typeOfResource("/file")).thenReturn(ResourceType.FILE);
+ Mockito.when(resourceTypeChecker.typeOfResource("/file/")).thenReturn(ResourceType.FILE);
+ Mockito.when(resourceTypeChecker.typeOfResource("/folder")).thenReturn(ResourceType.FOLDER);
+ Mockito.when(resourceTypeChecker.typeOfResource("/folder/")).thenReturn(ResourceType.FOLDER);
+ Mockito.when(resourceTypeChecker.typeOfResource("/404")).thenReturn(ResourceType.UNKNOWN);
+ Mockito.when(resourceTypeChecker.typeOfResource("/404/")).thenReturn(ResourceType.UNKNOWN);
+ }
+
+ /* FILE */
+
+ @Test
+ public void testFileRequest1() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/file");
+ Mockito.when(request.getRequestURI()).thenReturn("/file");
+ filter.doFilter(request, response, chain);
+ Mockito.verify(resourceTypeChecker).typeOfResource("/file");
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertEquals("/file", wrappedReq.getValue().getRequestURI());
+ }
+
+ @Test
+ public void testFileRequest2() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/file/");
+ Mockito.when(request.getRequestURI()).thenReturn("/file/");
+ filter.doFilter(request, response, chain);
+ Mockito.verify(resourceTypeChecker).typeOfResource("/file/");
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertEquals("/file", wrappedReq.getValue().getRequestURI());
+ }
+
+ @Test
+ public void testCopyFileRequest() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/file/");
+ Mockito.when(request.getRequestURI()).thenReturn("/file/");
+ Mockito.when(request.getMethod()).thenReturn("COPY");
+ Mockito.when(request.getHeader("Destination")).thenReturn("/404/");
+ filter.doFilter(request, response, chain);
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertEquals("/404", wrappedReq.getValue().getHeader("Destination"));
+ }
+
+ /* FOLDER */
+
+ @Test
+ public void testFolderRequest1() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/folder");
+ Mockito.when(request.getRequestURI()).thenReturn("/folder");
+ filter.doFilter(request, response, chain);
+ Mockito.verify(resourceTypeChecker).typeOfResource("/folder");
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertEquals("/folder/", wrappedReq.getValue().getRequestURI());
+ }
+
+ @Test
+ public void testFolderRequest2() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/folder/");
+ Mockito.when(request.getRequestURI()).thenReturn("/folder/");
+ filter.doFilter(request, response, chain);
+ Mockito.verify(resourceTypeChecker).typeOfResource("/folder/");
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertEquals("/folder/", wrappedReq.getValue().getRequestURI());
+ }
+
+ @Test
+ public void testMoveFolderRequest() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/folder");
+ Mockito.when(request.getRequestURI()).thenReturn("/folder");
+ Mockito.when(request.getMethod()).thenReturn("MOVE");
+ Mockito.when(request.getHeader("Destination")).thenReturn("/404");
+ filter.doFilter(request, response, chain);
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertEquals("/404/", wrappedReq.getValue().getHeader("Destination"));
+ }
+
+ /* UNKNOWN */
+
+ @Test
+ public void testUnknownPutRequest() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/404/");
+ Mockito.when(request.getRequestURI()).thenReturn("/404/");
+ Mockito.when(request.getMethod()).thenReturn("PUT");
+ filter.doFilter(request, response, chain);
+ Mockito.verify(resourceTypeChecker).typeOfResource("/404/");
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertEquals("/404", wrappedReq.getValue().getRequestURI());
+ }
+
+ @Test
+ public void testUnknownMkcolRequest() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/404");
+ Mockito.when(request.getRequestURI()).thenReturn("/404");
+ Mockito.when(request.getMethod()).thenReturn("MKCOL");
+ filter.doFilter(request, response, chain);
+ Mockito.verify(resourceTypeChecker).typeOfResource("/404");
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertEquals("/404/", wrappedReq.getValue().getRequestURI());
+ }
+
+ @Test
+ public void testUnknownPropfindRequest() throws IOException, ServletException {
+ Mockito.when(request.getPathInfo()).thenReturn("/404");
+ Mockito.when(request.getRequestURI()).thenReturn("/404");
+ Mockito.when(request.getMethod()).thenReturn("PROPFIND");
+ filter.doFilter(request, response, chain);
+ Mockito.verify(resourceTypeChecker).typeOfResource("/404");
+
+ ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+ Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+ Assert.assertSame(request, wrappedReq.getValue());
+ }
+
+}
diff --git a/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java b/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java
index 7a36ceca5..a89475c9f 100644
--- a/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java
+++ b/main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java
@@ -19,7 +19,7 @@ import javax.servlet.DispatcherType;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.webdav.filters.AcceptRangeFilter;
import org.cryptomator.webdav.filters.LoggingHttpFilter;
-import org.cryptomator.webdav.filters.PutIdleTimeoutFilter;
+import org.cryptomator.webdav.filters.MacChunkedPutCompatibilityFilter;
import org.cryptomator.webdav.filters.UriNormalizationFilter;
import org.cryptomator.webdav.filters.UriNormalizationFilter.ResourceTypeChecker;
import org.cryptomator.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType;
@@ -69,7 +69,7 @@ class FileSystemBasedWebDavServer {
servletContext.addServlet(servletHolder, "/*");
servletContext.addFilter(AcceptRangeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(new FilterHolder(new UriNormalizationFilter(resourceTypeChecker)), "/*", EnumSet.of(DispatcherType.REQUEST));
- servletContext.addFilter(PutIdleTimeoutFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
+ servletContext.addFilter(MacChunkedPutCompatibilityFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(LoggingHttpFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
servletCollection.mapContexts();