diff --git a/.changes/next-release/feature-AWSSDKforJavav2-9d16701.json b/.changes/next-release/feature-AWSSDKforJavav2-9d16701.json new file mode 100644 index 000000000000..82cf178b8dd0 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-9d16701.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "This update enables reusing the initially computed payload checksum of a request across all request attempts. This ensures that even if the content is changed from one attempt to the next, the checksum included in the request will remain the same and the request will be rejected by the service." +} diff --git a/core/auth/pom.xml b/core/auth/pom.xml index a5532075147d..09cbe9c726e5 100644 --- a/core/auth/pom.xml +++ b/core/auth/pom.xml @@ -93,6 +93,11 @@ http-auth-spi ${awsjavasdk.version} + + software.amazon.awssdk + checksums-spi + ${awsjavasdk.version} + software.amazon.eventstream eventstream diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/chunkedencoding/AwsSignedChunkedEncodingInputStream.java b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/chunkedencoding/AwsSignedChunkedEncodingInputStream.java index 3174eb7c6caa..a7772fe9f54e 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/chunkedencoding/AwsSignedChunkedEncodingInputStream.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/chunkedencoding/AwsSignedChunkedEncodingInputStream.java @@ -19,11 +19,13 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; import software.amazon.awssdk.core.checksums.Algorithm; import software.amazon.awssdk.core.checksums.SdkChecksum; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.internal.chunked.AwsChunkedEncodingConfig; import software.amazon.awssdk.core.internal.io.AwsChunkedEncodingInputStream; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.BinaryUtils; /** @@ -60,12 +62,15 @@ public final class AwsSignedChunkedEncodingInputStream extends AwsChunkedEncodin * @param config The configuration allows the user to customize chunk size and buffer size. * See {@link AwsChunkedEncodingConfig} for default values. */ - private AwsSignedChunkedEncodingInputStream(InputStream in, SdkChecksum sdkChecksum, + private AwsSignedChunkedEncodingInputStream(InputStream in, + ChecksumAlgorithm checksumAlgorithm, + SdkChecksum sdkChecksum, + PayloadChecksumStore checksumStore, String checksumHeaderForTrailer, String headerSignature, AwsChunkSigner chunkSigner, AwsChunkedEncodingConfig config) { - super(in, sdkChecksum, checksumHeaderForTrailer, config); + super(in, checksumAlgorithm, sdkChecksum, checksumStore, checksumHeaderForTrailer, config); this.chunkSigner = chunkSigner; this.previousChunkSignature = headerSignature; this.headerSignature = headerSignature; @@ -103,9 +108,14 @@ public Builder awsChunkSigner(AwsChunkSigner awsChunkSigner) { public AwsSignedChunkedEncodingInputStream build() { - return new AwsSignedChunkedEncodingInputStream(this.inputStream, this.sdkChecksum, this.checksumHeaderForTrailer, + return new AwsSignedChunkedEncodingInputStream(this.inputStream, + this.checksumAlgorithm, + this.sdkChecksum, + this.checksumStore, + this.checksumHeaderForTrailer, this.headerSignature, - this.awsChunkSigner, this.awsChunkedEncodingConfig); + this.awsChunkSigner, + this.awsChunkedEncodingConfig); } } diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSigner.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSigner.java index 0124b2ea2c56..de5d16b92799 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSigner.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSigner.java @@ -35,12 +35,15 @@ import software.amazon.awssdk.http.Header; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.aws.internal.signer.CredentialScope; +import software.amazon.awssdk.http.auth.aws.internal.signer.NoOpPayloadChecksumStore; import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.ChecksumTrailerProvider; import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.ChunkedEncodedInputStream; import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.TrailerProvider; import software.amazon.awssdk.http.auth.aws.internal.signer.io.ChecksumInputStream; import software.amazon.awssdk.http.auth.aws.internal.signer.io.ResettableContentStreamProvider; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.BinaryUtils; +import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.Pair; import software.amazon.awssdk.utils.StringInputStream; import software.amazon.awssdk.utils.Validate; @@ -51,16 +54,20 @@ */ @SdkInternalApi public final class AwsChunkedV4aPayloadSigner implements V4aPayloadSigner { + private static final Logger LOG = Logger.loggerFor(AwsChunkedV4aPayloadSigner.class); private final CredentialScope credentialScope; private final int chunkSize; private final ChecksumAlgorithm checksumAlgorithm; + private final PayloadChecksumStore payloadChecksumStore; private final List>> preExistingTrailers = new ArrayList<>(); private AwsChunkedV4aPayloadSigner(Builder builder) { this.credentialScope = Validate.paramNotNull(builder.credentialScope, "CredentialScope"); this.chunkSize = Validate.isPositive(builder.chunkSize, "ChunkSize"); this.checksumAlgorithm = builder.checksumAlgorithm; + this.payloadChecksumStore = builder.checksumStore == null ? NoOpPayloadChecksumStore.create() : + builder.checksumStore; } public static Builder builder() { @@ -241,21 +248,41 @@ private void setupChecksumTrailerIfNeeded(ChunkedEncodedInputStream.Builder buil return; } String checksumHeaderName = checksumHeaderName(checksumAlgorithm); + + String cachedChecksum = getCachedChecksum(); + + if (cachedChecksum != null) { + LOG.debug(() -> String.format("Cached payload checksum available for algorithm %s: %s. Using cached value", + checksumAlgorithm.algorithmId(), checksumHeaderName)); + builder.addTrailer(() -> Pair.of(checksumHeaderName, Collections.singletonList(cachedChecksum))); + return; + } + SdkChecksum sdkChecksum = fromChecksumAlgorithm(checksumAlgorithm); ChecksumInputStream checksumInputStream = new ChecksumInputStream( builder.inputStream(), Collections.singleton(sdkChecksum) ); - TrailerProvider checksumTrailer = new ChecksumTrailerProvider(sdkChecksum, checksumHeaderName); + TrailerProvider checksumTrailer = + new ChecksumTrailerProvider(sdkChecksum, checksumHeaderName, checksumAlgorithm, payloadChecksumStore); builder.inputStream(checksumInputStream).addTrailer(checksumTrailer); } + private String getCachedChecksum() { + byte[] checksumBytes = payloadChecksumStore.getChecksumValue(checksumAlgorithm); + if (checksumBytes != null) { + return BinaryUtils.toBase64(checksumBytes); + } + return null; + } + static final class Builder { private CredentialScope credentialScope; private Integer chunkSize; private ChecksumAlgorithm checksumAlgorithm; + private PayloadChecksumStore checksumStore; public Builder credentialScope(CredentialScope credentialScope) { this.credentialScope = credentialScope; @@ -272,6 +299,11 @@ public Builder checksumAlgorithm(ChecksumAlgorithm checksumAlgorithm) { return this; } + public Builder checksumStore(PayloadChecksumStore checksumStore) { + this.checksumStore = checksumStore; + return this; + } + public AwsChunkedV4aPayloadSigner build() { return new AwsChunkedV4aPayloadSigner(this); } diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/DefaultAwsCrtV4aHttpSigner.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/DefaultAwsCrtV4aHttpSigner.java index ec756d35053e..af2a14e93823 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/DefaultAwsCrtV4aHttpSigner.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/DefaultAwsCrtV4aHttpSigner.java @@ -33,6 +33,7 @@ import static software.amazon.awssdk.http.auth.aws.internal.signer.util.CredentialUtils.sanitizeCredentials; import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.PRESIGN_URL_MAX_EXPIRATION_DURATION; import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_TRAILER; +import static software.amazon.awssdk.http.auth.spi.signer.SdkInternalHttpSignerProperty.CHECKSUM_STORE; import java.time.Clock; import java.time.Duration; @@ -47,11 +48,13 @@ import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.aws.internal.signer.Checksummer; import software.amazon.awssdk.http.auth.aws.internal.signer.CredentialScope; +import software.amazon.awssdk.http.auth.aws.internal.signer.NoOpPayloadChecksumStore; import software.amazon.awssdk.http.auth.aws.signer.AwsV4aHttpSigner; import software.amazon.awssdk.http.auth.aws.signer.RegionSet; import software.amazon.awssdk.http.auth.spi.signer.AsyncSignRequest; import software.amazon.awssdk.http.auth.spi.signer.AsyncSignedRequest; import software.amazon.awssdk.http.auth.spi.signer.BaseSignRequest; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.http.auth.spi.signer.SignRequest; import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; @@ -70,7 +73,7 @@ public final class DefaultAwsCrtV4aHttpSigner implements AwsV4aHttpSigner { @Override public SignedRequest sign(SignRequest request) { - Checksummer checksummer = checksummer(request, null); + Checksummer checksummer = checksummer(request, null, checksumStore(request)); V4aProperties v4aProperties = v4aProperties(request); AwsSigningConfig signingConfig = signingConfig(request, v4aProperties); V4aPayloadSigner payloadSigner = v4aPayloadSigner(request, v4aProperties); @@ -104,7 +107,7 @@ private static V4aProperties v4aProperties(BaseSignRequest request, + SignRequest request, V4aProperties v4aProperties) { boolean isPayloadSigning = isPayloadSigning(request); @@ -117,6 +120,7 @@ private static V4aPayloadSigner v4aPayloadSigner( .credentialScope(v4aProperties.getCredentialScope()) .chunkSize(DEFAULT_CHUNK_SIZE_IN_BYTES) .checksumAlgorithm(request.property(CHECKSUM_ALGORITHM)) + .checksumStore(checksumStore(request)) .build(); } @@ -252,4 +256,12 @@ private static V4aRequestSigningResult sign(SdkHttpRequest request, HttpRequest signingResult.getSignature(), signingConfig); } + + private static PayloadChecksumStore checksumStore(SignRequest request) { + PayloadChecksumStore cache = request.property(CHECKSUM_STORE); + if (cache == null) { + return NoOpPayloadChecksumStore.create(); + } + return cache; + } } diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/AwsChunkedV4PayloadSigner.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/AwsChunkedV4PayloadSigner.java index 391894b28b39..8e2e3a3a168b 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/AwsChunkedV4PayloadSigner.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/AwsChunkedV4PayloadSigner.java @@ -45,7 +45,9 @@ import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.TrailerProvider; import software.amazon.awssdk.http.auth.aws.internal.signer.io.ChecksumInputStream; import software.amazon.awssdk.http.auth.aws.internal.signer.io.ResettableContentStreamProvider; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.BinaryUtils; +import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.Pair; import software.amazon.awssdk.utils.Validate; @@ -55,16 +57,20 @@ */ @SdkInternalApi public final class AwsChunkedV4PayloadSigner implements V4PayloadSigner { + private static final Logger LOG = Logger.loggerFor(AwsChunkedV4PayloadSigner.class); private final CredentialScope credentialScope; private final int chunkSize; private final ChecksumAlgorithm checksumAlgorithm; + private final PayloadChecksumStore payloadChecksumStore; private final List>> preExistingTrailers = new ArrayList<>(); private AwsChunkedV4PayloadSigner(Builder builder) { this.credentialScope = Validate.paramNotNull(builder.credentialScope, "CredentialScope"); this.chunkSize = Validate.isPositive(builder.chunkSize, "ChunkSize"); this.checksumAlgorithm = builder.checksumAlgorithm; + this.payloadChecksumStore = builder.checksumStore == null ? NoOpPayloadChecksumStore.create() : + builder.checksumStore; } public static Builder builder() { @@ -259,22 +265,43 @@ private void setupChecksumTrailerIfNeeded(ChunkedEncodedInputStream.Builder buil if (checksumAlgorithm == null) { return; } + String checksumHeaderName = checksumHeaderName(checksumAlgorithm); + + String cachedChecksum = getCachedChecksum(); + + if (cachedChecksum != null) { + LOG.debug(() -> String.format("Cached payload checksum available for algorithm %s: %s. Using cached value", + checksumAlgorithm.algorithmId(), checksumHeaderName)); + builder.addTrailer(() -> Pair.of(checksumHeaderName, Collections.singletonList(cachedChecksum))); + return; + } + SdkChecksum sdkChecksum = fromChecksumAlgorithm(checksumAlgorithm); ChecksumInputStream checksumInputStream = new ChecksumInputStream( builder.inputStream(), Collections.singleton(sdkChecksum) ); - TrailerProvider checksumTrailer = new ChecksumTrailerProvider(sdkChecksum, checksumHeaderName); + TrailerProvider checksumTrailer = + new ChecksumTrailerProvider(sdkChecksum, checksumHeaderName, checksumAlgorithm, payloadChecksumStore); builder.inputStream(checksumInputStream).addTrailer(checksumTrailer); } + private String getCachedChecksum() { + byte[] checksumBytes = payloadChecksumStore.getChecksumValue(checksumAlgorithm); + if (checksumBytes != null) { + return BinaryUtils.toBase64(checksumBytes); + } + return null; + } + static class Builder { private CredentialScope credentialScope; private Integer chunkSize; private ChecksumAlgorithm checksumAlgorithm; + private PayloadChecksumStore checksumStore; public Builder credentialScope(CredentialScope credentialScope) { this.credentialScope = credentialScope; @@ -291,6 +318,11 @@ public Builder checksumAlgorithm(ChecksumAlgorithm checksumAlgorithm) { return this; } + public Builder checksumStore(PayloadChecksumStore checksumStore) { + this.checksumStore = checksumStore; + return this; + } + public AwsChunkedV4PayloadSigner build() { return new AwsChunkedV4PayloadSigner(this); } diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/Checksummer.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/Checksummer.java index 071ed2bdec9c..32f73a37d865 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/Checksummer.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/Checksummer.java @@ -29,6 +29,7 @@ import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; import software.amazon.awssdk.http.ContentStreamProvider; import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.BinaryUtils; /** @@ -49,6 +50,7 @@ public interface Checksummer { */ static Checksummer create() { return new FlexibleChecksummer( + NoOpPayloadChecksumStore.create(), option().headerName(X_AMZ_CONTENT_SHA256).algorithm(SHA256).formatter(BinaryUtils::toHex).build() ); } @@ -57,9 +59,10 @@ static Checksummer create() { * Get a flexible checksummer that performs two checksums: the given checksum-algorithm and the SHA-256 checksum. It places * the SHA-256 checksum in x-amz-content-sha256 header, and the given checksum-algorithm in the x-amz-checksum-[name] header. */ - static Checksummer forFlexibleChecksum(ChecksumAlgorithm checksumAlgorithm) { + static Checksummer forFlexibleChecksum(ChecksumAlgorithm checksumAlgorithm, PayloadChecksumStore cache) { if (checksumAlgorithm != null) { return new FlexibleChecksummer( + cache, option().headerName(X_AMZ_CONTENT_SHA256).algorithm(SHA256).formatter(BinaryUtils::toHex) .build(), option().headerName(checksumHeaderName(checksumAlgorithm)).algorithm(checksumAlgorithm) @@ -82,9 +85,12 @@ static Checksummer forPrecomputed256Checksum(String precomputedSha256) { * given checksum string. It places the precomputed checksum in x-amz-content-sha256 header, and the given checksum-algorithm * in the x-amz-checksum-[name] header. */ - static Checksummer forFlexibleChecksum(String precomputedSha256, ChecksumAlgorithm checksumAlgorithm) { + static Checksummer forFlexibleChecksum(String precomputedSha256, + ChecksumAlgorithm checksumAlgorithm, + PayloadChecksumStore cache) { if (checksumAlgorithm != null) { return new FlexibleChecksummer( + cache, option().headerName(X_AMZ_CONTENT_SHA256).algorithm(new ConstantChecksumAlgorithm(precomputedSha256)) .formatter(b -> new String(b, StandardCharsets.UTF_8)).build(), option().headerName(checksumHeaderName(checksumAlgorithm)).algorithm(checksumAlgorithm) @@ -96,7 +102,7 @@ static Checksummer forFlexibleChecksum(String precomputedSha256, ChecksumAlgorit } static Checksummer forNoOp() { - return new FlexibleChecksummer(); + return new FlexibleChecksummer(NoOpPayloadChecksumStore.create()); } /** diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/DefaultAwsV4HttpSigner.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/DefaultAwsV4HttpSigner.java index 8ad79a12007e..66ee6d4cf733 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/DefaultAwsV4HttpSigner.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/DefaultAwsV4HttpSigner.java @@ -24,6 +24,7 @@ import static software.amazon.awssdk.http.auth.aws.internal.signer.util.OptionalDependencyLoaderUtil.getEventStreamV4PayloadSigner; import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.PRESIGN_URL_MAX_EXPIRATION_DURATION; import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_TRAILER; +import static software.amazon.awssdk.http.auth.spi.signer.SdkInternalHttpSignerProperty.CHECKSUM_STORE; import java.time.Clock; import java.time.Duration; @@ -38,6 +39,7 @@ import software.amazon.awssdk.http.auth.spi.signer.AsyncSignRequest; import software.amazon.awssdk.http.auth.spi.signer.AsyncSignedRequest; import software.amazon.awssdk.http.auth.spi.signer.BaseSignRequest; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.http.auth.spi.signer.SignRequest; import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; @@ -55,7 +57,7 @@ public final class DefaultAwsV4HttpSigner implements AwsV4HttpSigner { @Override public SignedRequest sign(SignRequest request) { - Checksummer checksummer = checksummer(request, null); + Checksummer checksummer = checksummer(request, null, checksumStore(request)); V4Properties v4Properties = v4Properties(request); V4RequestSigner v4RequestSigner = v4RequestSigner(request, v4Properties); V4PayloadSigner payloadSigner = v4PayloadSigner(request, v4Properties); @@ -140,7 +142,7 @@ private static Checksummer asyncChecksummer(BaseSignRequest request) { + PayloadChecksumStore cache = request.property(CHECKSUM_STORE); + if (cache == null) { + return NoOpPayloadChecksumStore.create(); + } + return cache; + } } diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/FlexibleChecksummer.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/FlexibleChecksummer.java index 2cf8e2cfef26..6c1539462788 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/FlexibleChecksummer.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/FlexibleChecksummer.java @@ -35,6 +35,7 @@ import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.aws.internal.signer.io.ChecksumInputStream; import software.amazon.awssdk.http.auth.aws.internal.signer.io.ChecksumSubscriber; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.Validate; /** @@ -44,10 +45,12 @@ */ @SdkInternalApi public final class FlexibleChecksummer implements Checksummer { + private final PayloadChecksumStore cache; private final Collection + + software.amazon.awssdk + checksums-spi + ${awsjavasdk.version} + org.reactivestreams reactive-streams diff --git a/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/internal/signer/DefaultPayloadChecksumStore.java b/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/internal/signer/DefaultPayloadChecksumStore.java new file mode 100644 index 000000000000..d2b995c19c6d --- /dev/null +++ b/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/internal/signer/DefaultPayloadChecksumStore.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.auth.spi.internal.signer; + +import java.util.concurrent.ConcurrentHashMap; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; + +/** + * Default implementation of {@link PayloadChecksumStore}. + */ +@SdkInternalApi +@ThreadSafe +public class DefaultPayloadChecksumStore implements PayloadChecksumStore { + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + @Override + public byte[] putChecksumValue(ChecksumAlgorithm algorithm, byte[] value) { + return cache.put(algorithm.algorithmId(), value); + } + + @Override + public byte[] getChecksumValue(ChecksumAlgorithm algorithm) { + return cache.get(algorithm.algorithmId()); + } + + @Override + public boolean containsChecksumValue(ChecksumAlgorithm algorithm) { + return cache.containsKey(algorithm.algorithmId()); + } +} diff --git a/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/signer/PayloadChecksumStore.java b/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/signer/PayloadChecksumStore.java new file mode 100644 index 000000000000..f8c8a9d47c7d --- /dev/null +++ b/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/signer/PayloadChecksumStore.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.auth.spi.signer; + +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; +import software.amazon.awssdk.http.auth.spi.internal.signer.DefaultPayloadChecksumStore; + +/** + * Storage object for storing computed checksums for a request payload. + */ +@SdkProtectedApi +public interface PayloadChecksumStore { + /** + * Store the checksum value computed using the given algorithm. + * + * @return The previous value stored for this algorithm or {@code null} if not present. + */ + byte[] putChecksumValue(ChecksumAlgorithm algorithm, byte[] checksum); + + /** + * Retrieve the stored checksum value for the given algorithm. + * + * @return The checksum value for the given algorithm or {@code null} if not present. + */ + byte[] getChecksumValue(ChecksumAlgorithm algorithm); + + /** + * Returns {@code true} if the store contains a checksum value for the given algorithm, {@code false} otherwise. + */ + boolean containsChecksumValue(ChecksumAlgorithm algorithm); + + /** + * Returns the default implementation of this interface. + */ + static PayloadChecksumStore create() { + return new DefaultPayloadChecksumStore(); + } +} diff --git a/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/signer/SdkInternalHttpSignerProperty.java b/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/signer/SdkInternalHttpSignerProperty.java new file mode 100644 index 000000000000..d95db35f6e46 --- /dev/null +++ b/core/http-auth-spi/src/main/java/software/amazon/awssdk/http/auth/spi/signer/SdkInternalHttpSignerProperty.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.auth.spi.signer; + +import software.amazon.awssdk.annotations.SdkProtectedApi; + +/** + * {@code HttpSigner} properties intended for use only by internal components of the SDK and SDK-provided implementations of + * this SPI. + */ +@SdkProtectedApi +public final class SdkInternalHttpSignerProperty { + + /** + * An object for storing checksums calculated for a payload. + * + *

Note, checksums may not be relevant to some signers. + */ + public static final SignerProperty CHECKSUM_STORE = + SignerProperty.create(SdkInternalHttpSignerProperty.class, "ChecksumStore"); + + private SdkInternalHttpSignerProperty() { + } +} diff --git a/core/http-auth-spi/src/test/java/software/amazon/awssdk/http/auth/spi/signer/PayloadChecksumStoreTest.java b/core/http-auth-spi/src/test/java/software/amazon/awssdk/http/auth/spi/signer/PayloadChecksumStoreTest.java new file mode 100644 index 000000000000..4ab329b0ee5d --- /dev/null +++ b/core/http-auth-spi/src/test/java/software/amazon/awssdk/http/auth/spi/signer/PayloadChecksumStoreTest.java @@ -0,0 +1,78 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.auth.spi.signer; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; +import software.amazon.awssdk.http.auth.spi.internal.signer.DefaultPayloadChecksumStore; + +public class PayloadChecksumStoreTest { + private static final ChecksumAlgorithm ALGORITHM = () -> "crc32"; + + @Test + void putChecksumValue_noPreviousEntry_returnsNull() { + DefaultPayloadChecksumStore cache = new DefaultPayloadChecksumStore(); + + byte[] previous = cache.putChecksumValue(ALGORITHM, new byte[] {1, 2, 3}); + + assertThat(previous).isNull(); + } + + @Test + public void putChecksumValue_previousEntry_returnsValue() { + DefaultPayloadChecksumStore cache = new DefaultPayloadChecksumStore(); + + byte[] previous = {1, 2, 3}; + cache.putChecksumValue(ALGORITHM, previous); + + byte[] cached = cache.putChecksumValue(ALGORITHM, new byte[] {4, 5, 6}); + + assertThat(cached).isEqualTo(previous); + } + + @Test + public void getChecksumValue_noEntry_returnsNull() { + DefaultPayloadChecksumStore cache = new DefaultPayloadChecksumStore(); + + assertThat(cache.getChecksumValue(ALGORITHM)).isNull(); + } + + @Test + public void getChecksumValue_hasEntry_returnsValue() { + DefaultPayloadChecksumStore cache = new DefaultPayloadChecksumStore(); + + byte[] value = {1, 2, 3}; + cache.putChecksumValue(ALGORITHM, value); + + assertThat(cache.getChecksumValue(ALGORITHM)).isEqualTo(value); + } + + @Test + void containsChecksumValue_noEntry_returnsFalse() { + DefaultPayloadChecksumStore cache = new DefaultPayloadChecksumStore(); + assertThat(cache.containsChecksumValue(ALGORITHM)).isFalse(); + } + + @Test + void containsChecksumValue_hasEntry_returnsValue() { + DefaultPayloadChecksumStore cache = new DefaultPayloadChecksumStore(); + byte[] value = {1, 2, 3}; + cache.putChecksumValue(ALGORITHM, value); + assertThat(cache.containsChecksumValue(ALGORITHM)).isTrue(); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkInternalExecutionAttribute.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkInternalExecutionAttribute.java index bd09c48d9f43..3fe0f69d3ab8 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkInternalExecutionAttribute.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkInternalExecutionAttribute.java @@ -36,6 +36,8 @@ import software.amazon.awssdk.http.SdkHttpExecutionAttributes; import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme; import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeProvider; +import software.amazon.awssdk.http.auth.spi.signer.HttpSigner; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.identity.spi.IdentityProviders; import software.amazon.awssdk.utils.AttributeMap; @@ -204,6 +206,12 @@ public final class SdkInternalExecutionAttribute extends SdkExecutionAttribute { public static final ExecutionAttribute TOKEN_CONFIGURED_FROM_ENV = new ExecutionAttribute<>( "TokenConfiguredFromEnv"); + /** + * The store used by {@link HttpSigner} implementations to store payload checksums. + */ + public static final ExecutionAttribute CHECKSUM_STORE = + new ExecutionAttribute<>("ChecksumStore"); + /** * The backing attribute for RESOLVED_CHECKSUM_SPECS. * This holds the real ChecksumSpecs value, and is used to map to the ChecksumAlgorithm signer property diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/ChecksumCalculatingAsyncRequestBody.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/ChecksumCalculatingAsyncRequestBody.java index 2226c67fcdc9..c903829d07e2 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/ChecksumCalculatingAsyncRequestBody.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/ChecksumCalculatingAsyncRequestBody.java @@ -34,7 +34,9 @@ import software.amazon.awssdk.core.async.SdkPublisher; import software.amazon.awssdk.core.checksums.Algorithm; import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.internal.checksums.NoOpPayloadChecksumStore; import software.amazon.awssdk.core.internal.util.HttpChecksumUtils; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.BinaryUtils; import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.async.DelegatingSubscriber; @@ -55,6 +57,7 @@ public class ChecksumCalculatingAsyncRequestBody implements AsyncRequestBody { private final ChecksumAlgorithm algorithm; private final String trailerHeader; private final long totalBytes; + private final PayloadChecksumStore payloadChecksumStore; private ChecksumCalculatingAsyncRequestBody(DefaultBuilder builder) { @@ -66,6 +69,7 @@ private ChecksumCalculatingAsyncRequestBody(DefaultBuilder builder) { this.sdkChecksum = builder.algorithm != null ? SdkChecksum.forAlgorithm(algorithm) : null; this.trailerHeader = builder.trailerHeader; this.totalBytes = initTotalBytes(wrapped, builder.contentLengthHeader); + this.payloadChecksumStore = builder.checksumStore != null ? builder.checksumStore : NoOpPayloadChecksumStore.create(); } static long initTotalBytes(AsyncRequestBody wrapped, Long contentLengthHeader) { @@ -118,6 +122,8 @@ public interface Builder extends SdkBuilder s) { SynchronousChunkBuffer synchronousChunkBuffer = new SynchronousChunkBuffer(totalBytes); alwaysInvokeOnNext(wrapped.flatMapIterable(synchronousChunkBuffer::buffer)) - .subscribe(new ChecksumCalculatingSubscriber(s, sdkChecksum, trailerHeader, totalBytes)); + .subscribe(new ChecksumCalculatingSubscriber(s, + algorithm, + sdkChecksum, + payloadChecksumStore, + trailerHeader, + totalBytes)); } private SdkPublisher alwaysInvokeOnNext(SdkPublisher source) { @@ -192,17 +210,24 @@ private SdkPublisher alwaysInvokeOnNext(SdkPublisher sou private static final class ChecksumCalculatingSubscriber implements Subscriber { private final Subscriber wrapped; + private final ChecksumAlgorithm algorithm; private final SdkChecksum checksum; + private final PayloadChecksumStore checksumStore; private final String trailerHeader; private byte[] checksumBytes; private final AtomicLong remainingBytes; private Subscription subscription; ChecksumCalculatingSubscriber(Subscriber wrapped, + ChecksumAlgorithm algorithm, SdkChecksum checksum, - String trailerHeader, long totalBytes) { + PayloadChecksumStore checksumStore, + String trailerHeader, + long totalBytes) { this.wrapped = wrapped; + this.algorithm = algorithm; this.checksum = checksum; + this.checksumStore = checksumStore; this.trailerHeader = trailerHeader; this.remainingBytes = new AtomicLong(totalBytes); } @@ -223,7 +248,11 @@ public void onNext(ByteBuffer byteBuffer) { byteBuffer.reset(); } if (lastByte && checksumBytes == null && checksum != null) { - checksumBytes = checksum.getChecksumBytes(); + checksumBytes = checksumStore.getChecksumValue(algorithm); + if (checksumBytes == null) { + checksumBytes = checksum.getChecksumBytes(); + checksumStore.putChecksumValue(algorithm, checksumBytes); + } ByteBuffer allocatedBuffer = getFinalChecksumAppendedChunk(byteBuffer); wrapped.onNext(allocatedBuffer); } else if (byteBuffer.hasRemaining()) { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/checksums/NoOpPayloadChecksumStore.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/checksums/NoOpPayloadChecksumStore.java new file mode 100644 index 000000000000..68c4b23cc687 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/checksums/NoOpPayloadChecksumStore.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.core.internal.checksums; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; + +@SdkInternalApi +public final class NoOpPayloadChecksumStore implements PayloadChecksumStore { + private NoOpPayloadChecksumStore() { + } + + @Override + public byte[] putChecksumValue(ChecksumAlgorithm algorithm, byte[] checksum) { + return null; + } + + @Override + public byte[] getChecksumValue(ChecksumAlgorithm algorithm) { + return null; + } + + @Override + public boolean containsChecksumValue(ChecksumAlgorithm algorithm) { + return false; + } + + public static NoOpPayloadChecksumStore create() { + return new NoOpPayloadChecksumStore(); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java index d7bb3c699fb5..ca4b4d8f7f2c 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java @@ -22,6 +22,7 @@ import static software.amazon.awssdk.core.HttpChecksumConstant.SIGNING_METHOD; import static software.amazon.awssdk.core.interceptor.SdkExecutionAttribute.RESOLVED_CHECKSUM_SPECS; import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.AUTH_SCHEMES; +import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.CHECKSUM_STORE; import static software.amazon.awssdk.core.internal.io.AwsChunkedInputStream.DEFAULT_CHUNK_SIZE; import static software.amazon.awssdk.core.internal.util.ChunkContentUtils.calculateChecksumTrailerLength; import static software.amazon.awssdk.core.internal.util.ChunkContentUtils.calculateStreamContentLength; @@ -54,6 +55,7 @@ import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.aws.internal.signer.util.ChecksumUtil; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.BinaryUtils; import software.amazon.awssdk.utils.IoUtils; import software.amazon.awssdk.utils.Md5Utils; @@ -75,6 +77,8 @@ public HttpChecksumStage(ClientType clientType) { public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, RequestExecutionContext context) throws Exception { + ensurePayloadChecksumStorePresent(context.executionAttributes()); + if (sraSigningEnabled(context)) { return sraChecksum(request, context); } @@ -84,9 +88,10 @@ public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, Re private SdkHttpFullRequest.Builder legacyChecksum(SdkHttpFullRequest.Builder request, RequestExecutionContext context) { ChecksumSpecs resolvedChecksumSpecs = getResolvedChecksumSpecs(context.executionAttributes()); + PayloadChecksumStore checksumStore = getPayloadChecksumStore(context.executionAttributes()); if (md5ChecksumRequired(request, context)) { - addMd5ChecksumInHeader(request); + addMd5ChecksumInHeader(request, checksumStore); return request; } @@ -96,7 +101,7 @@ private SdkHttpFullRequest.Builder legacyChecksum(SdkHttpFullRequest.Builder req } if (flexibleChecksumInHeaderRequired(context, resolvedChecksumSpecs)) { - addFlexibleChecksumInHeader(request, context, resolvedChecksumSpecs); + addFlexibleChecksumInHeader(request, context, resolvedChecksumSpecs, checksumStore); return request; } @@ -171,10 +176,14 @@ private boolean md5ChecksumRequired(SdkHttpFullRequest.Builder request, RequestE * request body to use that buffered content. We obviously don't want to do that for giant streams, so we haven't opted to do * that yet. */ - private void addMd5ChecksumInHeader(SdkHttpFullRequest.Builder request) { + private void addMd5ChecksumInHeader(SdkHttpFullRequest.Builder request, PayloadChecksumStore checksumStore) { try { - String payloadMd5 = Md5Utils.md5AsBase64(request.contentStreamProvider().newStream()); - request.putHeader(Header.CONTENT_MD5, payloadMd5); + byte[] payloadMd5 = checksumStore.getChecksumValue(DefaultChecksumAlgorithm.MD5); + if (payloadMd5 == null) { + payloadMd5 = Md5Utils.computeMD5Hash(request.contentStreamProvider().newStream()); + checksumStore.putChecksumValue(DefaultChecksumAlgorithm.MD5, payloadMd5); + } + request.putHeader(Header.CONTENT_MD5, BinaryUtils.toBase64(payloadMd5)); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -234,7 +243,11 @@ private void addFlexibleChecksumInTrailer(SdkHttpFullRequest.Builder request, Re int chunkSize = 0; if (clientType == ClientType.SYNC) { - request.contentStreamProvider(new ChecksumCalculatingStreamProvider(request.contentStreamProvider(), checksumSpecs)); + request.contentStreamProvider( + new ChecksumCalculatingStreamProvider(request.contentStreamProvider(), + checksumSpecs, + getPayloadChecksumStore(context.executionAttributes()) + )); originalContentLength = context.executionContext().interceptorContext().requestBody().get().optionalContentLength().orElse(0L); chunkSize = DEFAULT_CHUNK_SIZE; @@ -244,6 +257,7 @@ private void addFlexibleChecksumInTrailer(SdkHttpFullRequest.Builder request, Re ChecksumCalculatingAsyncRequestBody.builder() .asyncRequestBody(context.requestProvider()) .algorithm(checksumSpecs.algorithmV2()) + .checksumStore(getPayloadChecksumStore(context.executionAttributes())) .trailerHeader(checksumSpecs.headerName()); Optional maybeContentLengthHeader = request.firstMatchingHeader("Content-Length") .map(Long::parseLong); @@ -307,32 +321,55 @@ private boolean flexibleChecksumInHeaderRequired(RequestExecutionContext context * that yet. */ private void addFlexibleChecksumInHeader(SdkHttpFullRequest.Builder request, RequestExecutionContext context, - ChecksumSpecs checksumSpecs) { + ChecksumSpecs checksumSpecs, PayloadChecksumStore checksumStore) { try { Algorithm legacyAlgorithm = checksumSpecs.algorithm(); - String payloadChecksum = BinaryUtils.toBase64(HttpChecksumUtils.computeChecksum( - context.executionContext().interceptorContext().requestBody().get().contentStreamProvider().newStream(), - legacyAlgorithm)); - request.putHeader(checksumSpecs.headerName(), payloadChecksum); + ChecksumAlgorithm newAlgorithm = HttpChecksumUtils.toNewChecksumAlgorithm(legacyAlgorithm); + byte[] payloadChecksum = checksumStore.getChecksumValue(newAlgorithm); + if (payloadChecksum == null) { + payloadChecksum = HttpChecksumUtils.computeChecksum( + context.executionContext().interceptorContext().requestBody().get().contentStreamProvider().newStream(), + legacyAlgorithm); + checksumStore.putChecksumValue(newAlgorithm, payloadChecksum); + } + String headerValue = BinaryUtils.toBase64(payloadChecksum); + request.putHeader(checksumSpecs.headerName(), headerValue); } catch (IOException e) { throw new UncheckedIOException(e); } } + private void ensurePayloadChecksumStorePresent(ExecutionAttributes executionAttributes) { + PayloadChecksumStore cache = getPayloadChecksumStore(executionAttributes); + if (cache == null) { + cache = PayloadChecksumStore.create(); + executionAttributes.putAttribute(CHECKSUM_STORE, cache); + } + } + + private PayloadChecksumStore getPayloadChecksumStore(ExecutionAttributes executionAttributes) { + return executionAttributes.getAttribute(CHECKSUM_STORE); + } + static final class ChecksumCalculatingStreamProvider implements ContentStreamProvider { private final ContentStreamProvider underlyingInputStreamProvider; private final String checksumHeaderForTrailer; private final ChecksumSpecs checksumSpecs; + private final PayloadChecksumStore checksumStore; private InputStream currentStream; + private final ChecksumAlgorithm checksumAlgorithm; private software.amazon.awssdk.core.checksums.SdkChecksum sdkChecksum; ChecksumCalculatingStreamProvider(ContentStreamProvider underlyingInputStreamProvider, - ChecksumSpecs checksumSpecs) { + ChecksumSpecs checksumSpecs, + PayloadChecksumStore checksumStore) { this.underlyingInputStreamProvider = underlyingInputStreamProvider; this.sdkChecksum = software.amazon.awssdk.core.checksums.SdkChecksum.forAlgorithm( checksumSpecs.algorithm()); + this.checksumAlgorithm = HttpChecksumUtils.toNewChecksumAlgorithm(checksumSpecs.algorithm()); this.checksumHeaderForTrailer = checksumSpecs.headerName(); this.checksumSpecs = checksumSpecs; + this.checksumStore = checksumStore; } @Override @@ -340,7 +377,9 @@ public InputStream newStream() { closeCurrentStream(); currentStream = AwsUnsignedChunkedEncodingInputStream.builder() .inputStream(underlyingInputStreamProvider.newStream()) + .checksumAlgorithm(checksumAlgorithm) .sdkChecksum(sdkChecksum) + .checksumStore(checksumStore) .checksumHeaderForTrailer(checksumHeaderForTrailer) .build(); return currentStream; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStage.java index d92628c2bba2..3d3240c3d4d8 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStage.java @@ -35,6 +35,8 @@ import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; import software.amazon.awssdk.http.auth.spi.signer.HttpSigner; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; +import software.amazon.awssdk.http.auth.spi.signer.SdkInternalHttpSignerProperty; import software.amazon.awssdk.http.auth.spi.signer.SignRequest; import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; import software.amazon.awssdk.identity.spi.Identity; @@ -89,8 +91,12 @@ private SdkHttpFullRequest sraSignRequest(SdkHttpFullReques CompletableFuture identityFuture = selectedAuthScheme.identity(); T identity = CompletableFutureUtils.joinLikeSync(identityFuture); + // Should not be null, added by HttpChecksumStage for SRA signed requests + PayloadChecksumStore payloadChecksumStore = + context.executionAttributes().getAttribute(SdkInternalExecutionAttribute.CHECKSUM_STORE); + Pair measuredSign = MetricUtils.measureDuration( - () -> doSraSign(request, selectedAuthScheme, identity)); + () -> doSraSign(request, selectedAuthScheme, identity, payloadChecksumStore)); context.attemptMetricCollector().reportMetric(CoreMetric.SIGNING_DURATION, measuredSign.right()); SdkHttpFullRequest signedRequest = measuredSign.left(); @@ -100,10 +106,12 @@ private SdkHttpFullRequest sraSignRequest(SdkHttpFullReques private SdkHttpFullRequest doSraSign(SdkHttpFullRequest request, SelectedAuthScheme selectedAuthScheme, - T identity) { + T identity, + PayloadChecksumStore payloadChecksumStore) { SignRequest.Builder signRequestBuilder = SignRequest .builder(identity) .putProperty(HttpSigner.SIGNING_CLOCK, signingClock()) + .putProperty(SdkInternalHttpSignerProperty.CHECKSUM_STORE, payloadChecksumStore) .request(request) .payload(request.contentStreamProvider().orElse(null)); AuthSchemeOption authSchemeOption = selectedAuthScheme.authSchemeOption(); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/io/AwsChunkedEncodingInputStream.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/io/AwsChunkedEncodingInputStream.java index ec4870f5e686..0b98608a67e7 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/io/AwsChunkedEncodingInputStream.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/io/AwsChunkedEncodingInputStream.java @@ -20,8 +20,11 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; import software.amazon.awssdk.core.checksums.SdkChecksum; +import software.amazon.awssdk.core.internal.checksums.NoOpPayloadChecksumStore; import software.amazon.awssdk.core.internal.chunked.AwsChunkedEncodingConfig; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.Validate; /** @@ -45,7 +48,9 @@ public abstract class AwsChunkedEncodingInputStream extends AwsChunkedInputStrea protected boolean isTrailingTerminated = true; private final int chunkSize; private final int maxBufferSize; + private final ChecksumAlgorithm checksumAlgorithm; private final SdkChecksum sdkChecksum; + private final PayloadChecksumStore checksumStore; private boolean isLastTrailingCrlf; /** @@ -58,7 +63,10 @@ public abstract class AwsChunkedEncodingInputStream extends AwsChunkedInputStrea * See {@link AwsChunkedEncodingConfig} for default values. */ protected AwsChunkedEncodingInputStream(InputStream in, - SdkChecksum sdkChecksum, String checksumHeaderForTrailer, + ChecksumAlgorithm checksumAlgorithm, + SdkChecksum sdkChecksum, + PayloadChecksumStore checksumStore, + String checksumHeaderForTrailer, AwsChunkedEncodingConfig config) { AwsChunkedEncodingConfig awsChunkedEncodingConfig = config == null ? AwsChunkedEncodingConfig.create() : config; @@ -78,14 +86,18 @@ protected AwsChunkedEncodingInputStream(InputStream in, if (maxBufferSize < chunkSize) { throw new IllegalArgumentException("Max buffer size should not be less than chunk size"); } + this.checksumAlgorithm = checksumAlgorithm; this.sdkChecksum = sdkChecksum; + this.checksumStore = checksumStore == null ? NoOpPayloadChecksumStore.create() : checksumStore; this.checksumHeaderForTrailer = checksumHeaderForTrailer; } protected abstract static class Builder { protected InputStream inputStream; + protected ChecksumAlgorithm checksumAlgorithm; protected SdkChecksum sdkChecksum; + protected PayloadChecksumStore checksumStore; protected String checksumHeaderForTrailer; protected AwsChunkedEncodingConfig awsChunkedEncodingConfig; @@ -110,6 +122,11 @@ public T awsChunkedEncodingConfig(AwsChunkedEncodingConfig awsChunkedEncodingCon return (T) this; } + public T checksumAlgorithm(ChecksumAlgorithm checksumAlgorithm) { + this.checksumAlgorithm = checksumAlgorithm; + return (T) this; + } + /** * * @param sdkChecksum Instance of SdkChecksum, this can be null if we do not want to calculate Checksum @@ -120,6 +137,11 @@ public T sdkChecksum(SdkChecksum sdkChecksum) { return (T) this; } + public T checksumStore(PayloadChecksumStore checksumStore) { + this.checksumStore = checksumStore; + return (T) this; + } + /** * * @param checksumHeaderForTrailer String value of Trailer header where checksum will be updated. @@ -166,7 +188,11 @@ private boolean setUpTrailingChunks() { return true; } if (calculatedChecksum == null) { - calculatedChecksum = sdkChecksum.getChecksumBytes(); + calculatedChecksum = checksumStore.getChecksumValue(checksumAlgorithm); + if (calculatedChecksum == null) { + calculatedChecksum = sdkChecksum.getChecksumBytes(); + checksumStore.putChecksumValue(checksumAlgorithm, calculatedChecksum); + } currentChunkIterator = new ChunkContentIterator(createChecksumChunkHeader()); return false; } else if (!isLastTrailingCrlf) { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/io/AwsUnsignedChunkedEncodingInputStream.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/io/AwsUnsignedChunkedEncodingInputStream.java index 4c7f46a248cf..14b1cab5bf6c 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/io/AwsUnsignedChunkedEncodingInputStream.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/io/AwsUnsignedChunkedEncodingInputStream.java @@ -18,9 +18,11 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; import software.amazon.awssdk.core.checksums.SdkChecksum; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.internal.chunked.AwsChunkedEncodingConfig; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.BinaryUtils; /** @@ -29,10 +31,13 @@ @SdkInternalApi public class AwsUnsignedChunkedEncodingInputStream extends AwsChunkedEncodingInputStream { - private AwsUnsignedChunkedEncodingInputStream(InputStream in, AwsChunkedEncodingConfig awsChunkedEncodingConfig, + private AwsUnsignedChunkedEncodingInputStream(InputStream in, + AwsChunkedEncodingConfig awsChunkedEncodingConfig, + ChecksumAlgorithm checksumAlgorithm, SdkChecksum sdkChecksum, + PayloadChecksumStore checksumStore, String checksumHeaderForTrailer) { - super(in, sdkChecksum, checksumHeaderForTrailer, awsChunkedEncodingConfig); + super(in, checksumAlgorithm, sdkChecksum, checksumStore, checksumHeaderForTrailer, awsChunkedEncodingConfig); } public static Builder builder() { @@ -85,8 +90,12 @@ protected byte[] createChecksumChunkHeader() { public static final class Builder extends AwsChunkedEncodingInputStream.Builder { public AwsUnsignedChunkedEncodingInputStream build() { return new AwsUnsignedChunkedEncodingInputStream( - this.inputStream, this.awsChunkedEncodingConfig, - this.sdkChecksum, this.checksumHeaderForTrailer); + this.inputStream, + this.awsChunkedEncodingConfig, + this.checksumAlgorithm, + this.sdkChecksum, + this.checksumStore, + this.checksumHeaderForTrailer); } } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/ChecksumCalculatingAsyncRequestBodyTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/ChecksumCalculatingAsyncRequestBodyTest.java index eddc94da63ed..c17d2c3fdaab 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/ChecksumCalculatingAsyncRequestBodyTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/ChecksumCalculatingAsyncRequestBodyTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.core.internal.async; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -42,6 +43,7 @@ import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.internal.util.Mimetype; import software.amazon.awssdk.http.async.SimpleSubscriber; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import software.amazon.awssdk.utils.BinaryUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -301,6 +303,61 @@ public void explicit0ContentLength_containsEmptyStringTrailingChecksum(TestCase assertThat(sb.toString()).isEqualTo(expectedEmptyString); } + @Test + void subscribe_checksumStoreContainsChecksumValue_reusesValue() { + byte[] content = "Hello world".getBytes(StandardCharsets.UTF_8); + AsyncRequestBody body = AsyncRequestBody.fromBytes(content); + + byte[] checksumValue = "my-checksum".getBytes(StandardCharsets.UTF_8); + PayloadChecksumStore store = PayloadChecksumStore.create(); + store.putChecksumValue(DefaultChecksumAlgorithm.CRC32, checksumValue); + + String trailerHeader = "x-amz-checksum-crc32"; + ChecksumCalculatingAsyncRequestBody checksumBody = + ChecksumCalculatingAsyncRequestBody.builder() + .contentLengthHeader((long) content.length) + .trailerHeader(trailerHeader) + .algorithm(DefaultChecksumAlgorithm.CRC32) + .checksumStore(store) + .asyncRequestBody(body) + .build(); + + String encoded = toString(checksumBody); + + assertThat(encoded).endsWith(String.format("%s:%s\r\n\r\n", trailerHeader, BinaryUtils.toBase64(checksumValue))); + } + + @Test + void subscribe_checksumStoreEmpty_storesComputedValue() { + byte[] content = "Hello world".getBytes(StandardCharsets.UTF_8); + AsyncRequestBody body = AsyncRequestBody.fromBytes(content); + + String expectedChecksum = "i9aeUg=="; + + PayloadChecksumStore store = PayloadChecksumStore.create(); + + String trailerHeader = "x-amz-checksum-crc32"; + ChecksumCalculatingAsyncRequestBody checksumBody = + ChecksumCalculatingAsyncRequestBody.builder() + .contentLengthHeader((long) content.length) + .trailerHeader(trailerHeader) + .algorithm(DefaultChecksumAlgorithm.CRC32) + .checksumStore(store) + .asyncRequestBody(body) + .build(); + + String encoded = toString(checksumBody); + + assertThat(encoded).endsWith(String.format("%s:%s\r\n\r\n", trailerHeader, expectedChecksum)); + assertThat(store.getChecksumValue(DefaultChecksumAlgorithm.CRC32)).isEqualTo(BinaryUtils.fromBase64(expectedChecksum)); + } + + private static String toString(Publisher publisher) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Flowable.fromPublisher(publisher).blockingForEach(chunk -> baos.write(BinaryUtils.copyAllBytesFrom(chunk))); + return new String(baos.toByteArray(), StandardCharsets.UTF_8); + } + static class EmptyBufferPublisher implements AsyncRequestBody { private final ByteBuffer[] buffers = new ByteBuffer[2]; diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStageNonSraTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStageNonSraTest.java index 0e1b9efe07e1..a099e91ab6ce 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStageNonSraTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStageNonSraTest.java @@ -23,6 +23,7 @@ import static software.amazon.awssdk.http.Header.CONTENT_LENGTH; import static software.amazon.awssdk.http.Header.CONTENT_MD5; +import java.nio.charset.StandardCharsets; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -41,6 +42,10 @@ import software.amazon.awssdk.core.signer.NoOpSigner; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; +import software.amazon.awssdk.utils.BinaryUtils; +import software.amazon.awssdk.utils.IoUtils; import utils.ValidSdkObjects; @RunWith(MockitoJUnitRunner.class) @@ -48,6 +53,8 @@ public class HttpChecksumStageNonSraTest { private static final String CHECKSUM_SPECS_HEADER = "x-amz-checksum-sha256"; private static final RequestBody REQUEST_BODY = RequestBody.fromString("TestBody"); private static final AsyncRequestBody ASYNC_REQUEST_BODY = AsyncRequestBody.fromString("TestBody"); + private static final String PAYLOAD_CHECKSUM_SHA256 = "/T5YuTxNWthvWXg+TJMwl60XKcAnLMrrOZe/jA9Y+eI="; + private final HttpChecksumStage syncStage = new HttpChecksumStage(ClientType.SYNC); private final HttpChecksumStage asyncStage = new HttpChecksumStage(ClientType.ASYNC); @@ -69,6 +76,40 @@ public void sync_md5Required_addsMd5Checksum_doesNotAddFlexibleChecksums() throw assertThat(requestBuilder.firstMatchingHeader(CHECKSUM_SPECS_HEADER)).isEmpty(); } + @Test + public void sync_md5Required_checksumValueInStore_usesExistingValue() throws Exception { + SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); + boolean isAsyncStreaming = false; + RequestExecutionContext ctx = md5RequiredRequestContext(isAsyncStreaming); + + byte[] checksumValue = "my-md5".getBytes(StandardCharsets.UTF_8); + PayloadChecksumStore store = PayloadChecksumStore.create(); + store.putChecksumValue(DefaultChecksumAlgorithm.MD5, checksumValue); + + ctx.executionAttributes().putAttribute(SdkInternalExecutionAttribute.CHECKSUM_STORE, store); + + syncStage.execute(requestBuilder, ctx); + + assertThat(requestBuilder.headers().get(CONTENT_MD5)).containsExactly(BinaryUtils.toBase64(checksumValue)); + } + + @Test + public void sync_md5Required_checksumStoreEmpty_storesComputedMd5() throws Exception { + SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); + boolean isAsyncStreaming = false; + RequestExecutionContext ctx = md5RequiredRequestContext(isAsyncStreaming); + + PayloadChecksumStore store = PayloadChecksumStore.create(); + ctx.executionAttributes().putAttribute(SdkInternalExecutionAttribute.CHECKSUM_STORE, store); + + syncStage.execute(requestBuilder, ctx); + + String expectedChecksum = "9dzKaiLL99all2ZyHa76RA=="; + + assertThat(requestBuilder.headers().get(CONTENT_MD5)).containsExactly(expectedChecksum); + assertThat(store.getChecksumValue(DefaultChecksumAlgorithm.MD5)).isEqualTo(BinaryUtils.fromBase64(expectedChecksum)); + } + @Test public void async_nonStreaming_md5Required_addsMd5Checksum_doesNotAddFlexibleChecksums() throws Exception { SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); @@ -119,6 +160,40 @@ public void syncWithCustomSigner_flexibleChecksumInTrailerRequired_addsFlexibleC assertThat(requestBuilder.firstMatchingHeader(CHECKSUM_SPECS_HEADER)).isEmpty(); } + @Test + public void syncWithCustomSigner_flexibleChecksumInTrailerRequired_storeEmpty_storesComputedValue() throws Exception { + SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); + + RequestExecutionContext ctx = noOpSignerRequestContext(ClientType.SYNC); + + PayloadChecksumStore store = PayloadChecksumStore.create(); + ctx.executionAttributes().putAttribute(SdkInternalExecutionAttribute.CHECKSUM_STORE, store); + + syncStage.execute(requestBuilder, ctx); + + String content = IoUtils.toUtf8String(requestBuilder.build().contentStreamProvider().get().newStream()); + assertThat(getTrailingChecksum(content)).isEqualTo(String.format("%s:%s", CHECKSUM_SPECS_HEADER, PAYLOAD_CHECKSUM_SHA256)); + assertThat(store.getChecksumValue(DefaultChecksumAlgorithm.SHA256)).isEqualTo(BinaryUtils.fromBase64(PAYLOAD_CHECKSUM_SHA256)); + } + + @Test + public void syncWithCustomSigner_flexibleChecksumInTrailerRequired_checksumValueInStore_usesExistingValue() throws Exception { + SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); + + RequestExecutionContext ctx = noOpSignerRequestContext(ClientType.SYNC); + PayloadChecksumStore store = PayloadChecksumStore.create(); + byte[] checksumValue = "my-sha256".getBytes(StandardCharsets.UTF_8); + store.putChecksumValue(DefaultChecksumAlgorithm.SHA256, checksumValue); + + ctx.executionAttributes().putAttribute(SdkInternalExecutionAttribute.CHECKSUM_STORE, store); + + syncStage.execute(requestBuilder, ctx); + + String content = IoUtils.toUtf8String(requestBuilder.build().contentStreamProvider().get().newStream()); + assertThat(getTrailingChecksum(content)).isEqualTo(String.format("%s:%s", CHECKSUM_SPECS_HEADER, + BinaryUtils.toBase64(checksumValue))); + } + @Test public void asyncWithCustomSigner_flexibleChecksumInTrailerRequired_addsFlexibleChecksumInTrailer() throws Exception { SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); @@ -181,7 +256,7 @@ public void sync_flexibleChecksumInHeaderRequired_addsFlexibleChecksumInHeader_d syncStage.execute(requestBuilder, ctx); - assertThat(requestBuilder.headers().get(CHECKSUM_SPECS_HEADER)).containsExactly("/T5YuTxNWthvWXg+TJMwl60XKcAnLMrrOZe/jA9Y+eI="); + assertThat(requestBuilder.headers().get(CHECKSUM_SPECS_HEADER)).containsExactly(PAYLOAD_CHECKSUM_SHA256); assertThat(requestBuilder.firstMatchingHeader(HEADER_FOR_TRAILER_REFERENCE)).isEmpty(); assertThat(requestBuilder.firstMatchingHeader("Content-encoding")).isEmpty(); @@ -191,6 +266,44 @@ public void sync_flexibleChecksumInHeaderRequired_addsFlexibleChecksumInHeader_d assertThat(requestBuilder.firstMatchingHeader(CONTENT_MD5)).isEmpty(); } + @Test + public void sync_flexibleChecksumInHeaderRequired_checksumValueInStore_usesExistingValue() throws Exception { + SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); + boolean isStreaming = false; + + RequestExecutionContext ctx = syncFlexibleChecksumRequiredRequestContext(isStreaming); + + byte[] checksumValue = "my-sha256".getBytes(StandardCharsets.UTF_8); + PayloadChecksumStore store = PayloadChecksumStore.create(); + // Test context uses SHA-256 as the flexible checksum + store.putChecksumValue(DefaultChecksumAlgorithm.SHA256, checksumValue); + + ctx.executionAttributes().putAttribute(SdkInternalExecutionAttribute.CHECKSUM_STORE, store); + + syncStage.execute(requestBuilder, ctx); + + assertThat(requestBuilder.headers().get(CHECKSUM_SPECS_HEADER)).containsExactly(BinaryUtils.toBase64(checksumValue)); + + } + + @Test + public void sync_flexibleChecksumInHeaderRequired_checksumStoreEmpty_storesComputedSha256() throws Exception { + SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); + boolean isStreaming = false; + + RequestExecutionContext ctx = syncFlexibleChecksumRequiredRequestContext(isStreaming); + + PayloadChecksumStore store = PayloadChecksumStore.create(); + + ctx.executionAttributes().putAttribute(SdkInternalExecutionAttribute.CHECKSUM_STORE, store); + + syncStage.execute(requestBuilder, ctx); + + assertThat(requestBuilder.headers().get(CHECKSUM_SPECS_HEADER)).containsExactly(PAYLOAD_CHECKSUM_SHA256); + // Test context uses SHA-256 as the flexible checksum + assertThat(store.getChecksumValue(DefaultChecksumAlgorithm.SHA256)).isEqualTo(BinaryUtils.fromBase64(PAYLOAD_CHECKSUM_SHA256)); + } + @Test public void async_flexibleChecksumInHeaderRequired_addsFlexibleChecksumInHeader_doesNotAddMd5ChecksumAndFlexibleChecksumInTrailer() throws Exception { SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); @@ -199,7 +312,7 @@ public void async_flexibleChecksumInHeaderRequired_addsFlexibleChecksumInHeader_ asyncStage.execute(requestBuilder, ctx); - assertThat(requestBuilder.headers().get(CHECKSUM_SPECS_HEADER)).containsExactly("/T5YuTxNWthvWXg+TJMwl60XKcAnLMrrOZe/jA9Y+eI="); + assertThat(requestBuilder.headers().get(CHECKSUM_SPECS_HEADER)).containsExactly(PAYLOAD_CHECKSUM_SHA256); assertThat(requestBuilder.firstMatchingHeader(HEADER_FOR_TRAILER_REFERENCE)).isEmpty(); assertThat(requestBuilder.firstMatchingHeader("Content-encoding")).isEmpty(); @@ -209,8 +322,21 @@ public void async_flexibleChecksumInHeaderRequired_addsFlexibleChecksumInHeader_ assertThat(requestBuilder.firstMatchingHeader(CONTENT_MD5)).isEmpty(); } + private static String getTrailingChecksum(String payload) { + for (String line : payload.split("\r\n")) { + if (line.startsWith("x-amz-checksum")) { + return line; + } + } + return null; + } + private SdkHttpFullRequest.Builder createHttpRequestBuilder() { - return SdkHttpFullRequest.builder().contentStreamProvider(REQUEST_BODY.contentStreamProvider()); + return SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host("sdk.aws") + .contentStreamProvider(REQUEST_BODY.contentStreamProvider()); } private RequestExecutionContext md5RequiredRequestContext(boolean isAsyncStreaming) { diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStageSraTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStageSraTest.java index e9fc44d106bb..ed166597a441 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStageSraTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStageSraTest.java @@ -20,6 +20,7 @@ import static software.amazon.awssdk.core.HttpChecksumConstant.SIGNING_METHOD; import static software.amazon.awssdk.core.interceptor.SdkExecutionAttribute.RESOLVED_CHECKSUM_SPECS; import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.AUTH_SCHEMES; +import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.CHECKSUM_STORE; import static software.amazon.awssdk.core.internal.signer.SigningMethod.UNSIGNED_PAYLOAD; import static software.amazon.awssdk.http.Header.CONTENT_LENGTH; import static software.amazon.awssdk.http.Header.CONTENT_MD5; @@ -44,6 +45,7 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.auth.aws.internal.signer.util.ChecksumUtil; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; import utils.ValidSdkObjects; public class HttpChecksumStageSraTest { @@ -143,6 +145,31 @@ public void async_flexibleChecksumInTrailer_addsFlexibleChecksumInTrailer() thro assertThat(requestBuilder.firstMatchingHeader(CHECKSUM_SPECS_HEADER)).isEmpty(); } + @Test + public void execute_checksumStoreAttributeNotPresent_shouldCreate() throws Exception { + SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); + RequestExecutionContext ctx = + flexibleChecksumRequestContext(ClientType.SYNC, ChecksumSpecs.builder().isRequestChecksumRequired(true), false); + + new HttpChecksumStage(ClientType.SYNC).execute(requestBuilder, ctx); + + assertThat(ctx.executionAttributes().getAttribute(CHECKSUM_STORE)).isNotNull(); + } + + @Test + public void execute_checksumStoreAttributePresent_shouldNotOverwrite() throws Exception { + PayloadChecksumStore cache = PayloadChecksumStore.create(); + + SdkHttpFullRequest.Builder requestBuilder = createHttpRequestBuilder(); + RequestExecutionContext ctx = + flexibleChecksumRequestContext(ClientType.SYNC, ChecksumSpecs.builder().isRequestChecksumRequired(true), false); + ctx.executionAttributes().putAttribute(CHECKSUM_STORE, cache); + + new HttpChecksumStage(ClientType.SYNC).execute(requestBuilder, ctx); + + assertThat(ctx.executionAttributes().getAttribute(CHECKSUM_STORE)).isSameAs(cache); + } + private SdkHttpFullRequest.Builder createHttpRequestBuilder() { return SdkHttpFullRequest.builder().contentStreamProvider(REQUEST_BODY.contentStreamProvider()); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStageTest.java index 865535a6298d..d86ba2c70e5b 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStageTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static software.amazon.awssdk.core.interceptor.SdkExecutionAttribute.TIME_OFFSET; +import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.CHECKSUM_STORE; import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME; import static software.amazon.awssdk.core.metrics.CoreMetric.SIGNING_DURATION; @@ -54,6 +55,8 @@ import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; import software.amazon.awssdk.http.auth.spi.signer.HttpSigner; +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore; +import software.amazon.awssdk.http.auth.spi.signer.SdkInternalHttpSignerProperty; import software.amazon.awssdk.http.auth.spi.signer.SignRequest; import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; import software.amazon.awssdk.http.auth.spi.signer.SignerProperty; @@ -372,6 +375,35 @@ public void execute_selectedAuthScheme_signer_doesPreSraSign() throws Exception verifyNoInteractions(httpSigner); } + @Test + public void execute_checksumStoreAttributePresent_propagatesChecksumStoreToSigner() throws Exception { + SelectedAuthScheme selectedAuthScheme = new SelectedAuthScheme<>( + CompletableFuture.completedFuture(identity), + httpSigner, + AuthSchemeOption.builder() + .schemeId("my.auth#myAuth") + .putSignerProperty(SIGNER_PROPERTY, "value") + .build()); + RequestExecutionContext context = createContext(selectedAuthScheme, null); + + PayloadChecksumStore cache = PayloadChecksumStore.create(); + context.executionAttributes().putAttribute(CHECKSUM_STORE, cache); + + SdkHttpRequest signedRequest = ValidSdkObjects.sdkHttpFullRequest().build(); + when(httpSigner.sign(ArgumentMatchers.>any())) + .thenReturn(SignedRequest.builder() + .request(signedRequest) + .build()); + + SdkHttpFullRequest request = ValidSdkObjects.sdkHttpFullRequest().build(); + stage.execute(request, context); + + ArgumentCaptor> signRequestCaptor = ArgumentCaptor.forClass(SignRequest.class); + verify(httpSigner).sign(signRequestCaptor.capture()); + + assertThat(signRequestCaptor.getValue().property(SdkInternalHttpSignerProperty.CHECKSUM_STORE)).isSameAs(cache); + } + private RequestExecutionContext createContext(SelectedAuthScheme selectedAuthScheme, Signer oldSigner) { SdkRequest sdkRequest = ValidSdkObjects.sdkRequest(); InterceptorContext interceptorContext = diff --git a/services/s3/pom.xml b/services/s3/pom.xml index 73d3b2a75b87..89c0ed167db7 100644 --- a/services/s3/pom.xml +++ b/services/s3/pom.xml @@ -158,6 +158,12 @@ ${awsjavasdk.version} test + + retries + software.amazon.awssdk + ${awsjavasdk.version} + test + org.apache.commons commons-lang3 diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/checksums/ChecksumReuseTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/checksums/ChecksumReuseTest.java new file mode 100644 index 000000000000..80f3009734d6 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/checksums/ChecksumReuseTest.java @@ -0,0 +1,322 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.services.s3.checksums; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.reactivex.Flowable; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.signer.AwsS3V4Signer; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.ExecutableHttpRequest; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; +import software.amazon.awssdk.http.async.SdkHttpContentPublisher; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.retries.StandardRetryStrategy; +import software.amazon.awssdk.retries.api.BackoffStrategy; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.utils.BinaryUtils; +import software.amazon.awssdk.utils.IoUtils; + +/** + * Tests to ensure that checksum values are not recomputed between retries. + */ +public class ChecksumReuseTest { + private static final String BUCKET = "test-bucket"; + private static final String KEY = "test-key"; + private static final AwsCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider.create( + AwsBasicCredentials.create("akid", "skid")); + + private static ExecutorService executorService; + + @BeforeAll + public static void setup() { + executorService = Executors.newSingleThreadExecutor(); + } + + @AfterAll + public static void teardown() { + executorService.shutdownNow(); + } + + @Test + public void putObject_serverResponds500_usesSameChecksumOnRetries() { + MockHttpClient httpClient = new MockHttpClient(); + + S3Client s3 = S3Client.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .requestChecksumCalculation(RequestChecksumCalculation.WHEN_SUPPORTED) + .httpClient(httpClient) + .overrideConfiguration(o -> o.retryStrategy(StandardRetryStrategy.builder() + .maxAttempts(4) + .backoffStrategy(BackoffStrategy.retryImmediately()) + .build())) + .build(); + + RequestBody requestBody = RequestBody.fromInputStream(new RandomInputStream(), 4096); + + assertThatThrownBy(() -> s3.putObject(r -> r.bucket(BUCKET).key(KEY).checksumAlgorithm(ChecksumAlgorithm.CRC32), + requestBody)) + .isInstanceOf(S3Exception.class) + // Ensure we actually retried + .matches(e -> ((SdkException) e).numAttempts() == 4); + + assertAllTrailingChecksumsMatch(httpClient.requestPayloads); + } + + @Test + public void putObject_nonSra_serverResponds500_usesSameChecksumOnRetries() { + MockHttpClient httpClient = new MockHttpClient(); + + S3Client s3 = S3Client.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .requestChecksumCalculation(RequestChecksumCalculation.WHEN_SUPPORTED) + .httpClient(httpClient) + .overrideConfiguration(o -> o.retryStrategy(StandardRetryStrategy.builder() + .maxAttempts(4) + .backoffStrategy(BackoffStrategy.retryImmediately()) + .build())) + .build(); + + RequestBody requestBody = RequestBody.fromInputStream(new RandomInputStream(), 4096); + + assertThatThrownBy(() -> s3.putObject(r -> r.bucket(BUCKET) + .key(KEY) + .checksumAlgorithm(ChecksumAlgorithm.CRC32) + .overrideConfiguration(o -> o.signer(AwsS3V4Signer.create())), + requestBody)) + .isInstanceOf(S3Exception.class) + // Ensure we actually retried + .matches(e -> ((SdkException) e).numAttempts() == 4); + + assertAllTrailingChecksumsMatch(httpClient.requestPayloads); + } + + @Test + void asyncPutObject_serverResponds500_usesSameChecksumOnRetries() { + MockAsyncHttpClient httpClient = new MockAsyncHttpClient(); + + S3AsyncClient s3 = S3AsyncClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .requestChecksumCalculation(RequestChecksumCalculation.WHEN_SUPPORTED) + .httpClient(httpClient) + .overrideConfiguration(o -> o.retryStrategy(StandardRetryStrategy.builder() + .maxAttempts(4) + .backoffStrategy(BackoffStrategy.retryImmediately()) + .build())) + .build(); + + AsyncRequestBody requestBody = AsyncRequestBody.fromInputStream(new RandomInputStream(), + 4096L, + executorService); + + CompletableFuture responseFuture = + s3.putObject(r -> r.bucket(BUCKET).key(KEY).checksumAlgorithm(ChecksumAlgorithm.CRC32), requestBody); + + assertThatThrownBy(responseFuture::join) + .hasCauseInstanceOf(S3Exception.class) + .matches(e -> ((SdkException) e.getCause()).numAttempts() == 4); + + assertAllTrailingChecksumsMatch(httpClient.requestPayloads); + } + + @Test + void asyncPutObject_nonSra_serverResponds500_usesSameChecksumOnRetries() { + MockAsyncHttpClient httpClient = new MockAsyncHttpClient(); + + S3AsyncClient s3 = S3AsyncClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .requestChecksumCalculation(RequestChecksumCalculation.WHEN_SUPPORTED) + .httpClient(httpClient) + .overrideConfiguration(o -> o.retryStrategy(StandardRetryStrategy.builder() + .maxAttempts(4) + .backoffStrategy(BackoffStrategy.retryImmediately()) + .build())) + .build(); + + AsyncRequestBody requestBody = AsyncRequestBody.fromInputStream(new RandomInputStream(), + 4096L, + executorService); + + CompletableFuture responseFuture = + s3.putObject(r -> r.bucket(BUCKET) + .key(KEY) + .checksumAlgorithm(ChecksumAlgorithm.CRC32) + .overrideConfiguration(o -> o.signer(AwsS3V4Signer.create())), + requestBody); + + assertThatThrownBy(responseFuture::join) + .hasCauseInstanceOf(S3Exception.class) + .matches(e -> ((SdkException) e.getCause()).numAttempts() == 4); + + assertAllTrailingChecksumsMatch(httpClient.requestPayloads); + } + + private void assertAllTrailingChecksumsMatch(List requestPayloads) { + List trailingChecksumHeaders = new ArrayList<>(); + + for (byte[] requestPayload : requestPayloads) { + String payloadAsString = new String(requestPayload, StandardCharsets.UTF_8); + for (String part : payloadAsString.split("\r\n")) { + if (part.startsWith("x-amz-checksum-crc32:")) { + trailingChecksumHeaders.add(part); + break; + } + } + } + + // sanity check, ensure each request has a trailing checksum header + assertThat(trailingChecksumHeaders).hasSize(4); + // All checksum trailers should be the same + assertThat(trailingChecksumHeaders.stream().distinct().count()).isEqualTo(1); + } + + private static class MockHttpClient implements SdkHttpClient { + private List requestPayloads = new ArrayList<>(); + + @Override + public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) { + return new MockExecutableHttpRequest(request.contentStreamProvider().get().newStream(), requestPayloads); + } + + @Override + public void close() { + } + } + + private static class MockExecutableHttpRequest implements ExecutableHttpRequest { + private final InputStream content; + private final List requestPayloads; + + private MockExecutableHttpRequest(InputStream content, List requestPayloads) { + this.content = content; + this.requestPayloads = requestPayloads; + } + + @Override + public HttpExecuteResponse call() throws IOException { + requestPayloads.add(IoUtils.toByteArray(content)); + + return HttpExecuteResponse.builder() + .response(SdkHttpFullResponse.builder() + .statusCode(503) + .build()) + .responseBody(AbortableInputStream.create( + new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)))) + .build(); + } + + @Override + public void abort() { + } + } + + /** + * A stream that randomly returns either 'a' or 'b' from each read() invocation. + */ + private static class RandomInputStream extends InputStream { + private final Random rng = new Random(); + + @Override + public int read() throws IOException { + if (rng.nextBoolean()) { + return 'a'; + } + return 'b'; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public synchronized void mark(int readlimit) { + } + + @Override + public synchronized void reset() throws IOException { + } + } + + private static class MockAsyncHttpClient implements SdkAsyncHttpClient { + private final List requestPayloads = new ArrayList<>(); + + @Override + public CompletableFuture execute(AsyncExecuteRequest request) { + SdkHttpContentPublisher contentPublisher = request.requestContentPublisher(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Flowable.fromPublisher(contentPublisher).blockingForEach(bb -> baos.write(BinaryUtils.copyBytesFrom(bb))); + + requestPayloads.add( baos.toByteArray()); + + SdkAsyncHttpResponseHandler responseHandler = request.responseHandler(); + + SdkHttpFullResponse response = SdkHttpFullResponse.builder() + .statusCode(503) + .build(); + + responseHandler.onHeaders(response); + + CompletableFuture future = new CompletableFuture<>(); + responseHandler.onStream(Flowable.just(ByteBuffer.wrap("".getBytes(StandardCharsets.UTF_8))) + .doAfterTerminate(() -> future.complete(null))); + + return future; + } + + @Override + public void close() { + } + } +}