/**
* Copyright Microsoft Corporation
*
* Licensed 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 com.microsoft.windowsazure.services.blob.implementation;
import java.util.Date;
import java.util.List;
import javax.inject.Named;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.microsoft.windowsazure.core.RFC1123DateConverter;
import com.microsoft.windowsazure.core.pipeline.jersey.EntityStreamingListener;
import com.microsoft.windowsazure.services.blob.BlobConfiguration;
import com.microsoft.windowsazure.services.blob.implementation.SharedKeyUtils.QueryParam;
import com.sun.jersey.api.client.ClientRequest;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.filter.ClientFilter;
public class SharedKeyLiteFilter extends ClientFilter implements
EntityStreamingListener {
private static Log log = LogFactory.getLog(SharedKeyLiteFilter.class);
private final String accountName;
private final HmacSHA256Sign signer;
public SharedKeyLiteFilter(
@Named(BlobConfiguration.ACCOUNT_NAME) String accountName,
@Named(BlobConfiguration.ACCOUNT_KEY) String accountKey) {
this.accountName = accountName;
this.signer = new HmacSHA256Sign(accountKey);
}
@Override
public ClientResponse handle(ClientRequest cr) {
// Only sign if no other filter is handling authorization
if (cr.getProperties().get(SharedKeyUtils.AUTHORIZATION_FILTER_MARKER) == null) {
cr.getProperties().put(SharedKeyUtils.AUTHORIZATION_FILTER_MARKER,
null);
// Register ourselves as listener so we are called back when the
// entity is
// written to the output stream by the next filter in line.
if (cr.getProperties().get(EntityStreamingListener.class.getName()) == null) {
cr.getProperties().put(EntityStreamingListener.class.getName(),
this);
}
}
return this.getNext().handle(cr);
}
@Override
public void onBeforeStreamingEntity(ClientRequest clientRequest) {
// All headers should be known at this point, time to sign!
sign(clientRequest);
}
/*
* StringToSign = VERB + "\n" + Content-MD5 + "\n" + Content-Type + "\n" +
* Date + "\n" + CanonicalizedHeaders + CanonicalizedResource;
*/
public void sign(ClientRequest cr) {
// gather signed material
String requestMethod = cr.getMethod();
String contentMD5 = getHeader(cr, "Content-MD5");
String contentType = getHeader(cr, "Content-Type");
String date = getHeader(cr, "Date");
if (date == "") {
date = new RFC1123DateConverter().format(new Date());
cr.getHeaders().add("Date", date);
}
// build signed string
String stringToSign = requestMethod + "\n" + contentMD5 + "\n"
+ contentType + "\n" + date + "\n";
stringToSign += addCanonicalizedHeaders(cr);
stringToSign += addCanonicalizedResource(cr);
if (log.isDebugEnabled()) {
log.debug(String.format("String to sign: \"%s\"", stringToSign));
}
String signature = this.signer.sign(stringToSign);
cr.getHeaders().putSingle("Authorization",
"SharedKeyLite " + this.accountName + ":" + signature);
}
/**
* Constructing the Canonicalized Headers String
*
* To construct the CanonicalizedHeaders portion of the signature string,
* follow these steps:
*
* 1. Retrieve all headers for the resource that begin with x-ms-, including
* the x-ms-date header.
*
* 2. Convert each HTTP header name to lowercase.
*
* 3. Sort the headers lexicographically by header name in ascending order.
* Note that each header may appear only once in the string.
*
* 4. Unfold the string by replacing any breaking white space with a single
* space.
*
* 5. Trim any white space around the colon in the header.
*
* 6. Finally, append a new line character to each canonicalized header in
* the resulting list. Construct the CanonicalizedHeaders string by
* concatenating all headers in this list into a single string.
*/
private String addCanonicalizedHeaders(ClientRequest cr) {
return SharedKeyUtils.getCanonicalizedHeaders(cr);
}
/**
* This format supports Shared Key and Shared Key Lite for all versions of
* the Table service, and Shared Key Lite for the 2009-09-19 version of the
* Blob and Queue services. This format is identical to that used with
* previous versions of the storage services. Construct the
* CanonicalizedResource string in this format as follows:
*
* 1. Beginning with an empty string (""), append a forward slash (/),
* followed by the name of the account that owns the resource being
* accessed.
*
* 2. Append the resource's encoded URI path. 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.
*/
private String addCanonicalizedResource(ClientRequest cr) {
String result = "/" + this.accountName;
result += cr.getURI().getPath();
List<QueryParam> queryParams = SharedKeyUtils.getQueryParams(cr
.getURI().getQuery());
for (QueryParam p : queryParams) {
if ("comp".equals(p.getName())) {
result += "?" + p.getName() + "=" + p.getValues().get(0);
}
}
return result;
}
private String getHeader(ClientRequest cr, String headerKey) {
return SharedKeyUtils.getHeader(cr, headerKey);
}
}