Pārlūkot izejas kodu

- Fixes #128 and #119 by using unique directory id as associated data during filename encryption/decryption
- Using WeakValuedCache in all filesystem layers to prevent "twin" instances of the same folder
- Merge branch 'layered-io' of https://github.com/cryptomator/cryptomator into layered-io

Sebastian Stenzel 9 gadi atpakaļ
vecāks
revīzija
3b178030c7
21 mainītis faili ar 467 papildinājumiem un 66 dzēšanām
  1. 9 6
      main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java
  2. 13 0
      main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingFolderTest.java
  3. 2 2
      main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/TestDelegatingFolder.java
  4. 9 4
      main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java
  5. 2 2
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFolder.java
  6. 4 1
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java
  7. 24 9
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java
  8. 2 1
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoNode.java
  9. 12 0
      main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java
  10. 3 4
      main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
  11. 22 28
      main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java
  12. 4 4
      main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFolder.java
  13. 2 2
      main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFolder.java
  14. 2 2
      main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/filesystem/jackrabbit/FolderLocator.java
  15. 174 0
      main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/LoggingHttpFilter.java
  16. 27 0
      main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingHttpServletRequest.java
  17. 27 0
      main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingHttpServletResponse.java
  18. 73 0
      main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingServletInputStream.java
  19. 54 0
      main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingServletOutputStream.java
  20. 2 0
      main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java
  21. 0 1
      main/jackrabbit-filesystem-adapter/src/test/resources/log4j2.xml

+ 9 - 6
main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java

@@ -13,6 +13,7 @@ import java.time.Instant;
 import java.util.Optional;
 import java.util.stream.Stream;
 
+import org.cryptomator.common.WeakValuedCache;
 import org.cryptomator.filesystem.File;
 import org.cryptomator.filesystem.Folder;
 import org.cryptomator.filesystem.Node;
@@ -21,6 +22,8 @@ public abstract class DelegatingFolder<R extends DelegatingReadableFile, W exten
 		implements Folder {
 
 	private final D parent;
+	private final WeakValuedCache<Folder, D> folders = WeakValuedCache.usingLoader(this::newFolder);
+	private final WeakValuedCache<File, F> files = WeakValuedCache.usingLoader(this::newFile);
 
 	public DelegatingFolder(D parent, Folder delegate) {
 		super(delegate);
@@ -39,27 +42,27 @@ public abstract class DelegatingFolder<R extends DelegatingReadableFile, W exten
 
 	@Override
 	public Stream<D> folders() {
-		return delegate.folders().map(this::folder);
+		return delegate.folders().map(folders::get);
 	}
 
 	@Override
 	public Stream<F> files() throws UncheckedIOException {
-		return delegate.files().map(this::file);
+		return delegate.files().map(files::get);
 	}
 
 	@Override
 	public F file(String name) throws UncheckedIOException {
-		return file(delegate.file(name));
+		return files.get(delegate.file(name));
 	}
 
-	protected abstract F file(File delegate);
+	protected abstract F newFile(File delegate);
 
 	@Override
 	public D folder(String name) throws UncheckedIOException {
-		return folder(delegate.folder(name));
+		return folders.get(delegate.folder(name));
 	}
 
-	protected abstract D folder(Folder delegate);
+	protected abstract D newFolder(Folder delegate);
 
 	@Override
 	public void create() throws UncheckedIOException {

+ 13 - 0
main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingFolderTest.java

@@ -161,4 +161,17 @@ public class DelegatingFolderTest {
 		Mockito.verify(mockFolder).delete();
 	}
 
+	@Test
+	public void testSubresourcesAreSameInstance() {
+		Folder mockFolder = Mockito.mock(Folder.class);
+		Folder mockSubFolder = Mockito.mock(Folder.class);
+		File mockSubFile = Mockito.mock(File.class);
+		Mockito.when(mockFolder.folder("mockSubFolder")).thenReturn(mockSubFolder);
+		Mockito.when(mockFolder.file("mockSubFile")).thenReturn(mockSubFile);
+
+		DelegatingFolder<?, ?, ?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
+		Assert.assertSame(delegatingFolder.folder("mockSubFolder"), delegatingFolder.folder("mockSubFolder"));
+		Assert.assertSame(delegatingFolder.file("mockSubFile"), delegatingFolder.file("mockSubFile"));
+	}
+
 }

+ 2 - 2
main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/TestDelegatingFolder.java

@@ -10,12 +10,12 @@ class TestDelegatingFolder extends DelegatingFolder<DelegatingReadableFile, Dele
 	}
 
 	@Override
-	protected TestDelegatingFile file(File delegate) {
+	protected TestDelegatingFile newFile(File delegate) {
 		return new TestDelegatingFile(this, delegate);
 	}
 
 	@Override
-	protected TestDelegatingFolder folder(Folder delegate) {
+	protected TestDelegatingFolder newFolder(Folder delegate) {
 		return new TestDelegatingFolder(this, delegate);
 	}
 

+ 9 - 4
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java

@@ -26,7 +26,12 @@ class FilenameCryptorImpl implements FilenameCryptor {
 
 	private static final BaseNCodec BASE32 = new Base32();
 	private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
-	private static final SivMode AES_SIV = new SivMode();
+	private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
+		@Override
+		protected SivMode initialValue() {
+			return new SivMode();
+		};
+	};
 
 	private final SecretKey encryptionKey;
 	private final SecretKey macKey;
@@ -39,7 +44,7 @@ class FilenameCryptorImpl implements FilenameCryptor {
 	@Override
 	public String hashDirectoryId(String cleartextDirectoryId) {
 		final byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
-		byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes);
+		byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes);
 		final byte[] hashedBytes = SHA1.get().digest(encryptedBytes);
 		return BASE32.encodeAsString(hashedBytes);
 	}
@@ -47,7 +52,7 @@ class FilenameCryptorImpl implements FilenameCryptor {
 	@Override
 	public String encryptFilename(String cleartextName, byte[]... associatedData) {
 		final byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
-		final byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes, associatedData);
+		final byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes, associatedData);
 		return BASE32.encodeAsString(encryptedBytes);
 	}
 
@@ -55,7 +60,7 @@ class FilenameCryptorImpl implements FilenameCryptor {
 	public String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
 		final byte[] encryptedBytes = BASE32.decode(ciphertextName);
 		try {
-			final byte[] cleartextBytes = AES_SIV.decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
+			final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
 			return new String(cleartextBytes, UTF_8);
 		} catch (AEADBadTagException e) {
 			throw new AuthenticationFailedException("Authentication failed.", e);

+ 2 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFolder.java

@@ -22,12 +22,12 @@ class BlockAlignedFolder extends DelegatingFolder<BlockAlignedReadableFile, Bloc
 	}
 
 	@Override
-	protected BlockAlignedFile file(File delegate) {
+	protected BlockAlignedFile newFile(File delegate) {
 		return new BlockAlignedFile(this, delegate, blockSize);
 	}
 
 	@Override
-	protected BlockAlignedFolder folder(Folder delegate) {
+	protected BlockAlignedFolder newFolder(Folder delegate) {
 		return new BlockAlignedFolder(this, delegate, blockSize);
 	}
 

+ 4 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java

@@ -8,6 +8,8 @@
  *******************************************************************************/
 package org.cryptomator.filesystem.crypto;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.UncheckedIOException;
 import java.time.Instant;
 import java.util.Optional;
@@ -27,7 +29,8 @@ public class CryptoFile extends CryptoNode implements File {
 
 	@Override
 	protected String encryptedName() {
-		return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
+		final byte[] parentDirId = parent.getDirectoryId().getBytes(UTF_8);
+		return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId) + FILE_EXT;
 	}
 
 	@Override

+ 24 - 9
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java

@@ -23,6 +23,7 @@ import java.util.stream.Stream;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.cryptomator.common.WeakValuedCache;
 import org.cryptomator.crypto.engine.Cryptor;
 import org.cryptomator.filesystem.File;
 import org.cryptomator.filesystem.Folder;
@@ -31,8 +32,10 @@ import org.cryptomator.filesystem.WritableFile;
 
 class CryptoFolder extends CryptoNode implements Folder {
 
-	static final String FILE_EXT = ".dir";
+	static final String DIR_EXT = ".dir";
 
+	private final WeakValuedCache<String, CryptoFolder> folders = WeakValuedCache.usingLoader(this::newFolder);
+	private final WeakValuedCache<String, CryptoFile> files = WeakValuedCache.usingLoader(this::newFile);
 	private final AtomicReference<String> directoryId = new AtomicReference<>();
 
 	public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) {
@@ -41,7 +44,8 @@ class CryptoFolder extends CryptoNode implements Folder {
 
 	@Override
 	protected String encryptedName() {
-		return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
+		final byte[] parentDirId = parent().map(CryptoFolder::getDirectoryId).map(s -> s.getBytes(UTF_8)).orElse(null);
+		return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId) + DIR_EXT;
 	}
 
 	Folder physicalFolder() {
@@ -77,31 +81,41 @@ class CryptoFolder extends CryptoNode implements Folder {
 
 	@Override
 	public Stream<CryptoFile> files() {
-		return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFile.FILE_EXT)).map(this::decryptFileName).map(this::file);
+		return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFile.FILE_EXT)).map(this::decryptChildFileName).map(this::file);
 	}
 
-	private String decryptFileName(String encryptedFileName) {
+	private String decryptChildFileName(String encryptedFileName) {
+		final byte[] dirId = getDirectoryId().getBytes(UTF_8);
 		final String ciphertext = StringUtils.removeEnd(encryptedFileName, CryptoFile.FILE_EXT);
-		return cryptor.getFilenameCryptor().decryptFilename(ciphertext);
+		return cryptor.getFilenameCryptor().decryptFilename(ciphertext, dirId);
 	}
 
 	@Override
 	public CryptoFile file(String name) {
+		return files.get(name);
+	}
+
+	public CryptoFile newFile(String name) {
 		return new CryptoFile(this, name, cryptor);
 	}
 
 	@Override
 	public Stream<CryptoFolder> folders() {
-		return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFolder.FILE_EXT)).map(this::decryptFolderName).map(this::folder);
+		return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFolder.DIR_EXT)).map(this::decryptChildFolderName).map(this::folder);
 	}
 
-	private String decryptFolderName(String encryptedFolderName) {
-		final String ciphertext = StringUtils.removeEnd(encryptedFolderName, CryptoFolder.FILE_EXT);
-		return cryptor.getFilenameCryptor().decryptFilename(ciphertext);
+	private String decryptChildFolderName(String encryptedFolderName) {
+		final byte[] dirId = getDirectoryId().getBytes(UTF_8);
+		final String ciphertext = StringUtils.removeEnd(encryptedFolderName, CryptoFolder.DIR_EXT);
+		return cryptor.getFilenameCryptor().decryptFilename(ciphertext, dirId);
 	}
 
 	@Override
 	public CryptoFolder folder(String name) {
+		return folders.get(name);
+	}
+
+	public CryptoFolder newFolder(String name) {
 		return new CryptoFolder(this, name, cryptor);
 	}
 
@@ -139,6 +153,7 @@ class CryptoFolder extends CryptoNode implements Folder {
 
 		// directoryId is now used by target, we must no longer use the same id
 		// (we'll generate a new one when needed)
+		target.directoryId.set(getDirectoryId());
 		directoryId.set(null);
 	}
 

+ 2 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoNode.java

@@ -49,7 +49,8 @@ abstract class CryptoNode implements Node {
 
 	@Override
 	public boolean exists() {
-		return parent.children().anyMatch(node -> node.equals(this));
+		return physicalFile().exists();
+		// return parent.children().anyMatch(node -> node.equals(this));
 	}
 
 	@Override

+ 12 - 0
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java

@@ -77,6 +77,18 @@ public class FilenameCryptorImplTest {
 		filenameCryptor.decryptFilename(new String(encrypted, UTF_8));
 	}
 
+	@Test
+	public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() {
+		final byte[] keyBytes = new byte[32];
+		final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
+		final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
+		final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
+
+		final String encrypted1 = filenameCryptor.encryptFilename("test", "ad1".getBytes(UTF_8));
+		final String encrypted2 = filenameCryptor.encryptFilename("test", "ad2".getBytes(UTF_8));
+		Assert.assertNotEquals(encrypted1, encrypted2);
+	}
+
 	@Test
 	public void testDeterministicEncryptionOfFilenamesWithAssociatedData() {
 		final byte[] keyBytes = new byte[32];

+ 3 - 4
main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java

@@ -45,7 +45,7 @@ class InMemoryFile extends InMemoryNode implements File {
 		final WriteLock writeLock = lock.writeLock();
 		writeLock.lock();
 		final InMemoryFolder parent = parent().get();
-		parent.children.compute(this.name(), (k, v) -> {
+		parent.existingChildren.compute(this.name(), (k, v) -> {
 			if (v == null || v == this) {
 				this.lastModified = Instant.now();
 				this.creationTime = Instant.now();
@@ -54,7 +54,6 @@ class InMemoryFile extends InMemoryNode implements File {
 				throw new UncheckedIOException(new FileExistsException(k));
 			}
 		});
-		parent.volatileFiles.remove(name);
 		return new InMemoryWritableFile(this::setLastModified, this::setCreationTime, this::getContent, this::setContent, this::delete, writeLock);
 	}
 
@@ -76,11 +75,11 @@ class InMemoryFile extends InMemoryNode implements File {
 
 	private void delete(Void param) {
 		final InMemoryFolder parent = parent().get();
-		parent.children.computeIfPresent(this.name(), (k, v) -> {
+		parent.existingChildren.computeIfPresent(this.name(), (k, v) -> {
 			// returning null removes the entry.
 			return null;
 		});
-		assert !this.exists();
+		assert!this.exists();
 	}
 
 	@Override

+ 22 - 28
main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java

@@ -10,20 +10,21 @@ package org.cryptomator.filesystem.inmem;
 
 import java.io.UncheckedIOException;
 import java.time.Instant;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.stream.Stream;
 
 import org.apache.commons.io.FileExistsException;
+import org.cryptomator.common.WeakValuedCache;
 import org.cryptomator.filesystem.Folder;
 
 class InMemoryFolder extends InMemoryNode implements Folder {
 
-	final Map<String, InMemoryNode> children = new TreeMap<>();
-	final Map<String, InMemoryFile> volatileFiles = new HashMap<>();
-	final Map<String, InMemoryFolder> volatileFolders = new HashMap<>();
+	final Map<String, InMemoryNode> existingChildren = new TreeMap<>();
+
+	private final WeakValuedCache<String, InMemoryFolder> folders = WeakValuedCache.usingLoader(this::newFolder);
+	private final WeakValuedCache<String, InMemoryFile> files = WeakValuedCache.usingLoader(this::newFile);
 
 	public InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified, Instant creationTime) {
 		super(parent, name, lastModified, creationTime);
@@ -31,31 +32,25 @@ class InMemoryFolder extends InMemoryNode implements Folder {
 
 	@Override
 	public Stream<InMemoryNode> children() {
-		return children.values().stream();
+		return existingChildren.values().stream();
 	}
 
 	@Override
 	public InMemoryFile file(String name) {
-		final InMemoryNode node = children.get(name);
-		if (node instanceof InMemoryFile) {
-			return (InMemoryFile) node;
-		} else {
-			return volatileFiles.computeIfAbsent(name, (n) -> {
-				return new InMemoryFile(this, n, Instant.MIN, Instant.MIN);
-			});
-		}
+		return files.get(name);
+	}
+
+	private InMemoryFile newFile(String name) {
+		return new InMemoryFile(this, name, Instant.MIN, Instant.MIN);
 	}
 
 	@Override
 	public InMemoryFolder folder(String name) {
-		final InMemoryNode node = children.get(name);
-		if (node instanceof InMemoryFolder) {
-			return (InMemoryFolder) node;
-		} else {
-			return volatileFolders.computeIfAbsent(name, (n) -> {
-				return new InMemoryFolder(this, n, Instant.MIN, Instant.MIN);
-			});
-		}
+		return folders.get(name);
+	}
+
+	private InMemoryFolder newFolder(String name) {
+		return new InMemoryFolder(this, name, Instant.MIN, Instant.MIN);
 	}
 
 	@Override
@@ -64,7 +59,7 @@ class InMemoryFolder extends InMemoryNode implements Folder {
 			return;
 		}
 		parent.create();
-		parent.children.compute(name, (k, v) -> {
+		parent.existingChildren.compute(name, (k, v) -> {
 			if (v == null) {
 				this.lastModified = Instant.now();
 				return this;
@@ -72,7 +67,6 @@ class InMemoryFolder extends InMemoryNode implements Folder {
 				throw new UncheckedIOException(new FileExistsException(k));
 			}
 		});
-		parent.volatileFolders.remove(name);
 		assert this.exists();
 		creationTime = Instant.now();
 	}
@@ -82,22 +76,22 @@ 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
 	public void delete() {
 		// remove ourself from parent:
-		parent.children.computeIfPresent(name, (k, v) -> {
+		parent.existingChildren.computeIfPresent(name, (k, v) -> {
 			// returning null removes the entry.
 			return null;
 		});
 		// delete all children:
-		for (Iterator<Map.Entry<String, InMemoryNode>> iterator = children.entrySet().iterator(); iterator.hasNext();) {
+		for (Iterator<Map.Entry<String, InMemoryNode>> iterator = existingChildren.entrySet().iterator(); iterator.hasNext();) {
 			Map.Entry<String, InMemoryNode> entry = iterator.next();
 			iterator.remove();
 			// recursively on folders:
@@ -108,7 +102,7 @@ class InMemoryFolder extends InMemoryNode implements Folder {
 				subFolder.delete();
 			}
 		}
-		assert !this.exists();
+		assert!this.exists();
 	}
 
 	@Override

+ 4 - 4
main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFolder.java

@@ -28,16 +28,16 @@ class BlacklistingFolder extends DelegatingFolder<DelegatingReadableFile, Delega
 
 	@Override
 	public Stream<BlacklistingFolder> folders() {
-		return delegate.folders().filter(hiddenNodes.negate()).map(this::folder);
+		return delegate.folders().filter(hiddenNodes.negate()).map(this::newFolder);
 	}
 
 	@Override
 	public Stream<BlacklistingFile> files() {
-		return delegate.files().filter(hiddenNodes.negate()).map(this::file);
+		return delegate.files().filter(hiddenNodes.negate()).map(this::newFile);
 	}
 
 	@Override
-	protected BlacklistingFile file(File delegate) {
+	protected BlacklistingFile newFile(File delegate) {
 		if (hiddenNodes.test(delegate)) {
 			throw new UncheckedIOException("'" + delegate.name() + "' is a reserved name.", new FileAlreadyExistsException(delegate.name()));
 		}
@@ -45,7 +45,7 @@ class BlacklistingFolder extends DelegatingFolder<DelegatingReadableFile, Delega
 	}
 
 	@Override
-	protected BlacklistingFolder folder(Folder delegate) {
+	protected BlacklistingFolder newFolder(Folder delegate) {
 		if (hiddenNodes.test(delegate)) {
 			throw new UncheckedIOException("'" + delegate.name() + "' is a reserved name.", new FileAlreadyExistsException(delegate.name()));
 		}

+ 2 - 2
main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFolder.java

@@ -43,12 +43,12 @@ class ShorteningFolder extends DelegatingFolder<DelegatingReadableFile, Delegati
 	}
 
 	@Override
-	protected ShorteningFile file(File delegate) {
+	protected ShorteningFile newFile(File delegate) {
 		return new ShorteningFile(this, delegate, null, shortener);
 	}
 
 	@Override
-	protected ShorteningFolder folder(Folder delegate) {
+	protected ShorteningFolder newFolder(Folder delegate) {
 		return new ShorteningFolder(this, delegate, null, shortener);
 	}
 

+ 2 - 2
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/filesystem/jackrabbit/FolderLocator.java

@@ -23,7 +23,7 @@ public class FolderLocator extends DelegatingFolder<DelegatingReadableFile, Dele
 	}
 
 	@Override
-	protected FileLocator file(File delegate) {
+	protected FileLocator newFile(File delegate) {
 		return new FileLocator(factory, prefix, this, delegate);
 	}
 
@@ -33,7 +33,7 @@ public class FolderLocator extends DelegatingFolder<DelegatingReadableFile, Dele
 	}
 
 	@Override
-	protected FolderLocator folder(Folder delegate) {
+	protected FolderLocator newFolder(Folder delegate) {
 		return new FolderLocator(factory, prefix, this, delegate);
 	}
 

+ 174 - 0
main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/LoggingHttpFilter.java

@@ -0,0 +1,174 @@
+package org.cryptomator.webdav.filters;
+
+import static java.lang.String.format;
+import static java.util.Arrays.asList;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoggingHttpFilter implements HttpFilter {
+
+	private static final Set<String> METHODS_TO_LOG_DETAILED = methodsToLog();
+
+	private static final Set<String> methodsToLog() {
+		String methodsToLog = System.getProperty("cryptomator.LoggingHttpFilter.methodsToLogDetailed");
+		if (methodsToLog == null) {
+			return Collections.emptySet();
+		} else {
+			return new HashSet<>(asList(methodsToLog.toUpperCase().split(",")));
+		}
+	}
+
+	private final Logger LOG = LoggerFactory.getLogger(LoggingHttpFilter.class);
+
+	@Override
+	public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+		if (METHODS_TO_LOG_DETAILED.contains(request.getMethod().toUpperCase())) {
+			logDetailed(request, response, chain);
+		} else {
+			logBasic(request, response, chain);
+		}
+	}
+
+	private void logBasic(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+		Optional<Throwable> thrown = Optional.empty();
+		try {
+			chain.doFilter(request, response);
+		} catch (IOException | ServletException e) {
+			thrown = Optional.of(e);
+			throw e;
+		} catch (RuntimeException | Error e) {
+			thrown = Optional.of(e);
+			throw e;
+		} finally {
+			if (thrown.isPresent()) {
+				logError(request, thrown.get());
+			} else {
+				logSuccess(request, response);
+			}
+		}
+	}
+
+	private void logDetailed(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+		RecordingHttpServletRequest recordingRequest = new RecordingHttpServletRequest(request);
+		RecordingHttpServletResponse recordingResponse = new RecordingHttpServletResponse(response);
+		Optional<Throwable> thrown = Optional.empty();
+		try {
+			chain.doFilter(recordingRequest, recordingResponse);
+		} catch (IOException | ServletException e) {
+			thrown = Optional.of(e);
+			throw e;
+		} catch (RuntimeException | Error e) {
+			thrown = Optional.of(e);
+			throw e;
+		} finally {
+			if (thrown.isPresent()) {
+				logError(recordingRequest, thrown.get());
+			} else {
+				logSuccess(recordingRequest, recordingResponse);
+			}
+		}
+	}
+
+	private void logSuccess(HttpServletRequest request, HttpServletResponse response) {
+		LOG.debug(format(
+				"## Request ##\n" + //
+						"%s %s %s\n" //
+						+ "%s\n" //
+						+ "## Response ##\n" //
+						+ "%s %s\n" //
+						+ "%s\n", //
+				request.getMethod(), request.getRequestURI(), request.getProtocol(), //
+				headers(request), //
+				request.getProtocol(), response.getStatus(), //
+				headers(response)));
+	}
+
+	private void logError(HttpServletRequest request, Throwable throwable) {
+		LOG.error(
+				format("## Request ##\n" + //
+						"%s %s %s\n" //
+						+ "%s\n" //
+						+ "%s\n\n", //
+				request.getMethod(), request.getRequestURI(), request.getProtocol(), //
+				headers(request)), //
+				throwable);
+	}
+
+	private void logSuccess(RecordingHttpServletRequest request, RecordingHttpServletResponse response) {
+		LOG.debug(format(
+				"## Request ##\n" + //
+						"%s %s %s\n" //
+						+ "%s\n" //
+						+ "%s\n\n" //
+						+ "## Response ##\n" //
+						+ "%s %s\n" //
+						+ "%s\n" //
+						+ "%s", //
+				request.getMethod(), request.getRequestURI(), request.getProtocol(), //
+				headers(request), //
+				new String(request.getRecording()), //
+				request.getProtocol(), response.getStatus(), //
+				headers(response), //
+				new String(response.getRecording())));
+	}
+
+	private void logError(RecordingHttpServletRequest request, Throwable throwable) {
+		LOG.error(
+				format("## Request ##\n" + //
+						"%s %s %s\n" //
+						+ "%s\n" //
+						+ "%s\n\n", //
+				request.getMethod(), request.getRequestURI(), request.getProtocol(), //
+				headers(request), //
+				new String(request.getRecording())), //
+				throwable);
+	}
+
+	private String headers(HttpServletResponse response) {
+		StringBuilder result = new StringBuilder();
+		for (String headerName : response.getHeaderNames()) {
+			for (String value : response.getHeaders(headerName)) {
+				result.append(headerName).append(": ").append(value).append('\n');
+			}
+		}
+		return result.toString();
+	}
+
+	private String headers(HttpServletRequest request) {
+		StringBuilder result = new StringBuilder();
+		Enumeration<String> headerNames = request.getHeaderNames();
+		while (headerNames.hasMoreElements()) {
+			String headerName = headerNames.nextElement();
+			Enumeration<String> values = request.getHeaders(headerName);
+			while (values.hasMoreElements()) {
+				result.append(headerName).append(": ").append(values.nextElement()).append('\n');
+			}
+		}
+		return result.toString();
+	}
+
+	@Override
+	public void init(FilterConfig filterConfig) throws ServletException {
+		// empty
+	}
+
+	@Override
+	public void destroy() {
+		// empty
+	}
+
+}

+ 27 - 0
main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingHttpServletRequest.java

@@ -0,0 +1,27 @@
+package org.cryptomator.webdav.filters;
+
+import java.io.IOException;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+class RecordingHttpServletRequest extends HttpServletRequestWrapper {
+
+	private final RecordingServletInputStream recording;
+
+	public RecordingHttpServletRequest(HttpServletRequest request) throws IOException {
+		super(request);
+		recording = new RecordingServletInputStream(request.getInputStream());
+	}
+
+	@Override
+	public ServletInputStream getInputStream() throws IOException {
+		return recording;
+	}
+
+	public byte[] getRecording() {
+		return recording.getRecording();
+	}
+
+}

+ 27 - 0
main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingHttpServletResponse.java

@@ -0,0 +1,27 @@
+package org.cryptomator.webdav.filters;
+
+import java.io.IOException;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+class RecordingHttpServletResponse extends HttpServletResponseWrapper {
+
+	private final RecordingServletOutputStream recording;
+
+	public RecordingHttpServletResponse(HttpServletResponse response) throws IOException {
+		super(response);
+		recording = new RecordingServletOutputStream(response.getOutputStream());
+	}
+
+	@Override
+	public ServletOutputStream getOutputStream() throws IOException {
+		return recording;
+	}
+
+	public byte[] getRecording() {
+		return recording.getRecording();
+	}
+
+}

+ 73 - 0
main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingServletInputStream.java

@@ -0,0 +1,73 @@
+package org.cryptomator.webdav.filters;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+
+import org.apache.commons.io.input.TeeInputStream;
+
+class RecordingServletInputStream extends ServletInputStream {
+
+	private final ServletInputStream delegate;
+	private final TeeInputStream teeInputStream;
+	private final ByteArrayOutputStream recording = new ByteArrayOutputStream(4096);
+
+	public RecordingServletInputStream(ServletInputStream delegate) {
+		this.delegate = delegate;
+		this.teeInputStream = new TeeInputStream(delegate, recording);
+	}
+
+	public int read() throws IOException {
+		return teeInputStream.read();
+	}
+
+	public int read(byte[] b) throws IOException {
+		return teeInputStream.read(b);
+	}
+
+	public int read(byte[] b, int off, int len) throws IOException {
+		return teeInputStream.read(b, off, len);
+	}
+
+	public boolean isFinished() {
+		return delegate.isFinished();
+	}
+
+	public boolean isReady() {
+		return delegate.isReady();
+	}
+
+	public void setReadListener(ReadListener readListener) {
+		delegate.setReadListener(readListener);
+	}
+
+	public long skip(long n) throws IOException {
+		return teeInputStream.skip(n);
+	}
+
+	public int available() throws IOException {
+		return teeInputStream.available();
+	}
+
+	public void close() throws IOException {
+		teeInputStream.close();
+	}
+
+	public byte[] getRecording() {
+		return recording.toByteArray();
+	}
+
+	public void mark(int readlimit) {
+	}
+
+	public void reset() throws IOException {
+		throw new IOException("Mark not supported");
+	}
+
+	public boolean markSupported() {
+		return false;
+	}
+
+}

+ 54 - 0
main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingServletOutputStream.java

@@ -0,0 +1,54 @@
+package org.cryptomator.webdav.filters;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+
+import org.apache.commons.io.output.TeeOutputStream;
+
+class RecordingServletOutputStream extends ServletOutputStream {
+
+	private final ServletOutputStream delegate;
+	private final TeeOutputStream teeOutputStream;
+	private final ByteArrayOutputStream recording = new ByteArrayOutputStream(4096);
+
+	public RecordingServletOutputStream(ServletOutputStream delegate) {
+		this.delegate = delegate;
+		this.teeOutputStream = new TeeOutputStream(delegate, recording);
+	}
+
+	public void write(int b) throws IOException {
+		teeOutputStream.write(b);
+	}
+
+	public void write(byte[] b) throws IOException {
+		teeOutputStream.write(b);
+	}
+
+	public void write(byte[] b, int off, int len) throws IOException {
+		teeOutputStream.write(b, off, len);
+	}
+
+	public void flush() throws IOException {
+		teeOutputStream.flush();
+	}
+
+	public void close() throws IOException {
+		teeOutputStream.close();
+	}
+
+	public boolean isReady() {
+		return delegate.isReady();
+	}
+
+	public void setWriteListener(WriteListener writeListener) {
+		delegate.setWriteListener(writeListener);
+	}
+
+	public byte[] getRecording() {
+		return recording.toByteArray();
+	}
+
+}

+ 2 - 0
main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java

@@ -18,6 +18,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.UriNormalizationFilter;
 import org.eclipse.jetty.server.Connector;
 import org.eclipse.jetty.server.Server;
@@ -54,6 +55,7 @@ class FileSystemBasedWebDavServer {
 		servletContext.addServlet(servletHolder, "/*");
 		servletContext.addFilter(AcceptRangeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
 		servletContext.addFilter(UriNormalizationFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
+		servletContext.addFilter(LoggingHttpFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
 		servletCollection.mapContexts();
 
 		server.setConnectors(new Connector[] { localConnector });

+ 0 - 1
main/jackrabbit-filesystem-adapter/src/test/resources/log4j2.xml

@@ -23,7 +23,6 @@
 	<Loggers>
 		<!-- show our own debug messages: -->
 		<Logger name="org.cryptomator" level="DEBUG" />
-		<Logger name="org.eclipse.jetty.server.Server" level="DEBUG" />
 		<!-- mute dependencies: -->
 		<Root level="INFO">
 			<AppenderRef ref="Console" />