2525
2626import java .io .InputStream ;
2727import java .nio .charset .StandardCharsets ;
28+ import java .util .ArrayList ;
2829import java .util .Collections ;
2930import java .util .List ;
3031import software .amazon .awssdk .annotations .SdkInternalApi ;
3132import software .amazon .awssdk .checksums .spi .ChecksumAlgorithm ;
3233import software .amazon .awssdk .http .ContentStreamProvider ;
34+ import software .amazon .awssdk .http .Header ;
3335import software .amazon .awssdk .http .SdkHttpRequest ;
3436import software .amazon .awssdk .http .auth .aws .internal .signer .CredentialScope ;
3537import software .amazon .awssdk .http .auth .aws .internal .signer .checksums .SdkChecksum ;
@@ -52,6 +54,7 @@ public final class AwsChunkedV4aPayloadSigner implements V4aPayloadSigner {
5254 private final CredentialScope credentialScope ;
5355 private final int chunkSize ;
5456 private final ChecksumAlgorithm checksumAlgorithm ;
57+ private final List <Pair <String , List <String >>> preExistingTrailers = new ArrayList <>();
5558
5659 private AwsChunkedV4aPayloadSigner (Builder builder ) {
5760 this .credentialScope = Validate .paramNotNull (builder .credentialScope , "CredentialScope" );
@@ -65,16 +68,14 @@ public static Builder builder() {
6568
6669 @ Override
6770 public ContentStreamProvider sign (ContentStreamProvider payload , V4aContext v4aContext ) {
68- SdkHttpRequest .Builder request = v4aContext .getSignedRequest ();
69- moveContentLength (request );
70-
7171 InputStream inputStream = payload != null ? payload .newStream () : new StringInputStream ("" );
7272 ChunkedEncodedInputStream .Builder chunkedEncodedInputStreamBuilder = ChunkedEncodedInputStream
7373 .builder ()
7474 .inputStream (inputStream )
7575 .chunkSize (chunkSize )
7676 .header (chunk -> Integer .toHexString (chunk .length ).getBytes (StandardCharsets .UTF_8 ));
77- setupPreExistingTrailers (chunkedEncodedInputStreamBuilder , request );
77+
78+ preExistingTrailers .forEach (trailer -> chunkedEncodedInputStreamBuilder .addTrailer (() -> trailer ));
7879
7980 switch (v4aContext .getSigningConfig ().getSignedBodyValue ()) {
8081 case STREAMING_ECDSA_SIGNED_PAYLOAD : {
@@ -83,12 +84,12 @@ public ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4aC
8384 break ;
8485 }
8586 case STREAMING_UNSIGNED_PAYLOAD_TRAILER :
86- setupChecksumTrailerIfNeeded (chunkedEncodedInputStreamBuilder , request );
87+ setupChecksumTrailerIfNeeded (chunkedEncodedInputStreamBuilder );
8788 break ;
8889 case STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER : {
8990 RollingSigner rollingSigner = new RollingSigner (v4aContext .getSignature (), v4aContext .getSigningConfig ());
9091 chunkedEncodedInputStreamBuilder .addExtension (new SigV4aChunkExtensionProvider (rollingSigner , credentialScope ));
91- setupChecksumTrailerIfNeeded (chunkedEncodedInputStreamBuilder , request );
92+ setupChecksumTrailerIfNeeded (chunkedEncodedInputStreamBuilder );
9293 chunkedEncodedInputStreamBuilder .addTrailer (
9394 new SigV4aTrailerProvider (chunkedEncodedInputStreamBuilder .trailers (), rollingSigner , credentialScope )
9495 );
@@ -101,49 +102,152 @@ public ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4aC
101102 return new ResettableContentStreamProvider (chunkedEncodedInputStreamBuilder ::build );
102103 }
103104
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 ();
112137 }
113- SdkChecksum sdkChecksum = fromChecksumAlgorithm (checksumAlgorithm );
114- ChecksumInputStream checksumInputStream = new ChecksumInputStream (
115- builder .inputStream (),
116- Collections .singleton (sdkChecksum )
117- );
118- String checksumHeaderName = checksumHeaderName (checksumAlgorithm );
119138
120- TrailerProvider checksumTrailer = new ChecksumTrailerProvider (sdkChecksum , checksumHeaderName );
139+ // terminating \r\n
140+ encodedContentLength += 2 ;
121141
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 ));
124147 }
125148
126149 /**
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 .
128151 * <p>
129152 * However, we need to validate that these are valid trailers. Since aws-chunked encoding adds the checksum as a trailer, it
130153 * isn't part of the request headers, but other trailers MUST be present in the request-headers.
131154 */
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 )) {
136157 List <String > values = request .matchingHeaders (header );
137158 if (values .isEmpty ()) {
138159 throw new IllegalArgumentException (header + " must be present in the request headers to be a valid trailer." );
139160 }
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 ));
143162 request .removeHeader (header );
144163 }
145164 }
146165
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+
147251 static final class Builder {
148252 private CredentialScope credentialScope ;
149253 private Integer chunkSize ;
0 commit comments