mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-23 21:21:31 +00:00
- 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
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user