From f7db3ee4fb4894e8ef5e7a192ab5dd65786eedc8 Mon Sep 17 00:00:00 2001 From: Andrew Gaul Date: Tue, 29 Jul 2014 22:56:52 -0700 Subject: [PATCH] Implement AWS Authorization Fixes #4. --- README.md | 9 +- src/main/java/org/gaul/s3proxy/S3Proxy.java | 39 +++++++- .../java/org/gaul/s3proxy/S3ProxyHandler.java | 94 ++++++++++++++++++- src/main/resources/s3proxy.conf | 7 +- .../java/org/gaul/s3proxy/S3ProxyTest.java | 2 +- 5 files changed, 141 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 76254372..d853038e 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,15 @@ Users can configure S3Proxy via a properties file. An example: jclouds.provider=transient jclouds.identity=identity jclouds.credential=credential -#jclouds.endpoint=http://127.0.0.1:8081 # optional for some providers +# endpoint is optional for some providers +#jclouds.endpoint=http://127.0.0.1:8081 jclouds.filesystem.basedir=/tmp/blobstore + s3proxy.endpoint=http://127.0.0.1:8080 +# authorization must be aws-v2 or none +s3proxy.authorization=aws-v2 +s3proxy.identity=identity +s3proxy.credential=credential ``` Users can also set a variety of Java and @@ -66,7 +72,6 @@ S3Proxy does not support: * single-part uploads larger than 2 GB ([upstream issue](https://github.com/jclouds/jclouds/pull/426)) * multi-part uploads -* authorization of clients * bucket ACLs * server-side copy * URL signing diff --git a/src/main/java/org/gaul/s3proxy/S3Proxy.java b/src/main/java/org/gaul/s3proxy/S3Proxy.java index cfc85eb7..7c830d2b 100644 --- a/src/main/java/org/gaul/s3proxy/S3Proxy.java +++ b/src/main/java/org/gaul/s3proxy/S3Proxy.java @@ -40,10 +40,16 @@ */ public final class S3Proxy { private static final String PROPERTY_S3PROXY_ENDPOINT = "s3proxy.endpoint"; + private static final String PROPERTY_S3PROXY_AUTHORIZATION = + "s3proxy.authorization"; + private static final String PROPERTY_S3PROXY_IDENTITY = "s3proxy.identity"; + private static final String PROPERTY_S3PROXY_CREDENTIAL = + "s3proxy.credential"; private final Server server; - public S3Proxy(BlobStore blobStore, URI endpoint) { + public S3Proxy(BlobStore blobStore, URI endpoint, String identity, + String credential) { Preconditions.checkNotNull(blobStore); Preconditions.checkNotNull(endpoint); // TODO: allow service paths? @@ -58,7 +64,7 @@ public S3Proxy(BlobStore blobStore, URI endpoint) { connector.setHost(endpoint.getHost()); connector.setPort(endpoint.getPort()); server.addConnector(connector); - server.setHandler(new S3ProxyHandler(blobStore)); + server.setHandler(new S3ProxyHandler(blobStore, identity, credential)); } public void start() throws Exception { @@ -86,13 +92,35 @@ public static void main(String[] args) throws Exception { String endpoint = properties.getProperty(Constants.PROPERTY_ENDPOINT); String s3ProxyEndpointString = properties.getProperty( PROPERTY_S3PROXY_ENDPOINT); + String s3ProxyAuthorization = properties.getProperty( + PROPERTY_S3PROXY_AUTHORIZATION); if (provider == null || identity == null || credential == null - || s3ProxyEndpointString == null) { + || s3ProxyEndpointString == null + || s3ProxyAuthorization == null) { System.err.println("Properties file must contain:\n" + Constants.PROPERTY_PROVIDER + "\n" + Constants.PROPERTY_IDENTITY + "\n" + Constants.PROPERTY_CREDENTIAL + "\n" + - PROPERTY_S3PROXY_ENDPOINT); + PROPERTY_S3PROXY_ENDPOINT + "\n" + + PROPERTY_S3PROXY_AUTHORIZATION); + System.exit(1); + } + + String localIdentity = null; + String localCredential = null; + if (s3ProxyAuthorization.equalsIgnoreCase("aws-v2")) { + localIdentity = properties.getProperty(PROPERTY_S3PROXY_IDENTITY); + localCredential = properties.getProperty( + PROPERTY_S3PROXY_CREDENTIAL); + if (localIdentity == null || localCredential == null) { + System.err.println("Both " + PROPERTY_S3PROXY_IDENTITY + + " and " + PROPERTY_S3PROXY_CREDENTIAL + + " must be set"); + System.exit(1); + } + } else if (!s3ProxyAuthorization.equalsIgnoreCase("none")) { + System.err.println(PROPERTY_S3PROXY_AUTHORIZATION + + " must be aws-v2 or none, was: " + s3ProxyAuthorization); System.exit(1); } @@ -105,7 +133,8 @@ public static void main(String[] args) throws Exception { } BlobStoreContext context = builder.build(BlobStoreContext.class); URI s3ProxyEndpoint = new URI(s3ProxyEndpointString); - S3Proxy s3Proxy = new S3Proxy(context.getBlobStore(), s3ProxyEndpoint); + S3Proxy s3Proxy = new S3Proxy(context.getBlobStore(), s3ProxyEndpoint, + localIdentity, localCredential); s3Proxy.start(); } } diff --git a/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java b/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java index 8fc20cfd..7e98c372 100644 --- a/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java +++ b/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java @@ -20,19 +20,28 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashSet; import java.util.Map; import java.util.Set; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.SortedSetMultimap; +import com.google.common.collect.TreeMultimap; import com.google.common.hash.HashCode; import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; @@ -69,8 +78,13 @@ final class S3ProxyHandler extends AbstractHandler { private static final String FAKE_REQUEST_ID = "4442587FB7D0A2F9"; private final BlobStore blobStore; - S3ProxyHandler(BlobStore blobStore) { + private final String identity; + private final String credential; + + S3ProxyHandler(BlobStore blobStore, String identity, String credential) { this.blobStore = Preconditions.checkNotNull(blobStore); + this.identity = identity; + this.credential = credential; } @Override @@ -83,6 +97,19 @@ public void handle(String target, Request baseRequest, String[] path = uri.split("/", 3); logger.debug("request: {}", request); + if (identity != null) { + String expectedAuthorization = createAuthorizationHeader(request, + identity, credential); + if (!expectedAuthorization.equals(request.getHeader( + HttpHeaders.AUTHORIZATION))) { + sendSimpleErrorResponse(response, + HttpServletResponse.SC_FORBIDDEN, + "SignatureDoesNotMatch", "Forbidden"); + baseRequest.setHandled(true); + return; + } + } + switch (method) { case "DELETE": if (uri.lastIndexOf("/") == 0) { @@ -618,6 +645,7 @@ private static void sendSimpleErrorResponse(HttpServletResponse response, private static void sendSimpleErrorResponse(HttpServletResponse response, int status, String code, String message, Optional extra) { + logger.debug("{} {} {} {}", status, code, message, extra); try (Writer writer = response.getWriter()) { response.setStatus(status); writer.write("\r\n" + @@ -640,4 +668,68 @@ private static void sendSimpleErrorResponse(HttpServletResponse response, ioe.getMessage()); } } + + /** + * Create Amazon V2 authorization header. Reference: + * http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html + */ + private static String createAuthorizationHeader(HttpServletRequest request, + String identity, String credential) { + // sort Amazon headers + SortedSetMultimap canonicalizedHeaders = + TreeMultimap.create(); + for (String headerName : Collections.list(request.getHeaderNames())) { + headerName = headerName.toLowerCase(); + if (!headerName.startsWith("x-amz-")) { + continue; + } + for (String headerValue : Collections.list(request.getHeaders( + headerName))) { + canonicalizedHeaders.put(headerName, Strings.nullToEmpty( + headerValue)); + } + } + + // build string to sign + StringBuilder builder = new StringBuilder() + .append(request.getMethod()).append('\n'); + String contentMD5 = request.getHeader(HttpHeaders.CONTENT_MD5); + if (contentMD5 != null) { + builder.append(contentMD5); + } + builder.append('\n'); + String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE); + if (contentType != null) { + builder.append(contentType); + } + builder.append('\n'); + if (!canonicalizedHeaders.containsKey("x-amz-date")) { + builder.append(request.getHeader(HttpHeaders.DATE)); + } + builder.append('\n'); + for (Map.Entry entry : canonicalizedHeaders.entries()) { + builder.append(entry.getKey()).append(':') + .append(entry.getValue()).append('\n'); + } + builder.append(request.getRequestURI()); + if ("".equals(request.getParameter("acl"))) { + builder.append("?acl"); + } + String stringToSign = builder.toString(); + logger.debug("stringToSign: {}", stringToSign); + + // sign string + 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 signature = BaseEncoding.base64().encode(mac.doFinal( + stringToSign.getBytes(StandardCharsets.UTF_8))); + + return "AWS" + " " + identity + ":" + signature; + } } diff --git a/src/main/resources/s3proxy.conf b/src/main/resources/s3proxy.conf index d0101209..2dcca0a8 100644 --- a/src/main/resources/s3proxy.conf +++ b/src/main/resources/s3proxy.conf @@ -1,7 +1,12 @@ jclouds.provider=transient jclouds.identity=identity jclouds.credential=credential +# endpoint is optional for some providers #jclouds.endpoint=http://127.0.0.1:8081 jclouds.filesystem.basedir=/tmp/blobstore + s3proxy.endpoint=http://127.0.0.1:8080 -#s3proxy.loglevel=INFO # TODO: not yet supported +# authorization must be aws-v2 or none +s3proxy.authorization=aws-v2 +s3proxy.identity=identity +s3proxy.credential=credential diff --git a/src/test/java/org/gaul/s3proxy/S3ProxyTest.java b/src/test/java/org/gaul/s3proxy/S3ProxyTest.java index dc58a3f7..1a6589e3 100644 --- a/src/test/java/org/gaul/s3proxy/S3ProxyTest.java +++ b/src/test/java/org/gaul/s3proxy/S3ProxyTest.java @@ -65,7 +65,7 @@ public void setUp() throws Exception { .endpoint(s3Endpoint.toString()) .build(BlobStoreContext.class); s3BlobStore = s3Context.getBlobStore(); - s3Proxy = new S3Proxy(blobStore, s3Endpoint); + s3Proxy = new S3Proxy(blobStore, s3Endpoint, "identity", "credential"); s3Proxy.start(); }