From 6b0f7e289ae513fd291f16bd0fc61346ac362814 Mon Sep 17 00:00:00 2001 From: "adrian.f.cole" Date: Thu, 24 Sep 2009 22:48:30 +0000 Subject: [PATCH] light work on ec2 git-svn-id: http://jclouds.googlecode.com/svn/trunk@1916 3d8758e0-26b5-11de-8745-db77d3ebf521 --- .../ec2/commands/options/EC2QuerySigner.java | 176 ++++++++++++++++++ .../commands/options/EC2QuerySignerTest.java | 103 ++++++++++ 2 files changed, 279 insertions(+) create mode 100755 aws/ec2/core/src/main/java/org/jclouds/aws/ec2/commands/options/EC2QuerySigner.java create mode 100755 aws/ec2/core/src/test/java/org/jclouds/aws/ec2/commands/options/EC2QuerySignerTest.java diff --git a/aws/ec2/core/src/main/java/org/jclouds/aws/ec2/commands/options/EC2QuerySigner.java b/aws/ec2/core/src/main/java/org/jclouds/aws/ec2/commands/options/EC2QuerySigner.java new file mode 100755 index 0000000000..5f47d5e303 --- /dev/null +++ b/aws/ec2/core/src/main/java/org/jclouds/aws/ec2/commands/options/EC2QuerySigner.java @@ -0,0 +1,176 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.aws.ec2.commands.options; + +import static com.google.common.base.Preconditions.checkState; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.ACTION; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.AWS_ACCESS_KEY_ID; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.EXPIRES; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.SIGNATURE; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.SIGNATURE_METHOD; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.SIGNATURE_VERSION; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.TIMESTAMP; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.VERSION; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriBuilder; + +import org.jclouds.aws.ec2.reference.CommonEC2Parameters; +import org.jclouds.aws.reference.AWSConstants; +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpRequestFilter; +import org.jclouds.http.HttpUtils; +import org.jclouds.util.DateService; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Contains the base options needed for all EC2 QUERY API operations.

+ * Extend this class in the following way to avoid massive boilerplate code: Usage: + *

+ * + *

+ * public static class MyRequestOptions extends BaseEC2RequestOptions<MyRequestOptions> {
+ *    static {
+ *       realClass = MyRequestOptions.class;
+ *    }
+ * 
+ *    @Override
+ *    public String getAction() {
+ *       return "MyRequest";
+ *    }
+ * 
+ *    public String getId() {
+ *       return queryParameters.get("id");
+ *    }
+ * 
+ *    public MyRequestOptions withId(String id) {
+ *       encodeAndReplaceParameter("id", id);
+ *       return this;
+ *    }
+ * 
+ *    public static class Builder extends BaseEC2RequestOptions.Builder {
+ *       public static MyRequestOptions withId(String id) {
+ *          MyRequestOptions options = new MyRequestOptions();
+ *          return options.withId(id);
+ *       }
+ *    }
+ * }
+ * 
+ * + * @see + * @see CommonEC2Parameters + * @author Adrian Cole + * + */ +public class EC2QuerySigner implements HttpRequestFilter { + + public static String[] mandatoryParametersForSignature = new String[] { ACTION, + SIGNATURE_METHOD, SIGNATURE_VERSION, VERSION }; + private final String accessKey; + private final String secretKey; + private final DateService dateService; + + @Inject + public EC2QuerySigner(@Named(AWSConstants.PROPERTY_AWS_ACCESSKEYID) String accessKey, + @Named(AWSConstants.PROPERTY_AWS_SECRETACCESSKEY) String secretKey, + DateService dateService) { + this.accessKey = accessKey; + this.secretKey = secretKey; + this.dateService = dateService; + } + + public HttpRequest filter(HttpRequest request) throws HttpException { + validateRequest(request); + request = addSigningParamsToRequest(request); + String stringToSign = buildStringToSign(request); + String signature = sign(stringToSign); + return addSignatureToRequest(request, signature); + } + + private void validateRequest(HttpRequest request) { + for (String parameter : mandatoryParametersForSignature) { + checkState(request.getEndpoint().getQuery().contains(parameter), "parameter " + parameter + + " is required for signature"); + } + checkState(request.getHeaders().get(HttpHeaders.HOST) != null, + "request is not ready to sign; host not present"); + } + + private HttpRequest addSignatureToRequest(HttpRequest request, String signature) { + UriBuilder builder = UriBuilder.fromUri(request.getEndpoint()); + builder.queryParam(SIGNATURE, signature); + return new HttpRequest(request.getMethod(), builder.build(), request.getHeaders(), request + .getEntity()); + } + + private String sign(String stringToSign) { + String signature; + try { + signature = HttpUtils.hmacSha256Base64(stringToSign, secretKey.getBytes()); + } catch (Exception e) { + throw new HttpException("error signing request", e); + } + return signature; + } + + private String buildStringToSign(HttpRequest request) { + // 1. Sort the UTF-8 query string components by parameter name with natural byte ordering. + // -- as queryParameters are a SortedSet, they are already sorted. + // 2. URL encode the parameter name and values according to the following rules... + // -- all queryParameters are URL encoded on the way in + // 3. Separate the encoded parameter names from their encoded values with the equals sign, + // even if the parameter value is empty. + // -- we do not allow null values. + // 4. Separate the name-value pairs with an ampersand. + // -- buildQueryString() does this. + StringBuilder toSign = new StringBuilder(); + toSign.append(request.getMethod()).append("\n").append( + request.getEndpoint().getHost().toLowerCase()).append("\n").append("/").append("\n"); + toSign.append(request.getEndpoint().getQuery()); + String stringToSign = toSign.toString(); + return stringToSign; + } + + private HttpRequest addSigningParamsToRequest(HttpRequest request) { + UriBuilder builder = UriBuilder.fromUri(request.getEndpoint()); + builder.queryParam(SIGNATURE_METHOD, "HmacSHA256"); + builder.queryParam(SIGNATURE_VERSION, "2"); + builder.queryParam(VERSION, "2009-04-04"); + + // timestamp is incompatible with expires + if (request.getEndpoint().getQuery().contains(EXPIRES)) { + // TODO tune this if necessary + builder.queryParam(TIMESTAMP, dateService.iso8601DateFormat()); + } + builder.queryParam(AWS_ACCESS_KEY_ID, accessKey); + return new HttpRequest(request.getMethod(), builder.build(), request.getHeaders(), request + .getEntity()); + } + +} diff --git a/aws/ec2/core/src/test/java/org/jclouds/aws/ec2/commands/options/EC2QuerySignerTest.java b/aws/ec2/core/src/test/java/org/jclouds/aws/ec2/commands/options/EC2QuerySignerTest.java new file mode 100755 index 0000000000..a91e4acadb --- /dev/null +++ b/aws/ec2/core/src/test/java/org/jclouds/aws/ec2/commands/options/EC2QuerySignerTest.java @@ -0,0 +1,103 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.aws.ec2.commands.options; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.ACTION; +import static org.jclouds.aws.ec2.reference.CommonEC2Parameters.EXPIRES; +import static org.testng.Assert.assertEquals; + +import java.net.URI; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriBuilder; + +import org.jclouds.aws.reference.AWSConstants; +import org.jclouds.http.HttpRequest; +import org.jclouds.util.DateService; +import org.testng.annotations.Test; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import org.jclouds.util.Jsr330; + +@Test(groups = "unit", testName = "s3.EC2QuerySignerTest") +public class EC2QuerySignerTest { + + @Test + void testExpires() { + UriBuilder builder = UriBuilder.fromUri(URI.create("https://ec2.amazonaws.com/")); + builder.queryParam(ACTION,"DescribeImages"); + builder.queryParam(EXPIRES,"2008-02-10T12%3A00%3A00Z"); + builder.queryParam("ImageId.1","ami-2bb65342"); + HttpRequest request = new HttpRequest(HttpMethod.GET,builder.build()); + createFilter(); + } + + @Test + void testAclQueryString() { + URI host = URI.create("http://s3.amazonaws.com:80/?acl"); + HttpRequest request = new HttpRequest(HttpMethod.GET, host); + StringBuilder builder = new StringBuilder(); + createFilter().appendUriPath(request, builder); + assertEquals(builder.toString(), "/?acl"); + } + + // "?acl", "?location", "?logging", or "?torrent" + + @Test + void testAppendBucketNameHostHeaderService() { + URI host = URI.create("http://s3.amazonaws.com:80"); + HttpRequest request = new HttpRequest(HttpMethod.GET, host); + request.getHeaders().put(HttpHeaders.HOST, "s3.amazonaws.com"); + StringBuilder builder = new StringBuilder(); + createFilter().appendBucketName(request, builder); + assertEquals(builder.toString(), ""); + } + + @Test + void testAppendBucketNameURIHost() { + URI host = URI.create("http://adriancole.s3int5.s3-external-3.amazonaws.com:80"); + HttpRequest request = new HttpRequest(HttpMethod.GET, host); + StringBuilder builder = new StringBuilder(); + createFilter().appendBucketName(request, builder); + assertEquals(builder.toString(), "/adriancole.s3int5"); + } + + + + private EC2QuerySigner createFilter() { + return Guice.createInjector(new AbstractModule() { + + protected void configure() { + bindConstant().annotatedWith(Jsr330.named(AWSConstants.PROPERTY_AWS_ACCESSKEYID)).to( + "foo"); + bindConstant().annotatedWith(Jsr330.named(AWSConstants.PROPERTY_AWS_SECRETACCESSKEY)).to( + "bar"); + bind(DateService.class); + + } + }).getInstance(EC2QuerySigner.class); + } + +} \ No newline at end of file