From 1d05e878ab6e1a1d980d2f7422f3e61d2a6ad37c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 21 Dec 2014 16:54:47 +0100 Subject: [PATCH] - Support for HTTP Range header fields, thus vastly improved performance for video streaming - Simplified cryptor implementation for partial decryption --- README.md | 9 +- .../{WebDAVServer.java => WebDavServer.java} | 11 +- ...ractSessionAwareWebDavResourceFactory.java | 36 +++++ .../webdav/jackrabbit/BidiLRUMap.java | 2 +- .../jackrabbit/WebDavLocatorFactory.java | 4 +- .../jackrabbit/WebDavResourceFactory.java | 35 +++-- .../webdav/jackrabbit/WebDavSession.java | 13 +- .../jackrabbit/WebDavSessionProvider.java | 6 +- .../resources/AbstractEncryptedNode.java | 14 +- .../jackrabbit/resources/EncryptedDir.java | 10 +- .../jackrabbit/resources/EncryptedFile.java | 9 +- .../resources/EncryptedFilePart.java | 144 ++++++++++++++++++ .../jackrabbit/resources/FileTimeUtils.java | 4 +- .../resources/HttpHeaderProperty.java | 20 +++ ...{PathUtils.java => ResourcePathUtils.java} | 4 +- .../crypto/aes256/Aes256Cryptor.java | 12 +- .../aes256/LimitFilterOutputStream.java | 46 ------ .../aes256/OffsetFilterOutputStream.java | 49 ------ .../aes256/RangeFilterOutputStream.java | 21 --- .../crypto/aes256/Aes256CryptorTest.java | 13 +- .../aes256/LimitFilterOutputStreamTest.java | 63 -------- .../aes256/OffsetFilterOutputStreamTest.java | 63 -------- .../aes256/RangeFilterOutputStreamTest.java | 76 --------- main/pom.xml | 15 +- .../org/cryptomator/ui/model/Directory.java | 6 +- .../ui/util/mount/FallbackWebDavMounter.java | 1 + 26 files changed, 293 insertions(+), 393 deletions(-) rename main/core/src/main/java/org/cryptomator/webdav/{WebDAVServer.java => WebDavServer.java} (89%) create mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractSessionAwareWebDavResourceFactory.java create mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java create mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/HttpHeaderProperty.java rename main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/{PathUtils.java => ResourcePathUtils.java} (92%) delete mode 100644 main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LimitFilterOutputStream.java delete mode 100644 main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/OffsetFilterOutputStream.java delete mode 100644 main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/RangeFilterOutputStream.java delete mode 100644 main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/LimitFilterOutputStreamTest.java delete mode 100644 main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/OffsetFilterOutputStreamTest.java delete mode 100644 main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/RangeFilterOutputStreamTest.java diff --git a/README.md b/README.md index 267ca745c..fd04ba388 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ If you want to take a look at the current beta version, go ahead and download [C ## Security - Default key length is 256 bit (falls back to 128 bit, if JCE isn't installed) - PBKDF2 key generation -- 4096 bit internal masterkey - Cryptographically secure random numbers for salts, IVs and the masterkey of course - Sensitive data is swiped from the heap asap - Lightweight: Complexity kills security @@ -31,16 +30,12 @@ If you want to take a look at the current beta version, go ahead and download [C ## Dependencies - Java 8 -- Maven -- Awesome 3rd party open source libraries (Apache Commons, Apache Jackrabbit, Jetty, Jackson, ...) +- see pom.xml ;-) ## TODO -### Core -- Support for HTTP range requests - ### UI -- Automount of WebDAV volumes for Win/Tux +- Native L&F - Drive icons in WebDAV volumes - Change password functionality - Better explanations on UI diff --git a/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java similarity index 89% rename from main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java rename to main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java index 27d862ae4..1e066b752 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java +++ b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java @@ -23,9 +23,9 @@ import org.eclipse.jetty.util.thread.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class WebDAVServer { +public final class WebDavServer { - private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class); + private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class); private static final String LOCALHOST = "::1"; private static final int MAX_PENDING_REQUESTS = 200; private static final int MAX_THREADS = 200; @@ -34,7 +34,7 @@ public final class WebDAVServer { private final Server server; private int port; - public WebDAVServer() { + public WebDavServer() { final BlockingQueue queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS); final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue); server = new Server(tp); @@ -50,9 +50,10 @@ public final class WebDAVServer { connector.setHost(LOCALHOST); final String contextPath = "/"; + final String servletPathSpec = "/*"; final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); - context.addServlet(getMiltonServletHolder(workDir, contextPath, cryptor), "/*"); + context.addServlet(getWebDavServletHolder(workDir, contextPath, cryptor), servletPathSpec); context.setContextPath(contextPath); server.setHandler(context); @@ -81,7 +82,7 @@ public final class WebDAVServer { return server.isStopped(); } - private ServletHolder getMiltonServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) { + private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) { final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor)); result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir); result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath); diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractSessionAwareWebDavResourceFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractSessionAwareWebDavResourceFactory.java new file mode 100644 index 000000000..edd85a9db --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractSessionAwareWebDavResourceFactory.java @@ -0,0 +1,36 @@ +package org.cryptomator.webdav.jackrabbit; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletRequest; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; + +abstract class AbstractSessionAwareWebDavResourceFactory implements DavResourceFactory { + + @Override + public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { + final DavSession session = request.getDavSession(); + if (session != null && session instanceof WebDavSession) { + return createDavResource(locator, (WebDavSession) session, request, response); + } else { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type."); + } + } + + protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session, DavServletRequest request, DavServletResponse response) throws DavException; + + @Override + public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { + if (session != null && session instanceof WebDavSession) { + return createDavResource(locator, (WebDavSession) session); + } else { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type."); + } + } + + protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session); + +} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java index 0a73599c3..dc8761d1f 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java @@ -8,7 +8,7 @@ import org.apache.commons.collections4.map.LRUMap; final class BidiLRUMap extends AbstractDualBidiMap { - public BidiLRUMap(int maxSize) { + BidiLRUMap(int maxSize) { super(new LRUMap(maxSize), new LRUMap(maxSize)); } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java index 7cd819f90..ee00c53aa 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java @@ -21,14 +21,14 @@ import org.cryptomator.crypto.Cryptor; import org.cryptomator.crypto.CryptorIOSupport; import org.cryptomator.crypto.SensitiveDataSwipeListener; -public class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport { +class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport { private static final int MAX_CACHED_PATHS = 10000; private final Path fsRoot; private final Cryptor cryptor; private final BidiMap pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // - public WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) { + WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) { super(httpRoot); this.fsRoot = FileSystems.getDefault().getPath(fsRoot); this.cryptor = cryptor; diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavResourceFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavResourceFactory.java index 455507350..a4a80fe5d 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavResourceFactory.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavResourceFactory.java @@ -11,6 +11,7 @@ package org.cryptomator.webdav.jackrabbit; import java.nio.file.Files; import java.nio.file.Path; +import org.apache.commons.httpclient.HttpStatus; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavMethods; import org.apache.jackrabbit.webdav.DavResource; @@ -24,28 +25,32 @@ import org.apache.jackrabbit.webdav.lock.SimpleLockManager; import org.cryptomator.crypto.Cryptor; import org.cryptomator.webdav.jackrabbit.resources.EncryptedDir; import org.cryptomator.webdav.jackrabbit.resources.EncryptedFile; +import org.cryptomator.webdav.jackrabbit.resources.EncryptedFilePart; import org.cryptomator.webdav.jackrabbit.resources.NonExistingNode; -import org.cryptomator.webdav.jackrabbit.resources.PathUtils; +import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils; +import org.eclipse.jetty.http.HttpHeader; -public class WebDavResourceFactory implements DavResourceFactory { +class WebDavResourceFactory implements DavResourceFactory { private final LockManager lockManager = new SimpleLockManager(); private final Cryptor cryptor; - public WebDavResourceFactory(Cryptor cryptor) { + WebDavResourceFactory(Cryptor cryptor) { this.cryptor = cryptor; } @Override public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { - final Path path = PathUtils.getPhysicalPath(locator); + final Path path = ResourcePathUtils.getPhysicalPath(locator); + final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); - if (Files.exists(path)) { - return createResource(locator, request.getDavSession()); - } else if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) { - return createDirectory(locator, request.getDavSession()); - } else if (DavMethods.METHOD_PUT.equals(request.getMethod())) { + if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) { + response.setStatus(HttpStatus.SC_PARTIAL_CONTENT); + return createFilePart(locator, request.getDavSession(), request); + } else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) { return createFile(locator, request.getDavSession()); + } else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) { + return createDirectory(locator, request.getDavSession()); } else { return createNonExisting(locator, request.getDavSession()); } @@ -53,17 +58,21 @@ public class WebDavResourceFactory implements DavResourceFactory { @Override public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { - final Path path = PathUtils.getPhysicalPath(locator); + final Path path = ResourcePathUtils.getPhysicalPath(locator); - if (Files.isDirectory(path)) { - return createDirectory(locator, session); - } else if (Files.isRegularFile(path)) { + if (Files.isRegularFile(path)) { return createFile(locator, session); + } else if (Files.isDirectory(path)) { + return createDirectory(locator, session); } else { return createNonExisting(locator, session); } } + private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) { + return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor); + } + private EncryptedFile createFile(DavResourceLocator locator, DavSession session) { return new EncryptedFile(this, locator, session, lockManager, cryptor); } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavSession.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavSession.java index 94da3bbac..79e90dc14 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavSession.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavSession.java @@ -9,8 +9,15 @@ package org.cryptomator.webdav.jackrabbit; import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.WebdavRequest; -public class WebDavSession implements DavSession { +class WebDavSession implements DavSession { + + private final WebdavRequest request; + + WebDavSession(WebdavRequest request) { + this.request = request; + } @Override public void addReference(Object reference) { @@ -42,4 +49,8 @@ public class WebDavSession implements DavSession { } + public WebdavRequest getRequest() { + return request; + } + } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavSessionProvider.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavSessionProvider.java index c6c3ff909..0b6ca8aab 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavSessionProvider.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavSessionProvider.java @@ -12,12 +12,12 @@ import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavSessionProvider; import org.apache.jackrabbit.webdav.WebdavRequest; -public class WebDavSessionProvider implements DavSessionProvider { +class WebDavSessionProvider implements DavSessionProvider { @Override public boolean attachSession(WebdavRequest request) throws DavException { - // every user gets a session - request.setDavSession(new WebDavSession()); + // every request gets a session + request.setDavSession(new WebDavSession(request)); return true; } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/AbstractEncryptedNode.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/AbstractEncryptedNode.java index d1c99d885..efaeb8f59 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/AbstractEncryptedNode.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/AbstractEncryptedNode.java @@ -38,7 +38,7 @@ import org.cryptomator.webdav.exceptions.IORuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public abstract class AbstractEncryptedNode implements DavResource { +abstract class AbstractEncryptedNode implements DavResource { private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class); private static final String DAV_COMPLIANCE_CLASSES = "1, 2"; @@ -72,7 +72,7 @@ public abstract class AbstractEncryptedNode implements DavResource { @Override public boolean exists() { - final Path path = PathUtils.getPhysicalPath(this); + final Path path = ResourcePathUtils.getPhysicalPath(this); return Files.exists(path); } @@ -104,7 +104,7 @@ public abstract class AbstractEncryptedNode implements DavResource { @Override public long getModificationTime() { - final Path path = PathUtils.getPhysicalPath(this); + final Path path = ResourcePathUtils.getPhysicalPath(this); try { return Files.getLastModifiedTime(path).toMillis(); } catch (IOException e) { @@ -173,8 +173,8 @@ public abstract class AbstractEncryptedNode implements DavResource { @Override public void move(DavResource dest) throws DavException { - final Path src = PathUtils.getPhysicalPath(this); - final Path dst = PathUtils.getPhysicalPath(dest); + final Path src = ResourcePathUtils.getPhysicalPath(this); + final Path dst = ResourcePathUtils.getPhysicalPath(dest); try { // check for conflicts: if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) { @@ -195,8 +195,8 @@ public abstract class AbstractEncryptedNode implements DavResource { @Override public void copy(DavResource dest, boolean shallow) throws DavException { - final Path src = PathUtils.getPhysicalPath(this); - final Path dst = PathUtils.getPhysicalPath(dest); + final Path src = ResourcePathUtils.getPhysicalPath(this); + final Path dst = ResourcePathUtils.getPhysicalPath(dest); try { // check for conflicts: if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) { diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedDir.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedDir.java index 2624ca4fd..17ca91926 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedDir.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedDir.java @@ -64,7 +64,7 @@ public class EncryptedDir extends AbstractEncryptedNode { } private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException { - final Path childPath = PathUtils.getPhysicalPath(resource); + final Path childPath = ResourcePathUtils.getPhysicalPath(resource); try { Files.createDirectories(childPath); } catch (SecurityException e) { @@ -76,7 +76,7 @@ public class EncryptedDir extends AbstractEncryptedNode { } private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException { - final Path childPath = PathUtils.getPhysicalPath(resource); + final Path childPath = ResourcePathUtils.getPhysicalPath(resource); SeekableByteChannel channel = null; try { channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE); @@ -94,7 +94,7 @@ public class EncryptedDir extends AbstractEncryptedNode { @Override public DavResourceIterator getMembers() { - final Path dir = PathUtils.getPhysicalPath(this); + final Path dir = ResourcePathUtils.getPhysicalPath(this); try { final DirectoryStream directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter()); final List result = new ArrayList<>(); @@ -116,7 +116,7 @@ public class EncryptedDir extends AbstractEncryptedNode { @Override public void removeMember(DavResource member) throws DavException { - final Path memberPath = PathUtils.getPhysicalPath(member); + final Path memberPath = ResourcePathUtils.getPhysicalPath(member); try { Files.walkFileTree(memberPath, new DeletingFileVisitor()); } catch (SecurityException e) { @@ -133,7 +133,7 @@ public class EncryptedDir extends AbstractEncryptedNode { @Override protected void determineProperties() { - final Path path = PathUtils.getPhysicalPath(this); + final Path path = ResourcePathUtils.getPhysicalPath(this); properties.add(new ResourceType(ResourceType.COLLECTION)); properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1)); if (Files.exists(path)) { diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java index ece0f5b83..9b6ee3ee5 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java @@ -30,6 +30,8 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.cryptomator.crypto.Cryptor; import org.cryptomator.webdav.exceptions.IORuntimeException; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,9 +65,10 @@ public class EncryptedFile extends AbstractEncryptedNode { @Override public void spool(OutputContext outputContext) throws IOException { - final Path path = PathUtils.getPhysicalPath(this); + final Path path = ResourcePathUtils.getPhysicalPath(this); if (Files.exists(path)) { outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis()); + outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()); SeekableByteChannel channel = null; try { channel = Files.newByteChannel(path, StandardOpenOption.READ); @@ -81,13 +84,12 @@ public class EncryptedFile extends AbstractEncryptedNode { } finally { IOUtils.closeQuietly(channel); } - } } @Override protected void determineProperties() { - final Path path = PathUtils.getPhysicalPath(this); + final Path path = ResourcePathUtils.getPhysicalPath(this); if (Files.exists(path)) { SeekableByteChannel channel = null; try { @@ -98,6 +100,7 @@ public class EncryptedFile extends AbstractEncryptedNode { final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime()))); properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime()))); + properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString())); } catch (IOException e) { LOG.error("Error determining metadata " + path.toString(), e); throw new IORuntimeException(e); diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java new file mode 100644 index 000000000..953ba4341 --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java @@ -0,0 +1,144 @@ +package org.cryptomator.webdav.jackrabbit.resources; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletRequest; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.cryptomator.crypto.Cryptor; +import org.cryptomator.webdav.exceptions.IORuntimeException; +import org.eclipse.jetty.http.HttpHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Delivers only the requested range of bytes from a file. + * + * @see {@link https://tools.ietf.org/html/rfc7233#section-4} + */ +public class EncryptedFilePart extends EncryptedFile { + + private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class); + private static final String BYTE_UNIT_PREFIX = "bytes="; + private static final char RANGE_SET_SEP = ','; + private static final char RANGE_SEP = '-'; + + /** + * e.g. range -500 (gets the last 500 bytes) -> (-1, 500) + */ + private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L; + + /** + * e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG) + */ + private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE; + + private final Set> requestedContentRanges = new HashSet>(); + + public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor) { + super(factory, locator, session, lockManager, cryptor); + final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); + if (rangeHeader == null) { + throw new IllegalArgumentException("HTTP request doesn't contain a range header"); + } + determineByteRanges(rangeHeader); + } + + private void determineByteRanges(String rangeHeader) { + final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX); + final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP); + if (byteRanges.length == 0) { + throw new IllegalArgumentException("Invalid range: " + rangeHeader); + } + for (final String byteRange : byteRanges) { + final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP); + if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) { + throw new IllegalArgumentException("Invalid range: " + rangeHeader); + } + final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]); + final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]); + if (lower > upper) { + throw new IllegalArgumentException("Invalid range: " + rangeHeader); + } + requestedContentRanges.add(new ImmutablePair(lower, upper)); + } + } + + /** + * @return One range, that spans all requested ranges. + */ + private Pair getUnionRange(Long fileSize) { + final long lastByte = fileSize - 1; + final MutablePair result = new MutablePair(); + for (Pair range : requestedContentRanges) { + final long left; + final long right; + if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) { + left = lastByte - range.getRight(); + right = lastByte; + } else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) { + left = range.getLeft(); + right = lastByte; + } else { + left = range.getLeft(); + right = range.getRight(); + } + if (result.getLeft() == null || left < result.getLeft()) { + result.setLeft(left); + } + if (result.getRight() == null || right > result.getRight()) { + result.setRight(right); + } + } + return result; + } + + @Override + public void spool(OutputContext outputContext) throws IOException { + final Path path = ResourcePathUtils.getPhysicalPath(this); + if (Files.exists(path)) { + outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis()); + SeekableByteChannel channel = null; + try { + channel = Files.newByteChannel(path, StandardOpenOption.READ); + final Long fileSize = cryptor.decryptedContentLength(channel); + final Pair range = getUnionRange(fileSize); + final Long rangeLength = range.getRight() - range.getLeft() + 1; + outputContext.setContentLength(rangeLength); + outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize)); + if (outputContext.hasStream()) { + cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength); + } + } catch (EOFException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Unexpected end of stream during delivery of partial content (client hung up)."); + } + } catch (IOException e) { + LOG.error("Error reading file " + path.toString(), e); + throw new IORuntimeException(e); + } finally { + IOUtils.closeQuietly(channel); + } + } + } + + private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) { + return String.format("%d-%d/%d", firstByte, lastByte, completeLength); + } + +} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/FileTimeUtils.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/FileTimeUtils.java index a49f28eb6..3608bc3b4 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/FileTimeUtils.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/FileTimeUtils.java @@ -14,13 +14,13 @@ import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.temporal.Temporal; -public final class FileTimeUtils { +final class FileTimeUtils { private FileTimeUtils() { throw new IllegalStateException("not instantiable"); } - public static String toRfc1123String(FileTime time) { + static String toRfc1123String(FileTime time) { final Temporal date = OffsetDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC); return DateTimeFormatter.RFC_1123_DATE_TIME.format(date); } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/HttpHeaderProperty.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/HttpHeaderProperty.java new file mode 100644 index 000000000..1be9c7575 --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/HttpHeaderProperty.java @@ -0,0 +1,20 @@ +package org.cryptomator.webdav.jackrabbit.resources; + +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; + +class HttpHeaderProperty extends AbstractDavProperty { + + private final String value; + + public HttpHeaderProperty(String key, String value) { + super(DavPropertyName.create(key), true); + this.value = value; + } + + @Override + public String getValue() { + return value; + } + +} diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/PathUtils.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/ResourcePathUtils.java similarity index 92% rename from main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/PathUtils.java rename to main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/ResourcePathUtils.java index 4c568ce2b..6450c6984 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/PathUtils.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/ResourcePathUtils.java @@ -14,9 +14,9 @@ import java.nio.file.Path; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceLocator; -public final class PathUtils { +public final class ResourcePathUtils { - private PathUtils() { + private ResourcePathUtils() { throw new IllegalStateException("not instantiable"); } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java index c0ec1a3b4..8f6bef585 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java @@ -32,6 +32,7 @@ import java.util.zip.CRC32; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; @@ -398,8 +399,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo // read content final InputStream in = new SeekableByteChannelInputStream(encryptedFile); - final OutputStream cipheredOut = new CipherOutputStream(plaintextFile, cipher); - return IOUtils.copyLarge(in, cipheredOut); + final InputStream cipheredIn = new CipherInputStream(in, cipher); + return IOUtils.copyLarge(cipheredIn, plaintextFile); } @Override @@ -416,8 +417,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo // seek relevant position and update iv: long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction! - long numberOfRelevantBlocks = 1 + length / AES_BLOCK_LENGTH; - long numberOfRelevantBytes = numberOfRelevantBlocks * AES_BLOCK_LENGTH; long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH; long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock; countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, firstRelevantBlock); @@ -431,9 +430,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo // read content final InputStream in = new SeekableByteChannelInputStream(encryptedFile); - final OutputStream rangedOut = new RangeFilterOutputStream(plaintextFile, offsetInsideFirstRelevantBlock, length); - final OutputStream cipheredOut = new CipherOutputStream(rangedOut, cipher); - return IOUtils.copyLarge(in, cipheredOut, 0, numberOfRelevantBytes); + final InputStream cipheredIn = new CipherInputStream(in, cipher); + return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length); } @Override diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LimitFilterOutputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LimitFilterOutputStream.java deleted file mode 100644 index 6b36349f6..000000000 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LimitFilterOutputStream.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.cryptomator.crypto.aes256; - -import java.io.IOException; -import java.io.OutputStream; - -class LimitFilterOutputStream extends java.io.FilterOutputStream { - - private final long limit; - private long bytesWritten; - - LimitFilterOutputStream(OutputStream out, long limit) { - super(out); - if (limit < 0) { - throw new IllegalArgumentException("Limit must be greater than or equal 0."); - } - this.limit = limit; - } - - @Override - public void write(int b) throws IOException { - this.write(new byte[] {(byte) b}); - } - - @Override - public void write(byte[] b) throws IOException { - this.write(b, 0, b.length); - } - - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - final long adjustedLength = Math.min(bytesRemainingUntilReachingLimit(), len); - - // adjustedLength is <= len, so it must be INT and we can safely cast: - out.write(b, off, (int) adjustedLength); - bytesWritten += adjustedLength; - } - - private long bytesRemainingUntilReachingLimit() { - if (bytesWritten < limit) { - return limit - bytesWritten; - } else { - return 0l; - } - } - -} diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/OffsetFilterOutputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/OffsetFilterOutputStream.java deleted file mode 100644 index 1d40e6b93..000000000 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/OffsetFilterOutputStream.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.cryptomator.crypto.aes256; - -import java.io.IOException; -import java.io.OutputStream; - -class OffsetFilterOutputStream extends java.io.FilterOutputStream { - - private final long offset; - private long bytesWritten; - - OffsetFilterOutputStream(OutputStream out, long offset) { - super(out); - if (offset < 0) { - throw new IllegalArgumentException("Offset must be greater than or equal 0."); - } - this.offset = offset; - } - - @Override - public void write(int b) throws IOException { - this.write(new byte[] {(byte) b}); - } - - @Override - public void write(byte[] b) throws IOException { - this.write(b, 0, b.length); - } - - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - final long adjustedOffset = remainingOffset() + off; - final long adjustedLength = len - remainingOffset(); - - if (adjustedOffset < b.length && adjustedLength <= b.length) { - // b.length is INT, so by definition adjustedOffset and adjustedLength must be INT too and we can safely cast: - out.write(b, (int) adjustedOffset, (int) adjustedLength); - } - bytesWritten += len; - } - - private long remainingOffset() { - if (bytesWritten < offset) { - return offset - bytesWritten; - } else { - return 0l; - } - } - -} diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/RangeFilterOutputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/RangeFilterOutputStream.java deleted file mode 100644 index 1e5fb5a2f..000000000 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/RangeFilterOutputStream.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.cryptomator.crypto.aes256; - -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * Passthrough of all bytes except for certain bytes at the begin and end of the stream, which will get cut off. - */ -class RangeFilterOutputStream extends FilterOutputStream { - - RangeFilterOutputStream(OutputStream out, long offset, long limit) { - super(new OffsetFilterOutputStream(new LimitFilterOutputStream(out, limit), offset)); - } - - @Override - public void write(byte b[], int off, int len) throws IOException { - out.write(b, off, len); - } - -} diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java index a40157bb7..e670cb64b 100644 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java +++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Random; @@ -95,9 +96,9 @@ public class Aes256CryptorTest { @Test public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { // our test plaintext data: - final byte[] plaintextData = new byte[500 * Integer.BYTES]; + final byte[] plaintextData = new byte[65536 * Integer.BYTES]; final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData); - for (int i = 0; i < 500; i++) { + for (int i = 0; i < 65536; i++) { bbIn.putInt(i); } final InputStream plaintextIn = new ByteArrayInputStream(plaintextData); @@ -115,18 +116,14 @@ public class Aes256CryptorTest { // decrypt: final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData); final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); - final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 313 * Integer.BYTES, 50 * Integer.BYTES); + final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES); IOUtils.closeQuietly(encryptedIn); IOUtils.closeQuietly(plaintextOut); Assert.assertTrue(numDecryptedBytes > 0); // check decrypted data: final byte[] result = plaintextOut.toByteArray(); - final byte[] expected = new byte[50 * Integer.BYTES]; - final ByteBuffer bbOut = ByteBuffer.wrap(expected); - for (int i = 313; i < 363; i++) { - bbOut.putInt(i); - } + final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES); Assert.assertArrayEquals(expected, result); } diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/LimitFilterOutputStreamTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/LimitFilterOutputStreamTest.java deleted file mode 100644 index 03274e665..000000000 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/LimitFilterOutputStreamTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.cryptomator.crypto.aes256; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; - -import org.apache.commons.io.IOUtils; -import org.junit.Assert; -import org.junit.Test; - -public class LimitFilterOutputStreamTest { - - @Test - public void testNoLimit() throws IOException { - final byte[] testData = createTestData(256); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new LimitFilterOutputStream(out, Long.MAX_VALUE); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 0, 256); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - @Test - public void testLimit43() throws IOException { - final byte[] testData = createTestData(256); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new LimitFilterOutputStream(out, 43l); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 0, 43); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - @Test - public void testLimit307() throws IOException { - final byte[] testData = createTestData(512); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new LimitFilterOutputStream(out, 307l); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 0, 307); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - private byte[] createTestData(int length) { - final byte[] testData = new byte[length]; - for (int i = 0; i < length; i++) { - testData[i] = (byte) i; - } - return testData; - } - -} diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/OffsetFilterOutputStreamTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/OffsetFilterOutputStreamTest.java deleted file mode 100644 index 0b472b85e..000000000 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/OffsetFilterOutputStreamTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.cryptomator.crypto.aes256; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; - -import org.apache.commons.io.IOUtils; -import org.junit.Assert; -import org.junit.Test; - -public class OffsetFilterOutputStreamTest { - - @Test - public void testNoOffset() throws IOException { - final byte[] testData = createTestData(256); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new OffsetFilterOutputStream(out, 0l); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 0, 256); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - @Test - public void testOffset43() throws IOException { - final byte[] testData = createTestData(256); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new OffsetFilterOutputStream(out, 43l); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 43, 256); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - @Test - public void testOffset307() throws IOException { - final byte[] testData = createTestData(512); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new OffsetFilterOutputStream(out, 307l); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 307, 512); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - private byte[] createTestData(int length) { - final byte[] testData = new byte[length]; - for (int i = 0; i < length; i++) { - testData[i] = (byte) i; - } - return testData; - } - -} diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/RangeFilterOutputStreamTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/RangeFilterOutputStreamTest.java deleted file mode 100644 index 2722a1b5c..000000000 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/RangeFilterOutputStreamTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.cryptomator.crypto.aes256; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; - -import org.apache.commons.io.IOUtils; -import org.junit.Assert; -import org.junit.Test; - -public class RangeFilterOutputStreamTest { - - @Test - public void testNoOffsetUnlimited() throws IOException { - final byte[] testData = createTestData(256); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new RangeFilterOutputStream(out, 0l, Long.MAX_VALUE); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 0, 256); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - @Test - public void testNoOffsetButLimit() throws IOException { - final byte[] testData = createTestData(256); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new RangeFilterOutputStream(out, 0l, 97l); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 0, 97); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - @Test - public void testNoLimitButOffset() throws IOException { - final byte[] testData = createTestData(256); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new RangeFilterOutputStream(out, 43l, Long.MAX_VALUE); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 43, 256); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - @Test - public void testOffsettedAndLimited() throws IOException { - final byte[] testData = createTestData(256); - final InputStream in = new ByteArrayInputStream(testData); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - final OutputStream decorator = new RangeFilterOutputStream(out, 43l, 57l); - IOUtils.copy(in, decorator); - - final byte[] expected = Arrays.copyOfRange(testData, 43, 100); - Assert.assertArrayEquals(expected, out.toByteArray()); - } - - private byte[] createTestData(int length) { - final byte[] testData = new byte[length]; - for (int i = 0; i < length; i++) { - testData[i] = (byte) i; - } - return testData; - } - -} diff --git a/main/pom.xml b/main/pom.xml index e2304fee8..54a3a6934 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -1,9 +1,6 @@ - - + + 4.0.0 org.cryptomator main @@ -29,6 +26,7 @@ 2.1 + 1.7.7 4.11 2.4 4.0 @@ -61,6 +59,11 @@ + + org.slf4j + slf4j-api + ${slf4j.version} + org.apache.logging.log4j log4j-core @@ -141,7 +144,7 @@ core ui - + diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java index 2e92e2ca1..3e9ebc058 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java @@ -20,7 +20,7 @@ import org.cryptomator.ui.util.MasterKeyFilter; import org.cryptomator.ui.util.mount.CommandFailedException; import org.cryptomator.ui.util.mount.WebDavMount; import org.cryptomator.ui.util.mount.WebDavMounter; -import org.cryptomator.webdav.WebDAVServer; +import org.cryptomator.webdav.WebDavServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,7 +34,7 @@ public class Directory implements Serializable { private static final long serialVersionUID = 3754487289683599469L; private static final Logger LOG = LoggerFactory.getLogger(Directory.class); - private final WebDAVServer server = new WebDAVServer(); + private final WebDavServer server = new WebDavServer(); private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor()); private final ObjectProperty unlocked = new SimpleObjectProperty(this, "unlocked", Boolean.FALSE); private final Path path; @@ -137,7 +137,7 @@ public class Directory implements Serializable { this.unlocked.set(unlocked); } - public WebDAVServer getServer() { + public WebDavServer getServer() { return server; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java index 3bb9f4850..8f114cf6c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java @@ -33,6 +33,7 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy { private void displayMountInstructions() { // TODO display message to user pointing to cryptomator.org/mounting#mount which describes what to do + // Machine-readable mount instructions: http://tools.ietf.org/html/rfc4709#page-5 :-) } private void displayUnmountInstructions() {