diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/common/ServiceException.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/common/ServiceException.java index 37a6e0be9304f..ed0518534fbc0 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/common/ServiceException.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/common/ServiceException.java @@ -14,6 +14,7 @@ public class ServiceException extends Exception { String errorCode; String errorMessage; Map errorValues; + String rawResponseBody; public ServiceException() { init(); @@ -38,6 +39,14 @@ private void init() { errorValues = new HashMap(); } + @Override + public String getMessage() { + if (this.rawResponseBody == null) + return super.getMessage(); + else + return super.getMessage() + "\nResponse Body: " + this.rawResponseBody; + } + public int getHttpStatusCode() { return httpStatusCode; } @@ -94,4 +103,11 @@ public void setServiceName(String serviceName) { this.serviceName = serviceName; } + public void setRawResponseBody(String body) { + this.rawResponseBody = body; + } + + public String getRawResponseBody() { + return rawResponseBody; + } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/Exports.java index 10a7c9948cf3d..2ac8a38de32cf 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/Exports.java @@ -1,9 +1,14 @@ package com.microsoft.windowsazure.services.blob; +import java.util.Map; + import com.microsoft.windowsazure.common.Builder; import com.microsoft.windowsazure.services.blob.implementation.BlobExceptionProcessor; import com.microsoft.windowsazure.services.blob.implementation.BlobRestProxy; +import com.microsoft.windowsazure.services.blob.implementation.HttpURLConnectionClient; +import com.microsoft.windowsazure.services.blob.implementation.SharedKeyFilter; import com.microsoft.windowsazure.services.blob.implementation.SharedKeyLiteFilter; +import com.sun.jersey.api.client.config.ClientConfig; public class Exports implements Builder.Exports { public void register(Builder.Registry registry) { @@ -12,5 +17,15 @@ public void register(Builder.Registry registry) { registry.add(BlobExceptionProcessor.class); registry.add(BlobRestProxy.class); registry.add(SharedKeyLiteFilter.class); + registry.add(SharedKeyFilter.class); + + registry.add(new Builder.Factory() { + public HttpURLConnectionClient create(String profile, Builder builder, Map properties) { + ClientConfig clientConfig = (ClientConfig) properties.get("ClientConfig"); + HttpURLConnectionClient client = HttpURLConnectionClient.create(clientConfig); + //client.addFilter(new LoggingFilter()); + return client; + } + }); } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/BlobRestProxy.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/BlobRestProxy.java index d69a806095d40..051208e69aecb 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/BlobRestProxy.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/BlobRestProxy.java @@ -56,26 +56,23 @@ import com.microsoft.windowsazure.services.blob.models.SetBlobPropertiesResult; import com.microsoft.windowsazure.services.blob.models.SetContainerMetadataOptions; import com.microsoft.windowsazure.utils.jersey.ClientFilterAdapter; -import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource.Builder; import com.sun.jersey.core.util.Base64; public class BlobRestProxy implements BlobContract { - // private static Log log = LogFactory.getLog(BlobRestProxy.class); - private static final String API_VERSION = "2011-08-18"; - private final Client channel; + private final HttpURLConnectionClient channel; private final String accountName; private final String url; private final RFC1123DateConverter dateMapper; private final ServiceFilter[] filters; - private final SharedKeyLiteFilter filter; + private final SharedKeyFilter filter; @Inject - public BlobRestProxy(Client channel, @Named(BlobConfiguration.ACCOUNT_NAME) String accountName, @Named(BlobConfiguration.URL) String url, - SharedKeyLiteFilter filter) { + public BlobRestProxy(HttpURLConnectionClient channel, @Named(BlobConfiguration.ACCOUNT_NAME) String accountName, @Named(BlobConfiguration.URL) String url, + SharedKeyFilter filter) { this.channel = channel; this.accountName = accountName; @@ -86,9 +83,10 @@ public BlobRestProxy(Client channel, @Named(BlobConfiguration.ACCOUNT_NAME) Stri channel.addFilter(filter); } - public BlobRestProxy(Client channel, ServiceFilter[] filters, String accountName, String url, SharedKeyLiteFilter filter, RFC1123DateConverter dateMapper) { + public BlobRestProxy(HttpURLConnectionClient client, ServiceFilter[] filters, String accountName, String url, SharedKeyFilter filter, + RFC1123DateConverter dateMapper) { - this.channel = channel; + this.channel = client; this.filters = filters; this.accountName = accountName; this.url = url; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/EntityStreamingListener.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/EntityStreamingListener.java new file mode 100644 index 0000000000000..fa5774e5c2a44 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/EntityStreamingListener.java @@ -0,0 +1,14 @@ +package com.microsoft.windowsazure.services.blob.implementation; + +import com.sun.jersey.api.client.ClientRequest; + +public interface EntityStreamingListener { + /** + * This method is called just before the entity is streamed to the underlying connection. This is the last chance + * for filters to inspect and modify the headers of the client request if necessary. + * + * @param clientRequest + * The client request + */ + void onBeforeStreamingEntity(ClientRequest clientRequest); +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClient.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClient.java new file mode 100644 index 0000000000000..6824d019cf7d1 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClient.java @@ -0,0 +1,21 @@ +package com.microsoft.windowsazure.services.blob.implementation; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.config.ClientConfig; + +public class HttpURLConnectionClient extends Client { + private final HttpURLConnectionClientHandler rootHandler; + + public HttpURLConnectionClient(HttpURLConnectionClientHandler handler, ClientConfig config) { + super(handler, config); + this.rootHandler = handler; + } + + public static HttpURLConnectionClient create(ClientConfig config) { + return new HttpURLConnectionClient(new HttpURLConnectionClientHandler(), config); + } + + public HttpURLConnectionClientHandler getRootHandler() { + return rootHandler; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClientHandler.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClientHandler.java new file mode 100644 index 0000000000000..cb0c994b85b37 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClientHandler.java @@ -0,0 +1,267 @@ +package com.microsoft.windowsazure.services.blob.implementation; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.MultivaluedMap; + +import com.microsoft.windowsazure.services.blob.implementation.JerseyHelpers.EnumCommaStringBuilder; +import com.sun.jersey.api.client.ClientHandlerException; +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.CommittingOutputStream; +import com.sun.jersey.api.client.TerminatingClientHandler; +import com.sun.jersey.api.client.config.ClientConfig; +import com.sun.jersey.core.header.InBoundHeaders; + +public class HttpURLConnectionClientHandler extends TerminatingClientHandler { + /** + * Empty "no-op" listener if none registered + */ + private static final EntityStreamingListener EMPTY_STREAMING_LISTENER = new EntityStreamingListener() { + public void onBeforeStreamingEntity(ClientRequest clientRequest) { + } + }; + + /** + * OutputStream used for buffering entity body when "Content-Length" is not known in advance. + */ + private final class BufferingOutputStream extends OutputStream { + private final ByteArrayOutputStream outputStream; + private final HttpURLConnection urlConnection; + private final ClientRequest clientRequest; + private final EntityStreamingListener entityStreamingListener; + private boolean closed; + + private BufferingOutputStream(HttpURLConnection urlConnection, ClientRequest clientRequest, + EntityStreamingListener entityStreamingListener) { + this.outputStream = new ByteArrayOutputStream(); + this.urlConnection = urlConnection; + this.clientRequest = clientRequest; + this.entityStreamingListener = entityStreamingListener; + } + + @Override + public void close() throws IOException { + outputStream.close(); + + if (!closed) { + closed = true; + + // Give the listener a last change to modify headers now that the content length is known + setContentLengthHeader(clientRequest, outputStream.size()); + entityStreamingListener.onBeforeStreamingEntity(clientRequest); + + // Write headers, then entity to the http connection. + setURLConnectionHeaders(clientRequest.getHeaders(), urlConnection); + + // Since we buffered the entity and we know the content size, we might as well + // use the "fixed length" streaming mode of HttpURLConnection to stream + // the buffer directly. + urlConnection.setFixedLengthStreamingMode(outputStream.size()); + OutputStream httpOutputStream = urlConnection.getOutputStream(); + outputStream.writeTo(httpOutputStream); + httpOutputStream.flush(); + httpOutputStream.close(); + } + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void write(byte[] b, int off, int len) { + outputStream.write(b, off, len); + } + + @Override + public void write(byte[] b) throws IOException { + outputStream.write(b); + } + + @Override + public void write(int b) { + outputStream.write(b); + } + } + + /** + * OutputStream used for directly streaming entity to url connection stream. Headers are written just before sending + * the first bytes to the output stream. + */ + private final class StreamingOutputStream extends CommittingOutputStream { + private final HttpURLConnection urlConnection; + private final ClientRequest clientRequest; + + private StreamingOutputStream(HttpURLConnection urlConnection, ClientRequest clientRequest) { + this.urlConnection = urlConnection; + this.clientRequest = clientRequest; + } + + @Override + protected OutputStream getOutputStream() throws IOException { + return urlConnection.getOutputStream(); + } + + @Override + public void commit() throws IOException { + setURLConnectionHeaders(clientRequest.getHeaders(), urlConnection); + } + } + + /** + * Simple response implementation around an HttpURLConnection response + */ + private final class URLConnectionResponse extends ClientResponse { + private final String method; + private final HttpURLConnection urlConnection; + + URLConnectionResponse(int status, InBoundHeaders headers, InputStream entity, String method, + HttpURLConnection urlConnection) { + super(status, headers, entity, getMessageBodyWorkers()); + this.method = method; + this.urlConnection = urlConnection; + } + + @Override + public boolean hasEntity() { + if (method.equals("HEAD") || getEntityInputStream() == null) + return false; + + // Length "-1" means "unknown" + int length = urlConnection.getContentLength(); + return length > 0 || length == -1; + } + + @Override + public String toString() { + return urlConnection.getRequestMethod() + " " + urlConnection.getURL() + " returned a response status of " + + this.getStatus() + " " + this.getClientResponseStatus(); + } + } + + public ClientResponse handle(final ClientRequest ro) throws ClientHandlerException { + try { + return doHandle(ro); + } + catch (Exception e) { + throw new ClientHandlerException(e); + } + } + + private ClientResponse doHandle(final ClientRequest clientRequest) throws IOException, MalformedURLException, + ProtocolException { + final HttpURLConnection urlConnection = (HttpURLConnection) clientRequest.getURI().toURL().openConnection(); + final EntityStreamingListener entityStreamingListener = getEntityStreamingListener(clientRequest); + + urlConnection.setRequestMethod(clientRequest.getMethod()); + + // Write the request headers + setURLConnectionHeaders(clientRequest.getHeaders(), urlConnection); + + // Write the entity (if any) + Object entity = clientRequest.getEntity(); + if (entity != null) { + urlConnection.setDoOutput(true); + + writeRequestEntity(clientRequest, new RequestEntityWriterListener() { + private boolean inStreamingMode; + + public void onRequestEntitySize(long size) { + if (size != -1 && size < Integer.MAX_VALUE) { + inStreamingMode = true; + setContentLengthHeader(clientRequest, (int) size); + entityStreamingListener.onBeforeStreamingEntity(clientRequest); + + urlConnection.setFixedLengthStreamingMode((int) size); + } + else { + Integer chunkedEncodingSize = + (Integer) clientRequest.getProperties() + .get(ClientConfig.PROPERTY_CHUNKED_ENCODING_SIZE); + if (chunkedEncodingSize != null) { + inStreamingMode = true; + entityStreamingListener.onBeforeStreamingEntity(clientRequest); + + urlConnection.setChunkedStreamingMode(chunkedEncodingSize); + } + } + } + + public OutputStream onGetOutputStream() throws IOException { + if (inStreamingMode) + return new StreamingOutputStream(urlConnection, clientRequest); + else + return new BufferingOutputStream(urlConnection, clientRequest, entityStreamingListener); + } + }); + } + else { + entityStreamingListener.onBeforeStreamingEntity(clientRequest); + setURLConnectionHeaders(clientRequest.getHeaders(), urlConnection); + } + + // Return the in-bound response + return new URLConnectionResponse(urlConnection.getResponseCode(), getInBoundHeaders(urlConnection), + getInputStream(urlConnection), clientRequest.getMethod(), urlConnection); + } + + private EntityStreamingListener getEntityStreamingListener(final ClientRequest clientRequest) { + EntityStreamingListener result = + (EntityStreamingListener) clientRequest.getProperties().get(EntityStreamingListener.class.getName()); + + if (result != null) + return result; + + return EMPTY_STREAMING_LISTENER; + } + + private void setContentLengthHeader(ClientRequest clientRequest, int size) { + clientRequest.getHeaders().putSingle("Content-Length", size); + } + + private void setURLConnectionHeaders(MultivaluedMap headers, HttpURLConnection urlConnection) { + for (Map.Entry> e : headers.entrySet()) { + List vs = e.getValue(); + if (vs.size() == 1) { + urlConnection.setRequestProperty(e.getKey(), ClientRequest.getHeaderValue(vs.get(0))); + } + else { + EnumCommaStringBuilder sb = new EnumCommaStringBuilder(); + for (Object v : e.getValue()) { + sb.add(ClientRequest.getHeaderValue(v)); + } + urlConnection.setRequestProperty(e.getKey(), sb.toString()); + } + } + } + + private InBoundHeaders getInBoundHeaders(HttpURLConnection urlConnection) { + InBoundHeaders headers = new InBoundHeaders(); + for (Map.Entry> e : urlConnection.getHeaderFields().entrySet()) { + if (e.getKey() != null) + headers.put(e.getKey(), e.getValue()); + } + return headers; + } + + private InputStream getInputStream(HttpURLConnection urlConnection) throws IOException { + if (urlConnection.getResponseCode() < 300) { + return urlConnection.getInputStream(); + } + else { + InputStream ein = urlConnection.getErrorStream(); + return (ein != null) ? ein : new ByteArrayInputStream(new byte[0]); + } + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/JerseyHelpers.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/JerseyHelpers.java index 046af8c0d3875..c480f975be11f 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/JerseyHelpers.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/JerseyHelpers.java @@ -37,12 +37,16 @@ public static WebResource setCanonicalizedResource(WebResource webResource, Stri public static class EnumCommaStringBuilder { private final StringBuilder sb = new StringBuilder(); + public void add(String representation) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(representation); + } + public void addValue(boolean value, String representation) { if (value) { - if (sb.length() > 0) { - sb.append(","); - } - sb.append(representation); + add(representation); } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyFilter.java new file mode 100644 index 0000000000000..0f2537e444529 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyFilter.java @@ -0,0 +1,265 @@ +package com.microsoft.windowsazure.services.blob.implementation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import javax.inject.Named; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.microsoft.windowsazure.services.blob.BlobConfiguration; +import com.sun.jersey.api.client.ClientHandlerException; +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.filter.ClientFilter; + +public class SharedKeyFilter extends ClientFilter implements EntityStreamingListener { + private static Log log = LogFactory.getLog(SharedKeyFilter.class); + + private final String accountName; + private final HmacSHA256Sign signer; + + public SharedKeyFilter(@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) throws ClientHandlerException { + // Only sign if no other filter has done it yet + if (cr.getHeaders().getFirst("Authorization") == null) { + sign(cr); + } + + // 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); + } + + /* + * StringToSign = VERB + "\n" + + * Content-Encoding + "\n" + * Content-Language + "\n" + * Content-Length + "\n" + * Content-MD5 + "\n" + + * Content-Type + "\n" + + * Date + "\n" + + * If-Modified-Since + "\n" + * If-Match + "\n" + * If-None-Match + "\n" + * If-Unmodified-Since + "\n" + * Range + "\n" + * CanonicalizedHeaders + + * CanonicalizedResource; + */ + public void sign(ClientRequest cr) { + // gather signed material + addOptionalDateHeader(cr); + + // build signed string + String stringToSign = + cr.getMethod() + "\n" + getHeader(cr, "Content-Encoding") + "\n" + getHeader(cr, "Content-Language") + + "\n" + getHeader(cr, "Content-Length") + "\n" + getHeader(cr, "Content-MD5") + "\n" + + getHeader(cr, "Content-Type") + "\n" + getHeader(cr, "Date") + "\n" + + getHeader(cr, "If-Modified-Since") + "\n" + getHeader(cr, "If-Match") + "\n" + + getHeader(cr, "If-None-Match") + "\n" + getHeader(cr, "If-Unmodified-Since") + "\n" + + getHeader(cr, "Range") + "\n"; + + stringToSign += getCanonicalizedHeaders(cr); + stringToSign += getCanonicalizedResource(cr); + + if (log.isDebugEnabled()) { + log.debug(String.format("String to sign: \"%s\"", stringToSign)); + } + //System.out.println(String.format("String to sign: \"%s\"", stringToSign)); + + String signature = this.signer.sign(stringToSign); + cr.getHeaders().putSingle("Authorization", "SharedKey " + this.accountName + ":" + signature); + } + + private void addOptionalDateHeader(ClientRequest cr) { + String date = getHeader(cr, "Date"); + if (date == "") { + date = new RFC1123DateConverter().format(new Date()); + cr.getHeaders().add("Date", date); + } + } + + /** + * 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 getCanonicalizedHeaders(ClientRequest cr) { + return SharedKeyUtils.getCanonicalizedHeaders(cr); + } + + /** + * This format supports Shared Key authentication for the 2009-09-19 version of the Blob and Queue 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, without any query parameters. + * + * 3. Retrieve all query parameters on the resource URI, including the comp parameter if it exists. + * + * 4. Convert all parameter names to lowercase. + * + * 5. Sort the query parameters lexicographically by parameter name, in ascending order. + * + * 6. URL-decode each query parameter name and value. + * + * 7. Append each query parameter name and value to the string in the following format, making sure to include the + * colon (:) between the name and the value: + * + * parameter-name:parameter-value + * + * 8. If a query parameter has more than one value, sort all values lexicographically, then include them in a + * comma-separated list: + * + * parameter-name:parameter-value-1,parameter-value-2,parameter-value-n + * + * 9. Append a new line character (\n) after each name-value pair. + */ + private String getCanonicalizedResource(ClientRequest cr) { + // 1. Beginning with an empty string (""), append a forward slash (/), followed by the name of the account that owns + // the resource being accessed. + String result = "/" + this.accountName; + + // 2. Append the resource's encoded URI path, without any query parameters. + result += cr.getURI().getPath(); + + // 3. Retrieve all query parameters on the resource URI, including the comp parameter if it exists. + // 6. URL-decode each query parameter name and value. + List queryParams = getQueryParams(cr.getURI().getQuery()); + + // 4. Convert all parameter names to lowercase. + for (QueryParam param : queryParams) { + param.setName(param.getName().toLowerCase(Locale.US)); + } + + // 5. Sort the query parameters lexicographically by parameter name, in ascending order. + Collections.sort(queryParams); + + // 7. Append each query parameter name and value to the string + // 8. If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list + for (int i = 0; i < queryParams.size(); i++) { + QueryParam param = queryParams.get(i); + + List values = param.getValues(); + //Collections.sort(values); + + // 9. Append a new line character (\n) after each name-value pair. + result += "\n"; + result += param.getName(); + result += ":"; + for (int j = 0; j < values.size(); j++) { + if (j > 0) { + result += ","; + } + result += values.get(j); + } + } + + return result; + } + + private List getQueryParams(String queryString) { + ArrayList result = new ArrayList(); + + if (queryString != null) { + String[] params = queryString.split("&"); + for (String param : params) { + result.add(getQueryParam(param)); + } + } + + return result; + } + + private QueryParam getQueryParam(String param) { + QueryParam result = new QueryParam(); + + int index = param.indexOf("="); + if (index < 0) { + result.setName(param); + } + else { + result.setName(param.substring(0, index)); + + String value = param.substring(index + 1); + int commaIndex = value.indexOf(','); + if (commaIndex < 0) { + result.addValue(value); + } + else { + for (String v : value.split(",")) { + result.addValue(v); + } + } + } + + return result; + } + + private static class QueryParam implements Comparable { + private String name; + private final List values = new ArrayList(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getValues() { + return values; + } + + public void addValue(String value) { + values.add(value); + } + + public int compareTo(QueryParam o) { + return this.name.compareTo(o.name); + } + } + + private String getHeader(ClientRequest cr, String headerKey) { + return SharedKeyUtils.getHeader(cr, headerKey); + } + + public void onBeforeStreamingEntity(ClientRequest clientRequest) { + // All headers should be known at this point, time to sign! + sign(clientRequest); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyLiteFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyLiteFilter.java index 92d21f6d6ae45..0c86663cac1f2 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyLiteFilter.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyLiteFilter.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; -import java.util.List; import java.util.Locale; import javax.inject.Named; @@ -23,7 +22,8 @@ public class SharedKeyLiteFilter extends ClientFilter { private final String accountName; private final HmacSHA256Sign signer; - public SharedKeyLiteFilter(@Named(BlobConfiguration.ACCOUNT_NAME) String accountName, @Named(BlobConfiguration.ACCOUNT_KEY) String accountKey) { + public SharedKeyLiteFilter(@Named(BlobConfiguration.ACCOUNT_NAME) String accountName, + @Named(BlobConfiguration.ACCOUNT_KEY) String accountKey) { this.accountName = accountName; this.signer = new HmacSHA256Sign(accountKey); @@ -74,19 +74,25 @@ public void sign(ClientRequest cr) { 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. + * 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) { ArrayList msHeaders = new ArrayList(); @@ -95,6 +101,7 @@ private String addCanonicalizedHeaders(ClientRequest cr) { msHeaders.add(key.toLowerCase(Locale.US)); } } + Collections.sort(msHeaders); String result = ""; @@ -110,11 +117,6 @@ private String addCanonicalizedResource(ClientRequest cr) { } private String getHeader(ClientRequest cr, String headerKey) { - List values = cr.getHeaders().get(headerKey); - if (values == null || values.size() != 1) { - return nullEmpty(null); - } - - return nullEmpty(values.get(0).toString()); + return SharedKeyUtils.getHeader(cr, headerKey); } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyUtils.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyUtils.java new file mode 100644 index 0000000000000..b2b736fb30e57 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyUtils.java @@ -0,0 +1,53 @@ +package com.microsoft.windowsazure.services.blob.implementation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import com.sun.jersey.api.client.ClientRequest; + +public class SharedKeyUtils { + /* + * 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. + */ + public static String getCanonicalizedHeaders(ClientRequest cr) { + ArrayList msHeaders = new ArrayList(); + for (String key : cr.getHeaders().keySet()) { + if (key.toLowerCase(Locale.US).startsWith("x-ms-")) { + msHeaders.add(key.toLowerCase(Locale.US)); + } + } + Collections.sort(msHeaders); + + String result = ""; + for (String msHeader : msHeaders) { + result += msHeader + ":" + cr.getHeaders().getFirst(msHeader) + "\n"; + } + return result; + } + + public static String getHeader(ClientRequest cr, String headerKey) { + List values = cr.getHeaders().get(headerKey); + if (values == null || values.size() != 1) { + return nullEmpty(null); + } + + return nullEmpty(values.get(0).toString()); + } + + private static String nullEmpty(String value) { + return value != null ? value : ""; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/models/SetBlobPropertiesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/models/SetBlobPropertiesOptions.java index 2e56aa7a634a8..3171956874bbf 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/models/SetBlobPropertiesOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/models/SetBlobPropertiesOptions.java @@ -1,6 +1,5 @@ package com.microsoft.windowsazure.services.blob.models; - public class SetBlobPropertiesOptions extends BlobServiceOptions { private String leaseId; private String contentType; @@ -71,8 +70,8 @@ public Long getSequenceNumber() { return sequenceNumber; } - public SetBlobPropertiesOptions setSequenceNUmber(Long sequenceNUmber) { - this.sequenceNumber = sequenceNUmber; + public SetBlobPropertiesOptions setSequenceNumber(Long sequenceNumber) { + this.sequenceNumber = sequenceNumber; return this; } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/utils/ServiceExceptionFactory.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/utils/ServiceExceptionFactory.java index c912956194a5f..51b8eec3a23a7 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/utils/ServiceExceptionFactory.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/utils/ServiceExceptionFactory.java @@ -24,13 +24,13 @@ else if (UniformInterfaceException.class.isAssignableFrom(scan.getClass())) { return exception; } - static ServiceException populate(ServiceException exception, String serviceName, - UniformInterfaceException cause) { + static ServiceException populate(ServiceException exception, String serviceName, UniformInterfaceException cause) { exception.setServiceName(serviceName); if (cause != null) { ClientResponse response = cause.getResponse(); if (response != null) { + // Set status Status status = response.getClientResponseStatus(); if (status == null) { status = Status.fromStatusCode(response.getStatus()); @@ -42,18 +42,29 @@ static ServiceException populate(ServiceException exception, String serviceName, exception.setHttpStatusCode(status.getStatusCode()); exception.setHttpReasonPhrase(status.getReasonPhrase()); } + + // Set raw response body + if (response.hasEntity()) { + try { + String body = response.getEntity(String.class); + exception.setRawResponseBody(body); + } + catch (Exception e) { + // Skip exceptions as getting the response body as a string is a best effort thing + } + } } } return exception; } - static ServiceException populate(ServiceException exception, String serviceName, - ServiceException cause) { + static ServiceException populate(ServiceException exception, String serviceName, ServiceException cause) { exception.setServiceName(cause.getServiceName()); exception.setHttpStatusCode(cause.getHttpStatusCode()); exception.setHttpReasonPhrase(cause.getHttpReasonPhrase()); exception.setErrorCode(cause.getErrorCode()); exception.setErrorMessage(cause.getErrorMessage()); + exception.setRawResponseBody(cause.getRawResponseBody()); exception.setErrorValues(cause.getErrorValues()); return exception; } diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/IntegrationTestBase.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/IntegrationTestBase.java index 744e7a6f823fe..450f2c428afc1 100644 --- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/IntegrationTestBase.java +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/IntegrationTestBase.java @@ -22,11 +22,6 @@ protected static Configuration createConfiguration() { setConfigValue(config, env, BlobConfiguration.ACCOUNT_KEY, "xxx"); setConfigValue(config, env, BlobConfiguration.URL, "http://xxx.blob.core.windows.net"); - // when mock running - // config.setProperty("serviceBus.uri", "http://localhost:8086"); - // config.setProperty("wrapClient.uri", - // "http://localhost:8081/WRAPv0.9"); - return config; } @@ -41,16 +36,10 @@ private static void setConfigValue(Configuration config, Map pro @BeforeClass public static void initializeSystem() { System.out.println("initialize"); - // System.setProperty("http.proxyHost", "itgproxy"); - // System.setProperty("http.proxyPort", "80"); - // System.setProperty("http.keepAlive", "false"); } @Before public void initialize() throws Exception { System.out.println("initialize"); - // System.setProperty("http.proxyHost", "itgproxy"); - // System.setProperty("http.proxyPort", "80"); - // System.setProperty("http.keepAlive", "false"); } } diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/queue/IntegrationTestBase.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/queue/IntegrationTestBase.java index 5c96a872956e0..0154fda420244 100644 --- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/queue/IntegrationTestBase.java +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/queue/IntegrationTestBase.java @@ -22,11 +22,6 @@ protected static Configuration createConfiguration() { setConfigValue(config, env, QueueConfiguration.ACCOUNT_KEY, "xxx"); setConfigValue(config, env, QueueConfiguration.URL, "http://xxx.queue.core.windows.net"); - // when mock running - // config.setProperty("serviceBus.uri", "http://localhost:8086"); - // config.setProperty("wrapClient.uri", - // "http://localhost:8081/WRAPv0.9"); - return config; } @@ -41,16 +36,10 @@ private static void setConfigValue(Configuration config, Map pro @BeforeClass public static void initializeSystem() { System.out.println("initialize"); - // System.setProperty("http.proxyHost", "itgproxy"); - // System.setProperty("http.proxyPort", "80"); - // System.setProperty("http.keepAlive", "false"); } @Before public void initialize() throws Exception { System.out.println("initialize"); - // System.setProperty("http.proxyHost", "itgproxy"); - // System.setProperty("http.proxyPort", "80"); - // System.setProperty("http.keepAlive", "false"); } }