Skip to content

Commit

Permalink
Implement AWS Authorization
Browse files Browse the repository at this point in the history
Fixes #4.
  • Loading branch information
gaul committed Jul 30, 2014
1 parent 3f52992 commit f7db3ee
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 10 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
39 changes: 34 additions & 5 deletions src/main/java/org/gaul/s3proxy/S3Proxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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();
}
}
94 changes: 93 additions & 1 deletion src/main/java/org/gaul/s3proxy/S3ProxyHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -618,6 +645,7 @@ private static void sendSimpleErrorResponse(HttpServletResponse response,

private static void sendSimpleErrorResponse(HttpServletResponse response,
int status, String code, String message, Optional<String> extra) {
logger.debug("{} {} {} {}", status, code, message, extra);
try (Writer writer = response.getWriter()) {
response.setStatus(status);
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
Expand All @@ -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<String, String> 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<String, String> 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;
}
}
7 changes: 6 additions & 1 deletion src/main/resources/s3proxy.conf
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/test/java/org/gaul/s3proxy/S3ProxyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down

0 comments on commit f7db3ee

Please sign in to comment.