| /* |
| * 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> |
| * <HTTPMethod>\n |
| * <br> |
| * <CanonicalURI>\n |
| * <br> |
| * <CanonicalQueryString>\n |
| * <br> |
| * <CanonicalHeaders>\n |
| * <br> |
| * <SignedHeaders>\n |
| * <br> |
| * <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(); |
| } |
| |
| } |