Skip to content

Commit 623c54f

Browse files
Add custom marshaller for pre-signed URL GET operations (#6231)
* presigned url get object request custom marshaller * used protocolFactory and fixed sonar cube issues * removed unused test method, removed overriden method
1 parent 93a5ed3 commit 623c54f

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 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+
16+
package software.amazon.awssdk.services.s3.internal.presignedurl;
17+
18+
import java.net.URI;
19+
import software.amazon.awssdk.annotations.SdkInternalApi;
20+
import software.amazon.awssdk.core.exception.SdkClientException;
21+
import software.amazon.awssdk.core.runtime.transform.Marshaller;
22+
import software.amazon.awssdk.http.SdkHttpFullRequest;
23+
import software.amazon.awssdk.http.SdkHttpMethod;
24+
import software.amazon.awssdk.protocols.core.OperationInfo;
25+
import software.amazon.awssdk.protocols.core.ProtocolMarshaller;
26+
import software.amazon.awssdk.protocols.xml.AwsXmlProtocolFactory;
27+
import software.amazon.awssdk.services.s3.internal.presignedurl.model.PresignedUrlGetObjectRequestWrapper;
28+
import software.amazon.awssdk.utils.Validate;
29+
30+
/**
31+
* {@link PresignedUrlGetObjectRequestWrapper} Marshaller
32+
*
33+
* <p>
34+
* Marshalls presigned URL requests by using the complete URL directly and adding optional Range headers.
35+
* Unlike regular S3 marshalers, this preserves all embedded authentication parameters in the presigned URL.
36+
* </p>
37+
*/
38+
@SdkInternalApi
39+
public class PresignedUrlGetObjectRequestMarshaller implements Marshaller<PresignedUrlGetObjectRequestWrapper> {
40+
private static final OperationInfo SDK_OPERATION_BINDING = OperationInfo.builder()
41+
.requestUri("").httpMethod(SdkHttpMethod.GET).hasExplicitPayloadMember(false).hasPayloadMembers(false)
42+
.putAdditionalMetadata(AwsXmlProtocolFactory.ROOT_MARSHALL_LOCATION_ATTRIBUTE, null)
43+
.putAdditionalMetadata(AwsXmlProtocolFactory.XML_NAMESPACE_ATTRIBUTE, null).build();
44+
45+
private final AwsXmlProtocolFactory protocolFactory;
46+
47+
public PresignedUrlGetObjectRequestMarshaller(AwsXmlProtocolFactory protocolFactory) {
48+
this.protocolFactory = protocolFactory;
49+
}
50+
51+
/**
52+
* Marshalls the presigned URL request into an HTTP GET request.
53+
*
54+
* @param presignedUrlGetObjectRequestWrapper the request to marshall
55+
* @return HTTP request ready for execution
56+
* @throws SdkClientException if URL conversion fails
57+
*/
58+
@Override
59+
public SdkHttpFullRequest marshall(PresignedUrlGetObjectRequestWrapper presignedUrlGetObjectRequestWrapper) {
60+
Validate.paramNotNull(presignedUrlGetObjectRequestWrapper, "presignedUrlGetObjectRequestWrapper");
61+
try {
62+
ProtocolMarshaller<SdkHttpFullRequest> protocolMarshaller = protocolFactory
63+
.createProtocolMarshaller(SDK_OPERATION_BINDING);
64+
URI presignedUri = presignedUrlGetObjectRequestWrapper.url().toURI();
65+
66+
return protocolMarshaller.marshall(presignedUrlGetObjectRequestWrapper)
67+
.toBuilder()
68+
.uri(presignedUri)
69+
.build();
70+
} catch (Exception e) {
71+
throw SdkClientException.builder()
72+
.message("Unable to marshall pre-signed URL Request: " + e.getMessage())
73+
.cause(e).build();
74+
}
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/*
2+
* Copyright 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+
16+
package software.amazon.awssdk.services.s3.internal.presignedurl;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.when;
23+
import java.net.URL;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.NullAndEmptySource;
28+
import org.junit.jupiter.params.provider.ValueSource;
29+
import software.amazon.awssdk.core.exception.SdkClientException;
30+
import software.amazon.awssdk.http.SdkHttpFullRequest;
31+
import software.amazon.awssdk.http.SdkHttpMethod;
32+
import software.amazon.awssdk.protocols.core.OperationInfo;
33+
import software.amazon.awssdk.protocols.core.ProtocolMarshaller;
34+
import software.amazon.awssdk.protocols.xml.AwsXmlProtocolFactory;
35+
import software.amazon.awssdk.services.s3.internal.presignedurl.model.PresignedUrlGetObjectRequestWrapper;
36+
37+
class PresignedUrlGetObjectRequestMarshallerTest {
38+
39+
private PresignedUrlGetObjectRequestMarshaller marshaller;
40+
private AwsXmlProtocolFactory mockProtocolFactory;
41+
private ProtocolMarshaller<SdkHttpFullRequest> mockProtocolMarshaller;
42+
private URL testUrl;
43+
44+
@BeforeEach
45+
void setUp() throws Exception {
46+
mockProtocolFactory = mock(AwsXmlProtocolFactory.class);
47+
mockProtocolMarshaller = mock(ProtocolMarshaller.class);
48+
when(mockProtocolFactory.createProtocolMarshaller(any(OperationInfo.class)))
49+
.thenReturn(mockProtocolMarshaller);
50+
marshaller = new PresignedUrlGetObjectRequestMarshaller(mockProtocolFactory);
51+
52+
testUrl = new URL("https://test-bucket.s3.us-east-1.amazonaws.com/test-key?" +
53+
"X-Amz-Date=20231215T000000Z&" +
54+
"X-Amz-Signature=example-signature&" +
55+
"X-Amz-Algorithm=AWS4-HMAC-SHA256&" +
56+
"X-Amz-SignedHeaders=host&" +
57+
"X-Amz-Security-Token=xxx&" +
58+
"X-Amz-Credential=EXAMPLE12345678901234%2F20231215%2Fus-east-1%2Fs3%2Faws4_request&" +
59+
"X-Amz-Expires=3600");
60+
}
61+
62+
@Test
63+
void marshall_withBasicRequest_shouldCreateCorrectHttpRequest() throws Exception {
64+
// Setup the mock marshaller to return a properly configured request
65+
SdkHttpFullRequest baseRequest = SdkHttpFullRequest.builder()
66+
.method(SdkHttpMethod.GET)
67+
.protocol("https")
68+
.host("example.com")
69+
.build();
70+
when(mockProtocolMarshaller.marshall(any(PresignedUrlGetObjectRequestWrapper.class)))
71+
.thenReturn(baseRequest);
72+
73+
PresignedUrlGetObjectRequestWrapper request = PresignedUrlGetObjectRequestWrapper.builder()
74+
.url(testUrl)
75+
.build();
76+
SdkHttpFullRequest result = marshaller.marshall(request);
77+
78+
// Verify HTTP method and URI components
79+
assertThat(result.method()).isEqualTo(SdkHttpMethod.GET);
80+
assertThat(result.getUri())
81+
.satisfies(uri -> {
82+
assertThat(uri.getScheme()).isEqualTo("https");
83+
assertThat(uri.getHost()).isEqualTo("test-bucket.s3.us-east-1.amazonaws.com");
84+
assertThat(uri.getPath()).isEqualTo("/test-key");
85+
});
86+
87+
// Verify query parameters are preserved
88+
assertThat(result.getUri().getQuery())
89+
.contains("X-Amz-Date=20231215T000000Z")
90+
.contains("X-Amz-Signature=example-signature")
91+
.contains("X-Amz-Algorithm=AWS4-HMAC-SHA256")
92+
.contains("X-Amz-SignedHeaders=host")
93+
.contains("X-Amz-Security-Token=xxx")
94+
.contains("X-Amz-Credential=EXAMPLE12345678901234")
95+
.contains("X-Amz-Expires=3600");
96+
97+
assertThat(result.headers()).doesNotContainKey("Range");
98+
}
99+
100+
@ParameterizedTest
101+
@ValueSource(strings = {
102+
"bytes=0-100", // First 101 bytes
103+
"bytes=100-", // From byte 100 to end
104+
"bytes=-100", // Last 100 bytes
105+
"bytes=0-0", // Single byte
106+
"bytes=100-200" // Specific range
107+
})
108+
void marshall_withValidRangeFormats_shouldAddRangeHeader(String rangeValue) throws Exception {
109+
// Setup the mock marshaller to return a request with the Range header already set
110+
SdkHttpFullRequest baseRequest = SdkHttpFullRequest.builder()
111+
.method(SdkHttpMethod.GET)
112+
.protocol("https")
113+
.host("example.com")
114+
.putHeader("Range", rangeValue) // Add the Range header to the mock response
115+
.build();
116+
117+
when(mockProtocolMarshaller.marshall(any(PresignedUrlGetObjectRequestWrapper.class)))
118+
.thenReturn(baseRequest);
119+
120+
PresignedUrlGetObjectRequestWrapper request = PresignedUrlGetObjectRequestWrapper.builder()
121+
.url(testUrl)
122+
.range(rangeValue)
123+
.build();
124+
125+
SdkHttpFullRequest result = marshaller.marshall(request);
126+
127+
// Verify the Range header is preserved
128+
assertThat(result.headers())
129+
.containsKey("Range")
130+
.satisfies(headers -> assertThat(headers.get("Range")).contains(rangeValue));
131+
}
132+
133+
@ParameterizedTest
134+
@NullAndEmptySource
135+
void marshall_withNullOrEmptyRange_shouldNotAddRangeHeader(String rangeValue) throws Exception {
136+
// Setup the mock marshaller to return a properly configured request
137+
SdkHttpFullRequest baseRequest = SdkHttpFullRequest.builder()
138+
.method(SdkHttpMethod.GET)
139+
.protocol("https")
140+
.host("example.com")
141+
.build();
142+
when(mockProtocolMarshaller.marshall(any(PresignedUrlGetObjectRequestWrapper.class)))
143+
.thenReturn(baseRequest);
144+
145+
PresignedUrlGetObjectRequestWrapper request = PresignedUrlGetObjectRequestWrapper.builder()
146+
.url(testUrl)
147+
.range(rangeValue)
148+
.build();
149+
SdkHttpFullRequest result = marshaller.marshall(request);
150+
151+
assertThat(result.headers()).doesNotContainKey("Range");
152+
}
153+
154+
@Test
155+
void marshall_withNullRequest_shouldThrowException() {
156+
assertThatThrownBy(() -> marshaller.marshall(null))
157+
.isInstanceOf(NullPointerException.class)
158+
.hasMessageContaining("presignedUrlGetObjectRequestWrapper must not be null");
159+
}
160+
161+
@Test
162+
void marshall_withMalformedUrl_shouldThrowSdkClientException() throws Exception {
163+
// Setup the mock marshaller to return a properly configured request
164+
SdkHttpFullRequest baseRequest = SdkHttpFullRequest.builder()
165+
.method(SdkHttpMethod.GET)
166+
.protocol("https")
167+
.host("example.com")
168+
.build();
169+
when(mockProtocolMarshaller.marshall(any(PresignedUrlGetObjectRequestWrapper.class)))
170+
.thenReturn(baseRequest);
171+
172+
URL malformedUrl = new URL("https", "test-bucket.s3.us-east-1.amazonaws.com", -1, "/test key with spaces");
173+
PresignedUrlGetObjectRequestWrapper request = PresignedUrlGetObjectRequestWrapper.builder()
174+
.url(malformedUrl)
175+
.build();
176+
177+
assertThatThrownBy(() -> marshaller.marshall(request))
178+
.isInstanceOf(SdkClientException.class)
179+
.hasMessageContaining("Unable to marshall pre-signed URL Request");
180+
}
181+
}

0 commit comments

Comments
 (0)