From 20c2ce214eee03868f029504d0b6dfc38a96a3b0 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Thu, 17 Nov 2011 10:46:48 -0800 Subject: [PATCH 1/4] Removing unused code --- .../services/blob/IntegrationTestBase.java | 11 ----------- .../services/queue/IntegrationTestBase.java | 11 ----------- 2 files changed, 22 deletions(-) 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"); } } From c7a4c345daacdc7f381c757e1fd01ecfac7126f6 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Thu, 17 Nov 2011 17:02:49 -0800 Subject: [PATCH 2/4] Add "raw" response body to service exceptions --- .../windowsazure/common/ServiceException.java | 16 ++++++++++++++++ .../utils/ServiceExceptionFactory.java | 19 +++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) 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/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; } From 822f42358e693adaa77b6b99b7a97adf504f31a8 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Thu, 17 Nov 2011 17:03:01 -0800 Subject: [PATCH 3/4] Fix typo --- .../services/blob/models/SetBlobPropertiesOptions.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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; } From 6af17f70d9fa4be46f2069770da7ac8d325388d2 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Fri, 18 Nov 2011 10:03:29 -0800 Subject: [PATCH 4/4] Implement full ShareKey authorization for Blob Service The particular problem we had to solve here is to enable the SharedKey filter to have access to the "Content-Length" of the request, since that header is needed for authorizing the request. Since the "Content-Length" of the request is known only very late in the pipeline, so we had to introduce a new listener interface (EntityStreamingListener) that filters can register into. Filters then get called back later in the pipeline, just before starting sending bytes on the connection. This allows them to access all the headers (and also gives them a last chance to update them). --- .../windowsazure/services/blob/Exports.java | 15 + .../blob/implementation/BlobRestProxy.java | 16 +- .../EntityStreamingListener.java | 14 + .../HttpURLConnectionClient.java | 21 ++ .../HttpURLConnectionClientHandler.java | 267 ++++++++++++++++++ .../blob/implementation/JerseyHelpers.java | 12 +- .../blob/implementation/SharedKeyFilter.java | 265 +++++++++++++++++ .../implementation/SharedKeyLiteFilter.java | 40 +-- .../blob/implementation/SharedKeyUtils.java | 53 ++++ 9 files changed, 671 insertions(+), 32 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/EntityStreamingListener.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClient.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClientHandler.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyFilter.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyUtils.java 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 : ""; + } +}