blob: e28b97533d405ec726d6e7dcdb57a931180e7e31 [file] [log] [blame]
/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds 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.collect.Iterables.get;
import static org.jclouds.aws.reference.AWSConstants.PROPERTY_AUTH_TAG;
import static org.jclouds.aws.reference.AWSConstants.PROPERTY_HEADER_TAG;
import static org.jclouds.http.utils.Queries.queryParser;
import static org.jclouds.s3.reference.S3Constants.PROPERTY_S3_SERVICE_PATH;
import static org.jclouds.s3.reference.S3Constants.PROPERTY_S3_VIRTUAL_HOST_BUCKETS;
import static org.jclouds.util.Strings2.toInputStream;
import java.util.Collection;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.ws.rs.core.HttpHeaders;
import org.jclouds.Constants;
import org.jclouds.aws.domain.SessionCredentials;
import org.jclouds.crypto.Crypto;
import org.jclouds.crypto.CryptoStreams;
import org.jclouds.date.TimeStamp;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpRequestFilter;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.internal.SignatureWire;
import org.jclouds.io.InputSuppliers;
import org.jclouds.logging.Logger;
import org.jclouds.rest.RequestSigner;
import org.jclouds.s3.util.S3Utils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
/**
* Signs the S3 request.
*
* @see <a href=
* "http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/index.html?RESTAuthentication.html"
* />
* @author Adrian Cole
*
*/
@Singleton
public class RequestAuthorizeSignature implements HttpRequestFilter, RequestSigner {
private static final Collection<String> FIRST_HEADERS_TO_SIGN = ImmutableList.of(HttpHeaders.DATE);
private static final Set<String> SIGNED_PARAMETERS = ImmutableSet.of("acl", "torrent", "logging", "location", "policy",
"requestPayment", "versioning", "versions", "versionId", "notification", "uploadId", "uploads",
"partNumber", "website", "response-content-type", "response-content-language", "response-expires",
"response-cache-control", "response-content-disposition", "response-content-encoding", "delete");
private final SignatureWire signatureWire;
private final Supplier<Credentials> creds;
private final Provider<String> timeStampProvider;
private final Crypto crypto;
private final HttpUtils utils;
@Resource
@Named(Constants.LOGGER_SIGNATURE)
Logger signatureLog = Logger.NULL;
private final String authTag;
private final String headerTag;
private final String servicePath;
private final boolean isVhostStyle;
@Inject
public RequestAuthorizeSignature(SignatureWire signatureWire, @Named(PROPERTY_AUTH_TAG) String authTag,
@Named(PROPERTY_S3_VIRTUAL_HOST_BUCKETS) boolean isVhostStyle,
@Named(PROPERTY_S3_SERVICE_PATH) String servicePath, @Named(PROPERTY_HEADER_TAG) String headerTag,
@org.jclouds.location.Provider Supplier<Credentials> creds,
@TimeStamp Provider<String> timeStampProvider, Crypto crypto, HttpUtils utils) {
this.isVhostStyle = isVhostStyle;
this.servicePath = servicePath;
this.headerTag = headerTag;
this.authTag = authTag;
this.signatureWire = signatureWire;
this.creds = creds;
this.timeStampProvider = timeStampProvider;
this.crypto = crypto;
this.utils = utils;
}
public HttpRequest filter(HttpRequest request) throws HttpException {
request = replaceDateHeader(request);
Credentials current = creds.get();
if (current instanceof SessionCredentials) {
request = replaceSecurityTokenHeader(request, SessionCredentials.class.cast(current));
}
String signature = calculateSignature(createStringToSign(request));
request = replaceAuthorizationHeader(request, signature);
utils.logRequest(signatureLog, request, "<<");
return request;
}
HttpRequest replaceSecurityTokenHeader(HttpRequest request, SessionCredentials current) {
return request.toBuilder().replaceHeader("x-amz-security-token", current.getSessionToken()).build();
}
HttpRequest replaceAuthorizationHeader(HttpRequest request, String signature) {
request = request.toBuilder()
.replaceHeader(HttpHeaders.AUTHORIZATION, authTag + " " + creds.get().identity + ":" + signature).build();
return request;
}
HttpRequest replaceDateHeader(HttpRequest request) {
request = request.toBuilder().replaceHeader(HttpHeaders.DATE, timeStampProvider.get()).build();
return request;
}
public String createStringToSign(HttpRequest request) {
utils.logRequest(signatureLog, request, ">>");
SortedSetMultimap<String, String> canonicalizedHeaders = TreeMultimap.create();
StringBuilder buffer = new StringBuilder();
// re-sign the request
appendMethod(request, buffer);
appendPayloadMetadata(request, buffer);
appendHttpHeaders(request, canonicalizedHeaders);
// Remove default date timestamp if "x-amz-date" is set.
if (canonicalizedHeaders.containsKey("x-" + headerTag + "-date")) {
canonicalizedHeaders.removeAll("date");
}
appendAmzHeaders(canonicalizedHeaders, buffer);
appendBucketName(request, buffer);
appendUriPath(request, buffer);
if (signatureWire.enabled())
signatureWire.output(buffer.toString());
return buffer.toString();
}
String calculateSignature(String toSign) throws HttpException {
String signature = sign(toSign);
if (signatureWire.enabled())
signatureWire.input(toInputStream(signature));
return signature;
}
public String sign(String toSign) {
String signature;
try {
signature = CryptoStreams.base64(CryptoStreams.mac(InputSuppliers.of(toSign), crypto.hmacSHA1(
creds.get().credential.getBytes())));
} catch (Exception e) {
throw new HttpException("error signing request", e);
}
return signature;
}
void appendMethod(HttpRequest request, StringBuilder toSign) {
toSign.append(request.getMethod()).append("\n");
}
@VisibleForTesting
void appendAmzHeaders(SortedSetMultimap<String, String> canonicalizedHeaders, StringBuilder toSign) {
for (Entry<String, String> header : canonicalizedHeaders.entries()) {
String key = header.getKey();
if (key.startsWith("x-" + headerTag + "-")) {
toSign.append(String.format("%s:%s\n", key.toLowerCase(), header.getValue()));
}
}
}
void appendPayloadMetadata(HttpRequest request, StringBuilder buffer) {
// note that we fall back to headers, and some requests such as ?uploads do not have a
// payload, yet specify payload related parameters
buffer.append(
request.getPayload() == null ? Strings.nullToEmpty(request.getFirstHeaderOrNull("Content-MD5")) :
HttpUtils.nullToEmpty(request.getPayload() == null ? null : request.getPayload().getContentMetadata()
.getContentMD5())).append("\n");
buffer.append(
Strings.nullToEmpty(request.getPayload() == null ? request.getFirstHeaderOrNull(HttpHeaders.CONTENT_TYPE)
: request.getPayload().getContentMetadata().getContentType())).append("\n");
for (String header : FIRST_HEADERS_TO_SIGN)
buffer.append(HttpUtils.nullToEmpty(request.getHeaders().get(header))).append("\n");
}
@VisibleForTesting
void appendHttpHeaders(HttpRequest request, SortedSetMultimap<String, String> canonicalizedHeaders) {
Multimap<String, String> headers = request.getHeaders();
for (Entry<String, String> header : headers.entries()) {
if (header.getKey() == null)
continue;
String key = header.getKey().toString().toLowerCase(Locale.getDefault());
// Ignore any headers that are not particularly interesting.
if (key.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE) || key.equalsIgnoreCase("Content-MD5")
|| key.equalsIgnoreCase(HttpHeaders.DATE) || key.startsWith("x-" + headerTag + "-")) {
canonicalizedHeaders.put(key, header.getValue());
}
}
}
@VisibleForTesting
void appendBucketName(HttpRequest req, StringBuilder toSign) {
String bucketName = S3Utils.getBucketName(req);
// If we have a payload/bucket/container that is not all lowercase, vhost-style URLs are not an option and must be
// automatically converted to their path-based equivalent. This should only be possible for AWS-S3 since it is
// the only S3 implementation configured to allow uppercase payload/bucket/container names.
//
// http://code.google.com/p/jclouds/issues/detail?id=992
if (isVhostStyle && bucketName!= null && bucketName.equals(bucketName.toLowerCase()))
toSign.append(servicePath).append(bucketName);
}
@VisibleForTesting
void appendUriPath(HttpRequest request, StringBuilder toSign) {
toSign.append(request.getEndpoint().getRawPath());
// ...however, there are a few exceptions that must be included in the
// signed URI.
if (request.getEndpoint().getQuery() != null) {
Multimap<String, String> params = queryParser().apply(request.getEndpoint().getQuery());
char separator = '?';
for (String paramName : Ordering.natural().sortedCopy(params.keySet())) {
// Skip any parameters that aren't part of the canonical signed string
if (SIGNED_PARAMETERS.contains(paramName) == false)
continue;
toSign.append(separator).append(paramName);
String paramValue = get(params.get(paramName), 0);
if (paramValue != null) {
toSign.append("=").append(paramValue);
}
separator = '&';
}
}
}
}