20
20
import static org .assertj .core .api .Assertions .assertThatNoException ;
21
21
22
22
import com .fasterxml .jackson .databind .ObjectMapper ;
23
+
24
+ import net .bytebuddy .utility .RandomString ;
25
+
23
26
import java .io .ByteArrayInputStream ;
24
27
import java .io .IOException ;
25
28
import java .io .InputStream ;
26
29
import java .net .URL ;
27
30
import java .nio .charset .StandardCharsets ;
31
+ import java .security .MessageDigest ;
28
32
import java .time .Duration ;
33
+ import java .util .Base64 ;
29
34
import java .util .List ;
30
35
import org .apache .http .HttpEntity ;
31
36
import org .apache .http .HttpResponse ;
47
52
import software .amazon .awssdk .auth .credentials .StaticCredentialsProvider ;
48
53
import software .amazon .awssdk .core .ResponseInputStream ;
49
54
import software .amazon .awssdk .core .sync .RequestBody ;
55
+ import software .amazon .awssdk .http .HttpStatusCode ;
50
56
import software .amazon .awssdk .regions .Region ;
51
57
import software .amazon .awssdk .services .s3 .S3Client ;
52
58
import software .amazon .awssdk .services .s3 .model .GetObjectResponse ;
62
68
* @author Maciej Walkowiak
63
69
* @author Yuki Yoshida
64
70
* @author Ziemowit Stolarczyk
71
+ * @author Hardik Singh Behl
65
72
*/
66
73
@ Testcontainers
67
74
class S3TemplateIntegrationTests {
@@ -70,7 +77,7 @@ class S3TemplateIntegrationTests {
70
77
71
78
@ Container
72
79
static LocalStackContainer localstack = new LocalStackContainer (
73
- DockerImageName .parse ("localstack/localstack:3.8.1" ));
80
+ DockerImageName .parse ("localstack/localstack:3.8.1" )). withEnv ( "S3_SKIP_SIGNATURE_VALIDATION" , "0" ) ;
74
81
75
82
private static S3Client client ;
76
83
@@ -268,15 +275,21 @@ void createsWorkingSignedGetURL() throws IOException {
268
275
269
276
@ Test
270
277
void createsWorkingSignedPutURL () throws IOException {
271
- ObjectMetadata metadata = ObjectMetadata .builder ().metadata ("testkey" , "testvalue" ).build ();
278
+ String fileContent = RandomString .make ();
279
+ long contentLength = fileContent .length ();
280
+ String contentMD5 = calculateContentMD5 (fileContent );
281
+
282
+ ObjectMetadata metadata = ObjectMetadata .builder ().metadata ("testkey" , "testvalue" ).contentLength (contentLength )
283
+ .contentMD5 (contentMD5 ).build ();
272
284
URL signedPutUrl = s3Template .createSignedPutURL (BUCKET_NAME , "file.txt" , Duration .ofMinutes (1 ), metadata ,
273
285
"text/plain" );
274
286
275
287
CloseableHttpClient httpClient = HttpClients .createDefault ();
276
288
HttpPut httpPut = new HttpPut (signedPutUrl .toString ());
277
289
httpPut .setHeader ("x-amz-meta-testkey" , "testvalue" );
278
290
httpPut .setHeader ("Content-Type" , "text/plain" );
279
- HttpEntity body = new StringEntity ("hello" );
291
+ httpPut .setHeader ("Content-MD5" , contentMD5 );
292
+ HttpEntity body = new StringEntity (fileContent );
280
293
httpPut .setEntity (body );
281
294
282
295
HttpResponse response = httpClient .execute (httpPut );
@@ -285,11 +298,36 @@ void createsWorkingSignedPutURL() throws IOException {
285
298
HeadObjectResponse headObjectResponse = client
286
299
.headObject (HeadObjectRequest .builder ().bucket (BUCKET_NAME ).key ("file.txt" ).build ());
287
300
288
- assertThat (headObjectResponse .contentLength ()).isEqualTo (5 );
301
+ assertThat (response .getStatusLine ().getStatusCode ()).isEqualTo (HttpStatusCode .OK );
302
+ assertThat (headObjectResponse .contentLength ()).isEqualTo (contentLength );
289
303
assertThat (headObjectResponse .metadata ().containsKey ("testkey" )).isTrue ();
290
304
assertThat (headObjectResponse .metadata ().get ("testkey" )).isEqualTo ("testvalue" );
291
305
}
292
306
307
+ @ Test
308
+ void signedPutURLFailsForNonMatchingSignature () throws IOException {
309
+ String fileContent = RandomString .make ();
310
+ long contentLength = fileContent .length ();
311
+ String contentMD5 = calculateContentMD5 (fileContent );
312
+ String maliciousContent = RandomString .make ();
313
+
314
+ ObjectMetadata metadata = ObjectMetadata .builder ().contentLength (contentLength ).contentMD5 (contentMD5 ).build ();
315
+ URL signedPutUrl = s3Template .createSignedPutURL (BUCKET_NAME , "file.txt" , Duration .ofMinutes (1 ), metadata ,
316
+ "text/plain" );
317
+
318
+ CloseableHttpClient httpClient = HttpClients .createDefault ();
319
+ HttpPut httpPut = new HttpPut (signedPutUrl .toString ());
320
+ httpPut .setHeader ("Content-Type" , "text/plain" );
321
+ httpPut .setHeader ("Content-MD5" , contentMD5 );
322
+ HttpEntity body = new StringEntity (fileContent + maliciousContent );
323
+ httpPut .setEntity (body );
324
+
325
+ HttpResponse response = httpClient .execute (httpPut );
326
+ httpClient .close ();
327
+
328
+ assertThat (response .getStatusLine ().getStatusCode ()).isEqualTo (HttpStatusCode .FORBIDDEN );
329
+ }
330
+
293
331
private void bucketDoesNotExist (ListBucketsResponse r , String bucketName ) {
294
332
assertThat (r .buckets ().stream ().filter (b -> b .name ().equals (bucketName )).findAny ()).isEmpty ();
295
333
}
@@ -298,6 +336,17 @@ private void bucketExists(ListBucketsResponse r, String bucketName) {
298
336
assertThat (r .buckets ().stream ().filter (b -> b .name ().equals (bucketName )).findAny ()).isPresent ();
299
337
}
300
338
339
+ private String calculateContentMD5 (String content ) {
340
+ try {
341
+ MessageDigest md = MessageDigest .getInstance ("MD5" );
342
+ byte [] contentBytes = content .getBytes (StandardCharsets .UTF_8 );
343
+ byte [] mdBytes = md .digest (contentBytes );
344
+ return Base64 .getEncoder ().encodeToString (mdBytes );
345
+ } catch (Exception exception ) {
346
+ throw new RuntimeException ("Failed to calculate Content-MD5" , exception );
347
+ }
348
+ }
349
+
301
350
static class Person {
302
351
private String firstName ;
303
352
private String lastName ;
0 commit comments