| /* |
| * 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(); |
| } |
| } |