blob: b63f5a9ae1a6a6f7eb5b07b776b9c69c9d349de5 [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.aws.filters;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Ordering.natural;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.io.ByteStreams.readBytes;
import static org.jclouds.aws.filters.FormSignerUtils.getAnnotatedApiVersion;
import static org.jclouds.aws.reference.FormParameters.ACTION;
import static org.jclouds.aws.reference.FormParameters.AWS_ACCESS_KEY_ID;
import static org.jclouds.aws.reference.FormParameters.SECURITY_TOKEN;
import static org.jclouds.aws.reference.FormParameters.SIGNATURE;
import static org.jclouds.aws.reference.FormParameters.SIGNATURE_METHOD;
import static org.jclouds.aws.reference.FormParameters.SIGNATURE_VERSION;
import static org.jclouds.aws.reference.FormParameters.TIMESTAMP;
import static org.jclouds.aws.reference.FormParameters.VERSION;
import static org.jclouds.crypto.Macs.asByteProcessor;
import static org.jclouds.http.utils.Queries.encodeQueryLine;
import static org.jclouds.http.utils.Queries.queryParser;
import static org.jclouds.util.Strings2.toInputStream;
import java.util.Comparator;
import java.util.Set;
import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import org.jclouds.Constants;
import org.jclouds.aws.domain.SessionCredentials;
import org.jclouds.crypto.Crypto;
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.logging.Logger;
import org.jclouds.rest.RequestSigner;
import org.jclouds.rest.annotations.ApiVersion;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
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.TreeMultimap;
import com.google.common.io.ByteProcessor;
import com.google.common.net.HttpHeaders;
import com.google.inject.ImplementedBy;
@ImplementedBy(FormSigner.FormSignerV2.class)
public interface FormSigner extends HttpRequestFilter {
static final class FormSignerV2 implements FormSigner, RequestSigner {
public static final Set<String> mandatoryParametersForSignature = ImmutableSet
.of(ACTION, SIGNATURE_METHOD, SIGNATURE_VERSION, VERSION);
private final SignatureWire signatureWire;
private final String apiVersion;
private final Supplier<Credentials> creds;
private final Provider<String> dateService;
private final Crypto crypto;
private final HttpUtils utils;
@Resource @Named(Constants.LOGGER_SIGNATURE)
private Logger signatureLog = Logger.NULL;
@Inject FormSignerV2(SignatureWire signatureWire, @ApiVersion String apiVersion,
@org.jclouds.location.Provider Supplier<Credentials> creds, @TimeStamp Provider<String> dateService,
Crypto crypto, HttpUtils utils) {
this.signatureWire = signatureWire;
this.apiVersion = apiVersion;
this.creds = creds;
this.dateService = dateService;
this.crypto = crypto;
this.utils = utils;
}
public HttpRequest filter(HttpRequest request) throws HttpException {
checkNotNull(request.getFirstHeaderOrNull(HttpHeaders.HOST), "request is not ready to sign; host not present");
Multimap<String, String> decodedParams = queryParser().apply(request.getPayload().getRawContent().toString());
Optional<String> optAnnotatedVersion = getAnnotatedApiVersion(request);
String version;
if (optAnnotatedVersion.isPresent()) {
String annotatedVersion = optAnnotatedVersion.get();
version = annotatedVersion.compareTo(apiVersion) > 0 ? annotatedVersion : apiVersion;
} else {
version = apiVersion;
}
decodedParams.replaceValues(VERSION, ImmutableSet.of(version));
addSigningParams(decodedParams);
validateParams(decodedParams);
String stringToSign = createStringToSign(request, decodedParams);
String signature = sign(stringToSign);
addSignature(decodedParams, signature);
request = setPayload(request, decodedParams);
utils.logRequest(signatureLog, request, "<<");
return request;
}
HttpRequest setPayload(HttpRequest request, Multimap<String, String> decodedParams) {
String queryLine = buildQueryLine(decodedParams);
request.setPayload(queryLine);
request.getPayload().getContentMetadata().setContentType("application/x-www-form-urlencoded");
return request;
}
private static final Comparator<String> actionFirstAccessKeyLast = new Comparator<String>() {
static final int LEFT_IS_GREATER = 1;
static final int RIGHT_IS_GREATER = -1;
@Override
public int compare(String left, String right) {
if (Objects.equal(left, right)) {
return 0;
}
if ("Action".equals(right) || "AWSAccessKeyId".equals(left)) {
return LEFT_IS_GREATER;
}
if ("Action".equals(left) || "AWSAccessKeyId".equals(right)) {
return RIGHT_IS_GREATER;
}
return natural().compare(left, right);
}
};
private static String buildQueryLine(Multimap<String, String> decodedParams) {
Multimap<String, String> sortedParams = TreeMultimap.create(actionFirstAccessKeyLast, natural());
sortedParams.putAll(decodedParams);
return encodeQueryLine(sortedParams);
}
@VisibleForTesting void validateParams(Multimap<String, String> params) {
for (String parameter : mandatoryParametersForSignature) {
checkState(params.containsKey(parameter), "parameter " + parameter + " is required for signature");
}
}
@VisibleForTesting void addSignature(Multimap<String, String> params, String signature) {
params.replaceValues(SIGNATURE, ImmutableList.of(signature));
}
@VisibleForTesting
public String sign(String toSign) {
String signature;
try {
ByteProcessor<byte[]> hmacSHA256 = asByteProcessor(
crypto.hmacSHA256(creds.get().credential.getBytes(UTF_8)));
signature = base64().encode(readBytes(toInputStream(toSign), hmacSHA256));
if (signatureWire.enabled())
signatureWire.input(toInputStream(signature));
} catch (Exception e) {
throw new HttpException("error signing request", e);
}
return signature;
}
@VisibleForTesting
public String createStringToSign(HttpRequest request, Multimap<String, String> decodedParams) {
utils.logRequest(signatureLog, request, ">>");
StringBuilder stringToSign = new StringBuilder();
// StringToSign = HTTPVerb + "\n" +
stringToSign.append(request.getMethod()).append("\n");
// ValueOfHostHeaderInLowercase + "\n" +
stringToSign.append(request.getFirstHeaderOrNull(HttpHeaders.HOST).toLowerCase()).append("\n");
// HTTPRequestURI + "\n" +
stringToSign.append(request.getEndpoint().getPath()).append("\n");
// CanonicalizedFormString <from the preceding step>
stringToSign.append(buildCanonicalizedString(decodedParams));
if (signatureWire.enabled())
signatureWire.output(stringToSign.toString());
return stringToSign.toString();
}
@VisibleForTesting String buildCanonicalizedString(Multimap<String, String> decodedParams) {
// note that aws wants to percent encode the canonicalized string without skipping '/' and '?'
return encodeQueryLine(TreeMultimap.create(decodedParams), ImmutableList.<Character>of());
}
@VisibleForTesting void addSigningParams(Multimap<String, String> params) {
params.removeAll(SIGNATURE);
params.removeAll(SECURITY_TOKEN);
Credentials current = creds.get();
if (current instanceof SessionCredentials) {
params.put(SECURITY_TOKEN, SessionCredentials.class.cast(current).getSessionToken());
}
params.replaceValues(SIGNATURE_METHOD, ImmutableList.of("HmacSHA256"));
params.replaceValues(SIGNATURE_VERSION, ImmutableList.of("2"));
params.replaceValues(TIMESTAMP, ImmutableList.of(dateService.get()));
params.replaceValues(AWS_ACCESS_KEY_ID, ImmutableList.of(creds.get().identity));
}
public String createStringToSign(HttpRequest input) {
return createStringToSign(input, queryParser().apply(input.getPayload().getRawContent().toString()));
}
}
}