- 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:
Sebastian Stenzel
2016-01-10 16:27:56 +01:00
21 changed files with 467 additions and 66 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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