25
25
26
26
import java .io .InputStream ;
27
27
import java .nio .charset .StandardCharsets ;
28
+ import java .util .ArrayList ;
28
29
import java .util .Collections ;
29
30
import java .util .List ;
30
31
import software .amazon .awssdk .annotations .SdkInternalApi ;
31
32
import software .amazon .awssdk .checksums .spi .ChecksumAlgorithm ;
32
33
import software .amazon .awssdk .http .ContentStreamProvider ;
34
+ import software .amazon .awssdk .http .Header ;
33
35
import software .amazon .awssdk .http .SdkHttpRequest ;
34
36
import software .amazon .awssdk .http .auth .aws .internal .signer .CredentialScope ;
35
37
import software .amazon .awssdk .http .auth .aws .internal .signer .checksums .SdkChecksum ;
@@ -52,6 +54,7 @@ public final class AwsChunkedV4aPayloadSigner implements V4aPayloadSigner {
52
54
private final CredentialScope credentialScope ;
53
55
private final int chunkSize ;
54
56
private final ChecksumAlgorithm checksumAlgorithm ;
57
+ private final List <Pair <String , List <String >>> preExistingTrailers = new ArrayList <>();
55
58
56
59
private AwsChunkedV4aPayloadSigner (Builder builder ) {
57
60
this .credentialScope = Validate .paramNotNull (builder .credentialScope , "CredentialScope" );
@@ -65,16 +68,14 @@ public static Builder builder() {
65
68
66
69
@ Override
67
70
public ContentStreamProvider sign (ContentStreamProvider payload , V4aContext v4aContext ) {
68
- SdkHttpRequest .Builder request = v4aContext .getSignedRequest ();
69
- moveContentLength (request );
70
-
71
71
InputStream inputStream = payload != null ? payload .newStream () : new StringInputStream ("" );
72
72
ChunkedEncodedInputStream .Builder chunkedEncodedInputStreamBuilder = ChunkedEncodedInputStream
73
73
.builder ()
74
74
.inputStream (inputStream )
75
75
.chunkSize (chunkSize )
76
76
.header (chunk -> Integer .toHexString (chunk .length ).getBytes (StandardCharsets .UTF_8 ));
77
- setupPreExistingTrailers (chunkedEncodedInputStreamBuilder , request );
77
+
78
+ preExistingTrailers .forEach (trailer -> chunkedEncodedInputStreamBuilder .addTrailer (() -> trailer ));
78
79
79
80
switch (v4aContext .getSigningConfig ().getSignedBodyValue ()) {
80
81
case STREAMING_ECDSA_SIGNED_PAYLOAD : {
@@ -83,12 +84,12 @@ public ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4aC
83
84
break ;
84
85
}
85
86
case STREAMING_UNSIGNED_PAYLOAD_TRAILER :
86
- setupChecksumTrailerIfNeeded (chunkedEncodedInputStreamBuilder , request );
87
+ setupChecksumTrailerIfNeeded (chunkedEncodedInputStreamBuilder );
87
88
break ;
88
89
case STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER : {
89
90
RollingSigner rollingSigner = new RollingSigner (v4aContext .getSignature (), v4aContext .getSigningConfig ());
90
91
chunkedEncodedInputStreamBuilder .addExtension (new SigV4aChunkExtensionProvider (rollingSigner , credentialScope ));
91
- setupChecksumTrailerIfNeeded (chunkedEncodedInputStreamBuilder , request );
92
+ setupChecksumTrailerIfNeeded (chunkedEncodedInputStreamBuilder );
92
93
chunkedEncodedInputStreamBuilder .addTrailer (
93
94
new SigV4aTrailerProvider (chunkedEncodedInputStreamBuilder .trailers (), rollingSigner , credentialScope )
94
95
);
@@ -101,49 +102,152 @@ public ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4aC
101
102
return new ResettableContentStreamProvider (chunkedEncodedInputStreamBuilder ::build );
102
103
}
103
104
104
- /**
105
- * Add the checksum as a chunk-trailer and add it to the request's trailer header.
106
- * <p>
107
- * The checksum-algorithm MUST be set if this is called, otherwise it will throw.
108
- */
109
- private void setupChecksumTrailerIfNeeded (ChunkedEncodedInputStream .Builder builder , SdkHttpRequest .Builder request ) {
110
- if (checksumAlgorithm == null ) {
111
- return ;
105
+ @ Override
106
+ public void beforeSigning (SdkHttpRequest .Builder request , ContentStreamProvider payload , String checksum ) {
107
+ long encodedContentLength = 0 ;
108
+ long contentLength = moveContentLength (request , payload != null ? payload .newStream () : new StringInputStream ("" ));
109
+ setupPreExistingTrailers (request );
110
+
111
+ // pre-existing trailers
112
+ encodedContentLength += calculateExistingTrailersLength ();
113
+
114
+ switch (checksum ) {
115
+ case STREAMING_ECDSA_SIGNED_PAYLOAD : {
116
+ long extensionsLength = 161 ; // ;chunk-signature:<sigv4a-ecsda hex signature, 144 bytes>
117
+ encodedContentLength += calculateChunksLength (contentLength , extensionsLength );
118
+ break ;
119
+ }
120
+ case STREAMING_UNSIGNED_PAYLOAD_TRAILER :
121
+ if (checksumAlgorithm != null ) {
122
+ encodedContentLength += calculateChecksumTrailerLength (checksumHeaderName (checksumAlgorithm ));
123
+ }
124
+ encodedContentLength += calculateChunksLength (contentLength , 0 );
125
+ break ;
126
+ case STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER : {
127
+ long extensionsLength = 161 ; // ;chunk-signature:<sigv4a-ecsda hex signature, 144 bytes>
128
+ encodedContentLength += calculateChunksLength (contentLength , extensionsLength );
129
+ if (checksumAlgorithm != null ) {
130
+ encodedContentLength += calculateChecksumTrailerLength (checksumHeaderName (checksumAlgorithm ));
131
+ }
132
+ encodedContentLength += 170 ; // x-amz-trailer-signature:<sigv4a-ecsda hex signature, 144 bytes>\r\n
133
+ break ;
134
+ }
135
+ default :
136
+ throw new UnsupportedOperationException ();
112
137
}
113
- SdkChecksum sdkChecksum = fromChecksumAlgorithm (checksumAlgorithm );
114
- ChecksumInputStream checksumInputStream = new ChecksumInputStream (
115
- builder .inputStream (),
116
- Collections .singleton (sdkChecksum )
117
- );
118
- String checksumHeaderName = checksumHeaderName (checksumAlgorithm );
119
138
120
- TrailerProvider checksumTrailer = new ChecksumTrailerProvider (sdkChecksum , checksumHeaderName );
139
+ // terminating \r\n
140
+ encodedContentLength += 2 ;
121
141
122
- request .appendHeader (X_AMZ_TRAILER , checksumHeaderName );
123
- builder .inputStream (checksumInputStream ).addTrailer (checksumTrailer );
142
+ if (checksumAlgorithm != null ) {
143
+ String checksumHeaderName = checksumHeaderName (checksumAlgorithm );
144
+ request .appendHeader (X_AMZ_TRAILER , checksumHeaderName );
145
+ }
146
+ request .putHeader (Header .CONTENT_LENGTH , Long .toString (encodedContentLength ));
124
147
}
125
148
126
149
/**
127
- * Create chunk-trailers for each pre-existing trailer given in the request.
150
+ * Set up a map of pre-existing trailer (headers) for the given request to be used when chunk-encoding the payload .
128
151
* <p>
129
152
* However, we need to validate that these are valid trailers. Since aws-chunked encoding adds the checksum as a trailer, it
130
153
* isn't part of the request headers, but other trailers MUST be present in the request-headers.
131
154
*/
132
- private void setupPreExistingTrailers (ChunkedEncodedInputStream .Builder builder , SdkHttpRequest .Builder request ) {
133
- List <String > trailerHeaders = request .matchingHeaders (X_AMZ_TRAILER );
134
-
135
- for (String header : trailerHeaders ) {
155
+ private void setupPreExistingTrailers (SdkHttpRequest .Builder request ) {
156
+ for (String header : request .matchingHeaders (X_AMZ_TRAILER )) {
136
157
List <String > values = request .matchingHeaders (header );
137
158
if (values .isEmpty ()) {
138
159
throw new IllegalArgumentException (header + " must be present in the request headers to be a valid trailer." );
139
160
}
140
-
141
- // Add the trailer to the aws-chunked stream-builder, and remove it from the request headers
142
- builder .addTrailer (() -> Pair .of (header , values ));
161
+ preExistingTrailers .add (Pair .of (header , values ));
143
162
request .removeHeader (header );
144
163
}
145
164
}
146
165
166
+ private long calculateChunksLength (long contentLength , long extensionsLength ) {
167
+ long lengthInBytes = 0 ;
168
+ long chunkHeaderLength = Integer .toHexString (chunkSize ).length ();
169
+ long numChunks = contentLength / chunkSize ;
170
+
171
+ // normal chunks
172
+ // x<metadata>\r\n<data>\r\n
173
+ lengthInBytes += numChunks * (chunkHeaderLength + extensionsLength + 2 + chunkSize + 2 );
174
+
175
+ // remaining chunk
176
+ // x<metadata>\r\n<data>\r\n
177
+ long remainingBytes = contentLength % chunkSize ;
178
+ if (remainingBytes > 0 ) {
179
+ long remainingChunkHeaderLength = Long .toHexString (remainingBytes ).length ();
180
+ lengthInBytes += remainingChunkHeaderLength + extensionsLength + 2 + remainingBytes + 2 ;
181
+ }
182
+
183
+ // final chunk
184
+ // 0<metadata>\r\n
185
+ lengthInBytes += 1 + extensionsLength + 2 ;
186
+
187
+ return lengthInBytes ;
188
+ }
189
+
190
+ private long calculateExistingTrailersLength () {
191
+ long lengthInBytes = 0 ;
192
+
193
+ for (Pair <String , List <String >> trailer : preExistingTrailers ) {
194
+ // size of trailer
195
+ lengthInBytes += calculateTrailerLength (trailer );
196
+ }
197
+
198
+ return lengthInBytes ;
199
+ }
200
+
201
+ private long calculateTrailerLength (Pair <String , List <String >> trailer ) {
202
+ // size of trailer-header and colon
203
+ long lengthInBytes = trailer .left ().length () + 1 ;
204
+
205
+ // size of trailer-values
206
+ for (String value : trailer .right ()) {
207
+ lengthInBytes += value .length ();
208
+ }
209
+
210
+ // size of commas between trailer-values, 1 less comma than # of values
211
+ lengthInBytes += trailer .right ().size () - 1 ;
212
+
213
+ // terminating \r\n
214
+ return lengthInBytes + 2 ;
215
+ }
216
+
217
+ private long calculateChecksumTrailerLength (String checksumHeaderName ) {
218
+ // size of checksum trailer-header and colon
219
+ long lengthInBytes = checksumHeaderName .length () + 1 ;
220
+
221
+ // get the base checksum for the algorithm
222
+ SdkChecksum sdkChecksum = fromChecksumAlgorithm (checksumAlgorithm );
223
+ // size of checksum value as hex-string
224
+ lengthInBytes += sdkChecksum .getChecksum ().length ();
225
+
226
+ // terminating \r\n
227
+ return lengthInBytes + 2 ;
228
+ }
229
+
230
+ /**
231
+ * Add the checksum as a trailer to the chunk-encoded stream.
232
+ * <p>
233
+ * If the checksum-algorithm is not present, then nothing is done.
234
+ */
235
+ private void setupChecksumTrailerIfNeeded (ChunkedEncodedInputStream .Builder builder ) {
236
+ if (checksumAlgorithm == null ) {
237
+ return ;
238
+ }
239
+ String checksumHeaderName = checksumHeaderName (checksumAlgorithm );
240
+ SdkChecksum sdkChecksum = fromChecksumAlgorithm (checksumAlgorithm );
241
+ ChecksumInputStream checksumInputStream = new ChecksumInputStream (
242
+ builder .inputStream (),
243
+ Collections .singleton (sdkChecksum )
244
+ );
245
+
246
+ TrailerProvider checksumTrailer = new ChecksumTrailerProvider (sdkChecksum , checksumHeaderName );
247
+
248
+ builder .inputStream (checksumInputStream ).addTrailer (checksumTrailer );
249
+ }
250
+
147
251
static final class Builder {
148
252
private CredentialScope credentialScope ;
149
253
private Integer chunkSize ;
0 commit comments