diff --git a/pom.xml b/pom.xml
index e6fc4d6..63dccab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -281,6 +281,11 @@
4.12
test
+
+ commons-fileupload
+ commons-fileupload
+ 1.3.1
+
org.apache.jclouds
jclouds-allblobstore
diff --git a/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java b/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java
index a85244a..1b37016 100644
--- a/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java
+++ b/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java
@@ -18,6 +18,7 @@ package org.gaul.s3proxy;
import static java.util.Objects.requireNonNull;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -74,6 +75,7 @@ import com.google.common.io.ByteStreams;
import com.google.common.net.HostAndPort;
import com.google.common.net.HttpHeaders;
+import org.apache.commons.fileupload.MultipartStream;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.jclouds.blobstore.BlobRequestSigner;
@@ -287,7 +289,8 @@ final class S3ProxyHandler extends AbstractHandler {
}
// anonymous request
- if ((method.equals("GET") || method.equals("HEAD")) &&
+ if ((method.equals("GET") || method.equals("HEAD") ||
+ method.equals("POST")) &&
request.getHeader(HttpHeaders.AUTHORIZATION) == null &&
request.getParameter("AWSAccessKeyId") == null &&
defaultBlobStore != null) {
@@ -592,6 +595,12 @@ final class S3ProxyHandler extends AbstractHandler {
return;
}
break;
+ case "POST":
+ if (path.length <= 2 || path[2].isEmpty()) {
+ handlePostBlob(request, response, blobStore, path[1]);
+ return;
+ }
+ break;
default:
break;
}
@@ -1345,6 +1354,110 @@ final class S3ProxyHandler extends AbstractHandler {
}
}
+ private void handlePostBlob(HttpServletRequest request,
+ HttpServletResponse response, BlobStore blobStore,
+ String containerName)
+ throws IOException, S3Exception {
+ String boundaryHeader = request.getHeader(HttpHeaders.CONTENT_TYPE);
+ if (boundaryHeader == null ||
+ !boundaryHeader.startsWith("multipart/form-data; boundary=")) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ String boundary =
+ boundaryHeader.substring(boundaryHeader.indexOf('=') + 1);
+
+ String blobName = null;
+ String contentType = null;
+ String identity = null;
+ // TODO: handle policy
+ byte[] policy = null;
+ String signature = null;
+ byte[] payload = null;
+ try (InputStream is = request.getInputStream()) {
+ MultipartStream multipartStream = new MultipartStream(is,
+ boundary.getBytes(StandardCharsets.UTF_8), 4096, null);
+ boolean nextPart = multipartStream.skipPreamble();
+ while (nextPart) {
+ String header = multipartStream.readHeaders();
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ multipartStream.readBodyData(baos);
+ if (startsWithIgnoreCase(header,
+ "Content-Disposition: form-data;" +
+ " name=\"acl\"")) {
+ // TODO: acl
+ } else if (startsWithIgnoreCase(header,
+ "Content-Disposition: form-data;" +
+ " name=\"AWSAccessKeyId\"")) {
+ identity = new String(baos.toByteArray());
+ } else if (startsWithIgnoreCase(header,
+ "Content-Disposition: form-data;" +
+ " name=\"Content-Type\"")) {
+ contentType = new String(baos.toByteArray());
+ } else if (startsWithIgnoreCase(header,
+ "Content-Disposition: form-data;" +
+ " name=\"file\"")) {
+ // TODO: buffers entire payload
+ payload = baos.toByteArray();
+ } else if (startsWithIgnoreCase(header,
+ "Content-Disposition: form-data;" +
+ " name=\"key\"")) {
+ blobName = new String(baos.toByteArray());
+ } else if (startsWithIgnoreCase(header,
+ "Content-Disposition: form-data;" +
+ " name=\"policy\"")) {
+ policy = baos.toByteArray();
+ } else if (startsWithIgnoreCase(header,
+ "Content-Disposition: form-data;" +
+ " name=\"signature\"")) {
+ signature = new String(baos.toByteArray());
+ }
+ }
+ nextPart = multipartStream.readBoundary();
+ }
+ }
+
+ if (identity == null || signature == null || blobName == null ||
+ policy == null) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ Map.Entry provider =
+ blobStoreLocator.locateBlobStore(identity, null, null);
+ if (provider == null) {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ String credential = provider.getKey();
+
+ Mac mac;
+ try {
+ mac = Mac.getInstance("HmacSHA1");
+ mac.init(new SecretKeySpec(credential.getBytes(
+ StandardCharsets.UTF_8), "HmacSHA1"));
+ } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+ throw Throwables.propagate(e);
+ }
+ String expectedSignature = BaseEncoding.base64().encode(
+ mac.doFinal(policy));
+ if (!signature.equals(expectedSignature)) {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ BlobBuilder.PayloadBlobBuilder builder = blobStore
+ .blobBuilder(blobName)
+ .payload(payload);
+ if (contentType != null) {
+ builder.contentType(contentType);
+ }
+ Blob blob = builder.build();
+ blobStore.putBlob(containerName, blob);
+
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
+
private void handleInitiateMultipartUpload(HttpServletRequest request,
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName) throws IOException {
@@ -2120,4 +2233,8 @@ final class S3ProxyHandler extends AbstractHandler {
}
return eTag;
}
+
+ private static boolean startsWithIgnoreCase(String string, String prefix) {
+ return string.toLowerCase().startsWith(prefix.toLowerCase());
+ }
}