mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-10-07 01:03:31 +00:00
- Support for HTTP Range header fields, thus vastly improved performance for video streaming
- Simplified cryptor implementation for partial decryption
This commit is contained in:
parent
f76091ddc0
commit
1d05e878ab
@ -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
|
||||
|
@ -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<Runnable> 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);
|
@ -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);
|
||||
|
||||
}
|
@ -8,7 +8,7 @@ import org.apache.commons.collections4.map.LRUMap;
|
||||
|
||||
final class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
|
||||
|
||||
public BidiLRUMap(int maxSize) {
|
||||
BidiLRUMap(int maxSize) {
|
||||
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
|
||||
}
|
||||
|
||||
|
@ -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<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
|
||||
|
||||
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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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()) {
|
||||
|
@ -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<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
|
||||
final List<DavResource> 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<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
if (Files.exists(path)) {
|
||||
|
@ -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<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(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);
|
||||
|
@ -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<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
|
||||
|
||||
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<Long, Long>(lower, upper));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return One range, that spans all requested ranges.
|
||||
*/
|
||||
private Pair<Long, Long> getUnionRange(Long fileSize) {
|
||||
final long lastByte = fileSize - 1;
|
||||
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
|
||||
for (Pair<Long, Long> 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<Long, Long> 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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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<String> {
|
||||
|
||||
private final String value;
|
||||
|
||||
public HttpHeaderProperty(String key, String value) {
|
||||
super(DavPropertyName.create(key), true);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
15
main/pom.xml
15
main/pom.xml
@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the
|
||||
terms of the MIT license. See the LICENSE.txt file for more info. Contributors:
|
||||
Sebastian Stenzel - initial API and implementation -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
@ -29,6 +26,7 @@
|
||||
|
||||
<!-- dependency versions -->
|
||||
<log4j.version>2.1</log4j.version>
|
||||
<slf4j.version>1.7.7</slf4j.version>
|
||||
<junit.version>4.11</junit.version>
|
||||
<commons-io.version>2.4</commons-io.version>
|
||||
<commons-collections.version>4.0</commons-collections.version>
|
||||
@ -61,6 +59,11 @@
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
@ -141,7 +144,7 @@
|
||||
<module>core</module>
|
||||
<module>ui</module>
|
||||
</modules>
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
@ -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<Boolean> unlocked = new SimpleObjectProperty<Boolean>(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;
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user