Explorar o código

WebDAV range request refinements

Sebastian Stenzel %!s(int64=9) %!d(string=hai) anos
pai
achega
e8e80f306b

+ 1 - 3
main/core/pom.xml

@@ -18,10 +18,8 @@
 	<name>Cryptomator WebDAV and I/O module</name>
 
 	<properties>
-		<jetty.version>9.3.0.v20150612</jetty.version>
+		<jetty.version>9.3.1.v20150714</jetty.version>
 		<jackrabbit.version>2.10.1</jackrabbit.version>
-		<commons.transaction.version>1.2</commons.transaction.version>
-		<jta.version>1.1</jta.version>
 	</properties>
 
 	<dependencies>

+ 27 - 1
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java

@@ -5,6 +5,8 @@ import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.format.DateTimeParseException;
 
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -53,14 +55,18 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
 		final Path filePath = getEncryptedFilePath(locator.getResourcePath());
 		final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
 		final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
+		final String ifRangeHeader = request.getHeader(HttpHeader.IF_RANGE.asString());
 		if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
 			// DIRECTORY
 			return createDirectory(locator, request.getDavSession(), dirFilePath);
-		} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader)) {
+		} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
 			// FILE RANGE
 			final Pair<String, String> requestRange = getRequestRange(rangeHeader);
 			response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT);
 			return createFilePart(locator, request.getDavSession(), requestRange, filePath);
+		} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && !isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
+			// FULL FILE (if-range not fulfilled)
+			return createFile(locator, request.getDavSession(), filePath);
 		} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && !isRangeSatisfiable(rangeHeader)) {
 			// FULL FILE (unsatisfiable range)
 			response.setStatus(DavServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
@@ -102,6 +108,26 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
 		return createFile(locator, session, existingFile);
 	}
 
+	/**
+	 * @return <code>true</code> if a partial response should be generated according to an If-Range precondition.
+	 */
+	private boolean isIfRangePreconditionFulfilled(String ifRangeHeader, Path filePath) throws DavException {
+		if (ifRangeHeader == null) {
+			// no header set -> fulfilled implicitly
+			return true;
+		} else {
+			try {
+				final FileTime expectedTime = FileTimeUtils.fromRfc1123String(ifRangeHeader);
+				final FileTime actualTime = Files.getLastModifiedTime(filePath);
+				return expectedTime.compareTo(actualTime) == 0;
+			} catch (DateTimeParseException e) {
+				throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unsupported If-Range header: " + ifRangeHeader);
+			} catch (IOException e) {
+				throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
+			}
+		}
+	}
+
 	/**
 	 * @return <code>true</code> if and only if exactly one byte range has been requested.
 	 */

+ 6 - 1
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java

@@ -30,8 +30,8 @@ import org.slf4j.LoggerFactory;
 
 public class WebDavServlet extends AbstractWebdavServlet {
 
-	private static final Logger LOG = LoggerFactory.getLogger(WebDavServlet.class);
 	private static final long serialVersionUID = 7965170007048673022L;
+	private static final Logger LOG = LoggerFactory.getLogger(WebDavServlet.class);
 	public static final String CFG_FS_ROOT = "cfg.fs.root";
 	private DavSessionProvider davSessionProvider;
 	private DavLocatorFactory davLocatorFactory;
@@ -91,6 +91,7 @@ public class WebDavServlet extends AbstractWebdavServlet {
 
 	@Override
 	protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
+		long t0 = System.nanoTime();
 		try {
 			super.doGet(request, response, resource);
 		} catch (MacAuthenticationFailedException e) {
@@ -98,6 +99,10 @@ public class WebDavServlet extends AbstractWebdavServlet {
 			cryptoWarningHandler.macAuthFailed(resource.getLocator().getResourcePath());
 			response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
 		}
+		if (LOG.isDebugEnabled()) {
+			long t1 = System.nanoTime();
+			LOG.debug("REQUEST TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
+		}
 	}
 
 }

+ 101 - 9
main/core/src/test/java/org/cryptomator/webdav/jackrabbit/RangeRequestTest.java

@@ -8,9 +8,12 @@ import java.net.URL;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.Random;
+import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.ForkJoinTask;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.commons.httpclient.HttpClient;
 import org.apache.commons.httpclient.HttpMethod;
@@ -28,11 +31,14 @@ import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.io.Files;
 
 public class RangeRequestTest {
 
+	private static final Logger LOG = LoggerFactory.getLogger(RangeRequestTest.class);
 	private static final Aes256Cryptor CRYPTOR = new Aes256Cryptor();
 	private static final WebDavServer SERVER = new WebDavServer();
 	private static final File TMP_VAULT = Files.createTempDir();
@@ -57,7 +63,7 @@ public class RangeRequestTest {
 	}
 
 	@Test
-	public void testAsyncRangeRequests() throws IOException, URISyntaxException {
+	public void testAsyncRangeRequests() throws IOException, URISyntaxException, InterruptedException {
 		final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "asyncRangeRequestTestFile.txt");
 
 		final MultiThreadedHttpConnectionManager cm = new MultiThreadedHttpConnectionManager();
@@ -79,11 +85,87 @@ public class RangeRequestTest {
 		Assert.assertEquals(201, putResponse);
 
 		// multiple async range requests:
-		final Collection<ForkJoinTask<?>> tasks = new ArrayList<>();
+		final List<ForkJoinTask<?>> tasks = new ArrayList<>();
 		final Random generator = new Random(System.currentTimeMillis());
+
+		final AtomicBoolean success = new AtomicBoolean(true);
+
+		// 10 full interrupted requests:
+		for (int i = 0; i < 10; i++) {
+			final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
+				try {
+					final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
+					final int statusCode = client.executeMethod(getMethod);
+					if (statusCode != 200) {
+						LOG.error("Invalid status code for interrupted full request");
+						success.set(false);
+					}
+					getMethod.getResponseBodyAsStream().read();
+					getMethod.getResponseBodyAsStream().close();
+					getMethod.releaseConnection();
+				} catch (IOException e) {
+					throw new RuntimeException(e);
+				}
+			});
+			tasks.add(task);
+		}
+
+		// 50 crappy interrupted range requests:
+		for (int i = 0; i < 50; i++) {
+			final int lower = generator.nextInt(plaintextData.length);
+			final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
+				try {
+					final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
+					getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
+					final int statusCode = client.executeMethod(getMethod);
+					if (statusCode != 206) {
+						LOG.error("Invalid status code for interrupted range request");
+						success.set(false);
+					}
+					getMethod.getResponseBodyAsStream().read();
+					getMethod.getResponseBodyAsStream().close();
+					getMethod.releaseConnection();
+				} catch (IOException e) {
+					throw new RuntimeException(e);
+				}
+			});
+			tasks.add(task);
+		}
+
+		// 50 normal open range requests:
+		for (int i = 0; i < 50; i++) {
+			final int lower = generator.nextInt(plaintextData.length - 512);
+			final int upper = plaintextData.length - 1;
+			final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
+				try {
+					final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
+					getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
+					final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
+					final int statusCode = client.executeMethod(getMethod);
+					final byte[] responseBody = new byte[upper - lower + 10];
+					final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
+					getMethod.releaseConnection();
+					if (statusCode != 206) {
+						LOG.error("Invalid status code for open range request");
+						success.set(false);
+					} else if (upper - lower + 1 != bytesRead) {
+						LOG.error("Invalid response length for open range request");
+						success.set(false);
+					} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
+						LOG.error("Invalid response body for open range request");
+						success.set(false);
+					}
+				} catch (IOException e) {
+					throw new RuntimeException(e);
+				}
+			});
+			tasks.add(task);
+		}
+
+		// 200 normal closed range requests:
 		for (int i = 0; i < 200; i++) {
-			final int pos1 = generator.nextInt(plaintextData.length);
-			final int pos2 = generator.nextInt(plaintextData.length);
+			final int pos1 = generator.nextInt(plaintextData.length - 512);
+			final int pos2 = pos1 + 512;
 			final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
 				try {
 					final int lower = Math.min(pos1, pos2);
@@ -94,20 +176,30 @@ public class RangeRequestTest {
 					final byte[] responseBody = new byte[upper - lower + 1];
 					IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
 					getMethod.releaseConnection();
-					Assert.assertEquals(206, statusCode);
-					Assert.assertArrayEquals(Arrays.copyOfRange(plaintextData, lower, upper + 1), responseBody);
+					if (statusCode != 206 || !Arrays.equals(Arrays.copyOfRange(plaintextData, lower, upper + 1), responseBody)) {
+						LOG.error("Invalid content for closed range request");
+						success.set(false);
+					}
 				} catch (IOException e) {
 					throw new RuntimeException(e);
 				}
-			}).fork();
+			});
 			tasks.add(task);
 		}
 
+		Collections.shuffle(tasks, generator);
+
+		final ForkJoinPool pool = new ForkJoinPool(4);
+		for (ForkJoinTask<?> task : tasks) {
+			pool.execute(task);
+		}
 		for (ForkJoinTask<?> task : tasks) {
 			task.join();
 		}
-
+		pool.shutdown();
 		cm.shutdown();
+
+		Assert.assertTrue(success.get());
 	}
 
 	@Test

+ 6 - 4
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java

@@ -379,10 +379,6 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
 		final byte[] nonce = new byte[8];
 		headerBuf.position(16);
 		headerBuf.get(nonce);
-		final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
-		nonceAndCounterBuf.put(nonce);
-		nonceAndCounterBuf.putLong(0L);
-		final byte[] nonceAndCounter = nonceAndCounterBuf.array();
 
 		// read sensitive header data:
 		final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
@@ -412,6 +408,12 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
 		final Long fileSize = sensitiveHeaderContentBuf.getLong();
 		sensitiveHeaderContentBuf.get(fileKeyBytes);
 
+		// append counter to nonce:
+		final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
+		nonceAndCounterBuf.put(nonce);
+		nonceAndCounterBuf.putLong(0L);
+		final byte[] nonceAndCounter = nonceAndCounterBuf.array();
+
 		// content decryption:
 		encryptedFile.position(104l);
 		final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);

+ 1 - 1
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java

@@ -84,7 +84,7 @@ interface AesCryptographicConfiguration {
 	/**
 	 * Number of bytes, a content block over which a MAC is calculated consists of.
 	 */
-	int CONTENT_MAC_BLOCK = 128 * 1024;
+	int CONTENT_MAC_BLOCK = 32 * 1024;
 
 	/**
 	 * How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.

+ 4 - 4
main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java

@@ -46,14 +46,14 @@ public class SamplingCryptorDecorator extends AbstractCryptorDecorator implement
 
 	@Override
 	public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
-		final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
-		return cryptor.decryptFile(encryptedFile, countingInputStream, authenticate);
+		final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
+		return cryptor.decryptFile(encryptedFile, countingOutputStream, authenticate);
 	}
 
 	@Override
 	public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
-		final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
-		return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length, authenticate);
+		final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
+		return cryptor.decryptRange(encryptedFile, countingOutputStream, pos, length, authenticate);
 	}
 
 	@Override