Skip to content
This repository has been archived by the owner on Jul 25, 2020. It is now read-only.

[JCLOUDS-1401] Properly URL-encode the CanonicalQueryString when it contains funny characters #1226

Closed
wants to merge 4 commits into from

Conversation

jglick
Copy link
Contributor

@jglick jglick commented Jul 10, 2018

JCLOUDS-1401

I managed to write integration tests for this in Jenkins (which fail without this patch and pass with it). ~~~I would have liked to add a test case for it to AWSS3ClientLiveTest (or S3ClientLiveTest? not sure I grasp the distinction) but did not manage to get the existing tests to run; I either got errors about an unknown access key, or a 403, regardless of what parameters I tried; and there is no apparent way to pick up values from ~/.aws/credentials. Testing makes no specific mention of S3 and what the identity and credential would be, concretely; Getting Started does not give format examples or give a hint about how the internal properties map would look; and developer guides in the source tree like that for Google Cloud seem to be absent for S3.~~~

@carlossg @kuisathaverat

@nacx
Copy link
Member

nacx commented Jul 10, 2018

Thanks, @jglick!

I'd say before running live tests you could add a small unit test that just verifies the createStringToSign method so we don't break it accidentally in the future.

You can also add a mock unit test to the S3ClientMockTest class. Mock tests just mock HTTP requests and provide a convenient way to do assertions on the generated requests. You could write one test that verifies the signature that has been sent for a request.

For the live tests, we don't mention explicitly the S3 parameters in the wiki because they are the same for all providers. You can invoke the live tests as follows (note this follows the generic format explained in the testing page):

mvn clean install -Plive -pl :s3 -Dtest=S3ClientLiveTest \
   -Dtest.s3.identity=<access key> \
   -Dtest.s3.credential=<access secret> \
   -Dtest.s3.endpoint=https://s3.amazonaws.com

Since S3 is a generic API (if you read this quick page you'll get the difference between the S3 api and the AWS-S3 provider) that can be implemented by many providers, we need to set the endpoint parameter indicating where to run the live tests against.

Since you are already using AWS, you could run the aws-s3 provider live tests too, as follows:

mvn clean install -Plive -pl :aws-s3 -Dtest=AWSS3ClientLiveTest \
   -Dtest.aws-s3.identity=<acces key> \
   -Dtest.aws-s3.credential=<access secret>

Note that since you are now using the aws-s3 provider there is no need to provide the endpoint, as the AWS provider does already know where it needs to connect.

The credentials you need to pass in the identity and credential property are the same ones you pass to the jclouds ContextBuilder when creating the context, and in general, the additional properties you have to pass to the tests in the command line, are those extra properties you configure when creating the context.

@jglick
Copy link
Contributor Author

jglick commented Jul 10, 2018

I did try running AWSS3ClientLiveTest with the arguments you mention, and it did not work, as noted in the PR description. Of course the failure messages were pretty opaque for security reasons. Noting that these parameters are identical to that used by ContextBuilder is not particularly helpful to me since our tests do not require such keys to be passed in explicitly—the code picks up ~/.aws/credentials automatically and calls credentialsSupplier on a SessionCredentials ultimately using the AWS SDK, and that works fine, and there is no apparent system property corresponding to the sessionToken. So I will guess I will have to find a colleague with better understanding of AWS authentication to assist me.

It is good to at least have confirmation that identity is an “access key” while credential is an “access secret” in AWS terminology—this is what would be useful to clarify in a README.md.

I am less confident in the value of a unit test or mock test since it is not self-evident what the actual designed signature would be. The fix is just that which produces a signature that Amazon in fact accepts.

@jglick
Copy link
Contributor Author

jglick commented Jul 10, 2018

I think I figured out that the existing tests just do not support session credentials (and I cannot access my account without them), so I need to patch the tests.

since you are now using the aws-s3 provider there is no need to provide the endpoint, as the AWS provider does already know where it needs to connect.

This is not true—the inherited testCopyCannedAccessPolicyPublic and testPutCannedAccessPolicyPublic fail with an NPE when it is omitted.

@jglick
Copy link
Contributor Author

jglick commented Jul 10, 2018

There are some tests that fail even without my patch:

org.jclouds.aws.AWSResponseException: request PUT https://s3.amazonaws.com/jglick-blobstore-TestBucket HTTP/1.1 failed with code 400, error: AWSError{requestId='…', requestToken='…', code='InvalidBucketName', message='The specified bucket is not valid.', context='{BucketName=jglick-blobstore-TestBucket, HostId=…}'}
	at org.jclouds.aws.handlers.ParseAWSErrorFromXmlContent.handleError(ParseAWSErrorFromXmlContent.java:75)
	at org.jclouds.http.handlers.DelegatingErrorHandler.handleError(DelegatingErrorHandler.java:65)
	at org.jclouds.http.internal.BaseHttpCommandExecutorService.shouldContinue(BaseHttpCommandExecutorService.java:138)
	at org.jclouds.http.internal.BaseHttpCommandExecutorService.invoke(BaseHttpCommandExecutorService.java:107)
	at org.jclouds.rest.internal.InvokeHttpMethod.invoke(InvokeHttpMethod.java:91)
	at org.jclouds.rest.internal.InvokeHttpMethod.apply(InvokeHttpMethod.java:74)
	at org.jclouds.rest.internal.InvokeHttpMethod.apply(InvokeHttpMethod.java:45)
	at org.jclouds.rest.internal.DelegatesToInvocationFunction.handle(DelegatesToInvocationFunction.java:156)
	at org.jclouds.rest.internal.DelegatesToInvocationFunction.invoke(DelegatesToInvocationFunction.java:123)
	at com.sun.proxy.$Proxy48.putBucketInRegion(Unknown Source)
	at org.jclouds.s3.blobstore.S3BlobStore.createContainerInLocation(S3BlobStore.java:456)
	at org.jclouds.aws.s3.blobstore.AWSS3BlobStore.createContainerInLocation(AWSS3BlobStore.java:105)
	at org.jclouds.s3.blobstore.S3BlobStore.createContainerInLocation(S3BlobStore.java:146)
	at org.jclouds.aws.s3.AWSS3ClientLiveTest.testUseBucketWithUpperCaseName(AWSS3ClientLiveTest.java:109)

org.jclouds.http.HttpResponseException: Server rejected operation connecting to PUT https://jglick-blobstore11eu.s3-eu-central-1.amazonaws.com/test-blob?X-Amz-Security-Token=…&X-Amz-Algorithm=…&X-Amz-Credential=…/eu-central-1/s3/aws4_request&X-Amz-Date=…&X-Amz-Expires=…&X-Amz-SignedHeaders=host&X-Amz-Signature=… HTTP/1.1
	at sun.net.www.protocol.http.HttpURLConnection.expect100Continue(HttpURLConnection.java:1269)
	at sun.net.www.protocol.http.HttpURLConnection.getOutputStream0(HttpURLConnection.java:1348)
	at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1309)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:259)
	at org.jclouds.http.internal.JavaUrlHttpCommandExecutorService.writePayloadToConnection(JavaUrlHttpCommandExecutorService.java:295)
	at org.jclouds.http.internal.JavaUrlHttpCommandExecutorService.convert(JavaUrlHttpCommandExecutorService.java:171)
	at org.jclouds.http.internal.JavaUrlHttpCommandExecutorService.convert(JavaUrlHttpCommandExecutorService.java:65)
	at org.jclouds.http.internal.BaseHttpCommandExecutorService.invoke(BaseHttpCommandExecutorService.java:97)
	at org.jclouds.rest.internal.InvokeHttpMethod.invoke(InvokeHttpMethod.java:91)
	at org.jclouds.rest.internal.InvokeHttpMethod.apply(InvokeHttpMethod.java:74)
	at org.jclouds.rest.internal.InvokeHttpMethod.apply(InvokeHttpMethod.java:45)
	at org.jclouds.rest.internal.DelegatesToInvocationFunction.handle(DelegatesToInvocationFunction.java:156)
	at org.jclouds.rest.internal.DelegatesToInvocationFunction.invoke(DelegatesToInvocationFunction.java:123)
	at com.sun.proxy.$Proxy49.invoke(Unknown Source)
	at org.jclouds.aws.s3.AWSS3ClientLiveTest.testV4SignatureOps(AWSS3ClientLiveTest.java:192)

These seem to be related to the region, perhaps? And:

java.lang.NullPointerException: null
	at java.net.URI$Parser.parse(URI.java:3042)
	at java.net.URI.<init>(URI.java:588)
	at java.net.URI.create(URI.java:850)
	at org.jclouds.s3.S3ClientLiveTest.getObjectURL(S3ClientLiveTest.java:139)
	at org.jclouds.s3.S3ClientLiveTest.testCopyCannedAccessPolicyPublic(S3ClientLiveTest.java:176)

java.lang.NullPointerException: null
	at java.net.URI$Parser.parse(URI.java:3042)
	at java.net.URI.<init>(URI.java:588)
	at java.net.URI.create(URI.java:850)
	at org.jclouds.s3.S3ClientLiveTest.getObjectURL(S3ClientLiveTest.java:139)
	at org.jclouds.s3.S3ClientLiveTest.testPutCannedAccessPolicyPublic(S3ClientLiveTest.java:155)

which seem to be caused by an undefined endpoint, though I am not sure why since providers/aws-s3/pom.xml does seem to default it. Anyway, I will ignore these for now.

Copy link
Contributor Author

@jglick jglick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nacx please take another look. I added both a unit test and a live test. A mock test seems redundant given those.

PageSet<? extends StorageMetadata> list = view.getBlobStore().list(containerName,
ListContainerOptions.Builder.prefix(dirName + "/"));
assertEquals(list.size(), 1);
StorageMetadata md = list.iterator().next();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails with the reported AWS error if you s/rawQuery/query/.

private static final String LIST_BUCKET_RESULT = "AWS4-HMAC-SHA256 "
+ "Credential=AKIAPAEBI3QI4EXAMPLE/20150203/cn-north-1/s3/aws4_request, "
+ "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
+ "Signature=6cc5d0758e2599be7cb172fd57cefab2828201a2b4d372972a83dc304de93958";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merely based on running the test input against rawQuery. I did not attempt to consult the AWS documentation and manually construct the signature base as per these instructions.

The key to the fix is that Queries.parseKeyValueFromStringToDecodedMap is used to URL-decode the parameters of the query string after splitting on &, and we then reëncode in getCanonicalizedQueryString, so using URI.getQuery() (which decodes the value) not only double-decodes, it actually loses information. For example,

URI u = new URI("https://x.com/?p1=a&p2=b%26c%3Dd&p3=e");
System.out.println(u.getRawQuery());
System.out.println(u.getQuery());

will print

p1=a&p2=b%26c%3Dd&p3=e
p1=a&p2=b&c=d&p3=e

builder.credentialsSupplier(new Supplier<Credentials>() {
@Override
public Credentials get() {
return SessionCredentials.builder().identity(identity).credential(credential).sessionToken(sessionToken).build();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this I was unable to run live tests at all. (Maybe something about IAM?) The new parameter is optional.

Copy link

@oleg-nenashev oleg-nenashev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@nacx
Copy link
Member

nacx commented Jul 11, 2018

Thanks for the patch and for taking your time to run the live tests @jglick! Merged to master and 2.1.x.

I'll have a look at the failing tests, as if you run just the S3 ones and pass the endpoint, all succeed. There must be some minor mess about the endpoints in the aws-s3 tests.

@jglick
Copy link
Contributor Author

jglick commented Jul 11, 2018

@nacx

if you run just the S3 ones and pass the endpoint, all succeed

Confirmed that when running S3ClientLiveTest directly in the s3 module, with the endpoint defined, all tests pass including testCopyCannedAccessPolicyPublic and testPutCannedAccessPolicyPublic. So there must be something amiss with how the endpoint property is defaulted in the aws-s3 module’s POM (here and here). I can only confirm that the property is absent from System.getProperties() unless you pass -Dtest.aws-s3.endpoint=https://s3.amazonaws.com on the command line.

The other two failures I mentioned were in AWSS3ClientLiveTest and I suspect are caused by my running using session credentials defined in the US-East region. Just a guess—my grasp of AWS is limited and certainly does not include the nuances of region selection.

@carlossg
Copy link
Member

Thanks Ignasi!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants