blob: a0c66576e8734ed205cf5ae0aa7d2b27692a08aa [file] [log] [blame]
/*
* 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.filters;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.hash.Hashing.sha256;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.net.HttpHeaders.HOST;
import static org.jclouds.aws.filters.FormSignerUtils.getAnnotatedApiVersion;
import static org.jclouds.aws.reference.FormParameters.ACTION;
import static org.jclouds.aws.reference.FormParameters.VERSION;
import static org.jclouds.http.utils.Queries.queryParser;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import org.jclouds.aws.domain.SessionCredentials;
import org.jclouds.date.TimeStamp;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest;
import org.jclouds.location.Provider;
import org.jclouds.providers.ProviderMetadata;
import org.jclouds.rest.annotations.ApiVersion;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.inject.ImplementedBy;
public final class FormSignerV4 implements FormSigner {
// Specifying a default for how to parse the service and region in this way allows
// tests or other downstream services to not have to use guice overrides.
@ImplementedBy(ServiceAndRegion.AWSServiceAndRegion.class)
public interface ServiceAndRegion {
String service();
String region(String host);
static final class AWSServiceAndRegion implements ServiceAndRegion {
private final String service;
@Inject AWSServiceAndRegion(ProviderMetadata provider) {
this(provider.getEndpoint());
}
AWSServiceAndRegion(String endpoint) {
this.service = parseServiceAndRegion(URI.create(checkNotNull(endpoint, "endpoint")).getHost()).get(0);
}
@Override public String service() {
return service;
}
@Override public String region(String host) {
return parseServiceAndRegion(host).get(1);
}
/** This will only work for amazon deployments, and perhaps not all of them. */
private static List<String> parseServiceAndRegion(String host) {
return Splitter.on('.').splitToList(host);
}
}
}
private final String apiVersion;
private final Supplier<Credentials> creds;
private final javax.inject.Provider<String> iso8601Timestamp;
private final ServiceAndRegion serviceAndRegion;
@Inject FormSignerV4(@ApiVersion String apiVersion, @Provider Supplier<Credentials> creds,
@TimeStamp javax.inject.Provider<String> iso8601Timestamp, ServiceAndRegion serviceAndRegion) {
this.apiVersion = apiVersion;
this.creds = creds;
this.iso8601Timestamp = iso8601Timestamp;
this.serviceAndRegion = serviceAndRegion;
}
/**
* Adds the Authorization header to the request.
*
* Also if the method for the operation (or its class) is annotated with a version that is higher than the
* default (apiVersion), then the Version parameter of the request is set to be the value from the annotation.
*
* @param request The HTTP request for the API call.
* @return The request
* @throws HttpException
*/
@Override
public HttpRequest filter(HttpRequest request) throws HttpException {
String host = request.getFirstHeaderOrNull(HOST);
checkArgument(host != null, "request is not ready to sign; host not present");
String form = request.getPayload().getRawContent().toString();
Multimap<String, String> decodedParams = queryParser().apply(form);
checkArgument(decodedParams.containsKey(ACTION), "request is not ready to sign; Action not present %s", form);
String timestamp = iso8601Timestamp.get();
String datestamp = timestamp.substring(0, 8);
String service = serviceAndRegion.service();
String region = serviceAndRegion.region(host);
String credentialScope = Joiner.on('/').join(datestamp, region, service, "aws4_request");
// content-type is not a required signing param. However, examples use this, so we include it to ease testing.
ImmutableMap.Builder<String, String> signedHeadersBuilder = ImmutableMap.<String, String> builder() //
.put("content-type", request.getPayload().getContentMetadata().getContentType()) //
.put("host", host) //
.put("x-amz-date", timestamp);
HttpRequest.Builder<?> requestBuilder = request.toBuilder() //
.removeHeader(AUTHORIZATION) //
.replaceHeader("X-Amz-Date", timestamp);
if (!decodedParams.containsKey(VERSION)) {
Optional<String> optAnnotatedVersion = getAnnotatedApiVersion(request);
if (optAnnotatedVersion.isPresent()) {
String annotatedVersion = optAnnotatedVersion.get();
// allow an explicit version annotation to _upgrade_ the version past apiVersion (but not downgrade)
String greater = annotatedVersion.compareTo(apiVersion) > 0 ? annotatedVersion : apiVersion;
requestBuilder.addFormParam(VERSION, greater);
} else {
requestBuilder.addFormParam(VERSION, apiVersion);
}
}
Credentials credentials = creds.get();
if (credentials instanceof SessionCredentials) {
String token = SessionCredentials.class.cast(credentials).getSessionToken();
requestBuilder.replaceHeader("X-Amz-Security-Token", token);
signedHeadersBuilder.put("x-amz-security-token", token);
}
ImmutableMap<String, String> signedHeaders = signedHeadersBuilder.build();
String stringToSign = createStringToSign(requestBuilder.build(), signedHeaders, credentialScope);
byte[] signatureKey = signatureKey(credentials.credential, datestamp, region, service);
String signature = base16().lowerCase().encode(hmacSHA256(stringToSign, signatureKey));
StringBuilder authorization = new StringBuilder("AWS4-HMAC-SHA256 ");
authorization.append("Credential=").append(credentials.identity).append('/').append(credentialScope).append(", ");
authorization.append("SignedHeaders=").append(Joiner.on(';').join(signedHeaders.keySet())).append(", ");
authorization.append("Signature=").append(signature);
return requestBuilder.addHeader(AUTHORIZATION, authorization.toString()).build();
}
static byte[] signatureKey(String secretKey, String datestamp, String region, String service) {
byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8);
byte[] kDate = hmacSHA256(datestamp, kSecret);
byte[] kRegion = hmacSHA256(region, kDate);
byte[] kService = hmacSHA256(service, kRegion);
byte[] kSigning = hmacSHA256("aws4_request", kService);
return kSigning;
}
static byte[] hmacSHA256(String data, byte[] key) {
try {
String algorithm = "HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data.getBytes(UTF_8));
} catch (GeneralSecurityException e) {
throw new HttpException(e);
}
}
static String createStringToSign(HttpRequest request, Map<String, String> signedHeaders, String credentialScope) {
StringBuilder canonicalRequest = new StringBuilder();
// HTTPRequestMethod + '\n' +
canonicalRequest.append(request.getMethod()).append("\n");
// CanonicalURI + '\n' +
canonicalRequest.append(request.getEndpoint().getPath()).append("\n");
// CanonicalQueryString + '\n' +
checkArgument(request.getEndpoint().getQuery() == null, "Query parameters not yet supported %s", request);
canonicalRequest.append("\n");
// CanonicalHeaders + '\n' +
for (Map.Entry<String, String> entry : signedHeaders.entrySet()) {
canonicalRequest.append(entry.getKey()).append(':').append(entry.getValue()).append('\n');
}
canonicalRequest.append("\n");
// SignedHeaders + '\n' +
canonicalRequest.append(Joiner.on(';').join(signedHeaders.keySet())).append('\n');
// HexEncode(Hash(Payload))
String payload = request.getPayload().getRawContent().toString();
canonicalRequest.append(base16().lowerCase().encode(sha256().hashString(payload, UTF_8).asBytes()));
StringBuilder toSign = new StringBuilder();
// Algorithm + '\n' +
toSign.append("AWS4-HMAC-SHA256").append('\n');
// RequestDate + '\n' +
toSign.append(signedHeaders.get("x-amz-date")).append('\n');
// CredentialScope + '\n' +
toSign.append(credentialScope).append('\n');
// HexEncode(Hash(CanonicalRequest))
toSign.append(base16().lowerCase().encode(sha256().hashString(canonicalRequest.toString(), UTF_8).asBytes()));
return toSign.toString();
}
}