瀏覽代碼

several WebDAV compliance fixes

Sebastian Stenzel 9 年之前
父節點
當前提交
12fcf5aeaf

+ 8 - 5
main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java

@@ -11,12 +11,12 @@ package org.cryptomator.filesystem.inmem;
 import java.io.FileNotFoundException;
 import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
+import java.nio.file.FileAlreadyExistsException;
 import java.time.Instant;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
 
-import org.apache.commons.io.FileExistsException;
 import org.cryptomator.filesystem.File;
 import org.cryptomator.filesystem.ReadableFile;
 import org.cryptomator.filesystem.WritableFile;
@@ -53,12 +53,15 @@ class InMemoryFile extends InMemoryNode implements File {
 		writeLock.lock();
 		final InMemoryFolder parent = parent().get();
 		parent.existingChildren.compute(this.name(), (k, v) -> {
-			if (v == null || v == this) {
+			if (v != null && v != this) {
+				// other file or folder with same name already exists.
+				throw new UncheckedIOException(new FileAlreadyExistsException(k));
+			} else {
+				if (v == null) {
+					this.creationTime = Instant.now();
+				}
 				this.lastModified = Instant.now();
-				this.creationTime = Instant.now();
 				return this;
-			} else {
-				throw new UncheckedIOException(new FileExistsException(k));
 			}
 		});
 		return new InMemoryWritableFile(this::setLastModified, this::setCreationTime, this::getContent, this::setContent, this::delete, writeLock);

+ 14 - 8
main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java

@@ -15,7 +15,7 @@ import java.io.UncheckedIOException;
 import java.time.Instant;
 import java.util.Iterator;
 import java.util.Map;
-import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Stream;
 
 import org.apache.commons.io.FileExistsException;
@@ -24,7 +24,7 @@ import org.cryptomator.filesystem.Folder;
 
 class InMemoryFolder extends InMemoryNode implements Folder {
 
-	final Map<String, InMemoryNode> existingChildren = new TreeMap<>();
+	final Map<String, InMemoryNode> existingChildren = new ConcurrentHashMap<>();
 
 	private final WeakValuedCache<String, InMemoryFolder> folders = WeakValuedCache.usingLoader(this::newFolder);
 	private final WeakValuedCache<String, InMemoryFile> files = WeakValuedCache.usingLoader(this::newFile);
@@ -67,11 +67,12 @@ class InMemoryFolder extends InMemoryNode implements Folder {
 		}
 		parent.create();
 		parent.existingChildren.compute(name, (k, v) -> {
-			if (v == null) {
+			if (v != null) {
+				// other file or folder with same name already exists.
+				throw new UncheckedIOException(new FileExistsException(k));
+			} else {
 				this.lastModified = Instant.now();
 				return this;
-			} else {
-				throw new UncheckedIOException(new FileExistsException(k));
 			}
 		});
 		assert this.exists();
@@ -83,11 +84,11 @@ class InMemoryFolder extends InMemoryNode implements Folder {
 		if (target.exists()) {
 			target.delete();
 		}
-		assert !target.exists();
+		assert!target.exists();
 		target.create();
 		this.copyTo(target);
 		this.delete();
-		assert !this.exists();
+		assert!this.exists();
 	}
 
 	@Override
@@ -109,7 +110,12 @@ class InMemoryFolder extends InMemoryNode implements Folder {
 				subFolder.delete();
 			}
 		}
-		assert !this.exists();
+		assert!this.exists();
+	}
+
+	@Override
+	public void setCreationTime(Instant instant) throws UncheckedIOException {
+		creationTime = instant;
 	}
 
 	@Override

+ 2 - 0
main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java

@@ -20,6 +20,7 @@ import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.filesystem.Folder;
 import org.cryptomator.webdav.filters.AcceptRangeFilter;
 import org.cryptomator.webdav.filters.MacChunkedPutCompatibilityFilter;
+import org.cryptomator.webdav.filters.MkcolComplianceFilter;
 import org.cryptomator.webdav.filters.UriNormalizationFilter;
 import org.cryptomator.webdav.filters.UriNormalizationFilter.ResourceTypeChecker;
 import org.cryptomator.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType;
@@ -64,6 +65,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(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));
 		if (SystemUtils.IS_OS_MAC_OSX) {

+ 51 - 0
main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/MkcolComplianceFilter.java

@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * 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.webdav.filters;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Responds with status code 415, if an attempt is made to create a collection with a body.
+ * 
+ * See https://tools.ietf.org/html/rfc2518#section-8.3.1:
+ * "If the server receives a MKCOL request entity type it does not support or understand
+ * it MUST respond with a 415 (Unsupported Media Type) status code."
+ */
+public class MkcolComplianceFilter implements HttpFilter {
+
+	private static final String METHOD_MKCOL = "MKCOL";
+	private static final String HEADER_TRANSFER_ENCODING = "Transfer-Encoding";
+
+	@Override
+	public void init(FilterConfig filterConfig) throws ServletException {
+		// no-op
+	}
+
+	@Override
+	public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+		boolean hasBody = request.getContentLengthLong() > 0 || request.getHeader(HEADER_TRANSFER_ENCODING) != null;
+		if (METHOD_MKCOL.equalsIgnoreCase(request.getMethod()) && hasBody) {
+			response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "MKCOL with body not supported.");
+		} else {
+			chain.doFilter(request, response);
+		}
+	}
+
+	@Override
+	public void destroy() {
+		// no-op
+	}
+
+}

+ 71 - 50
main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java

@@ -9,7 +9,7 @@
 package org.cryptomator.webdav.filters;
 
 import java.io.IOException;
-import java.util.function.Function;
+import java.net.URI;
 
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -37,7 +37,25 @@ public class UriNormalizationFilter implements HttpFilter {
 	private static final String[] FILE_METHODS = {"PUT"};
 	private static final String[] DIRECTORY_METHODS = {"MKCOL"};
 
+	@FunctionalInterface
+	public interface ResourceTypeChecker {
+
+		enum ResourceType {
+			FILE, FOLDER, UNKNOWN;
+		};
+
+		/**
+		 * Checks if the resource with the given resource name is a file, a folder or doesn't exist.
+		 * 
+		 * @param resourcePath Relative URI of the resource in question.
+		 * @return Type of the resource or {@link ResourceType#UNKNOWN UNKNOWN} for non-existing resources. Never <code>null</code>.
+		 */
+		ResourceType typeOfResource(String resourcePath);
+
+	}
+
 	private final ResourceTypeChecker resourceTypeChecker;
+	private String contextPath;
 
 	public UriNormalizationFilter(ResourceTypeChecker resourceTypeChecker) {
 		this.resourceTypeChecker = resourceTypeChecker;
@@ -45,13 +63,24 @@ public class UriNormalizationFilter implements HttpFilter {
 
 	@Override
 	public void init(FilterConfig filterConfig) throws ServletException {
-		// no-op
+		contextPath = filterConfig.getServletContext().getContextPath();
 	}
 
 	@Override
 	public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
-		ResourceType resourceType = resourceTypeChecker.typeOfResource(request.getPathInfo());
-		HttpServletRequest normalizedRequest = resourceType.normalizedRequest(request);
+		final ResourceType resourceType = resourceTypeChecker.typeOfResource(request.getPathInfo());
+		final HttpServletRequest normalizedRequest;
+		switch (resourceType) {
+		case FILE:
+			normalizedRequest = normalizedFileRequest(request);
+			break;
+		case FOLDER:
+			normalizedRequest = normalizedFolderRequest(request);
+			break;
+		default:
+			normalizedRequest = normalizedRequestForUnknownResource(request);
+			break;
+		}
 		chain.doFilter(normalizedRequest, response);
 	}
 
@@ -60,17 +89,17 @@ public class UriNormalizationFilter implements HttpFilter {
 		// no-op
 	}
 
-	private static HttpServletRequest normalizedFileRequest(HttpServletRequest originalRequest) {
-		LOG.debug("Treating resource as file: {}", originalRequest.getRequestURI());
+	private HttpServletRequest normalizedFileRequest(HttpServletRequest originalRequest) {
+		LOG.trace("Treating resource as file: {}", originalRequest.getRequestURI());
 		return new FileUriRequest(originalRequest);
 	}
 
-	private static HttpServletRequest normalizedFolderRequest(HttpServletRequest originalRequest) {
-		LOG.debug("Treating resource as folder: {}", originalRequest.getRequestURI());
+	private HttpServletRequest normalizedFolderRequest(HttpServletRequest originalRequest) {
+		LOG.trace("Treating resource as folder: {}", originalRequest.getRequestURI());
 		return new FolderUriRequest(originalRequest);
 	}
 
-	private static HttpServletRequest normalizedRequestForUnknownResource(HttpServletRequest originalRequest) {
+	private HttpServletRequest normalizedRequestForUnknownResource(HttpServletRequest originalRequest) {
 		final String requestMethod = originalRequest.getMethod().toUpperCase();
 		if (ArrayUtils.contains(FILE_METHODS, requestMethod)) {
 			return normalizedFileRequest(originalRequest);
@@ -82,39 +111,10 @@ public class UriNormalizationFilter implements HttpFilter {
 		}
 	}
 
-	@FunctionalInterface
-	public interface ResourceTypeChecker {
-
-		enum ResourceType {
-			FILE(UriNormalizationFilter::normalizedFileRequest), //
-			FOLDER(UriNormalizationFilter::normalizedFolderRequest), //
-			UNKNOWN(UriNormalizationFilter::normalizedRequestForUnknownResource);
-
-			private final Function<HttpServletRequest, HttpServletRequest> wrapper;
-
-			private ResourceType(Function<HttpServletRequest, HttpServletRequest> wrapper) {
-				this.wrapper = wrapper;
-			}
-
-			private HttpServletRequest normalizedRequest(HttpServletRequest request) {
-				return wrapper.apply(request);
-			}
-		};
-
-		/**
-		 * Checks if the resource with the given resource name is a file, a folder or doesn't exist.
-		 * 
-		 * @param resourcePath Relative URI of the resource in question.
-		 * @return Type of the resource or {@link ResourceType#UNKNOWN UNKNOWN} for non-existing resources. Never <code>null</code>.
-		 */
-		ResourceType typeOfResource(String resourcePath);
-
-	}
-
 	/**
 	 * Adjusts headers containing URIs depending on the request URI.
 	 */
-	private static class SuffixPreservingRequest extends HttpServletRequestWrapper {
+	private class SuffixPreservingRequest extends HttpServletRequestWrapper {
 
 		private static final String HEADER_DESTINATION = "Destination";
 		private static final String METHOD_MOVE = "MOVE";
@@ -122,32 +122,53 @@ public class UriNormalizationFilter implements HttpFilter {
 
 		public SuffixPreservingRequest(HttpServletRequest request) {
 			super(request);
+			request.getContextPath();
 		}
 
 		@Override
 		public String getHeader(String name) {
 			if ((METHOD_MOVE.equalsIgnoreCase(getMethod()) || METHOD_COPY.equalsIgnoreCase(getMethod())) && HEADER_DESTINATION.equalsIgnoreCase(name)) {
-				return sameSuffixAsUri(super.getHeader(name));
+				final String uri = URI.create(super.getHeader(name)).getPath();
+				return bestGuess(uri);
 			} else {
 				return super.getHeader(name);
 			}
 		}
 
-		private String sameSuffixAsUri(String str) {
-			final String uri = this.getRequestURI();
-			if (uri.endsWith("/")) {
-				return StringUtils.appendIfMissing(str, "/");
-			} else {
-				return StringUtils.removeEnd(str, "/");
+		private String bestGuess(String uri) {
+			final String pathWithinContext = StringUtils.removeStart(uri, contextPath);
+			final ResourceType resourceType = resourceTypeChecker.typeOfResource(pathWithinContext);
+			switch (resourceType) {
+			case FILE:
+				System.out.println("DST is file " + uri);
+				return asFileUri(uri);
+			case FOLDER:
+				System.out.println("DST is folder " + uri);
+				return asFolderUri(uri);
+			default:
+				System.out.println("DST doesn't exist " + uri);
+				if (this.getRequestURI().endsWith("/")) {
+					return asFolderUri(uri);
+				} else {
+					return asFileUri(uri);
+				}
 			}
 		}
 
+		protected String asFileUri(String uri) {
+			return StringUtils.removeEnd(uri, "/");
+		}
+
+		protected String asFolderUri(String uri) {
+			return StringUtils.appendIfMissing(uri, "/");
+		}
+
 	}
 
 	/**
 	 * HTTP request, whose URI never ends on "/".
 	 */
-	private static class FileUriRequest extends SuffixPreservingRequest {
+	private class FileUriRequest extends SuffixPreservingRequest {
 
 		public FileUriRequest(HttpServletRequest request) {
 			super(request);
@@ -155,7 +176,7 @@ public class UriNormalizationFilter implements HttpFilter {
 
 		@Override
 		public String getRequestURI() {
-			return StringUtils.removeEnd(super.getRequestURI(), "/");
+			return asFileUri(super.getRequestURI());
 		}
 
 	}
@@ -163,7 +184,7 @@ public class UriNormalizationFilter implements HttpFilter {
 	/**
 	 * HTTP request, whose URI always ends on "/".
 	 */
-	private static class FolderUriRequest extends SuffixPreservingRequest {
+	private class FolderUriRequest extends SuffixPreservingRequest {
 
 		public FolderUriRequest(HttpServletRequest request) {
 			super(request);
@@ -171,7 +192,7 @@ public class UriNormalizationFilter implements HttpFilter {
 
 		@Override
 		public String getRequestURI() {
-			return StringUtils.appendIfMissing(super.getRequestURI(), "/");
+			return asFolderUri(super.getRequestURI());
 		}
 
 	}

+ 36 - 1
main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java

@@ -17,6 +17,7 @@ import java.util.Optional;
 import org.apache.jackrabbit.webdav.DavException;
 import org.apache.jackrabbit.webdav.DavResource;
 import org.apache.jackrabbit.webdav.DavResourceIterator;
+import org.apache.jackrabbit.webdav.DavServletResponse;
 import org.apache.jackrabbit.webdav.DavSession;
 import org.apache.jackrabbit.webdav.io.InputContext;
 import org.apache.jackrabbit.webdav.io.OutputContext;
@@ -25,6 +26,8 @@ import org.apache.jackrabbit.webdav.property.DavProperty;
 import org.apache.jackrabbit.webdav.property.DavPropertyName;
 import org.apache.jackrabbit.webdav.property.DavPropertySet;
 import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.Folder;
 import org.cryptomator.filesystem.ReadableFile;
 import org.cryptomator.filesystem.WritableFile;
 import org.cryptomator.filesystem.jackrabbit.FileLocator;
@@ -73,7 +76,23 @@ class DavFile extends DavNode<FileLocator> {
 	public void move(DavResource destination) throws DavException {
 		if (destination instanceof DavFile) {
 			DavFile dst = (DavFile) destination;
+			if (dst.node.exists()) {
+				// Overwrite header already checked by AbstractWebdavServlet#validateDestination
+				dst.node.delete();
+			} else if (!dst.node.parent().get().exists()) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist.");
+			}
 			node.moveTo(dst.node);
+		} else if (destination instanceof DavFolder) {
+			DavFolder dst = (DavFolder) destination;
+			Folder parent = dst.node.parent().get();
+			File newDst = parent.file(dst.node.name());
+			if (dst.node.exists()) {
+				dst.node.delete();
+			} else if (!parent.exists()) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist.");
+			}
+			node.moveTo(newDst);
 		} else {
 			throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName());
 		}
@@ -83,9 +102,25 @@ class DavFile extends DavNode<FileLocator> {
 	public void copy(DavResource destination, boolean shallow) throws DavException {
 		if (destination instanceof DavFile) {
 			DavFile dst = (DavFile) destination;
+			if (dst.node.exists()) {
+				// Overwrite header already checked by AbstractWebdavServlet#validateDestination
+				dst.node.delete();
+			} else if (!dst.node.parent().get().exists()) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist.");
+			}
 			node.copyTo(dst.node);
+		} else if (destination instanceof DavFolder) {
+			DavFolder dst = (DavFolder) destination;
+			Folder parent = dst.node.parent().get();
+			File newDst = parent.file(dst.node.name());
+			if (dst.node.exists()) {
+				dst.node.delete();
+			} else if (!parent.exists()) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist.");
+			}
+			node.copyTo(newDst);
 		} else {
-			throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName());
+			throw new IllegalArgumentException("Destination not a DavFile: " + destination.getClass().getName());
 		}
 	}
 

+ 39 - 4
main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java

@@ -124,7 +124,22 @@ class DavFolder extends DavNode<FolderLocator> {
 	public void move(DavResource destination) throws DavException {
 		if (destination instanceof DavFolder) {
 			DavFolder dst = (DavFolder) destination;
+			if (dst.node.exists()) {
+				dst.node.delete();
+			} else if (!dst.node.parent().get().exists()) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist.");
+			}
 			node.moveTo(dst.node);
+		} else if (destination instanceof DavFile) {
+			DavFile dst = (DavFile) destination;
+			Folder parent = dst.node.parent().get();
+			Folder newDst = parent.folder(dst.node.name());
+			if (dst.node.exists()) {
+				dst.node.delete();
+			} else if (!parent.exists()) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist.");
+			}
+			node.moveTo(newDst);
 		} else {
 			throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName());
 		}
@@ -132,11 +147,31 @@ class DavFolder extends DavNode<FolderLocator> {
 
 	@Override
 	public void copy(DavResource destination, boolean shallow) throws DavException {
-		if (shallow) {
-			throw new UnsupportedOperationException("Shallow copy of directories not supported.");
-		} else if (destination instanceof DavFolder) {
+		if (destination instanceof DavFolder) {
 			DavFolder dst = (DavFolder) destination;
-			node.copyTo(dst.node);
+			if (dst.node.exists()) {
+				dst.node.delete();
+			} else if (!dst.node.parent().get().exists()) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist.");
+			}
+			dst.node.create();
+			if (shallow) {
+				// http://www.webdav.org/specs/rfc2518.html#copy.for.collections
+				node.creationTime().ifPresent(dst::setCreationTime);
+				dst.setModificationTime(node.lastModified());
+			} else {
+				node.copyTo(dst.node);
+			}
+		} else if (destination instanceof DavFile) {
+			DavFile dst = (DavFile) destination;
+			Folder parent = dst.node.parent().get();
+			Folder newDst = parent.folder(dst.node.name());
+			if (dst.node.exists()) {
+				dst.node.delete();
+			} else if (!parent.exists()) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist.");
+			}
+			node.copyTo(newDst);
 		} else {
 			throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName());
 		}

+ 30 - 0
main/frontend-webdav/src/test/java/org/cryptomator/webdav/filters/UriNormalizationFilterTest.java

@@ -126,6 +126,36 @@ public class UriNormalizationFilterTest {
 		Assert.assertEquals("/404/", wrappedReq.getValue().getHeader("Destination"));
 	}
 
+	/* MIXED */
+
+	@Test
+	public void testCopyFileToFolderRequest() 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("/folder");
+		filter.doFilter(request, response, chain);
+
+		ArgumentCaptor<HttpServletRequest> wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+		Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+		Assert.assertEquals("/file", wrappedReq.getValue().getRequestURI());
+		Assert.assertEquals("/folder/", wrappedReq.getValue().getHeader("Destination"));
+	}
+
+	@Test
+	public void testMoveFolderToFileRequest() throws IOException, ServletException {
+		Mockito.when(request.getPathInfo()).thenReturn("/folder");
+		Mockito.when(request.getRequestURI()).thenReturn("/folder");
+		Mockito.when(request.getMethod()).thenReturn("COPY");
+		Mockito.when(request.getHeader("Destination")).thenReturn("/file/");
+		filter.doFilter(request, response, chain);
+
+		ArgumentCaptor<HttpServletRequest> wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class);
+		Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class));
+		Assert.assertEquals("/folder/", wrappedReq.getValue().getRequestURI());
+		Assert.assertEquals("/file", wrappedReq.getValue().getHeader("Destination"));
+	}
+
 	/* UNKNOWN */
 
 	@Test