24
24
import java .time .Instant ;
25
25
import java .util .ArrayList ;
26
26
import java .util .Arrays ;
27
+ import java .util .Collections ;
27
28
import java .util .Comparator ;
28
29
import java .util .List ;
30
+ import java .util .SortedMap ;
31
+ import java .util .TreeMap ;
29
32
import software .amazon .awssdk .annotations .SdkInternalApi ;
30
33
import software .amazon .awssdk .auth .credentials .AwsCredentials ;
31
34
import software .amazon .awssdk .auth .credentials .AwsSessionCredentials ;
41
44
import software .amazon .awssdk .core .internal .util .HttpChecksumUtils ;
42
45
import software .amazon .awssdk .core .signer .Presigner ;
43
46
import software .amazon .awssdk .http .SdkHttpFullRequest ;
47
+ import software .amazon .awssdk .http .SdkHttpRequest ;
44
48
import software .amazon .awssdk .utils .BinaryUtils ;
45
49
import software .amazon .awssdk .utils .Logger ;
46
50
import software .amazon .awssdk .utils .Pair ;
@@ -81,6 +85,7 @@ protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request,
81
85
ContentChecksum contentChecksum ) {
82
86
83
87
SdkHttpFullRequest .Builder mutableRequest = request .toBuilder ();
88
+
84
89
AwsCredentials sanitizedCredentials = sanitizeCredentials (signingParams .awsCredentials ());
85
90
if (sanitizedCredentials instanceof AwsSessionCredentials ) {
86
91
addSessionCredentials (mutableRequest , (AwsSessionCredentials ) sanitizedCredentials );
@@ -97,9 +102,11 @@ protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request,
97
102
putChecksumHeader (signingParams .checksumParams (), contentChecksum .contentFlexibleChecksum (),
98
103
mutableRequest , contentChecksum .contentHash ());
99
104
100
- CanonicalRequest canonicalRequest = createCanonicalRequest (mutableRequest ,
105
+ CanonicalRequest canonicalRequest = createCanonicalRequest (request ,
106
+ mutableRequest ,
101
107
contentChecksum .contentHash (),
102
- signingParams .doubleUrlEncode ());
108
+ signingParams .doubleUrlEncode (),
109
+ signingParams .normalizePath ());
103
110
104
111
String canonicalRequestString = canonicalRequest .string ();
105
112
String stringToSign = createStringToSign (canonicalRequestString , requestParams );
@@ -138,8 +145,11 @@ protected SdkHttpFullRequest.Builder doPresign(SdkHttpFullRequest request,
138
145
// Add the important parameters for v4 signing
139
146
String contentSha256 = calculateContentHashPresign (mutableRequest , signingParams );
140
147
141
- CanonicalRequest canonicalRequest = createCanonicalRequest (mutableRequest , contentSha256 ,
142
- signingParams .doubleUrlEncode ());
148
+ CanonicalRequest canonicalRequest = createCanonicalRequest (request ,
149
+ mutableRequest ,
150
+ contentSha256 ,
151
+ signingParams .doubleUrlEncode (),
152
+ signingParams .normalizePath ());
143
153
144
154
addPreSignInformationToRequest (mutableRequest , canonicalRequest , sanitizedCredentials ,
145
155
requestParams , expirationInSeconds );
@@ -237,10 +247,12 @@ protected final byte[] deriveSigningKey(AwsCredentials credentials, Instant sign
237
247
* .amazon.com/general/latest/gr/sigv4-create-canonical-request.html to
238
248
* generate the canonical request.
239
249
*/
240
- private CanonicalRequest createCanonicalRequest (SdkHttpFullRequest .Builder request ,
250
+ private CanonicalRequest createCanonicalRequest (SdkHttpFullRequest request ,
251
+ SdkHttpFullRequest .Builder requestBuilder ,
241
252
String contentSha256 ,
242
- boolean doubleUrlEncode ) {
243
- return new CanonicalRequest (request , contentSha256 , doubleUrlEncode );
253
+ boolean doubleUrlEncode ,
254
+ boolean normalizePath ) {
255
+ return new CanonicalRequest (request , requestBuilder , contentSha256 , doubleUrlEncode , normalizePath );
244
256
}
245
257
246
258
/**
@@ -251,6 +263,8 @@ private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest.Builder reque
251
263
private String createStringToSign (String canonicalRequest ,
252
264
Aws4SignerRequestParams requestParams ) {
253
265
266
+ LOG .debug (() -> "AWS4 Canonical Request: " + canonicalRequest );
267
+
254
268
String requestHash = BinaryUtils .toHex (hash (canonicalRequest ));
255
269
256
270
String stringToSign = requestParams .getSigningAlgorithm () +
@@ -330,7 +344,7 @@ private void addPreSignInformationToRequest(SdkHttpFullRequest.Builder mutableRe
330
344
* @param ch the character to be tested
331
345
* @return true if the character is white space, false otherwise.
332
346
*/
333
- private boolean isWhiteSpace (final char ch ) {
347
+ private static boolean isWhiteSpace (char ch ) {
334
348
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\u000b' || ch == '\r' || ch == '\f' ;
335
349
}
336
350
@@ -402,8 +416,15 @@ protected <B extends Aws4SignerParams.Builder> B extractSignerParams(B paramsBui
402
416
.signingRegion (executionAttributes .getAttribute (AwsSignerExecutionAttribute .SIGNING_REGION ))
403
417
.timeOffset (executionAttributes .getAttribute (AwsSignerExecutionAttribute .TIME_OFFSET ));
404
418
405
- if (executionAttributes .getAttribute (AwsSignerExecutionAttribute .SIGNER_DOUBLE_URL_ENCODE ) != null ) {
406
- paramsBuilder .doubleUrlEncode (executionAttributes .getAttribute (AwsSignerExecutionAttribute .SIGNER_DOUBLE_URL_ENCODE ));
419
+ Boolean doubleUrlEncode = executionAttributes .getAttribute (AwsSignerExecutionAttribute .SIGNER_DOUBLE_URL_ENCODE );
420
+ if (doubleUrlEncode != null ) {
421
+ paramsBuilder .doubleUrlEncode (doubleUrlEncode );
422
+ }
423
+
424
+ Boolean normalizePath =
425
+ executionAttributes .getAttribute (AwsSignerExecutionAttribute .SIGNER_NORMALIZE_PATH );
426
+ if (normalizePath != null ) {
427
+ paramsBuilder .normalizePath (normalizePath );
407
428
}
408
429
ChecksumSpecs checksumSpecs = executionAttributes .getAttribute (RESOLVED_CHECKSUM_SPECS );
409
430
if (checksumSpecs != null && checksumSpecs .algorithm () != null ) {
@@ -453,30 +474,41 @@ private SdkChecksum createSdkChecksumFromParams(T signingParams, SdkHttpFullRequ
453
474
return null ;
454
475
}
455
476
456
- private class CanonicalRequest {
457
- private final SdkHttpFullRequest .Builder request ;
477
+ static final class CanonicalRequest {
478
+ private final SdkHttpFullRequest request ;
479
+ private final SdkHttpFullRequest .Builder requestBuilder ;
458
480
private final String contentSha256 ;
459
481
private final boolean doubleUrlEncode ;
482
+ private final boolean normalizePath ;
460
483
461
484
private String canonicalRequestString ;
462
485
private StringBuilder signedHeaderStringBuilder ;
463
486
private List <Pair <String , List <String >>> canonicalHeaders ;
464
487
private String signedHeaderString ;
465
488
466
- private CanonicalRequest (SdkHttpFullRequest .Builder request , String contentSha256 , boolean doubleUrlEncode ) {
489
+ CanonicalRequest (SdkHttpFullRequest request ,
490
+ SdkHttpFullRequest .Builder requestBuilder ,
491
+ String contentSha256 ,
492
+ boolean doubleUrlEncode ,
493
+ boolean normalizePath ) {
467
494
this .request = request ;
495
+ this .requestBuilder = requestBuilder ;
468
496
this .contentSha256 = contentSha256 ;
469
497
this .doubleUrlEncode = doubleUrlEncode ;
498
+ this .normalizePath = normalizePath ;
470
499
}
471
500
472
501
public String string () {
473
502
if (canonicalRequestString == null ) {
474
503
StringBuilder canonicalRequest = new StringBuilder (512 );
475
- canonicalRequest .append (request .method ().toString ())
504
+ canonicalRequest .append (requestBuilder .method ().toString ())
476
505
.append (SignerConstant .LINE_SEPARATOR );
477
- addCanonicalizedResourcePath (canonicalRequest , request .encodedPath (), doubleUrlEncode );
506
+ addCanonicalizedResourcePath (canonicalRequest ,
507
+ request ,
508
+ doubleUrlEncode ,
509
+ normalizePath );
478
510
canonicalRequest .append (SignerConstant .LINE_SEPARATOR );
479
- addCanonicalizedQueryString (canonicalRequest , request );
511
+ addCanonicalizedQueryString (canonicalRequest , requestBuilder );
480
512
canonicalRequest .append (SignerConstant .LINE_SEPARATOR );
481
513
addCanonicalizedHeaderString (canonicalRequest , canonicalHeaders ());
482
514
canonicalRequest .append (SignerConstant .LINE_SEPARATOR )
@@ -488,6 +520,82 @@ public String string() {
488
520
return canonicalRequestString ;
489
521
}
490
522
523
+ private void addCanonicalizedResourcePath (StringBuilder result ,
524
+ SdkHttpRequest request ,
525
+ boolean urlEncode ,
526
+ boolean normalizePath ) {
527
+ String path = normalizePath ? request .getUri ().normalize ().getRawPath ()
528
+ : request .encodedPath ();
529
+
530
+ if (StringUtils .isEmpty (path )) {
531
+ result .append ("/" );
532
+ return ;
533
+ }
534
+
535
+ if (urlEncode ) {
536
+ path = SdkHttpUtils .urlEncodeIgnoreSlashes (path );
537
+ }
538
+
539
+ if (!path .startsWith ("/" )) {
540
+ result .append ("/" );
541
+ }
542
+ result .append (path );
543
+
544
+ // Normalization can leave a trailing slash at the end of the resource path,
545
+ // even if the input path doesn't end with one. Example input: /foo/bar/.
546
+ // Remove the trailing slash if the input path doesn't end with one.
547
+ boolean trimTrailingSlash = normalizePath &&
548
+ path .length () > 1 &&
549
+ !request .encodedPath ().endsWith ("/" ) &&
550
+ result .charAt (result .length () - 1 ) == '/' ;
551
+ if (trimTrailingSlash ) {
552
+ result .setLength (result .length () - 1 );
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Examines the specified query string parameters and returns a
558
+ * canonicalized form.
559
+ * <p>
560
+ * The canonicalized query string is formed by first sorting all the query
561
+ * string parameters, then URI encoding both the key and value and then
562
+ * joining them, in order, separating key value pairs with an '&'.
563
+ *
564
+ * @return A canonicalized form for the specified query string parameters.
565
+ */
566
+ private void addCanonicalizedQueryString (StringBuilder result , SdkHttpRequest .Builder httpRequest ) {
567
+
568
+ SortedMap <String , List <String >> sorted = new TreeMap <>();
569
+
570
+ /**
571
+ * Signing protocol expects the param values also to be sorted after url
572
+ * encoding in addition to sorted parameter names.
573
+ */
574
+ httpRequest .forEachRawQueryParameter ((key , values ) -> {
575
+ if (StringUtils .isEmpty (key )) {
576
+ // Do not sign empty keys.
577
+ return ;
578
+ }
579
+
580
+ String encodedParamName = SdkHttpUtils .urlEncode (key );
581
+
582
+ List <String > encodedValues = new ArrayList <>(values .size ());
583
+ for (String value : values ) {
584
+ String encodedValue = SdkHttpUtils .urlEncode (value );
585
+
586
+ // Null values should be treated as empty for the purposes of signing, not missing.
587
+ // For example "?foo=" instead of "?foo".
588
+ String signatureFormattedEncodedValue = encodedValue == null ? "" : encodedValue ;
589
+
590
+ encodedValues .add (signatureFormattedEncodedValue );
591
+ }
592
+ Collections .sort (encodedValues );
593
+ sorted .put (encodedParamName , encodedValues );
594
+ });
595
+
596
+ SdkHttpUtils .flattenQueryParameters (result , sorted );
597
+ }
598
+
491
599
public StringBuilder signedHeaderStringBuilder () {
492
600
if (signedHeaderStringBuilder == null ) {
493
601
signedHeaderStringBuilder = new StringBuilder ();
@@ -505,7 +613,7 @@ public String signedHeaderString() {
505
613
506
614
private List <Pair <String , List <String >>> canonicalHeaders () {
507
615
if (canonicalHeaders == null ) {
508
- canonicalHeaders = canonicalizeSigningHeaders (request );
616
+ canonicalHeaders = canonicalizeSigningHeaders (requestBuilder );
509
617
}
510
618
return canonicalHeaders ;
511
619
}
0 commit comments