several WebDAV compliance fixes

This commit is contained in:
Sebastian Stenzel
2016-02-10 19:23:43 +01:00
parent a1a81cc0ba
commit 12fcf5aeaf
8 changed files with 273 additions and 90 deletions

View File

@@ -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) {
this.lastModified = Instant.now();
this.creationTime = Instant.now();
return this;
if (v != null && v != this) {
// other file or folder with same name already exists.
throw new UncheckedIOException(new FileAlreadyExistsException(k));
} else {
throw new UncheckedIOException(new FileExistsException(k));
if (v == null) {
this.creationTime = Instant.now();
}
this.lastModified = Instant.now();
return this;
}
});
return new InMemoryWritableFile(this::setLastModified, this::setCreationTime, this::getContent, this::setContent, this::delete, writeLock);

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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,68 +37,11 @@ public class UriNormalizationFilter implements HttpFilter {
private static final String[] FILE_METHODS = {"PUT"};
private static final String[] DIRECTORY_METHODS = {"MKCOL"};
private final ResourceTypeChecker resourceTypeChecker;
public UriNormalizationFilter(ResourceTypeChecker resourceTypeChecker) {
this.resourceTypeChecker = resourceTypeChecker;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// no-op
}
@Override
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
ResourceType resourceType = resourceTypeChecker.typeOfResource(request.getPathInfo());
HttpServletRequest normalizedRequest = resourceType.normalizedRequest(request);
chain.doFilter(normalizedRequest, response);
}
@Override
public void destroy() {
// no-op
}
private static HttpServletRequest normalizedFileRequest(HttpServletRequest originalRequest) {
LOG.debug("Treating resource as file: {}", originalRequest.getRequestURI());
return new FileUriRequest(originalRequest);
}
private static HttpServletRequest normalizedFolderRequest(HttpServletRequest originalRequest) {
LOG.debug("Treating resource as folder: {}", originalRequest.getRequestURI());
return new FolderUriRequest(originalRequest);
}
private static HttpServletRequest normalizedRequestForUnknownResource(HttpServletRequest originalRequest) {
final String requestMethod = originalRequest.getMethod().toUpperCase();
if (ArrayUtils.contains(FILE_METHODS, requestMethod)) {
return normalizedFileRequest(originalRequest);
} else if (ArrayUtils.contains(DIRECTORY_METHODS, requestMethod)) {
return normalizedFolderRequest(originalRequest);
} else {
LOG.debug("Could not determine resource type of resource: {}", originalRequest.getRequestURI());
return originalRequest;
}
}
@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);
}
FILE, FOLDER, UNKNOWN;
};
/**
@@ -111,10 +54,67 @@ public class UriNormalizationFilter implements HttpFilter {
}
private final ResourceTypeChecker resourceTypeChecker;
private String contextPath;
public UriNormalizationFilter(ResourceTypeChecker resourceTypeChecker) {
this.resourceTypeChecker = resourceTypeChecker;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
contextPath = filterConfig.getServletContext().getContextPath();
}
@Override
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
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);
}
@Override
public void destroy() {
// no-op
}
private HttpServletRequest normalizedFileRequest(HttpServletRequest originalRequest) {
LOG.trace("Treating resource as file: {}", originalRequest.getRequestURI());
return new FileUriRequest(originalRequest);
}
private HttpServletRequest normalizedFolderRequest(HttpServletRequest originalRequest) {
LOG.trace("Treating resource as folder: {}", originalRequest.getRequestURI());
return new FolderUriRequest(originalRequest);
}
private HttpServletRequest normalizedRequestForUnknownResource(HttpServletRequest originalRequest) {
final String requestMethod = originalRequest.getMethod().toUpperCase();
if (ArrayUtils.contains(FILE_METHODS, requestMethod)) {
return normalizedFileRequest(originalRequest);
} else if (ArrayUtils.contains(DIRECTORY_METHODS, requestMethod)) {
return normalizedFolderRequest(originalRequest);
} else {
LOG.debug("Could not determine resource type of resource: {}", originalRequest.getRequestURI());
return originalRequest;
}
}
/**
* 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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}

View File

@@ -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