Skip to content

Commit

Permalink
Implement full ShareKey authorization for Blob Service
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Renaud Paquay committed Nov 18, 2011
1 parent 822f423 commit 6af17f7
Show file tree
Hide file tree
Showing 9 changed files with 671 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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<HttpURLConnectionClient>() {
public HttpURLConnectionClient create(String profile, Builder builder, Map<String, Object> properties) {
ClientConfig clientConfig = (ClientConfig) properties.get("ClientConfig");
HttpURLConnectionClient client = HttpURLConnectionClient.create(clientConfig);
//client.addFilter(new LoggingFilter());
return client;
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> headers, HttpURLConnection urlConnection) {
for (Map.Entry<String, List<Object>> e : headers.entrySet()) {
List<Object> 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<String, List<String>> 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]);
}
}
}
Loading

0 comments on commit 6af17f7

Please sign in to comment.