From fbb7740f17eb20924f65d1c17f5e5d05069fd53d Mon Sep 17 00:00:00 2001 From: "adrian.f.cole" Date: Sun, 9 Aug 2009 22:59:33 +0000 Subject: [PATCH] allow filtering to be chained by returning the request on filter() git-svn-id: http://jclouds.googlecode.com/svn/trunk@1854 3d8758e0-26b5-11de-8745-db77d3ebf521 --- .../s3/filters/RequestAuthorizeSignature.java | 37 ++++++------ .../RequestAuthorizeSignatureTest.java | 59 ++++++++++++++++--- .../org/jclouds/http/HttpRequestFilter.java | 4 +- .../jclouds/http/IntegrationTestClient.java | 3 +- .../rest/JaxrsAnnotationProcessorTest.java | 23 ++++++-- .../filters/AuthenticateRequest.java | 3 +- 6 files changed, 95 insertions(+), 34 deletions(-) diff --git a/aws/s3/core/src/main/java/org/jclouds/aws/s3/filters/RequestAuthorizeSignature.java b/aws/s3/core/src/main/java/org/jclouds/aws/s3/filters/RequestAuthorizeSignature.java index 8fbdc8f51a..5c41120bbf 100644 --- a/aws/s3/core/src/main/java/org/jclouds/aws/s3/filters/RequestAuthorizeSignature.java +++ b/aws/s3/core/src/main/java/org/jclouds/aws/s3/filters/RequestAuthorizeSignature.java @@ -23,14 +23,16 @@ */ package org.jclouds.aws.s3.filters; +import static com.google.common.base.Preconditions.checkNotNull; + import java.util.Collection; +import java.util.Collections; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import javax.ws.rs.core.HttpHeaders; -import static com.google.common.base.Preconditions.checkNotNull; import org.jclouds.aws.s3.reference.S3Constants; import org.jclouds.http.HttpException; @@ -53,8 +55,8 @@ import com.google.inject.name.Named; */ @Singleton public class RequestAuthorizeSignature implements HttpRequestFilter { - private final String[] firstHeadersToSign = new String[] { - "Content-MD5", HttpHeaders.CONTENT_TYPE, HttpHeaders.DATE }; + private final String[] firstHeadersToSign = new String[] { "Content-MD5", + HttpHeaders.CONTENT_TYPE, HttpHeaders.DATE }; private final String accessKey; private final String secretKey; @@ -100,18 +102,16 @@ public class RequestAuthorizeSignature implements HttpRequestFilter { timeStamp = new AtomicReference(createNewStamp()); } - public void filter(HttpRequest request) throws HttpException { - + public HttpRequest filter(HttpRequest request) throws HttpException { + replaceDateHeader(request); String toSign = createStringToSign(request); - - addAuthHeader(request, toSign); + calculateAndReplaceAuthHeader(request, toSign); + return request; } public String createStringToSign(HttpRequest request) { StringBuilder buffer = new StringBuilder(); // re-sign the request - removeOldHeaders(request); - addDateHeader(request); appendMethod(request, buffer); appendHttpHeaders(request, buffer); appendAmzHeaders(request, buffer); @@ -120,27 +120,30 @@ public class RequestAuthorizeSignature implements HttpRequestFilter { return buffer.toString(); } - private void removeOldHeaders(HttpRequest request) { - request.getHeaders().removeAll(HttpHeaders.AUTHORIZATION); - request.getHeaders().removeAll(HttpHeaders.DATE); + private void calculateAndReplaceAuthHeader(HttpRequest request, String toSign) + throws HttpException { + String signature = signString(toSign); + request.getHeaders().replaceValues(HttpHeaders.AUTHORIZATION, + Collections.singletonList("AWS " + accessKey + ":" + signature)); } - private void addAuthHeader(HttpRequest request, String toSign) throws HttpException { + public String signString(String toSign) { String signature; try { signature = HttpUtils.hmacSha1Base64(toSign, secretKey.getBytes()); } catch (Exception e) { throw new HttpException("error signing request", e); } - request.getHeaders().put(HttpHeaders.AUTHORIZATION, "AWS " + accessKey + ":" + signature); + return signature; } private void appendMethod(HttpRequest request, StringBuilder toSign) { toSign.append(request.getMethod()).append("\n"); } - private void addDateHeader(HttpRequest request) { - request.getHeaders().put(HttpHeaders.DATE, timestampAsHeaderString()); + private void replaceDateHeader(HttpRequest request) { + request.getHeaders().replaceValues(HttpHeaders.DATE, + Collections.singletonList(timestampAsHeaderString())); } private void appendAmzHeaders(HttpRequest request, StringBuilder toSign) { @@ -174,7 +177,7 @@ public class RequestAuthorizeSignature implements HttpRequestFilter { @VisibleForTesting void appendUriPath(HttpRequest request, StringBuilder toSign) { - toSign.append(request.getEndpoint().getPath()); + toSign.append(request.getEndpoint().getRawPath()); // ...however, there are a few exceptions that must be included in the signed URI. if (request.getEndpoint().getQuery() != null) { diff --git a/aws/s3/core/src/test/java/org/jclouds/aws/s3/filters/RequestAuthorizeSignatureTest.java b/aws/s3/core/src/test/java/org/jclouds/aws/s3/filters/RequestAuthorizeSignatureTest.java index aa0756b27b..7d7b2fd72a 100644 --- a/aws/s3/core/src/test/java/org/jclouds/aws/s3/filters/RequestAuthorizeSignatureTest.java +++ b/aws/s3/core/src/test/java/org/jclouds/aws/s3/filters/RequestAuthorizeSignatureTest.java @@ -33,22 +33,60 @@ import org.jclouds.aws.s3.reference.S3Constants; import org.jclouds.http.HttpMethod; import org.jclouds.http.HttpRequest; import org.jclouds.util.DateService; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.google.inject.AbstractModule; import com.google.inject.Guice; +import com.google.inject.Injector; import com.google.inject.name.Names; @Test(groups = "unit", testName = "s3.RequestAuthorizeSignatureTest") public class RequestAuthorizeSignatureTest { + private Injector injector; + private RequestAuthorizeSignature filter; + + @DataProvider(parallel = true) + public Object[][] dataProvider() { + return new Object[][] { + { new HttpRequest(HttpMethod.GET, URI.create("http://s3.amazonaws.com:80")) }, + { new HttpRequest( + HttpMethod.GET, + URI + .create("http://adriancole.s3int5.s3-external-3.amazonaws.com:80/testObject")) }, + { new HttpRequest(HttpMethod.GET, URI.create("http://s3.amazonaws.com:80/?acl")) + + } }; + } + + /** + * NOTE this test is dependent on how frequently the timestamp updates. At the time of writing, + * this was once per second. If this timestamp update interval is increased, it could make this + * test appear to hang for a long time. + */ + @Test(threadPoolSize = 3, dataProvider = "dataProvider", timeOut = 3000) + void testIdempotent(HttpRequest request) { + filter.filter(request); + String signature = request.getFirstHeaderOrNull(HttpHeaders.AUTHORIZATION); + String date = request.getFirstHeaderOrNull(HttpHeaders.DATE); + int iterations = 1; + while (filter.filter(request).getFirstHeaderOrNull(HttpHeaders.DATE).equals(date)) { + iterations++; + assertEquals(signature, request.getFirstHeaderOrNull(HttpHeaders.AUTHORIZATION)); + } + System.out.printf("%s: %d iterations before the timestamp updated %n", Thread.currentThread() + .getName(), iterations); + } + @Test void testAppendBucketNameHostHeader() { URI host = URI.create("http://s3.amazonaws.com:80"); HttpRequest request = new HttpRequest(HttpMethod.GET, host); request.getHeaders().put(HttpHeaders.HOST, "adriancole.s3int5.s3.amazonaws.com"); StringBuilder builder = new StringBuilder(); - createFilter().appendBucketName(request, builder); + filter.appendBucketName(request, builder); assertEquals(builder.toString(), "/adriancole.s3int5"); } @@ -57,7 +95,7 @@ public class RequestAuthorizeSignatureTest { URI host = URI.create("http://s3.amazonaws.com:80/?acl"); HttpRequest request = new HttpRequest(HttpMethod.GET, host); StringBuilder builder = new StringBuilder(); - createFilter().appendUriPath(request, builder); + filter.appendUriPath(request, builder); assertEquals(builder.toString(), "/?acl"); } @@ -69,7 +107,7 @@ public class RequestAuthorizeSignatureTest { HttpRequest request = new HttpRequest(HttpMethod.GET, host); request.getHeaders().put(HttpHeaders.HOST, "s3.amazonaws.com"); StringBuilder builder = new StringBuilder(); - createFilter().appendBucketName(request, builder); + filter.appendBucketName(request, builder); assertEquals(builder.toString(), ""); } @@ -78,13 +116,12 @@ public class RequestAuthorizeSignatureTest { URI host = URI.create("http://adriancole.s3int5.s3-external-3.amazonaws.com:80"); HttpRequest request = new HttpRequest(HttpMethod.GET, host); StringBuilder builder = new StringBuilder(); - createFilter().appendBucketName(request, builder); + filter.appendBucketName(request, builder); assertEquals(builder.toString(), "/adriancole.s3int5"); } @Test void testUpdatesOnlyOncePerSecond() throws NoSuchMethodException, InterruptedException { - RequestAuthorizeSignature filter = createFilter(); // filter.createNewStamp(); String timeStamp = filter.timestampAsHeaderString(); // replay(filter); @@ -96,8 +133,13 @@ public class RequestAuthorizeSignatureTest { // verify(filter); } - private RequestAuthorizeSignature createFilter() { - return Guice.createInjector(new AbstractModule() { + /** + * before class, as we need to ensure that the filter is threadsafe. + * + */ + @BeforeClass + protected void createFilter() { + injector = Guice.createInjector(new AbstractModule() { protected void configure() { bindConstant().annotatedWith(Names.named(S3Constants.PROPERTY_AWS_ACCESSKEYID)).to( @@ -107,7 +149,8 @@ public class RequestAuthorizeSignatureTest { bind(DateService.class); } - }).getInstance(RequestAuthorizeSignature.class); + }); + filter = injector.getInstance(RequestAuthorizeSignature.class); } } \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/http/HttpRequestFilter.java b/core/src/main/java/org/jclouds/http/HttpRequestFilter.java index 52159d4fe2..cd896a7dc7 100644 --- a/core/src/main/java/org/jclouds/http/HttpRequestFilter.java +++ b/core/src/main/java/org/jclouds/http/HttpRequestFilter.java @@ -25,9 +25,9 @@ package org.jclouds.http; /** * // TODO: Adrian: Document this! - * + * * @author Adrian Cole */ public interface HttpRequestFilter { - void filter(HttpRequest request) throws HttpException; + HttpRequest filter(HttpRequest request) throws HttpException; } diff --git a/core/src/test/java/org/jclouds/http/IntegrationTestClient.java b/core/src/test/java/org/jclouds/http/IntegrationTestClient.java index f7d6daa2a7..2d3287ed4c 100644 --- a/core/src/test/java/org/jclouds/http/IntegrationTestClient.java +++ b/core/src/test/java/org/jclouds/http/IntegrationTestClient.java @@ -101,10 +101,11 @@ public interface IntegrationTestClient { Future downloadFilter(@PathParam("id") String id, @HeaderParam("filterme") String header); static class Filter implements HttpRequestFilter { - public void filter(HttpRequest request) throws HttpException { + public HttpRequest filter(HttpRequest request) throws HttpException { if (request.getHeaders().containsKey("filterme")) { request.getHeaders().put("test", "test"); } + return request; } } diff --git a/core/src/test/java/org/jclouds/rest/JaxrsAnnotationProcessorTest.java b/core/src/test/java/org/jclouds/rest/JaxrsAnnotationProcessorTest.java index 3335192f32..742d1c502f 100644 --- a/core/src/test/java/org/jclouds/rest/JaxrsAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/JaxrsAnnotationProcessorTest.java @@ -29,6 +29,7 @@ import static org.testng.Assert.assertEquals; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.net.URI; +import java.net.URLEncoder; import java.util.Collection; import java.util.Collections; import java.util.Map; @@ -60,6 +61,7 @@ import org.jclouds.util.DateService; import org.joda.time.DateTime; import org.mortbay.jetty.HttpHeaders; import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.google.common.base.Function; @@ -201,14 +203,16 @@ public class JaxrsAnnotationProcessorTest { static class TestRequestFilter1 implements HttpRequestFilter { - public void filter(HttpRequest request) throws HttpException { + public HttpRequest filter(HttpRequest request) throws HttpException { + return null; } } static class TestRequestFilter2 implements HttpRequestFilter { - public void filter(HttpRequest request) throws HttpException { + public HttpRequest filter(HttpRequest request) throws HttpException { + return null; } } @@ -586,13 +590,22 @@ public class JaxrsAnnotationProcessorTest { } - public void testCreateGetRequest() throws SecurityException, NoSuchMethodException { + @DataProvider(name = "strings") + public Object[][] createData() { + return new Object[][] { { "apples" }, { "sp ace" }, { "unic¿de" }, { "qu?stion" } }; + } + + @Test(dataProvider = "strings") + public void testCreateGetRequest(String key) throws SecurityException, NoSuchMethodException, + UnsupportedEncodingException { Method method = TestRequest.class.getMethod("get", String.class, String.class); URI endpoint = URI.create("http://localhost"); HttpRequest httpMethod = factory.create(TestRequest.class).createRequest(endpoint, method, - new Object[] { "1", "localhost" }); + new Object[] { key, "localhost" }); assertEquals(httpMethod.getEndpoint().getHost(), "localhost"); - assertEquals(httpMethod.getEndpoint().getPath(), "/1"); + String expectedPath = "/" + URLEncoder.encode(key, "UTF-8").replaceAll("\\+", "%20"); + assertEquals(httpMethod.getEndpoint().getRawPath(), expectedPath); + assertEquals(httpMethod.getEndpoint().getPath(), "/" + key); assertEquals(httpMethod.getMethod(), HttpMethod.GET); assertEquals(httpMethod.getHeaders().size(), 1); assertEquals(httpMethod.getHeaders().get(HttpHeaders.HOST), Collections diff --git a/rackspace/core/src/main/java/org/jclouds/rackspace/filters/AuthenticateRequest.java b/rackspace/core/src/main/java/org/jclouds/rackspace/filters/AuthenticateRequest.java index bab1a5e693..baca6f3386 100755 --- a/rackspace/core/src/main/java/org/jclouds/rackspace/filters/AuthenticateRequest.java +++ b/rackspace/core/src/main/java/org/jclouds/rackspace/filters/AuthenticateRequest.java @@ -88,9 +88,10 @@ public class AuthenticateRequest implements HttpRequestFilter { authToken = new AtomicReference(); } - public void filter(HttpRequest request) throws HttpException { + public HttpRequest filter(HttpRequest request) throws HttpException { request.getHeaders().replaceValues(RackspaceHeaders.AUTH_TOKEN, Collections.singletonList(getAuthToken())); + return request; } } \ No newline at end of file