blob: 6405800fd241c7b8169654a55b8fbd4be2cf87ac [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.azure.storage.filters;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.io.ByteStreams.readBytes;
import static org.jclouds.crypto.Macs.asByteProcessor;
import static org.jclouds.util.Patterns.NEWLINE_PATTERN;
import static org.jclouds.util.Strings2.toInputStream;
import org.jclouds.http.Uris.UriBuilder;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import org.jclouds.http.Uris;
import java.net.URI;
import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import org.jclouds.Constants;
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.util.Strings2;
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.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Multimaps;
import com.google.common.io.ByteProcessor;
import com.google.common.net.HttpHeaders;
/**
* Signs the Azure Storage request.
*
* @see <a href= "http://msdn.microsoft.com/en-us/library/dd179428.aspx" />
*/
@Singleton
public class SharedKeyLiteAuthentication implements HttpRequestFilter {
private static final Collection<String> FIRST_HEADERS_TO_SIGN = ImmutableList.of(HttpHeaders.DATE);
private final SignatureWire signatureWire;
private final Supplier<Credentials> creds;
private final Provider<String> timeStampProvider;
private final Crypto crypto;
private final String credential;
private final String identity;
private final HttpUtils utils;
private final URI storageUrl;
private final boolean isSAS;
@Resource
@Named(Constants.LOGGER_SIGNATURE)
Logger signatureLog = Logger.NULL;
@Inject
public SharedKeyLiteAuthentication(SignatureWire signatureWire,
@org.jclouds.location.Provider Supplier<Credentials> creds, @TimeStamp Provider<String> timeStampProvider,
Crypto crypto, HttpUtils utils, @Named("sasAuth") boolean sasAuthentication) {
this.crypto = crypto;
this.utils = utils;
this.signatureWire = signatureWire;
this.storageUrl = URI.create("https://" + creds.get().identity + ".blob.core.windows.net/");
this.creds = creds;
this.identity = creds.get().identity;
this.credential = creds.get().credential;
this.timeStampProvider = timeStampProvider;
this.isSAS = sasAuthentication;
}
/**
* this is an updated filter method, which decides whether the SAS or SharedKeyLite
* is used and applies the right filtering.
*/
public HttpRequest filter(HttpRequest request) throws HttpException {
request = this.isSAS ? filterSAS(request, this.credential) : filterKey(request);
utils.logRequest(signatureLog, request, "<<");
return request;
}
/**
* this filter method is applied only for the cases with SAS Authentication.
*/
public HttpRequest filterSAS(HttpRequest request, String credential) throws HttpException, IllegalArgumentException {
URI requestUri = request.getEndpoint();
String formattedCredential = credential.startsWith("?") ? credential.substring(1) : credential;
String initialQuery = requestUri.getQuery();
String finalQuery = initialQuery == null ? formattedCredential : initialQuery + "&" + formattedCredential;
String[] parametersArray = cutUri(requestUri);
String containerName = parametersArray[1];
UriBuilder endpoint = Uris.uriBuilder(storageUrl).appendPath(containerName);
if (parametersArray.length >= 3) {
String[] blobNameParts = Arrays.copyOfRange(parametersArray, 2, parametersArray.length);
String blobName = Joiner.on("/").join(blobNameParts);
endpoint.appendPath(blobName).query(finalQuery);
} else {
endpoint.query("restype=container&" + finalQuery);
}
return removeAuthorizationHeader(
replaceDateHeader(request.toBuilder()
.endpoint(endpoint.build())
.build()));
}
/**
* this is a 'standard' filter method, applied when SharedKeyLite authentication is used.
*/
public HttpRequest filterKey(HttpRequest request) throws HttpException {
request = replaceDateHeader(request);
String signature = calculateSignature(createStringToSign(request));
return replaceAuthorizationHeader(request, signature);
}
HttpRequest replaceAuthorizationHeader(HttpRequest request, String signature) {
return request.toBuilder()
.replaceHeader(HttpHeaders.AUTHORIZATION, "SharedKeyLite " + creds.get().identity + ":" + signature)
.build();
}
/**
* this method removes Authorisation header, since it is not needed for SAS Authentication
*/
HttpRequest removeAuthorizationHeader(HttpRequest request) {
return request.toBuilder()
.removeHeader(HttpHeaders.AUTHORIZATION)
.build();
}
HttpRequest replaceDateHeader(HttpRequest request) {
Builder<String, String> builder = ImmutableMap.builder();
String date = timeStampProvider.get();
builder.put(HttpHeaders.DATE, date);
request = request.toBuilder().replaceHeaders(Multimaps.forMap(builder.build())).build();
return request;
}
/**
* this is the method to parse container name and blob name from the HttpRequest.
*/
public String[] cutUri(URI uri) throws IllegalArgumentException {
String path = uri.getPath();
String[] result = path.split("/");
if (result.length < 2) {
throw new IllegalArgumentException("there is neither ContainerName nor BlobName in the URI path");
}
return result;
}
public String createStringToSign(HttpRequest request) {
utils.logRequest(signatureLog, request, ">>");
StringBuilder buffer = new StringBuilder();
// re-sign the request
appendMethod(request, buffer);
appendPayloadMetadata(request, buffer);
appendHttpHeaders(request, buffer);
appendCanonicalizedHeaders(request, buffer);
appendCanonicalizedResource(request, buffer);
if (signatureWire.enabled())
signatureWire.output(buffer.toString());
return buffer.toString();
}
private void appendPayloadMetadata(HttpRequest request, StringBuilder buffer) {
buffer.append(
HttpUtils.nullToEmpty(request.getPayload() == null ? null : request.getPayload().getContentMetadata()
.getContentMD5())).append("\n");
buffer.append(
Strings.nullToEmpty(request.getPayload() == null ? null : request.getPayload().getContentMetadata()
.getContentType())).append("\n");
}
public String calculateSignature(String toSign) throws HttpException {
String signature = signString(toSign);
if (signatureWire.enabled())
signatureWire.input(Strings2.toInputStream(signature));
return signature;
}
public String signString(String toSign) {
try {
ByteProcessor<byte[]> hmacSHA256 = asByteProcessor(crypto.hmacSHA256(base64().decode(creds.get().credential)));
return base64().encode(readBytes(toInputStream(toSign), hmacSHA256));
} catch (Exception e) {
throw new HttpException("error signing request", e);
}
}
private void appendMethod(HttpRequest request, StringBuilder toSign) {
toSign.append(request.getMethod()).append("\n");
}
private void appendCanonicalizedHeaders(HttpRequest request, StringBuilder toSign) {
// TreeMap == Sort the headers alphabetically.
Map<String, String> headers = Maps.newTreeMap();
Multimap<String, String> requestHeaders = request.getHeaders();
for (String header : requestHeaders.keySet()) {
if (header.startsWith("x-ms-")) {
String value = Joiner.on(",").join(Iterables.transform(requestHeaders.get(header),
new Function<String, Object>()
{
@Override
public Object apply(final String value) {
return NEWLINE_PATTERN.matcher(value).replaceAll("");
}
})
);
headers.put(header.toLowerCase(), value);
}
}
for (Entry<String, String> entry : headers.entrySet()) {
toSign.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
}
}
private void appendHttpHeaders(HttpRequest request, StringBuilder toSign) {
for (String header : FIRST_HEADERS_TO_SIGN)
toSign.append(HttpUtils.nullToEmpty(request.getHeaders().get(header))).append("\n");
}
@VisibleForTesting
void appendCanonicalizedResource(HttpRequest request, StringBuilder toSign) {
// 1. Beginning with an empty string (""), append a forward slash (/), followed by the name of
// the identity that owns the resource being accessed.
toSign.append("/").append(creds.get().identity);
appendUriPath(request, toSign);
}
@VisibleForTesting
void appendUriPath(HttpRequest request, StringBuilder toSign) {
// 2. Append the resource's encoded URI path
toSign.append(request.getEndpoint().getRawPath());
// If the request URI addresses a component of the
// resource, append the appropriate query string. The query string should include the question
// mark and the comp parameter (for example, ?comp=metadata). No other parameters should be
// included on the query string.
if (request.getEndpoint().getQuery() != null) {
StringBuilder paramsToSign = new StringBuilder("?");
String[] params = request.getEndpoint().getQuery().split("&");
for (String param : params) {
String[] paramNameAndValue = param.split("=");
if ("comp".equals(paramNameAndValue[0])) {
paramsToSign.append(param);
}
}
if (paramsToSign.length() > 1) {
toSign.append(paramsToSign);
}
}
}
}