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