From cfd0e4e4105fc560f0065e7c14061856903d9be2 Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Wed, 29 Oct 2025 16:11:57 -0700 Subject: [PATCH 1/2] Add checksum support in request --- .../aws/restjson/RestJson1ProtocolTests.java | 2 - .../smithy/java/client/core/ClientTest.java | 2 + .../java/client/http/HttpMessageExchange.java | 3 + .../http/plugins/HttpChecksumPlugin.java | 67 +++++++++++++++++++ .../http/plugins/HttpChecksumPluginTest.java | 53 +++++++++++++++ 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 client/client-http/src/main/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPlugin.java create mode 100644 client/client-http/src/test/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPluginTest.java diff --git a/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java b/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java index 006df5ad8..2c93b2222 100644 --- a/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java +++ b/aws/client/aws-client-restjson/src/it/java/software/amazon/smithy/java/client/aws/restjson/RestJson1ProtocolTests.java @@ -35,8 +35,6 @@ public class RestJson1ProtocolTests { @HttpClientRequestTests @ProtocolTestFilter( skipTests = { - // TODO: support checksums in requests - "RestJsonHttpChecksumRequired", // TODO: These tests require a payload even when the httpPayload member is null. Should it? "RestJsonHttpWithHeadersButNoPayload", "RestJsonHttpWithEmptyStructurePayload", diff --git a/client/client-core/src/test/java/software/amazon/smithy/java/client/core/ClientTest.java b/client/client-core/src/test/java/software/amazon/smithy/java/client/core/ClientTest.java index 95113e535..e231b41bf 100644 --- a/client/client-core/src/test/java/software/amazon/smithy/java/client/core/ClientTest.java +++ b/client/client-core/src/test/java/software/amazon/smithy/java/client/core/ClientTest.java @@ -32,6 +32,7 @@ import software.amazon.smithy.java.client.http.mock.MockPlugin; import software.amazon.smithy.java.client.http.mock.MockQueue; import software.amazon.smithy.java.client.http.plugins.ApplyHttpRetryInfoPlugin; +import software.amazon.smithy.java.client.http.plugins.HttpChecksumPlugin; import software.amazon.smithy.java.client.http.plugins.UserAgentPlugin; import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.dynamicclient.DynamicClient; @@ -95,6 +96,7 @@ public void tracksPlugins() throws URISyntaxException { // And HttpMessageExchange applies the UserAgentPlugin and ApplyHttpRetryInfoPlugin. UserAgentPlugin.class, ApplyHttpRetryInfoPlugin.class, + HttpChecksumPlugin.class, // User plugins are applied last. FooPlugin.class)); } diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpMessageExchange.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpMessageExchange.java index f3509e961..edbb68ec2 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpMessageExchange.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpMessageExchange.java @@ -8,6 +8,7 @@ import software.amazon.smithy.java.client.core.ClientConfig; import software.amazon.smithy.java.client.core.MessageExchange; import software.amazon.smithy.java.client.http.plugins.ApplyHttpRetryInfoPlugin; +import software.amazon.smithy.java.client.http.plugins.HttpChecksumPlugin; import software.amazon.smithy.java.client.http.plugins.UserAgentPlugin; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.api.HttpResponse; @@ -19,6 +20,7 @@ * */ public final class HttpMessageExchange implements MessageExchange { @@ -33,5 +35,6 @@ private HttpMessageExchange() {} public void configureClient(ClientConfig.Builder config) { config.applyPlugin(new UserAgentPlugin()); config.applyPlugin(new ApplyHttpRetryInfoPlugin()); + config.applyPlugin(new HttpChecksumPlugin()); } } diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPlugin.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPlugin.java new file mode 100644 index 000000000..cb1e83dd2 --- /dev/null +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPlugin.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.http.plugins; + +import java.security.MessageDigest; +import java.util.Base64; +import software.amazon.smithy.java.client.core.ClientConfig; +import software.amazon.smithy.java.client.core.ClientPlugin; +import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor; +import software.amazon.smithy.java.client.core.interceptors.RequestHook; +import software.amazon.smithy.java.core.schema.TraitKey; +import software.amazon.smithy.java.http.api.HttpRequest; +import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Plugin that adds Content-MD5 header for operations with @httpChecksumRequired trait. + */ +@SmithyInternalApi +public final class HttpChecksumPlugin implements ClientPlugin { + + @Override + public void configureClient(ClientConfig.Builder config) { + config.addInterceptor(HttpChecksumInterceptor.INSTANCE); + } + + static final class HttpChecksumInterceptor implements ClientInterceptor { + private static final ClientInterceptor INSTANCE = new HttpChecksumInterceptor(); + + @Override + public RequestT modifyBeforeTransmit(RequestHook hook) { + return hook.mapRequest(HttpRequest.class, h -> { + var operation = h.operation(); + if (operation.schema().getTrait(TraitKey.get(HttpChecksumRequiredTrait.class)) != null) { + return addContentMd5Header(h.request()); + } + return h.request(); + }); + } + + HttpRequest addContentMd5Header(HttpRequest request) { + try { + var body = request.body(); + if (body != null) { + var buffer = body.waitForByteBuffer(); + var bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] hash = md5.digest(bytes); + String base64Hash = Base64.getEncoder().encodeToString(hash); + + return request.toBuilder() + .withReplacedHeader("Content-MD5", ListUtils.of(base64Hash)) + .build(); + } + return request; + } catch (Exception e) { + throw new RuntimeException("Failed to calculate MD5 checksum", e); + } + } + } +} diff --git a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPluginTest.java b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPluginTest.java new file mode 100644 index 000000000..48c208f71 --- /dev/null +++ b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPluginTest.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.http.plugins; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.java.http.api.HttpRequest; +import software.amazon.smithy.java.io.datastream.DataStream; + +public class HttpChecksumPluginTest { + + @Test + public void interceptorAddsContentMd5HeaderForKnownBody() throws Exception { + var interceptor = new HttpChecksumPlugin.HttpChecksumInterceptor(); + var req = HttpRequest.builder() + .uri(new URI("/")) + .method("POST") + .body(DataStream.ofBytes("test body".getBytes(StandardCharsets.UTF_8))) + .build(); + + var result = interceptor.addContentMd5Header(req); + + var headers = result.headers().allValues("Content-MD5"); + assertThat(headers, hasSize(1)); + assertThat(headers.get(0), equalTo("u/mv50Mcr1+Jpgi8MejYIg==")); + } + + @Test + public void interceptorReplacesExistingContentMd5Header() throws Exception { + var interceptor = new HttpChecksumPlugin.HttpChecksumInterceptor(); + var req = HttpRequest.builder() + .uri(new URI("/")) + .method("POST") + .body(DataStream.ofBytes("test body".getBytes(StandardCharsets.UTF_8))) + .withAddedHeader("Content-MD5", "wrong-hash") + .build(); + + var result = interceptor.addContentMd5Header(req); + + var headers = result.headers().allValues("Content-MD5"); + assertThat(headers, hasSize(1)); + assertThat(headers.get(0), equalTo("u/mv50Mcr1+Jpgi8MejYIg==")); + } + +} From fcbd17e16afb54cfcf5d54927a79810ae20ee3e6 Mon Sep 17 00:00:00 2001 From: Joe Wu Date: Thu, 30 Oct 2025 11:49:34 -0700 Subject: [PATCH 2/2] Address comments --- .../http/plugins/HttpChecksumPlugin.java | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPlugin.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPlugin.java index cb1e83dd2..5b894721c 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPlugin.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/plugins/HttpChecksumPlugin.java @@ -6,6 +6,7 @@ package software.amazon.smithy.java.client.http.plugins; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Base64; import software.amazon.smithy.java.client.core.ClientConfig; import software.amazon.smithy.java.client.core.ClientPlugin; @@ -13,6 +14,7 @@ import software.amazon.smithy.java.client.core.interceptors.RequestHook; import software.amazon.smithy.java.core.schema.TraitKey; import software.amazon.smithy.java.http.api.HttpRequest; +import software.amazon.smithy.java.io.ByteBufferUtils; import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait; import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.SmithyInternalApi; @@ -30,38 +32,37 @@ public void configureClient(ClientConfig.Builder config) { static final class HttpChecksumInterceptor implements ClientInterceptor { private static final ClientInterceptor INSTANCE = new HttpChecksumInterceptor(); + private static final TraitKey CHECKSUM_REQUIRED_TRAIT_KEY = + TraitKey.get(HttpChecksumRequiredTrait.class); @Override public RequestT modifyBeforeTransmit(RequestHook hook) { - return hook.mapRequest(HttpRequest.class, h -> { - var operation = h.operation(); - if (operation.schema().getTrait(TraitKey.get(HttpChecksumRequiredTrait.class)) != null) { - return addContentMd5Header(h.request()); - } - return h.request(); - }); + return hook.mapRequest(HttpRequest.class, HttpChecksumInterceptor::processRequest); } - HttpRequest addContentMd5Header(HttpRequest request) { - try { - var body = request.body(); - if (body != null) { - var buffer = body.waitForByteBuffer(); - var bytes = new byte[buffer.remaining()]; - buffer.get(bytes); + private static HttpRequest processRequest(RequestHook hook) { + if (hook.operation().schema().hasTrait(CHECKSUM_REQUIRED_TRAIT_KEY)) { + return addContentMd5Header(hook.request()); + } + return hook.request(); + } - MessageDigest md5 = MessageDigest.getInstance("MD5"); - byte[] hash = md5.digest(bytes); + static HttpRequest addContentMd5Header(HttpRequest request) { + var body = request.body(); + if (body != null) { + var buffer = body.waitForByteBuffer(); + var bytes = ByteBufferUtils.getBytes(buffer); + try { + byte[] hash = MessageDigest.getInstance("MD5").digest(bytes); String base64Hash = Base64.getEncoder().encodeToString(hash); - return request.toBuilder() .withReplacedHeader("Content-MD5", ListUtils.of(base64Hash)) .build(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Unable to fetch message digest instance for MD5", e); } - return request; - } catch (Exception e) { - throw new RuntimeException("Failed to calculate MD5 checksum", e); } + return request; } } }