From d4af465ca873b02add64fe99e176eca32c5a762f Mon Sep 17 00:00:00 2001 From: "Roman C. Coedo" Date: Tue, 22 Apr 2014 12:15:03 +0200 Subject: [PATCH] JCLOUDS-457: Created the skeleton of the Glacier API. --- apis/glacier/pom.xml | 128 ++++++++++++ .../jclouds/glacier/GlacierApiMetadata.java | 98 +++++++++ .../jclouds/glacier/GlacierAsyncClient.java | 46 +++++ .../org/jclouds/glacier/GlacierClient.java | 25 +++ .../config/GlacierRestClientModule.java | 64 ++++++ .../filters/RequestAuthorizeSignature.java | 73 +++++++ .../glacier/reference/GlacierHeaders.java | 28 +++ .../glacier/util/AWSRequestSignerV4.java | 189 ++++++++++++++++++ .../services/org.jclouds.apis.ApiMetadata | 1 + .../glacier/GlacierApiMetadataTest.java | 33 +++ .../glacier/GlacierClientMockTest.java | 77 +++++++ .../glacier/util/AWSRequestSignerV4Test.java | 56 ++++++ .../services/org.jclouds.apis.ApiMetadata | 1 + 13 files changed, 819 insertions(+) create mode 100644 apis/glacier/pom.xml create mode 100644 apis/glacier/src/main/java/org/jclouds/glacier/GlacierApiMetadata.java create mode 100644 apis/glacier/src/main/java/org/jclouds/glacier/GlacierAsyncClient.java create mode 100644 apis/glacier/src/main/java/org/jclouds/glacier/GlacierClient.java create mode 100644 apis/glacier/src/main/java/org/jclouds/glacier/config/GlacierRestClientModule.java create mode 100644 apis/glacier/src/main/java/org/jclouds/glacier/filters/RequestAuthorizeSignature.java create mode 100644 apis/glacier/src/main/java/org/jclouds/glacier/reference/GlacierHeaders.java create mode 100644 apis/glacier/src/main/java/org/jclouds/glacier/util/AWSRequestSignerV4.java create mode 100644 apis/glacier/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata create mode 100644 apis/glacier/src/test/java/org/jclouds/glacier/GlacierApiMetadataTest.java create mode 100644 apis/glacier/src/test/java/org/jclouds/glacier/GlacierClientMockTest.java create mode 100644 apis/glacier/src/test/java/org/jclouds/glacier/util/AWSRequestSignerV4Test.java create mode 100644 apis/glacier/src/test/resources/META-INF/services/org.jclouds.apis.ApiMetadata diff --git a/apis/glacier/pom.xml b/apis/glacier/pom.xml new file mode 100644 index 0000000000..5ac9979aeb --- /dev/null +++ b/apis/glacier/pom.xml @@ -0,0 +1,128 @@ + + + + 4.0.0 + + org.apache.jclouds.labs + jclouds-labs-aws + 1.8.0-SNAPSHOT + + + org.apache.jclouds.labs + glacier + jclouds glacier api + jclouds components to access an implementation of glacier + bundle + + + https://glacier.eu-west-1.amazonaws.com + 2012-06-01 + + ${test.aws.identity} + ${test.aws.credential} + + org.jclouds.glacier*;version="${project.version}" + + org.jclouds.labs*;version="${project.version}", + org.jclouds*;version="${project.version}", + * + + + + + + org.apache.jclouds.api + sts + ${project.version} + jar + + + org.apache.jclouds + jclouds-blobstore + ${project.version} + jar + + + org.apache.jclouds + jclouds-core + ${project.version} + test-jar + test + + + org.apache.jclouds + jclouds-blobstore + ${project.version} + test-jar + test + + + org.apache.jclouds.driver + jclouds-log4j + ${project.version} + test + + + log4j + log4j + test + + + com.google.mockwebserver + mockwebserver + test + + + + + + live + + + + org.apache.maven.plugins + maven-surefire-plugin + + + integration + integration-test + + test + + + + ${jclouds.blobstore.httpstream.url} + ${jclouds.blobstore.httpstream.md5} + ${test.glacier.endpoint} + ${test.glacier.api-version} + ${test.glacier.build-version} + ${test.glacier.identity} + ${test.glacier.credential} + + + + + + + + + + + diff --git a/apis/glacier/src/main/java/org/jclouds/glacier/GlacierApiMetadata.java b/apis/glacier/src/main/java/org/jclouds/glacier/GlacierApiMetadata.java new file mode 100644 index 0000000000..c547e21046 --- /dev/null +++ b/apis/glacier/src/main/java/org/jclouds/glacier/GlacierApiMetadata.java @@ -0,0 +1,98 @@ +/* + * 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.glacier; + +import static org.jclouds.aws.reference.AWSConstants.PROPERTY_HEADER_TAG; +import static org.jclouds.reflect.Reflection2.typeToken; + +import java.net.URI; +import java.util.Properties; + +import org.jclouds.blobstore.BlobStoreContext; +import org.jclouds.glacier.config.GlacierRestClientModule; +import org.jclouds.glacier.reference.GlacierHeaders; +import org.jclouds.rest.internal.BaseRestApiMetadata; + +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.TypeToken; +import com.google.inject.Module; + +/** + * Implementation of ApiMetadata for Amazon Glacier API + * + * @author Roman Coedo + */ +public class GlacierApiMetadata extends BaseRestApiMetadata { + + @Deprecated + public static final TypeToken> CONTEXT_TOKEN = new TypeToken>() { + + private static final long serialVersionUID = 1L; + }; + + private static Builder builder() { + return new Builder(); + } + + @Override + public Builder toBuilder() { + return builder().fromApiMetadata(this); + } + + public GlacierApiMetadata() { + this(builder()); + } + + protected GlacierApiMetadata(Builder builder) { + super(new Builder()); + } + + public static Properties defaultProperties() { + Properties properties = BaseRestApiMetadata.defaultProperties(); + properties.setProperty(PROPERTY_HEADER_TAG, GlacierHeaders.DEFAULT_AMAZON_HEADERTAG); + return properties; + } + + public static class Builder extends BaseRestApiMetadata.Builder { + + @SuppressWarnings("deprecation") + protected Builder() { + super(GlacierClient.class, GlacierAsyncClient.class); + id("glacier") + .name("Amazon Glacier API") + .identityName("Access Key ID") + .credentialName("Secret Access Key") + .defaultEndpoint("https://glacier.us-east-1.amazonaws.com") + .documentation(URI.create("http://docs.aws.amazon.com/amazonglacier/latest/dev/amazon-glacier-api.html")) + .version("2012-06-01") + .defaultProperties(GlacierApiMetadata.defaultProperties()) + .context(CONTEXT_TOKEN) + .view(typeToken(BlobStoreContext.class)) + .defaultModules(ImmutableSet.> of(GlacierRestClientModule.class)); + } + + @Override + public GlacierApiMetadata build() { + return new GlacierApiMetadata(this); + } + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/apis/glacier/src/main/java/org/jclouds/glacier/GlacierAsyncClient.java b/apis/glacier/src/main/java/org/jclouds/glacier/GlacierAsyncClient.java new file mode 100644 index 0000000000..7f66811eda --- /dev/null +++ b/apis/glacier/src/main/java/org/jclouds/glacier/GlacierAsyncClient.java @@ -0,0 +1,46 @@ +/* + * 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.glacier; + +import static org.jclouds.blobstore.attr.BlobScopes.CONTAINER; + +import java.io.Closeable; +import java.net.URI; + +import javax.inject.Named; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import org.jclouds.blobstore.attr.BlobScope; +import org.jclouds.glacier.filters.RequestAuthorizeSignature; +import org.jclouds.glacier.reference.GlacierHeaders; +import org.jclouds.rest.annotations.Headers; +import org.jclouds.rest.annotations.RequestFilters; + +import com.google.common.util.concurrent.ListenableFuture; + +@Headers(keys = GlacierHeaders.VERSION, values = "2012-06-01") +@RequestFilters(RequestAuthorizeSignature.class) +@BlobScope(CONTAINER) +public interface GlacierAsyncClient extends Closeable { + + @Named("CreateVault") + @PUT + @Path("/-/vaults/{vault}") + ListenableFuture createVault(@PathParam("vault") String vaultName); +} diff --git a/apis/glacier/src/main/java/org/jclouds/glacier/GlacierClient.java b/apis/glacier/src/main/java/org/jclouds/glacier/GlacierClient.java new file mode 100644 index 0000000000..3d392b9c8e --- /dev/null +++ b/apis/glacier/src/main/java/org/jclouds/glacier/GlacierClient.java @@ -0,0 +1,25 @@ +/* + * 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.glacier; + +import java.io.Closeable; +import java.net.URI; + +public interface GlacierClient extends Closeable { + + URI createVault(String vaultName); +} diff --git a/apis/glacier/src/main/java/org/jclouds/glacier/config/GlacierRestClientModule.java b/apis/glacier/src/main/java/org/jclouds/glacier/config/GlacierRestClientModule.java new file mode 100644 index 0000000000..7631ce9913 --- /dev/null +++ b/apis/glacier/src/main/java/org/jclouds/glacier/config/GlacierRestClientModule.java @@ -0,0 +1,64 @@ +/* + * 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.glacier.config; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Named; + +import org.jclouds.Constants; +import org.jclouds.date.DateService; +import org.jclouds.date.TimeStamp; +import org.jclouds.glacier.GlacierAsyncClient; +import org.jclouds.glacier.GlacierClient; +import org.jclouds.glacier.filters.RequestAuthorizeSignature; +import org.jclouds.rest.ConfiguresRestClient; +import org.jclouds.rest.config.RestClientModule; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.inject.Provides; +import com.google.inject.Scopes; + +@ConfiguresRestClient +public class GlacierRestClientModule extends RestClientModule { + + @Override + protected void configure() { + super.configure(); + bind(RequestAuthorizeSignature.class).in(Scopes.SINGLETON); + } + + @Provides + @TimeStamp + protected String provideTimeStamp(@TimeStamp Supplier cache) { + return cache.get(); + } + + @Provides + @TimeStamp + Supplier provideTimeStampCache(@Named(Constants.PROPERTY_SESSION_INTERVAL) long seconds, + final DateService dateService) { + return Suppliers.memoizeWithExpiration(new Supplier() { + + @Override + public String get() { + return dateService.iso8601SecondsDateFormat().replaceAll("[-:]", ""); + } + }, seconds, TimeUnit.SECONDS); + } +} diff --git a/apis/glacier/src/main/java/org/jclouds/glacier/filters/RequestAuthorizeSignature.java b/apis/glacier/src/main/java/org/jclouds/glacier/filters/RequestAuthorizeSignature.java new file mode 100644 index 0000000000..8f46a0fb27 --- /dev/null +++ b/apis/glacier/src/main/java/org/jclouds/glacier/filters/RequestAuthorizeSignature.java @@ -0,0 +1,73 @@ +/* + * 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.glacier.filters; + +import static com.google.common.base.Preconditions.checkNotNull; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.jclouds.Constants; +import org.jclouds.crypto.Crypto; +import org.jclouds.date.TimeStamp; +import org.jclouds.domain.Credentials; +import org.jclouds.glacier.reference.GlacierHeaders; +import org.jclouds.glacier.util.AWSRequestSignerV4; +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpRequestFilter; +import org.jclouds.http.HttpUtils; +import org.jclouds.logging.Logger; + +import com.google.common.base.Supplier; +import com.google.common.net.HttpHeaders; + +@Singleton +public class RequestAuthorizeSignature implements HttpRequestFilter { + + private final AWSRequestSignerV4 signer; + + @Resource + @Named(Constants.LOGGER_SIGNATURE) + Logger signatureLog = Logger.NULL; + + private final Provider timeStampProvider; + private final HttpUtils utils; + + @Inject + public RequestAuthorizeSignature(@TimeStamp Provider timeStampProvider, + @org.jclouds.location.Provider Supplier creds, Crypto crypto, HttpUtils utils) { + checkNotNull(creds); + this.signer = new AWSRequestSignerV4(creds.get().identity, creds.get().credential, checkNotNull(crypto)); + this.timeStampProvider = checkNotNull(timeStampProvider); + this.utils = checkNotNull(utils); + } + + @Override + public HttpRequest filter(HttpRequest request) throws HttpException { + request = request.toBuilder().removeHeader(HttpHeaders.DATE) + .replaceHeader(GlacierHeaders.ALTERNATE_DATE, timeStampProvider.get()) + .replaceHeader(HttpHeaders.HOST, request.getEndpoint().getHost()).build(); + utils.logRequest(signatureLog, request, ">>"); + request = this.signer.sign(request); + utils.logRequest(signatureLog, request, "<<"); + return request; + } +} diff --git a/apis/glacier/src/main/java/org/jclouds/glacier/reference/GlacierHeaders.java b/apis/glacier/src/main/java/org/jclouds/glacier/reference/GlacierHeaders.java new file mode 100644 index 0000000000..51a0afec64 --- /dev/null +++ b/apis/glacier/src/main/java/org/jclouds/glacier/reference/GlacierHeaders.java @@ -0,0 +1,28 @@ +/* + * 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.glacier.reference; + +public final class GlacierHeaders { + + public static final String DEFAULT_AMAZON_HEADERTAG = "amz"; + public static final String HEADER_PREFIX = "x-" + DEFAULT_AMAZON_HEADERTAG + "-"; + public static final String VERSION = HEADER_PREFIX + "glacier-version"; + public static final String ALTERNATE_DATE = HEADER_PREFIX + "date"; + + private GlacierHeaders() { + } +} diff --git a/apis/glacier/src/main/java/org/jclouds/glacier/util/AWSRequestSignerV4.java b/apis/glacier/src/main/java/org/jclouds/glacier/util/AWSRequestSignerV4.java new file mode 100644 index 0000000000..e893b0356a --- /dev/null +++ b/apis/glacier/src/main/java/org/jclouds/glacier/util/AWSRequestSignerV4.java @@ -0,0 +1,189 @@ +/* + * 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.glacier.util; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map.Entry; + +import javax.crypto.Mac; + +import org.jclouds.crypto.Crypto; +import org.jclouds.glacier.reference.GlacierHeaders; +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpRequest; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.collect.SortedSetMultimap; +import com.google.common.collect.TreeMultimap; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; +import com.google.common.io.ByteStreams; +import com.google.common.net.HttpHeaders; + +// TODO: Query parameters, not necessary for Glacier +// TODO: Endpoint on buildCredentialScope is being read from the static string. Uncool. +/** + * Signs requests using the AWSv4 signing algorithm + * + * @see + * @author Roman Coedo + */ +public class AWSRequestSignerV4 { + + public static final String AUTH_TAG = "AWS4"; + public static final String HEADER_TAG = "x-amz-"; + public static final String ALGORITHM = AUTH_TAG + "-HMAC-SHA256"; + public static final String TERMINATION_STRING = "aws4_request"; + public static final String REGION = "us-east-1"; + public static final String SERVICE = "glacier"; + + private final Crypto crypto; + private final String identity; + private final String credential; + + public AWSRequestSignerV4(String identity, String credential, Crypto crypto) { + this.crypto = checkNotNull(crypto); + this.identity = checkNotNull(identity); + this.credential = checkNotNull(credential); + } + + private static String buildHashedCanonicalRequest(String method, String endpoint, String hashedPayload, + String canonicalizedHeadersString, String signedHeaders) { + return sha256((method + "\n" + endpoint + "\n" + "" + "\n" + canonicalizedHeadersString + "\n" + signedHeaders + + "\n" + hashedPayload).getBytes()); + } + + private static String createStringToSign(String date, String credentialScope, String hashedCanonicalRequest) { + return ALGORITHM + "\n" + date + "\n" + credentialScope + "\n" + hashedCanonicalRequest; + } + + private static String formatDateWithoutTimestamp(String date) { + return date.substring(0, 8); + } + + private static String buildCredentialScope(String dateWithoutTimeStamp) { + return dateWithoutTimeStamp + "/" + REGION + "/" + SERVICE + "/" + TERMINATION_STRING; + } + + private static Multimap buildCanonicalizedHeadersMap(HttpRequest request) { + Multimap headers = request.getHeaders(); + SortedSetMultimap canonicalizedHeaders = TreeMultimap.create(); + for (Entry header : headers.entries()) { + if (header.getKey() == null) + continue; + String key = header.getKey().toString().toLowerCase(Locale.getDefault()); + // Ignore any headers that are not particularly interesting. + if (key.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE) || key.equalsIgnoreCase(HttpHeaders.CONTENT_MD5) + || key.equalsIgnoreCase(HttpHeaders.HOST) || key.startsWith(HEADER_TAG)) { + canonicalizedHeaders.put(key, header.getValue()); + } + } + return canonicalizedHeaders; + } + + private static String buildCanonicalizedHeadersString(Multimap canonicalizedHeadersMap) { + StringBuilder canonicalizedHeadersBuffer = new StringBuilder(); + for (Entry header : canonicalizedHeadersMap.entries()) { + String key = header.getKey(); + canonicalizedHeadersBuffer.append(key.toLowerCase()).append(':').append(header.getValue()).append('\n'); + } + return canonicalizedHeadersBuffer.toString(); + } + + private static String buildSignedHeaders(Multimap canonicalizedHeadersMap) { + return Joiner.on(';').join(Iterables.transform(canonicalizedHeadersMap.keySet(), new Function() { + + @Override + public String apply(String input) { + return input.toLowerCase(); + } + })); + } + + private static String sha256(byte[] unhashedBytes) { + return Hashing.sha256().hashBytes(unhashedBytes).toString(); + } + + private static String buildHashedPayload(HttpRequest request) { + try { + byte[] unhashedBytes = request.getPayload() == null ? "".getBytes() : ByteStreams.toByteArray(request + .getPayload().getInput()); + return sha256(unhashedBytes); + } catch (IOException e) { + throw new HttpException("Error signing request", e); + } + } + + private static String buildAuthHeader(String accessKey, String credentialScope, String signedHeaders, + String signature) { + return ALGORITHM + " " + "Credential=" + accessKey + "/" + credentialScope + "," + "SignedHeaders=" + + signedHeaders + "," + "Signature=" + signature; + } + + private byte[] hmacSha256(byte[] key, String s) { + try { + Mac hmacSHA256 = crypto.hmacSHA256(key); + return hmacSHA256.doFinal(s.getBytes()); + } catch (Exception e) { + throw new HttpException("Error signing request", e); + } + } + + private String buildSignature(String dateWithoutTimestamp, String stringToSign) { + byte[] kSecret = (AUTH_TAG + credential).getBytes(UTF_8); + byte[] kDate = hmacSha256(kSecret, dateWithoutTimestamp); + byte[] kRegion = hmacSha256(kDate, REGION); + byte[] kService = hmacSha256(kRegion, SERVICE); + byte[] kSigning = hmacSha256(kService, TERMINATION_STRING); + return BaseEncoding.base16().encode(hmacSha256(kSigning, stringToSign)).toLowerCase(); + } + + public HttpRequest sign(HttpRequest request) { + // Grab the needed data to build the signature + Multimap canonicalizedHeadersMap = buildCanonicalizedHeadersMap(request); + String canonicalizedHeadersString = buildCanonicalizedHeadersString(canonicalizedHeadersMap); + String signedHeaders = buildSignedHeaders(canonicalizedHeadersMap); + String date = request.getFirstHeaderOrNull(GlacierHeaders.ALTERNATE_DATE); + String dateWithoutTimestamp = formatDateWithoutTimestamp(date); + String method = request.getMethod(); + String endpoint = request.getEndpoint().getRawPath(); + String credentialScope = buildCredentialScope(dateWithoutTimestamp); + String hashedPayload = buildHashedPayload(request); + + // Task 1: Create a Canonical Request For Signature Version 4. + String hashedCanonicalRequest = buildHashedCanonicalRequest(method, endpoint, hashedPayload, + canonicalizedHeadersString, signedHeaders); + + // Task 2: Create a String to Sign for Signature Version 4. + String stringToSign = createStringToSign(date, credentialScope, hashedCanonicalRequest); + + // Task 3: Calculate the AWS Signature Version 4. + String signature = buildSignature(dateWithoutTimestamp, stringToSign); + + // Sign the request + String authHeader = buildAuthHeader(identity, credentialScope, signedHeaders, signature); + request = request.toBuilder().replaceHeader(HttpHeaders.AUTHORIZATION, authHeader).build(); + return request; + } +} diff --git a/apis/glacier/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata b/apis/glacier/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata new file mode 100644 index 0000000000..ef20fbae11 --- /dev/null +++ b/apis/glacier/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata @@ -0,0 +1 @@ +org.jclouds.glacier.GlacierApiMetadata diff --git a/apis/glacier/src/test/java/org/jclouds/glacier/GlacierApiMetadataTest.java b/apis/glacier/src/test/java/org/jclouds/glacier/GlacierApiMetadataTest.java new file mode 100644 index 0000000000..0cb3272010 --- /dev/null +++ b/apis/glacier/src/test/java/org/jclouds/glacier/GlacierApiMetadataTest.java @@ -0,0 +1,33 @@ +/* + * 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.glacier; + +import org.jclouds.blobstore.internal.BaseBlobStoreApiMetadataTest; +import org.testng.annotations.Test; + +/** + * + * @author Roman Coedo + */ +@Test(groups = "unit", testName = "GlacierApiMetadataTest") +public class GlacierApiMetadataTest extends BaseBlobStoreApiMetadataTest { + + public GlacierApiMetadataTest() { + super(new GlacierApiMetadata()); + } + +} diff --git a/apis/glacier/src/test/java/org/jclouds/glacier/GlacierClientMockTest.java b/apis/glacier/src/test/java/org/jclouds/glacier/GlacierClientMockTest.java new file mode 100644 index 0000000000..b3ec6c25e8 --- /dev/null +++ b/apis/glacier/src/test/java/org/jclouds/glacier/GlacierClientMockTest.java @@ -0,0 +1,77 @@ +/* + * 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.glacier; + +import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; +import static org.jclouds.Constants.PROPERTY_MAX_RETRIES; +import static org.jclouds.Constants.PROPERTY_SO_TIMEOUT; +import static org.testng.Assert.assertEquals; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.Properties; +import java.util.Set; + +import org.jclouds.ContextBuilder; +import org.jclouds.concurrent.config.ExecutorServiceModule; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Module; +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; +import com.google.mockwebserver.RecordedRequest; + +public class GlacierClientMockTest { + + private static final String VAULT_NAME = "ConcreteVaultName"; + + private static final Set modules = ImmutableSet. of(new ExecutorServiceModule(sameThreadExecutor(), + sameThreadExecutor())); + + static GlacierClient getGlacierClient(URL server) { + Properties overrides = new Properties(); + // prevent expect-100 bug http://code.google.com/p/mockwebserver/issues/detail?id=6 + overrides.setProperty(PROPERTY_SO_TIMEOUT, "0"); + overrides.setProperty(PROPERTY_MAX_RETRIES, "1"); + return ContextBuilder.newBuilder("glacier").credentials("accessKey", "secretKey").endpoint(server.toString()) + .modules(modules).overrides(overrides).buildApi(GlacierClient.class); + } + + public void testCreateVault() throws IOException, InterruptedException { + // Prepare the response + MockResponse mr = new MockResponse(); + mr.setResponseCode(201); + mr.addHeader("x-amzn-RequestId", "AAABZpJrTyioDC_HsOmHae8EZp_uBSJr6cnGOLKp_XJCl-Q"); + mr.addHeader("Date", "Sun, 25 Mar 2012 12:02:00 GMT"); + mr.addHeader("Location", "/111122223333/vaults/" + VAULT_NAME); + MockWebServer server = new MockWebServer(); + server.enqueue(mr); + server.play(); + + // Send the request and check the response + try { + GlacierClient client = getGlacierClient(server.getUrl("/")); + URI responseUri = client.createVault(VAULT_NAME); + assertEquals(responseUri.toString(), server.getUrl("/") + "111122223333/vaults/" + VAULT_NAME); + RecordedRequest request = server.takeRequest(); + assertEquals(request.getRequestLine(), "PUT /-/vaults/" + VAULT_NAME + " HTTP/1.1"); + } finally { + server.shutdown(); + } + } +} diff --git a/apis/glacier/src/test/java/org/jclouds/glacier/util/AWSRequestSignerV4Test.java b/apis/glacier/src/test/java/org/jclouds/glacier/util/AWSRequestSignerV4Test.java new file mode 100644 index 0000000000..24d479a2c2 --- /dev/null +++ b/apis/glacier/src/test/java/org/jclouds/glacier/util/AWSRequestSignerV4Test.java @@ -0,0 +1,56 @@ +/* + * 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.glacier.util; + +import static org.testng.Assert.assertEquals; + +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import org.jclouds.encryption.internal.JCECrypto; +import org.jclouds.http.HttpRequest; +import org.testng.annotations.Test; + +import com.google.common.collect.Multimap; +import com.google.common.collect.TreeMultimap; + +@Test(groups = "unit", testName = "AWSRequestSignerV4Test") +public class AWSRequestSignerV4Test { + + @Test + public void testSignatureCalculation() throws NoSuchAlgorithmException, CertificateException { + String auth = "AWS4-HMAC-SHA256 " + "Credential=AKIAIOSFODNN7EXAMPLE/20120525/us-east-1/glacier/aws4_request," + + "SignedHeaders=host;x-amz-date;x-amz-glacier-version," + + "Signature=3ce5b2f2fffac9262b4da9256f8d086b4aaf42eba5f111c21681a65a127b7c2a"; + String identity = "AKIAIOSFODNN7EXAMPLE"; + String credential = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"; + AWSRequestSignerV4 signer = new AWSRequestSignerV4(identity, credential, new JCECrypto()); + HttpRequest request = signer.sign(createRequest()); + assertEquals(request.getFirstHeaderOrNull("Authorization"), auth); + } + + private HttpRequest createRequest() { + Multimap headers = TreeMultimap.create(); + headers.put("Host", "glacier.us-east-1.amazonaws.com"); + headers.put("x-amz-date", "20120525T002453Z"); + headers.put("x-amz-glacier-version", "2012-06-01"); + HttpRequest request = HttpRequest.builder().method("PUT") + .endpoint("https://glacier.us-east-1.amazonaws.com/-/vaults/examplevault").headers(headers).build(); + return request; + } + +} diff --git a/apis/glacier/src/test/resources/META-INF/services/org.jclouds.apis.ApiMetadata b/apis/glacier/src/test/resources/META-INF/services/org.jclouds.apis.ApiMetadata new file mode 100644 index 0000000000..ef20fbae11 --- /dev/null +++ b/apis/glacier/src/test/resources/META-INF/services/org.jclouds.apis.ApiMetadata @@ -0,0 +1 @@ +org.jclouds.glacier.GlacierApiMetadata