Skip to content

Commit 60ac766

Browse files
committed
Allowing keys with leading slashes for S3
1 parent 8adcb9c commit 60ac766

File tree

8 files changed

+116
-20
lines changed

8 files changed

+116
-20
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"category": "Amazon S3",
3+
"type": "bugfix",
4+
"description": "Allows S3 to be used with object keys that have a leading slash \"/myKey\""
5+
}

core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/marshall/SimpleTypePathMarshaller.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public final class SimpleTypePathMarshaller {
3737
* so that it preserves the path structure.
3838
*/
3939
public static final XmlMarshaller<String> GREEDY_STRING =
40-
new SimplePathMarshaller<>(ValueToStringConverter.FROM_STRING, PathMarshaller.GREEDY);
40+
new SimplePathMarshaller<>(ValueToStringConverter.FROM_STRING, PathMarshaller.GREEDY_WITH_SLASHES);
4141

4242
public static final XmlMarshaller<Void> NULL = (val, context, paramName, sdkField) -> {
4343
throw new IllegalArgumentException(String.format("Parameter '%s' must not be null", paramName));

core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/PathMarshaller.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ public abstract class PathMarshaller {
3232
*/
3333
public static final PathMarshaller GREEDY = new GreedyPathMarshaller();
3434

35+
/**
36+
* Marshaller for greedy path labels that allows leading slahes. Value is not URL encoded and
37+
* replaced in the request URI.
38+
*/
39+
public static final PathMarshaller GREEDY_WITH_SLASHES = new GreedyLeadingSlashPathMarshaller();
40+
3541
private PathMarshaller() {
3642
}
3743

@@ -68,4 +74,14 @@ public String marshall(String resourcePath, String paramName, String pathValue)
6874
}
6975
}
7076

77+
private static class GreedyLeadingSlashPathMarshaller extends PathMarshaller {
78+
79+
@Override
80+
public String marshall(String resourcePath, String paramName, String pathValue) {
81+
Validate.notEmpty(pathValue, "%s cannot be empty.", paramName);
82+
return resourcePath.replace(String.format("{%s+}", paramName),
83+
SdkHttpUtils.urlEncodeIgnoreSlashes(pathValue));
84+
}
85+
}
86+
7187
}

http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/impl/ApacheHttpRequestFactory.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,27 @@ public class ApacheHttpRequestFactory {
5353

5454
public HttpRequestBase create(final HttpExecuteRequest request, final ApacheHttpRequestConfig requestConfig) {
5555
URI uri = request.httpRequest().getUri();
56-
HttpRequestBase base = createApacheRequest(request, uri.toString());
56+
57+
HttpRequestBase base = createApacheRequest(request, sanitizeUri(uri));
5758
addHeadersToRequest(base, request.httpRequest());
5859
addRequestConfig(base, request.httpRequest(), requestConfig);
5960

6061
return base;
6162
}
6263

64+
/**
65+
* The Apache HTTP client doesn't allow consecutive slashes in the URI. For S3
66+
* and other AWS services, this is allowed and required. This methods replaces
67+
* any occurrence of "//" in the URI path with "/%2F".
68+
*
69+
* @param uri The existing URI with double slashes not sanitized for Apache.
70+
* @return a new String containing the modified URI
71+
*/
72+
private String sanitizeUri(URI uri) {
73+
String newPath = uri.getPath().replace("//", "/%2F");
74+
return uri.toString().replace(uri.getPath(), newPath);
75+
}
76+
6377
private void addRequestConfig(final HttpRequestBase base,
6478
final SdkHttpRequest request,
6579
final ApacheHttpRequestConfig requestConfig) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package software.amazon.awssdk.services.s3;
16+
17+
import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName;
18+
19+
import java.nio.charset.StandardCharsets;
20+
import org.junit.BeforeClass;
21+
import org.junit.Test;
22+
import software.amazon.awssdk.core.sync.RequestBody;
23+
24+
public class KeysWithLeadingSlashIntegrationTest extends S3IntegrationTestBase {
25+
26+
private static final String BUCKET = temporaryBucketName(KeysWithLeadingSlashIntegrationTest.class);
27+
private static final String KEY = "/stupidkeywithillegalleadingslashthatsucks";
28+
private static final byte[] CONTENT = "Hello".getBytes(StandardCharsets.UTF_8);
29+
30+
@BeforeClass
31+
public static void setUp() throws Exception {
32+
S3IntegrationTestBase.setUp();
33+
createBucket(BUCKET);
34+
}
35+
36+
@Test
37+
public void putObject_KeyWithLeadingSlash_Succeeds() {
38+
s3.putObject(r -> r.bucket(BUCKET).key(KEY), RequestBody.fromBytes(CONTENT));
39+
String retrievedKey = s3.listObjects(r -> r.bucket(BUCKET)).contents().get(0).key();
40+
41+
assert(retrievedKey).equals(KEY);
42+
}
43+
}

test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-core-input.json

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -487,24 +487,6 @@
487487
}
488488
}
489489
},
490-
{
491-
"description": "Input with greedy label in path, leading slash removed",
492-
"given": {
493-
"input": {
494-
"NonGreedyPathParam": "pathParamValue",
495-
"GreedyPathParam": "/foo/bar/baz"
496-
}
497-
},
498-
"when": {
499-
"action": "marshall",
500-
"operation": "OperationWithGreedyLabel"
501-
},
502-
"then": {
503-
"serializedAs": {
504-
"uri": "/2016-03-11/operationWithGreedyLabel/pathParamValue/foo/bar/baz"
505-
}
506-
}
507-
},
508490
{
509491
"description": "Operation with null members that have the idempotent trait are autofilled.",
510492
"when": {

test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-input.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,24 @@
126126
}
127127
}
128128
}
129+
},
130+
{
131+
"description": "Input with greedy label in path, leading slash removed",
132+
"given": {
133+
"input": {
134+
"NonGreedyPathParam": "pathParamValue",
135+
"GreedyPathParam": "/foo/bar/baz"
136+
}
137+
},
138+
"when": {
139+
"action": "marshall",
140+
"operation": "OperationWithGreedyLabel"
141+
},
142+
"then": {
143+
"serializedAs": {
144+
"uri": "/2016-03-11/operationWithGreedyLabel/pathParamValue/foo/bar/baz"
145+
}
146+
}
129147
}
130148
// TODO This is a post process customization for API Gateway
131149
// {

test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-xml-input.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,24 @@
350350
}
351351
}
352352
}
353+
},
354+
{
355+
"description": "Input with greedy label in path, leading slash removed",
356+
"given": {
357+
"input": {
358+
"NonGreedyPathParam": "pathParamValue",
359+
"GreedyPathParam": "/foo/bar/baz"
360+
}
361+
},
362+
"when": {
363+
"action": "marshall",
364+
"operation": "OperationWithGreedyLabel"
365+
},
366+
"then": {
367+
"serializedAs": {
368+
"uri": "/2016-03-11/operationWithGreedyLabel/pathParamValue//foo/bar/baz"
369+
}
370+
}
353371
}
354372
// TODO this is not possible, payloads can only be structures or blobs. Only S3 utilizes this
355373
// {

0 commit comments

Comments
 (0)