blob: 1348e22348a21bbd49062850beca02af26c2ff55 [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.s3.filters;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.io.ByteStreams.readBytes;
import static org.jclouds.crypto.Macs.asByteProcessor;
import static org.jclouds.http.utils.Queries.queryParser;
import static org.jclouds.util.Strings2.toInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.InvalidKeyException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import javax.inject.Inject;
import javax.xml.ws.http.HTTPException;
import com.google.common.base.Joiner;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.escape.Escaper;
import com.google.common.hash.Hashing;
import com.google.common.hash.HashingInputStream;
import com.google.common.io.ByteProcessor;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
import com.google.common.net.PercentEscaper;
import com.google.inject.ImplementedBy;
import org.jclouds.crypto.Crypto;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.internal.SignatureWire;
import org.jclouds.io.Payload;
import org.jclouds.providers.ProviderMetadata;
/**
* Common methods and properties for all AWS4 signer variants
*/
public abstract class Aws4SignerBase {
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
protected final DateFormat timestampFormat;
protected final DateFormat dateFormat;
// Do not URL-encode any of the unreserved characters that RFC 3986 defines:
// A-Z, a-z, 0-9, hyphen (-), underscore (_), period (.), and tilde (~).
private static final Escaper AWS_URL_PARAMETER_ESCAPER = new PercentEscaper("-_.~", false);
private static final Escaper AWS_PATH_ESCAPER = new PercentEscaper("/-_.~", false);
// 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);
final class AWSServiceAndRegion implements ServiceAndRegion {
private final String service;
@Inject
AWSServiceAndRegion(ProviderMetadata provider) {
this(provider.getEndpoint());
}
AWSServiceAndRegion(String endpoint) {
this.service = AwsHostNameUtils.parseServiceName(URI.create(checkNotNull(endpoint, "endpoint")));
}
@Override
public String service() {
return service;
}
@Override
public String region(String host) {
return AwsHostNameUtils.parseRegionName(host, service());
}
}
}
protected final String headerTag;
protected final ServiceAndRegion serviceAndRegion;
protected final SignatureWire signatureWire;
protected final Supplier<Credentials> creds;
protected final Supplier<Date> timestampProvider;
protected final Crypto crypto;
protected Aws4SignerBase(SignatureWire signatureWire, String headerTag,
Supplier<Credentials> creds, Supplier<Date> timestampProvider,
ServiceAndRegion serviceAndRegion, Crypto crypto) {
this.signatureWire = signatureWire;
this.headerTag = headerTag;
this.creds = creds;
this.timestampProvider = timestampProvider;
this.serviceAndRegion = serviceAndRegion;
this.crypto = crypto;
this.timestampFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
timestampFormat.setTimeZone(GMT);
this.dateFormat = new SimpleDateFormat("yyyyMMdd");
dateFormat.setTimeZone(GMT);
}
protected String getContentType(HttpRequest request) {
Payload payload = request.getPayload();
// Default Content Type
String contentType = request.getFirstHeaderOrNull(HttpHeaders.CONTENT_TYPE);
if (payload != null
&& payload.getContentMetadata() != null
&& payload.getContentMetadata().getContentType() != null) {
contentType = payload.getContentMetadata().getContentType();
}
return contentType;
}
protected String getContentLength(HttpRequest request) {
Payload payload = request.getPayload();
// Default Content Type
String contentLength = request.getFirstHeaderOrNull(HttpHeaders.CONTENT_LENGTH);
if (payload != null
&& payload.getContentMetadata() != null
&& payload.getContentMetadata().getContentType() != null) {
Long length = payload.getContentMetadata().getContentLength();
contentLength =
length == null ? contentLength : String.valueOf(payload.getContentMetadata().getContentLength());
}
return contentLength;
}
// append all of 'x-amz-*' headers
protected void appendAmzHeaders(HttpRequest request,
ImmutableMap.Builder<String, String> signedHeadersBuilder) {
for (Map.Entry<String, String> header : request.getHeaders().entries()) {
String key = header.getKey();
if (key.startsWith("x-" + headerTag + "-")) {
signedHeadersBuilder.put(key.toLowerCase(), header.getValue());
}
}
}
/**
* caluclate AWS signature key.
* <p>
* <code>
* DateKey = hmacSHA256(datestamp, "AWS4"+ secretKey)
* <br>
* DateRegionKey = hmacSHA256(region, DateKey)
* <br>
* DateRegionServiceKey = hmacSHA256(service, DateRegionKey)
* <br>
* SigningKey = hmacSHA256("aws4_request", DateRegionServiceKey)
* <br>
* <p/>
* </code>
* </p>
*
* @param secretKey AWS access secret key
* @param datestamp date yyyyMMdd
* @param region AWS region
* @param service AWS service
* @return SigningKey
*/
protected 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;
}
/**
* hmac sha256
*
* @param toSign string to sign
* @param key hash key
*/
protected byte[] hmacSHA256(String toSign, byte[] key) {
try {
return readBytes(toInputStream(toSign), hmacSHA256(crypto, key));
} catch (IOException e) {
throw new HttpException("read sign error", e);
} catch (InvalidKeyException e) {
throw new HttpException("invalid key", e);
}
}
public static ByteProcessor<byte[]> hmacSHA256(Crypto crypto, byte[] signatureKey) throws InvalidKeyException {
return asByteProcessor(crypto.hmacSHA256(signatureKey));
}
/**
* hash input with sha256
*
* @param input
* @return hash result
* @throws HTTPException
*/
public static byte[] hash(InputStream input) throws HTTPException {
HashingInputStream his = new HashingInputStream(Hashing.sha256(), input);
try {
ByteStreams.copy(his, ByteStreams.nullOutputStream());
return his.hash().asBytes();
} catch (IOException e) {
throw new HttpException("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}
/**
* hash input with sha256
*
* @param bytes input bytes
* @return hash result
* @throws HTTPException
*/
public static byte[] hash(byte[] bytes) throws HTTPException {
try {
return ByteSource.wrap(bytes).hash(Hashing.sha256()).asBytes();
} catch (IOException e) {
throw new HttpException("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}
/**
* hash string (encoding UTF_8) with sha256
*
* @param input input stream
* @return hash result
* @throws HTTPException
*/
public static byte[] hash(String input) throws HTTPException {
return hash(new ByteArrayInputStream(input.getBytes(UTF_8)));
}
/**
* Examines the specified query string parameters and returns a
* canonicalized form.
* <p/>
* The canonicalized query string is formed by first sorting all the query
* string parameters, then URI encoding both the key and value and then
* joining them, in order, separating key value pairs with an '&'.
*
* @param queryString The query string parameters to be canonicalized.
* @return A canonicalized form for the specified query string parameters.
*/
protected String getCanonicalizedQueryString(String queryString) {
Multimap<String, String> params = queryParser().apply(queryString);
SortedMap<String, String> sorted = Maps.newTreeMap();
if (params == null) {
return "";
}
Iterator<Map.Entry<String, String>> pairs = params.entries().iterator();
while (pairs.hasNext()) {
Map.Entry<String, String> pair = pairs.next();
String key = pair.getKey();
String value = pair.getValue();
sorted.put(urlEncode(key), urlEncode(value));
}
return Joiner.on("&").withKeyValueSeparator("=").join(sorted);
}
/**
* Encode a string for use in the path of a URL; uses URLEncoder.encode,
* (which encodes a string for use in the query portion of a URL), then
* applies some postfilters to fix things up per the RFC. Can optionally
* handle strings which are meant to encode a path (ie include '/'es
* which should NOT be escaped).
*
* @param value the value to encode
* @return the encoded value
*/
public static String urlEncode(final String value) {
if (value == null) {
return "";
}
return AWS_URL_PARAMETER_ESCAPER.escape(value);
}
/**
* Lowercase base 16 encoding.
*
* @param bytes bytes
* @return base16 lower case hex string.
*/
public static String hex(final byte[] bytes) {
return base16().lowerCase().encode(bytes);
}
/**
* Create a Canonical Request to sign
* <h4>Canonical Request</h4>
* <p>
* <code>
* &lt;HTTPMethod>\n
* <br>
* &lt;CanonicalURI>\n
* <br>
* &lt;CanonicalQueryString>\n
* <br>
* &lt;CanonicalHeaders>\n
* <br>
* &lt;SignedHeaders>\n
* <br>
* &lt;HashedPayload>
* </code>
* </p>
* <p><b>HTTPMethod</b> is one of the HTTP methods, for example GET, PUT, HEAD, and DELETE.</p>
* <p><b>CanonicalURI</b> is the URI-encoded version of the absolute path component of the URI—everything starting
* with the "/" that follows the domain name and up to the end of the string or to the question mark character ('?')
* if you have query string parameters.</p>
* <p><b>CanonicalQueryString</b> specifies the URI-encoded query string parameters. You URI-encode name and values
* individually. You must also sort the parameters in the canonical query string alphabetically by key name.
* The sorting occurs after encoding.</p>
* <p><b>CanonicalHeaders</b> is a list of request headers with their values. Individual header name and value pairs are
* separated by the newline character ("\n"). Header names must be in lowercase. Header value must be trim space.
* <br>
* The <b>CanonicalHeaders</b> list must include the following:
* HTTP host header.
* If the Content-Type header is present in the request, it must be added to the CanonicalHeaders list.
* Any x-amz-* headers that you plan to include in your request must also be added.</p>
* <p><b>SignedHeaders</b> is an alphabetically sorted, semicolon-separated list of lowercase request header names.
* The request headers in the list are the same headers that you included in the CanonicalHeaders string.</p>
* <p><b>HashedPayload</b> is the hexadecimal value of the SHA256 hash of the request payload. </p>
* <p>If there is no payload in the request, you compute a hash of the empty string as follows:
* <code>Hex(SHA256Hash(""))</code> The hash returns the following value:
* e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 </p>
*
* @param method http request method
* @param endpoint http request endpoing
* @param signedHeaders signed headers
* @param timestamp ISO8601 timestamp
* @param credentialScope credential scope
* @return string to sign
*/
protected String createStringToSign(String method, URI endpoint, Map<String, String> signedHeaders,
String timestamp, String credentialScope, String hashedPayload) {
// lower case header keys
Map<String, String> lowerCaseHeaders = lowerCaseNaturalOrderKeys(signedHeaders);
StringBuilder canonicalRequest = new StringBuilder();
// HTTPRequestMethod + '\n' +
canonicalRequest.append(method).append("\n");
// CanonicalURI + '\n' +
canonicalRequest.append(AWS_PATH_ESCAPER.escape(endpoint.getPath())).append("\n");
// CanonicalQueryString + '\n' +
if (endpoint.getQuery() != null) {
canonicalRequest.append(getCanonicalizedQueryString(endpoint.getQuery()));
}
canonicalRequest.append("\n");
// CanonicalHeaders + '\n' +
for (Map.Entry<String, String> entry : lowerCaseHeaders.entrySet()) {
canonicalRequest.append(entry.getKey()).append(':').append(entry.getValue()).append('\n');
}
canonicalRequest.append("\n");
// SignedHeaders + '\n' +
canonicalRequest.append(Joiner.on(';').join(lowerCaseHeaders.keySet())).append('\n');
// HexEncode(Hash(Payload))
canonicalRequest.append(hashedPayload);
signatureWire.getWireLog().debug("<< " + canonicalRequest);
// Create a String to Sign
StringBuilder toSign = new StringBuilder();
// Algorithm + '\n' +
toSign.append("AWS4-HMAC-SHA256").append('\n');
// RequestDate + '\n' +
toSign.append(timestamp).append('\n');
// CredentialScope + '\n' +
toSign.append(credentialScope).append('\n');
// HexEncode(Hash(CanonicalRequest))
toSign.append(hex(hash(canonicalRequest.toString())));
return toSign.toString();
}
/**
* change the keys but keep the values in-tact.
*
* @param in input map to transform
* @return immutableSortedMap with the new lowercase keys.
*/
protected static Map<String, String> lowerCaseNaturalOrderKeys(Map<String, String> in) {
checkNotNull(in, "input map");
ImmutableSortedMap.Builder<String, String> returnVal = ImmutableSortedMap.<String, String>naturalOrder();
for (Map.Entry<String, String> entry : in.entrySet())
returnVal.put(entry.getKey().toLowerCase(Locale.US), entry.getValue());
return returnVal.build();
}
}