forked from Azure/azure-sdk-for-java
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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).
- Loading branch information
Renaud Paquay
committed
Nov 18, 2011
1 parent
822f423
commit 6af17f7
Showing
9 changed files
with
671 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
...java/com/microsoft/windowsazure/services/blob/implementation/EntityStreamingListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
21 changes: 21 additions & 0 deletions
21
...java/com/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
267 changes: 267 additions & 0 deletions
267
...m/microsoft/windowsazure/services/blob/implementation/HttpURLConnectionClientHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | ||
} | ||
} | ||
} |
Oops, something went wrong.