From 6c134c35cdd94476d1fbffc995581fb8d9fb4873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Wed, 4 Mar 2026 09:53:06 -0800 Subject: [PATCH] Add event streaming signing support --- .../smithy/java/auth/api/NullSigner.java | 4 +- .../smithy/java/auth/api/SignResult.java | 27 +++ .../amazon/smithy/java/auth/api/Signer.java | 2 +- .../aws/events/AwsEventDecoderFactory.java | 34 +-- .../aws/events/AwsEventEncoderFactory.java | 24 +- .../smithy/java/aws/events/AwsEventFrame.java | 2 +- .../java/aws/events/AwsEventShapeDecoder.java | 5 + .../java/aws/events/AwsEventShapeEncoder.java | 15 +- .../java/aws/events/AwsFrameDecoder.java | 10 +- .../aws/events/AwsEventShapeEncoderTest.java | 4 +- aws/aws-sigv4/build.gradle.kts | 1 + .../auth/scheme/sigv4/SigV4AuthScheme.java | 12 + .../auth/scheme/sigv4/SigV4EventSigner.java | 205 ++++++++++++++++++ .../client/auth/scheme/sigv4/SigV4Signer.java | 70 ++---- .../auth/scheme/sigv4/SigningResources.java | 78 +++++++ .../scheme/sigv4/SigV4EventSignerTest.java | 177 +++++++++++++++ .../auth/scheme/sigv4/SigV4TestRunner.java | 2 +- .../client/http/AmzSdkRequestPluginTest.java | 6 +- .../restjson/RestJsonClientProtocol.java | 4 +- .../client/restxml/RestXmlClientProtocol.java | 4 +- .../smithy/java/cli/SmithyCallTest.java | 2 +- .../client/core/auth/scheme/AuthScheme.java | 25 ++- .../smithy/java/client/core/Client.java | 8 + .../smithy/java/client/core/ClientCall.java | 7 +- .../java/client/core/ClientPipeline.java | 23 +- .../http/auth/HttpApiKeyAuthSigner.java | 8 +- .../client/http/auth/HttpBasicAuthSigner.java | 5 +- .../http/auth/HttpBearerAuthSigner.java | 5 +- .../http/auth/HttpDigestAuthSigner.java | 3 +- .../http/auth/HttpApiKeyAuthSignerTest.java | 21 +- .../http/auth/HttpBasicAuthSignerTest.java | 4 +- .../http/auth/HttpBearerAuthSignerTest.java | 4 +- .../java/client/rpcv2/RpcV2CborProtocol.java | 5 +- .../java/codegen/client/TestAuthScheme.java | 6 +- .../InputEventStreamingApiOperation.java | 20 +- .../serde/event/DefaultEventStreamWriter.java | 34 ++- .../java/core/serde/event/EventEncoder.java | 26 +++ .../core/serde/event/EventEncoderFactory.java | 7 + .../java/core/serde/event/FrameEncoder.java | 1 + .../java/core/serde/event/FrameProcessor.java | 48 ++++ .../core/serde/event/FrameTransformer.java | 30 --- .../event/ProtocolEventStreamWriter.java | 10 +- .../event/DefaultEventStreamReaderTest.java | 1 + .../java/core/serde/event/TestMessage.java | 10 + .../event-streaming-client/build.gradle.kts | 4 + .../event-streaming-client/model/main.smithy | 2 + .../eventstreaming/EventStreamTest.java | 20 +- .../smithy/java/mcp/server/HttpMcpProxy.java | 4 +- .../java/mcp/server/HttpMcpProxyTest.java | 5 +- .../java/server/ProxyOperationTrait.java | 4 +- 50 files changed, 856 insertions(+), 182 deletions(-) create mode 100644 auth-api/src/main/java/software/amazon/smithy/java/auth/api/SignResult.java create mode 100644 aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4EventSigner.java create mode 100644 aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigningResources.java create mode 100644 aws/aws-sigv4/src/test/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4EventSignerTest.java create mode 100644 core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameProcessor.java delete mode 100644 core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameTransformer.java diff --git a/auth-api/src/main/java/software/amazon/smithy/java/auth/api/NullSigner.java b/auth-api/src/main/java/software/amazon/smithy/java/auth/api/NullSigner.java index f20691f08..7bb4ad7ed 100644 --- a/auth-api/src/main/java/software/amazon/smithy/java/auth/api/NullSigner.java +++ b/auth-api/src/main/java/software/amazon/smithy/java/auth/api/NullSigner.java @@ -30,7 +30,7 @@ private NullSigner() {} * @return the request as-is. */ @Override - public Object sign(Object request, Identity identity, Context properties) { - return request; + public SignResult sign(Object request, Identity identity, Context properties) { + return new SignResult<>(request); } } diff --git a/auth-api/src/main/java/software/amazon/smithy/java/auth/api/SignResult.java b/auth-api/src/main/java/software/amazon/smithy/java/auth/api/SignResult.java new file mode 100644 index 000000000..d919b5d64 --- /dev/null +++ b/auth-api/src/main/java/software/amazon/smithy/java/auth/api/SignResult.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.auth.api; + +/** + * Holds the result of signing a request of type {@link RequestT}. + * + * @param signedRequest the signed request + * @param signature the signature + * @param the type of the request + */ +public record SignResult( + RequestT signedRequest, + String signature) { + + /** + * Creates a sign result with an empty string + * + * @param signedRequest the signed request + */ + public SignResult(RequestT signedRequest) { + this(signedRequest, ""); + } +} diff --git a/auth-api/src/main/java/software/amazon/smithy/java/auth/api/Signer.java b/auth-api/src/main/java/software/amazon/smithy/java/auth/api/Signer.java index e78f32db5..fcc88bbef 100644 --- a/auth-api/src/main/java/software/amazon/smithy/java/auth/api/Signer.java +++ b/auth-api/src/main/java/software/amazon/smithy/java/auth/api/Signer.java @@ -24,7 +24,7 @@ public interface Signer extends AutoClosea * @param properties Signing properties. * @return the signed request. */ - RequestT sign(RequestT request, IdentityT identity, Context properties); + SignResult sign(RequestT request, IdentityT identity, Context properties); @SuppressWarnings("unchecked") static Signer nullSigner() { diff --git a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventDecoderFactory.java b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventDecoderFactory.java index 1a491b9c0..a29e1f950 100644 --- a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventDecoderFactory.java +++ b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventDecoderFactory.java @@ -16,7 +16,7 @@ import software.amazon.smithy.java.core.serde.event.EventDecoder; import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.FrameDecoder; -import software.amazon.smithy.java.core.serde.event.FrameTransformer; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; /** * A {@link EventDecoderFactory} for AWS events. @@ -32,7 +32,7 @@ public final class AwsEventDecoderFactory> eventBuilder; - private final FrameTransformer transformer; + private final FrameProcessor frameProcessor; private AwsEventDecoderFactory( InitialEventType initialEventType, @@ -40,7 +40,7 @@ private AwsEventDecoderFactory( Schema eventSchema, Codec codec, Supplier> eventBuilder, - FrameTransformer transformer + FrameProcessor frameProcessor ) { this.initialEventType = Objects.requireNonNull(initialEventType, "initialEventType"); this.initialEventBuilder = Objects.requireNonNull(initialEventBuilder, "initialEventBuilder"); @@ -48,22 +48,22 @@ private AwsEventDecoderFactory( : eventSchema; this.codec = Objects.requireNonNull(codec, "codec"); this.eventBuilder = Objects.requireNonNull(eventBuilder, "eventBuilder"); - this.transformer = Objects.requireNonNull(transformer, "transformer"); + this.frameProcessor = Objects.requireNonNull(frameProcessor, "transformer"); } /** * Creates a new input stream decoder factory. * - * @param operation The input operation for the factory - * @param codec The protocol codec to decode the payload - * @param transformer The frame transformer - * @param The output event type + * @param operation The input operation for the factory + * @param codec The protocol codec to decode the payload + * @param frameProcessor The frame frameProcessor + * @param The output event type * @return A new event decoder factory */ public static AwsEventDecoderFactory forInputStream( InputEventStreamingApiOperation operation, Codec codec, - FrameTransformer transformer + FrameProcessor frameProcessor ) { return new AwsEventDecoderFactory<>( InitialEventType.INITIAL_REQUEST, @@ -71,22 +71,22 @@ private AwsEventDecoderFactory( operation.inputStreamMember(), codec, operation.inputEventBuilderSupplier(), - transformer); + frameProcessor); } /** * Creates a new output stream decoder factory. * - * @param operation The output operation for the factory - * @param codec The protocol codec to decode the payload - * @param transformer The frame transformer - * @param The output event type + * @param operation The output operation for the factory + * @param codec The protocol codec to decode the payload + * @param frameProcessor The frame frameProcessor + * @param The output event type * @return A new event decoder factory */ public static AwsEventDecoderFactory forOutputStream( OutputEventStreamingApiOperation operation, Codec codec, - FrameTransformer transformer + FrameProcessor frameProcessor ) { return new AwsEventDecoderFactory<>( InitialEventType.INITIAL_RESPONSE, @@ -94,7 +94,7 @@ private AwsEventDecoderFactory( operation.outputStreamMember(), codec, operation.outputEventBuilderSupplier(), - transformer); + frameProcessor); } @Override @@ -104,6 +104,6 @@ public EventDecoder newEventDecoder() { @Override public FrameDecoder newFrameDecoder() { - return new AwsFrameDecoder(transformer); + return new AwsFrameDecoder(frameProcessor); } } diff --git a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventEncoderFactory.java b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventEncoderFactory.java index de9ee4393..0a2bda538 100644 --- a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventEncoderFactory.java +++ b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventEncoderFactory.java @@ -15,7 +15,7 @@ import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; import software.amazon.smithy.java.core.serde.event.FrameEncoder; -import software.amazon.smithy.java.core.serde.event.FrameTransformer; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; /** * A {@link EventEncoderFactory} for AWS events. @@ -25,7 +25,7 @@ public final class AwsEventEncoderFactory implements EventEncoderFactory transformer; + private final FrameProcessor frameProcessor; private final Function exceptionHandler; private AwsEventEncoderFactory( @@ -33,14 +33,14 @@ private AwsEventEncoderFactory( Schema schema, Codec codec, String payloadMediaType, - FrameTransformer transformer, + FrameProcessor frameProcessor, Function exceptionHandler ) { this.initialEventType = Objects.requireNonNull(initialEventType, "initialEventType"); this.schema = Objects.requireNonNull(schema, "schema").isMember() ? schema.memberTarget() : schema; this.codec = Objects.requireNonNull(codec, "codec"); this.payloadMediaType = Objects.requireNonNull(payloadMediaType, "payloadMediaType"); - this.transformer = Objects.requireNonNull(transformer, "transformer"); + this.frameProcessor = Objects.requireNonNull(frameProcessor, "frameProcessor"); this.exceptionHandler = Objects.requireNonNull(exceptionHandler, "exceptionHandler"); } @@ -57,7 +57,7 @@ public static AwsEventEncoderFactory forInputStream( InputEventStreamingApiOperation operation, Codec codec, String payloadMediaType, - FrameTransformer transformer, + FrameProcessor transformer, Function exceptionHandler ) { return new AwsEventEncoderFactory(InitialEventType.INITIAL_REQUEST, @@ -81,7 +81,7 @@ public static AwsEventEncoderFactory forOutputStream( OutputEventStreamingApiOperation operation, Codec codec, String payloadMediaType, - FrameTransformer transformer, + FrameProcessor transformer, Function exceptionHandler ) { return new AwsEventEncoderFactory(InitialEventType.INITIAL_RESPONSE, @@ -98,7 +98,7 @@ public EventEncoder newEventEncoder() { schema, codec, payloadMediaType, - transformer, + frameProcessor, exceptionHandler); } @@ -111,4 +111,14 @@ public FrameEncoder newFrameEncoder() { public String contentType() { return "application/vnd.amazon.eventstream"; } + + @Override + public EventEncoderFactory withFrameProcessor(FrameProcessor frameProcessor) { + return new AwsEventEncoderFactory(initialEventType, + schema, + codec, + payloadMediaType, + frameProcessor, + exceptionHandler); + } } diff --git a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventFrame.java b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventFrame.java index 318c9aacb..359bb205e 100644 --- a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventFrame.java +++ b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventFrame.java @@ -12,7 +12,7 @@ public final class AwsEventFrame implements Frame { private final Message message; - AwsEventFrame(Message message) { + public AwsEventFrame(Message message) { this.message = message; } diff --git a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventShapeDecoder.java b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventShapeDecoder.java index ca2a7a95b..26a180b9a 100644 --- a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventShapeDecoder.java +++ b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventShapeDecoder.java @@ -58,6 +58,7 @@ public SerializableStruct decode(AwsEventFrame frame) { private E decodeEvent(AwsEventFrame frame) { var message = frame.unwrap(); + // TODO Add support for :message-type other than "event". var eventType = getEventType(message); var memberSchema = eventSchema.member(eventType); if (memberSchema == null) { @@ -99,6 +100,10 @@ private Schema getEventStreamMember(Schema schema) { throw new IllegalArgumentException("cannot find streaming member"); } + private String getMessageType(Message message) { + return message.getHeaders().get(":message-type").getString(); + } + private String getEventType(Message message) { return message.getHeaders().get(":event-type").getString(); } diff --git a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventShapeEncoder.java b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventShapeEncoder.java index eef6f7a3c..460e17500 100644 --- a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventShapeEncoder.java +++ b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsEventShapeEncoder.java @@ -26,7 +26,7 @@ import software.amazon.smithy.java.core.serde.SpecificShapeSerializer; import software.amazon.smithy.java.core.serde.event.EventEncoder; import software.amazon.smithy.java.core.serde.event.EventStreamingException; -import software.amazon.smithy.java.core.serde.event.FrameTransformer; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; import software.amazon.smithy.model.shapes.ShapeId; public final class AwsEventShapeEncoder implements EventEncoder { @@ -36,7 +36,7 @@ public final class AwsEventShapeEncoder implements EventEncoder { private final String payloadMediaType; private final Map, ShapeSerializer>> possibleTypes; private final Map possibleExceptions; - private final FrameTransformer frameTransformer; + private final FrameProcessor frameProcessor; private final Function exceptionHandler; public AwsEventShapeEncoder( @@ -44,7 +44,7 @@ public AwsEventShapeEncoder( Schema eventSchema, Codec codec, String payloadMediaType, - FrameTransformer frameTransformer, + FrameProcessor frameProcessor, Function exceptionHandler ) { this.initialEventType = Objects.requireNonNull(initialEventType, "initialEventType"); @@ -54,7 +54,7 @@ public AwsEventShapeEncoder( codec, initialEventType.value()); this.possibleExceptions = possibleExceptions(Objects.requireNonNull(eventSchema, "eventSchema")); - this.frameTransformer = Objects.requireNonNull(frameTransformer, "frameTransformer"); + this.frameProcessor = Objects.requireNonNull(frameProcessor, "frameTransformer"); this.exceptionHandler = Objects.requireNonNull(exceptionHandler, "exceptionHandler"); } @@ -67,7 +67,7 @@ public AwsEventFrame encode(SerializableStruct item) { headers.put(":event-type", HeaderValue.fromString(typeHolder.get())); headers.put(":content-type", HeaderValue.fromString(payloadMediaType)); var frame = new AwsEventFrame(new Message(headers, payload)); - return frameTransformer.apply(frame); + return frameProcessor.transformFrame(frame); } private byte[] encodeInput( @@ -154,6 +154,11 @@ public AwsEventFrame encodeFailure(Throwable exception) { } + @Override + public AwsEventFrame closingFrame() { + return frameProcessor.closingFrame(); + } + static Map, ShapeSerializer>> possibleTypes( Schema eventSchema, Codec codec, diff --git a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsFrameDecoder.java b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsFrameDecoder.java index 5e0bf597d..72c0e16c3 100644 --- a/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsFrameDecoder.java +++ b/aws/aws-event-streams/src/main/java/software/amazon/smithy/java/aws/events/AwsFrameDecoder.java @@ -10,14 +10,14 @@ import java.util.List; import software.amazon.eventstream.MessageDecoder; import software.amazon.smithy.java.core.serde.event.FrameDecoder; -import software.amazon.smithy.java.core.serde.event.FrameTransformer; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; public final class AwsFrameDecoder implements FrameDecoder { private final MessageDecoder decoder = new MessageDecoder(); - private final FrameTransformer transformer; + private final FrameProcessor frameProcessor; - public AwsFrameDecoder(FrameTransformer transformer) { - this.transformer = transformer; + public AwsFrameDecoder(FrameProcessor frameProcessor) { + this.frameProcessor = frameProcessor; } @Override @@ -27,7 +27,7 @@ public List decode(ByteBuffer buffer) { var result = new ArrayList(messages.size()); for (var message : messages) { var event = new AwsEventFrame(message); - var transformed = transformer.apply(event); + var transformed = frameProcessor.transformFrame(event); if (transformed != null) { result.add(transformed); } diff --git a/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/AwsEventShapeEncoderTest.java b/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/AwsEventShapeEncoderTest.java index 8bb8a8962..83ad33315 100644 --- a/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/AwsEventShapeEncoderTest.java +++ b/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/AwsEventShapeEncoderTest.java @@ -20,7 +20,7 @@ import software.amazon.smithy.java.aws.events.model.TestOperationInput; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.event.EventStreamingException; -import software.amazon.smithy.java.core.serde.event.FrameTransformer; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; import software.amazon.smithy.java.json.JsonCodec; class AwsEventShapeEncoderTest { @@ -136,7 +136,7 @@ static AwsEventShapeEncoder createEncoder() { TestOperation.instance().inputStreamMember(), // event schema createJsonCodec(), // codec "text/json", - FrameTransformer.identity(), + FrameProcessor.identity(), (e) -> new EventStreamingException("InternalServerException", "Internal Server Error")); } diff --git a/aws/aws-sigv4/build.gradle.kts b/aws/aws-sigv4/build.gradle.kts index 751d7aafd..bc0cf141b 100644 --- a/aws/aws-sigv4/build.gradle.kts +++ b/aws/aws-sigv4/build.gradle.kts @@ -14,6 +14,7 @@ extra["moduleName"] = "software.amazon.smithy.java.aws.sigv4" dependencies { implementation(project(":client:client-core")) api(project(":aws:client:aws-client-core")) + api(project(":aws:aws-event-streams")) implementation(project(":http:http-api")) implementation(project(":io")) implementation(project(":logging")) diff --git a/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4AuthScheme.java b/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4AuthScheme.java index bdb9d57ec..52e832f05 100644 --- a/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4AuthScheme.java +++ b/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4AuthScheme.java @@ -8,9 +8,11 @@ import software.amazon.smithy.aws.traits.auth.SigV4Trait; import software.amazon.smithy.java.auth.api.Signer; import software.amazon.smithy.java.aws.auth.api.identity.AwsCredentialsIdentity; +import software.amazon.smithy.java.aws.events.AwsEventFrame; import software.amazon.smithy.java.client.core.auth.scheme.AuthScheme; import software.amazon.smithy.java.client.core.auth.scheme.AuthSchemeFactory; import software.amazon.smithy.java.context.Context; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.model.shapes.ShapeId; @@ -72,6 +74,16 @@ public Signer signer() { return SigV4Signer.create(); } + @Override + @SuppressWarnings("unchecked") + public FrameProcessor eventSigner( + AwsCredentialsIdentity identity, + Context context, + String seedSignature + ) { + return new SigV4EventSigner(identity, context, seedSignature); + } + public static final class Factory implements AuthSchemeFactory { @Override diff --git a/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4EventSigner.java b/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4EventSigner.java new file mode 100644 index 000000000..e3088bcd1 --- /dev/null +++ b/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4EventSigner.java @@ -0,0 +1,205 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.aws.client.auth.scheme.sigv4; + +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.crypto.spec.SecretKeySpec; +import software.amazon.eventstream.HeaderValue; +import software.amazon.eventstream.Message; +import software.amazon.smithy.java.aws.auth.api.identity.AwsCredentialsIdentity; +import software.amazon.smithy.java.aws.events.AwsEventFrame; +import software.amazon.smithy.java.context.Context; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; +import software.amazon.smithy.java.io.ByteBufferUtils; + +/** + * Signs AWS event stream frames using AWS Signature Version 4. + *

+ * This signer implements the AWS4-HMAC-SHA256-PAYLOAD algorithm for event stream signing, + * which chains signatures across frames. Each frame's signature depends on the previous + * frame's signature, ensuring message ordering and integrity. + *

+ * The signer crates a new frame with the original message as payload and two headers: + *

    + *
  • {@code :date} - The signing timestamp
  • + *
  • {@code :chunk-signature} - The binary signature for this frame
  • + *
+ */ +class SigV4EventSigner implements FrameProcessor { + private static final String ALGORITHM = "AWS4-HMAC-SHA256-PAYLOAD"; + private static final String HMAC_SHA_256 = "HmacSHA256"; + private static final String TERMINATOR = "aws4_request"; + private static final byte[] EMPTY_PAYLOAD = new byte[0]; + private final AwsCredentialsIdentity identity; + private final Context signerContext; + private String previousSignature; + + /** + * Creates a new event stream signer. + * + * @param identity the AWS credentials to use for signing + * @param signerContext the signing context containing region, service name, and optional clock + * @param seedSignature the hex-encoded signature from the initial HTTP request + */ + public SigV4EventSigner(AwsCredentialsIdentity identity, Context signerContext, String seedSignature) { + this.identity = identity; + this.signerContext = signerContext; + this.previousSignature = seedSignature; + } + + @Override + public AwsEventFrame transformFrame(AwsEventFrame frame) { + var message = frame.unwrap(); + var payload = ByteBufferUtils.getBytes(message.toByteBuffer()); + return signAndFrameChunk(payload); + } + + public AwsEventFrame signAndFrameChunk(byte[] payload) { + var signingResources = SigningResources.get(); + try { + signingResources.reset(); + var clock = signerContext.getOrDefault(SigV4Settings.CLOCK, Clock.systemUTC()); + var instant = clock.instant(); + + var binarySignature = signChunk(payload, previousSignature, instant, signingResources); + + // Store hex signature for next iteration + previousSignature = HexFormat.of().formatHex(binarySignature); + + return new AwsEventFrame(buildSignedMessage(payload, binarySignature, instant)); + } finally { + SigningResources.release(signingResources); + } + } + + @Override + public AwsEventFrame closingFrame() { + return signAndFrameChunk(EMPTY_PAYLOAD); + } + + private byte[] signChunk(byte[] chunkBody, String prevSignature, Instant instant, SigningResources resources) { + var region = signerContext.expect(SigV4Settings.REGION); + var service = signerContext.expect(SigV4Settings.SIGNING_NAME); + + var dateTime = instant.atOffset(ZoneOffset.UTC).toLocalDateTime(); + var sb = resources.sb; + sb.setLength(0); + sb.append(dateTime.getYear()); + appendTwoDigits(dateTime.getMonthValue(), sb); + appendTwoDigits(dateTime.getDayOfMonth(), sb); + var dateStamp = sb.toString(); + + sb.setLength(0); + sb.append(dateStamp).append('T'); + appendTwoDigits(dateTime.getHour(), sb); + appendTwoDigits(dateTime.getMinute(), sb); + appendTwoDigits(dateTime.getSecond(), sb); + sb.append('Z'); + var requestTime = sb.toString(); + + sb.setLength(0); + sb.append(dateStamp).append('/').append(region).append('/').append(service).append('/').append(TERMINATOR); + var scope = sb.toString(); + + var stringToSign = buildStringToSign(requestTime, scope, prevSignature, chunkBody, instant, resources); + var signingKey = deriveSigningKey(identity.secretAccessKey(), dateStamp, region, service, resources); + + return sign(stringToSign, signingKey, resources); + } + + private static void appendTwoDigits(int value, StringBuilder sb) { + if (value < 10) { + sb.append('0'); + } + sb.append(value); + } + + private String buildStringToSign( + String dateTime, + String scope, + String prevSigHex, + byte[] payload, + Instant instant, + SigningResources resources + ) { + var nonSigHeadersHash = eventStreamNonSignatureHeadersHash(instant, resources); + var payloadHash = HexFormat.of().formatHex(hash(payload, resources)); + + var sb = resources.sb; + sb.setLength(0); + sb.append(ALGORITHM) + .append('\n') + .append(dateTime) + .append('\n') + .append(scope) + .append('\n') + .append(prevSigHex) + .append('\n') + .append(nonSigHeadersHash) + .append('\n') + .append(payloadHash); + + var toSign = resources.sb.toString(); + return toSign; + } + + private String eventStreamNonSignatureHeadersHash(Instant instant, SigningResources resources) { + var headers = Map.of(":date", HeaderValue.fromTimestamp(instant)); + var encodedHeaders = Message.encodeHeaders(headers.entrySet()); + return HexFormat.of().formatHex(hash(encodedHeaders, resources)); + } + + private byte[] deriveSigningKey( + String secretKey, + String dateStamp, + String region, + String service, + SigningResources resources + ) { + var kSecret = ("AWS4" + secretKey).getBytes(StandardCharsets.UTF_8); + var kDate = sign(dateStamp, kSecret, resources); + var kRegion = sign(region, kDate, resources); + var kService = sign(service, kRegion, resources); + return sign(TERMINATOR, kService, resources); + } + + private byte[] sign(String data, byte[] key, SigningResources resources) { + try { + var mac = resources.sha256Mac; + mac.reset(); + mac.init(new SecretKeySpec(key, HMAC_SHA_256)); + return mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + } catch (InvalidKeyException e) { + throw new RuntimeException("Invalid signing key", e); + } + } + + private byte[] hash(byte[] data, SigningResources resources) { + var digest = resources.sha256Digest; + digest.reset(); + digest.update(data); + return digest.digest(); + } + + private Message buildSignedMessage( + byte[] originalMessage, + byte[] binarySignature, + Instant instant + ) { + // using a linked hash map to preserve order, the :chunk-signature should always be the last header + var headers = new LinkedHashMap(); + headers.put(":date", HeaderValue.fromTimestamp(instant)); + headers.put(":chunk-signature", HeaderValue.fromByteArray(binarySignature)); + return new Message(headers, originalMessage); + } +} diff --git a/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4Signer.java b/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4Signer.java index 6989e9ada..83835767d 100644 --- a/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4Signer.java +++ b/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4Signer.java @@ -9,8 +9,6 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.time.Clock; import java.time.Instant; import java.time.LocalDateTime; @@ -23,8 +21,8 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; -import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import software.amazon.smithy.java.auth.api.SignResult; import software.amazon.smithy.java.auth.api.Signer; import software.amazon.smithy.java.aws.auth.api.identity.AwsCredentialsIdentity; import software.amazon.smithy.java.context.Context; @@ -33,6 +31,7 @@ import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.java.io.uri.URLEncoding; import software.amazon.smithy.java.logging.InternalLogger; +import software.amazon.smithy.utils.Pair; /** * AWS signature version 4 signing implementation. @@ -47,40 +46,11 @@ final class SigV4Signer implements Signer { "user-agent", "expect"); - private static final int POOL_SIZE = 32; - private static final int BUFFER_SIZE = 512; - private static final String HMAC_SHA_256 = "HmacSHA256"; private static final String ALGORITHM = "AWS4-HMAC-SHA256"; private static final String TERMINATOR = "aws4_request"; + private static final String HMAC_SHA_256 = "HmacSHA256"; private static final SigningCache SIGNER_CACHE = new SigningCache(300); - - private static final class SigningResources { - final StringBuilder sb; - final MessageDigest sha256Digest; - final Mac sha256Mac; - - SigningResources() { - this.sb = new StringBuilder(BUFFER_SIZE); - try { - this.sha256Digest = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Unable to fetch message digest instance for SHA-256", e); - } - try { - this.sha256Mac = Mac.getInstance(HMAC_SHA_256); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Unable to fetch Mac instance for HmacSHA256", e); - } - } - - void reset() { - sb.setLength(0); - sha256Digest.reset(); - sha256Mac.reset(); - } - } - - private static final Pool RESOURCES_POOL = new Pool<>(POOL_SIZE, SigningResources::new); + private static final String EMPTY_BODY_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; private final SigningResources signingResources; @@ -89,17 +59,17 @@ public static SigV4Signer create() { } private SigV4Signer() { - this.signingResources = RESOURCES_POOL.get(); + this.signingResources = SigningResources.get(); signingResources.reset(); } @Override public void close() { - RESOURCES_POOL.release(signingResources); + SigningResources.release(signingResources); } @Override - public HttpRequest sign(HttpRequest request, AwsCredentialsIdentity identity, Context properties) { + public SignResult sign(HttpRequest request, AwsCredentialsIdentity identity, Context properties) { var region = properties.expect(SigV4Settings.REGION); var name = properties.expect(SigV4Settings.SIGNING_NAME); var clock = properties.getOrDefault(SigV4Settings.CLOCK, Clock.systemUTC()); @@ -109,7 +79,7 @@ public HttpRequest sign(HttpRequest request, AwsCredentialsIdentity identity, Co // TODO: support UNSIGNED var payloadHash = getPayloadHash(request.body()); - var signedHeaders = createSignedHeaders( + var signatureAndSignedHeaders = createSignedHeaders( request.method(), request.uri(), request.headers(), @@ -121,17 +91,15 @@ public HttpRequest sign(HttpRequest request, AwsCredentialsIdentity identity, Co identity.secretAccessKey(), identity.sessionToken(), !request.body().hasKnownLength()); - // Don't let the cached buffers grow too large. - var sb = signingResources.sb; - if (sb.length() > BUFFER_SIZE) { - sb.setLength(BUFFER_SIZE); - sb.trimToSize(); - } - sb.setLength(0); - return request.toBuilder().headers(HttpHeaders.of(signedHeaders)).build(); + var signedHeaders = signatureAndSignedHeaders.right; + return new SignResult<>(request.toBuilder().headers(HttpHeaders.of(signedHeaders)).build(), + signatureAndSignedHeaders.left); } private String getPayloadHash(DataStream dataStream) { + if (!dataStream.hasKnownLength()) { + return EMPTY_BODY_HASH; + } return hexHash(dataStream.asByteBuffer()); } @@ -139,7 +107,7 @@ private String hexHash(ByteBuffer bytes) { return HexFormat.of().formatHex(hash(bytes)); } - private Map> createSignedHeaders( + private Pair>> createSignedHeaders( String method, URI uri, HttpHeaders httpHeaders, @@ -164,9 +132,6 @@ private Map> createSignedHeaders( var requestTime = formatRfc3339(signingDate, dateStamp, sb); headers.put("x-amz-date", List.of(requestTime)); - if (isStreaming) { - headers.put("x-amz-content-sha256", List.of(payloadHash)); - } if (sessionToken != null) { headers.put("x-amz-security-token", List.of(sessionToken)); } @@ -196,7 +161,7 @@ private Map> createSignedHeaders( var authorizationHeader = getAuthHeader(accessKeyId, scope, signedHeaders, signature, sb); headers.put("authorization", List.of(authorizationHeader)); - return headers; + return Pair.of(signature, headers); } private static String createScope(String dateStamp, String regionName, String serviceName, StringBuilder sb) { @@ -463,7 +428,8 @@ private String computeSignature( .append('\n') .append(HexFormat.of().formatHex(hash(canonicalRequest))); var toSign = sb.toString(); - return HexFormat.of().formatHex(sign(toSign, signingKey)); + var signatureBytes = sign(toSign, signingKey); + return HexFormat.of().formatHex(signatureBytes); } private byte[] sign(String data, byte[] key) { diff --git a/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigningResources.java b/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigningResources.java new file mode 100644 index 000000000..37a06fc63 --- /dev/null +++ b/aws/aws-sigv4/src/main/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigningResources.java @@ -0,0 +1,78 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.aws.client.auth.scheme.sigv4; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Mac; + +/** + * Precomputed resources needed for signing that can be pooled for resource reuse. + */ +final class SigningResources { + private static final String HMAC_SHA_256 = "HmacSHA256"; + private static final int BUFFER_SIZE = 512; + + private static final int POOL_SIZE = 32; + + static final Pool RESOURCES_POOL = new Pool<>(POOL_SIZE, SigningResources::new); + + final StringBuilder sb; + final MessageDigest sha256Digest; + final Mac sha256Mac; + + SigningResources() { + this.sb = new StringBuilder(BUFFER_SIZE); + try { + this.sha256Digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to fetch message digest instance for SHA-256", e); + } + try { + this.sha256Mac = Mac.getInstance(HMAC_SHA_256); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to fetch Mac instance for HmacSHA256", e); + } + } + + /** + * Shrink the string builder to a max size of 512 to keep it always at or below this + * when returned to the pool. + */ + void shrink() { + if (sb.length() > BUFFER_SIZE) { + sb.setLength(BUFFER_SIZE); + sb.trimToSize(); + } + sb.setLength(0); + } + + void reset() { + sb.setLength(0); + sha256Digest.reset(); + sha256Mac.reset(); + } + + /** + * Returns a signing resource instance from the internal pool if available or a creates + * a new instance if there's none available in the pool. + * + * @return a singing resources instance + */ + static SigningResources get() { + return RESOURCES_POOL.get(); + } + + /** + * Returns the signing resource to the pool. + * + * @param signingResources the signing resource to release to the internal pool. + */ + static void release(SigningResources signingResources) { + signingResources.shrink(); + RESOURCES_POOL.release(signingResources); + } +} diff --git a/aws/aws-sigv4/src/test/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4EventSignerTest.java b/aws/aws-sigv4/src/test/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4EventSignerTest.java new file mode 100644 index 000000000..5e6f4b1ed --- /dev/null +++ b/aws/aws-sigv4/src/test/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4EventSignerTest.java @@ -0,0 +1,177 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.aws.client.auth.scheme.sigv4; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Clock; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.List; +import org.junit.jupiter.api.Test; +import software.amazon.eventstream.HeaderValue; +import software.amazon.eventstream.Message; +import software.amazon.smithy.java.aws.auth.api.identity.AwsCredentialsIdentity; +import software.amazon.smithy.java.aws.events.AwsEventFrame; +import software.amazon.smithy.java.context.Context; + +class SigV4EventSignerTest { + + private static final AwsCredentialsIdentity TEST_CREDENTIALS = AwsCredentialsIdentity + .create("fake access key", "fake secret key"); + + private static final List SIGNING_INSTANTS = List.of( + // Note: This first Instant is used for signing the request not an event + OffsetDateTime.of(2026, 1, 16, 6, 30, 0, 0, ZoneOffset.UTC).toInstant(), + OffsetDateTime.of(2026, 1, 16, 6, 30, 1, 0, ZoneOffset.UTC).toInstant(), + OffsetDateTime.of(2026, 1, 16, 6, 30, 2, 0, ZoneOffset.UTC).toInstant(), + OffsetDateTime.of(2026, 1, 16, 6, 30, 3, 0, ZoneOffset.UTC).toInstant()); + + @Test + void testSignPayload() throws Exception { + var headers = new HashMap(); + headers.put("some-header", HeaderValue.fromString("value")); + var messageToSign = new Message(headers, "test payload".getBytes()); + + var epoch = Instant.ofEpochSecond(123_456_789L); + var testClock = Clock.fixed(epoch, ZoneOffset.UTC); + + var context = Context.create() + .put(SigV4Settings.REGION, "us-east-1") + .put(SigV4Settings.SIGNING_NAME, "testservice") + .put(SigV4Settings.CLOCK, testClock); + + // Previous signature is hex string + var prevSignature = hash("last message sts".getBytes()); + + var signer = new SigV4EventSigner(TEST_CREDENTIALS, context, prevSignature); + var result = signer.transformFrame(new AwsEventFrame(messageToSign)); + + assertNotNull(result); + var signedMessage = result.unwrap(); + + var dateHeader = signedMessage.getHeaders().get(":date"); + assertNotNull(dateHeader); + assertEquals(epoch, dateHeader.getTimestamp()); + + var sigHeader = signedMessage.getHeaders().get(":chunk-signature"); + assertNotNull(sigHeader); + var actualSignature = HexFormat.of().formatHex(sigHeader.getByteArray()); + + // Verify signature is valid hex and correct length + assertNotNull(actualSignature); + assertEquals(64, actualSignature.length()); + assertEquals("1ea04a4f6becd85ae3e38e379ffaf4bb95042603f209512476cc6416868b31ee", actualSignature); + } + + @Test + void testEventStreamSigning() throws Exception { + var credentials = AwsCredentialsIdentity.create("access", "secret"); + + var eventClock = eventSigningClock(); + + var context = Context.create() + .put(SigV4Settings.REGION, "us-west-2") + .put(SigV4Settings.SIGNING_NAME, "demo") + .put(SigV4Settings.CLOCK, eventClock); + + var seedSignature = "e1d8e8c8815e60969f2a34765c9a15945ffc0badbaa4b7e3b163ea19131e949b"; + var signer = new SigV4EventSigner(credentials, context, seedSignature); + + // Expected signatures from AWS SDK v2 test + var expectedSignatures = List.of( + "4d6dc4197ca9045f693131a8321bffd3f0b5308835a2770c980551d2fd4b2e56", + "75342279dd941fef5a494cc4b234545ee95be35805d5489ffb24959a9601d6ed", + "c83a5b9f19bc3fe8247eec6d4a5d7806f4b47a6457559c58055c7017c6f3ce3e", + "8b01a16c997aa5692d552b0b6cc4f308743882a0a035d9841ba0738a53420bef"); + + // Sign each payload + var payloads = List.of("A", "B", "C", ""); + + for (var idx = 0; idx < payloads.size(); idx++) { + var payload = payloads.get(idx).getBytes(StandardCharsets.UTF_8); + var message = new Message(new HashMap<>(), payload); + var signedFrame = signer.transformFrame(new AwsEventFrame(message)); + + assertNotNull(signedFrame); + var signedMessage = signedFrame.unwrap(); + + var sigHeader = signedMessage.getHeaders().get(":chunk-signature"); + assertNotNull(sigHeader); + var actualSignature = HexFormat.of().formatHex(sigHeader.getByteArray()); + + assertEquals(expectedSignatures.get(idx), + actualSignature, + "Signature mismatch for payload " + idx + ": " + payloads.get(idx)); + } + } + + @Test + void testSignatureChaining() throws Exception { + var context = Context.create() + .put(SigV4Settings.REGION, "us-east-2") + .put(SigV4Settings.SIGNING_NAME, "test"); + + var initialSignature = hash(new byte[0]); + var signer = new SigV4EventSigner(TEST_CREDENTIALS, context, initialSignature); + + var message1 = new Message(new HashMap<>(), "first".getBytes()); + var signed1 = signer.transformFrame(new AwsEventFrame(message1)); + assertNotNull(signed1); + + var sig1 = signed1.unwrap().getHeaders().get(":chunk-signature").getByteArray(); + + var message2 = new Message(new HashMap<>(), "second".getBytes()); + var signed2 = signer.transformFrame(new AwsEventFrame(message2)); + assertNotNull(signed2); + + var sig2 = signed2.unwrap().getHeaders().get(":chunk-signature").getByteArray(); + + // Signatures should be different due to chaining + var sig1Hex = HexFormat.of().formatHex(sig1); + var sig2Hex = HexFormat.of().formatHex(sig2); + assertNotNull(sig1Hex); + assertNotNull(sig2Hex); + assertEquals(64, sig1Hex.length()); + assertEquals(64, sig2Hex.length()); + } + + private static String hash(byte[] data) throws Exception { + var digest = MessageDigest.getInstance("SHA-256"); + return HexFormat.of().formatHex(digest.digest(data)); + } + + private static Clock eventSigningClock() { + return new Clock() { + private int idx = 0; + + @Override + public Instant instant() { + if (idx >= SIGNING_INSTANTS.size()) { + throw new IllegalStateException("Clock ran out of Instants to return! " + idx); + } + return SIGNING_INSTANTS.get(idx++); + } + + @Override + public ZoneId getZone() { + return ZoneOffset.UTC; + } + + @Override + public Clock withZone(ZoneId zone) { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/aws/aws-sigv4/src/test/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4TestRunner.java b/aws/aws-sigv4/src/test/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4TestRunner.java index 688b66d49..628b6af72 100644 --- a/aws/aws-sigv4/src/test/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4TestRunner.java +++ b/aws/aws-sigv4/src/test/java/software/amazon/smithy/java/aws/client/auth/scheme/sigv4/SigV4TestRunner.java @@ -159,7 +159,7 @@ private static HttpRequest parseRequest(String fileName) { Result createResult( Signer signer ) throws ExecutionException, InterruptedException { - var signedRequest = signer.sign(request, context.identity, context.properties); + var signedRequest = signer.sign(request, context.identity, context.properties).signedRequest(); boolean isValid = signedRequest.headers().equals(expected.headers()) && signedRequest.uri().equals(expected.uri()) && signedRequest.method().equals(expected.method()); diff --git a/aws/client/aws-client-http/src/test/java/software/amazon/smithy/java/aws/client/http/AmzSdkRequestPluginTest.java b/aws/client/aws-client-http/src/test/java/software/amazon/smithy/java/aws/client/http/AmzSdkRequestPluginTest.java index 289dbc671..0f490f9ff 100644 --- a/aws/client/aws-client-http/src/test/java/software/amazon/smithy/java/aws/client/http/AmzSdkRequestPluginTest.java +++ b/aws/client/aws-client-http/src/test/java/software/amazon/smithy/java/aws/client/http/AmzSdkRequestPluginTest.java @@ -88,7 +88,9 @@ public Builder toBuilder() { var requests = mock.getRequests(); assertThat(requests, hasSize(2)); - assertThat(requests.get(0).request().headers().firstValue("amz-sdk-request"), equalTo("attempt=1; max=3")); - assertThat(requests.get(1).request().headers().firstValue("amz-sdk-request"), equalTo("attempt=2; max=3")); + assertThat(requests.get(0).request().headers().firstValue("amz-sdk-request"), + equalTo("attempt=1; max=3")); + assertThat(requests.get(1).request().headers().firstValue("amz-sdk-request"), + equalTo("attempt=2; max=3")); } } diff --git a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java index 18484625d..20f2c595c 100644 --- a/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java +++ b/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java @@ -27,7 +27,7 @@ import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; -import software.amazon.smithy.java.core.serde.event.FrameTransformer; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.binding.RequestSerializer; import software.amazon.smithy.java.json.JsonCodec; @@ -115,7 +115,7 @@ protected EventEncoderFactory getEventEncoderFactory( inputOperation, payloadCodec(), payloadMediaType(), - FrameTransformer.identity(), + FrameProcessor.identity(), (e) -> new EventStreamingException("InternalServerException", "Internal Server Error")); } diff --git a/aws/client/aws-client-restxml/src/main/java/software/amazon/smithy/java/aws/client/restxml/RestXmlClientProtocol.java b/aws/client/aws-client-restxml/src/main/java/software/amazon/smithy/java/aws/client/restxml/RestXmlClientProtocol.java index f38e4d38e..de9650496 100644 --- a/aws/client/aws-client-restxml/src/main/java/software/amazon/smithy/java/aws/client/restxml/RestXmlClientProtocol.java +++ b/aws/client/aws-client-restxml/src/main/java/software/amazon/smithy/java/aws/client/restxml/RestXmlClientProtocol.java @@ -29,7 +29,7 @@ import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; -import software.amazon.smithy.java.core.serde.event.FrameTransformer; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; import software.amazon.smithy.java.http.api.HttpResponse; import software.amazon.smithy.java.xml.XmlCodec; import software.amazon.smithy.java.xml.XmlUtil; @@ -89,7 +89,7 @@ protected EventEncoderFactory getEventEncoderFactory( inputOperation, payloadCodec(), payloadMediaType(), - FrameTransformer.identity(), + FrameProcessor.identity(), (e) -> new EventStreamingException("InternalServerException", "Internal Server Error")); } diff --git a/cli/src/test/java/software/amazon/smithy/java/cli/SmithyCallTest.java b/cli/src/test/java/software/amazon/smithy/java/cli/SmithyCallTest.java index 78581dcd6..472933bf6 100644 --- a/cli/src/test/java/software/amazon/smithy/java/cli/SmithyCallTest.java +++ b/cli/src/test/java/software/amazon/smithy/java/cli/SmithyCallTest.java @@ -518,4 +518,4 @@ private Path createSprocketsModelFile() { } return tempDir; } -} \ No newline at end of file +} diff --git a/client/client-auth-api/src/main/java/software/amazon/smithy/java/client/core/auth/scheme/AuthScheme.java b/client/client-auth-api/src/main/java/software/amazon/smithy/java/client/core/auth/scheme/AuthScheme.java index b9ee0fa1e..9559803b1 100644 --- a/client/client-auth-api/src/main/java/software/amazon/smithy/java/client/core/auth/scheme/AuthScheme.java +++ b/client/client-auth-api/src/main/java/software/amazon/smithy/java/client/core/auth/scheme/AuthScheme.java @@ -11,6 +11,8 @@ import software.amazon.smithy.java.auth.api.identity.IdentityResolvers; import software.amazon.smithy.java.auth.api.identity.TokenIdentity; import software.amazon.smithy.java.context.Context; +import software.amazon.smithy.java.core.serde.event.Frame; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; import software.amazon.smithy.model.shapes.ShapeId; /** @@ -21,7 +23,7 @@ *
  • An identity resolver - An API that can be queried to acquire the customer's identity.
  • *
  • A signer - An API that can be used to sign requests.
  • * - * + *

    * See example auth schemes defined here. * * @param The {@link Identity} used by this authentication scheme. @@ -95,6 +97,23 @@ default Context getIdentityProperties(Context context) { */ Signer signer(); + /** + * Creates a signer used to sign event stream frames. + * + * @param identity the identity used to sign + * @param context the singer context + * @param seedSignature the seed signature + * @param the type of the frame + * @return the auth-scheme event signer + */ + default > FrameProcessor eventSigner( + IdentityT identity, + Context context, + String seedSignature + ) { + return FrameProcessor.identity(); + } + /** * Create a simple AuthScheme. * @@ -102,9 +121,9 @@ default Context getIdentityProperties(Context context) { * @param requestClass Request class supported by the auth scheme. * @param identityClass Identity class supported by the auth scheme. * @param signer Signed used with this auth scheme. + * @param Request type. + * @param Identity type. * @return the created AuthScheme. - * @param Request type. - * @param Identity type. */ static AuthScheme of( ShapeId schemeId, diff --git a/client/client-core/src/main/java/software/amazon/smithy/java/client/core/Client.java b/client/client-core/src/main/java/software/amazon/smithy/java/client/core/Client.java index a0ca5b72b..9639e558f 100644 --- a/client/client-core/src/main/java/software/amazon/smithy/java/client/core/Client.java +++ b/client/client-core/src/main/java/software/amazon/smithy/java/client/core/Client.java @@ -19,8 +19,11 @@ import software.amazon.smithy.java.client.core.plugins.AutoPlugin; import software.amazon.smithy.java.context.Context; import software.amazon.smithy.java.core.schema.ApiOperation; +import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.serde.TypeRegistry; +import software.amazon.smithy.java.core.serde.event.Frame; +import software.amazon.smithy.java.core.serde.event.ProtocolEventStreamWriter; import software.amazon.smithy.java.retries.api.RetryStrategy; import software.amazon.smithy.utils.SmithyInternalApi; @@ -67,6 +70,10 @@ protected O call( ApiOperation operation, RequestOverrideConfig overrideConfig ) { + ProtocolEventStreamWriter> writer = null; + if (operation instanceof InputEventStreamingApiOperation eso) { + writer = ProtocolEventStreamWriter.of(input.getMemberValue(eso.inputEventStreamMember())); + } ClientPipeline callPipeline = pipeline; IdentityResolvers callIdentityResolvers = identityResolvers; ClientInterceptor callInterceptor = interceptor; @@ -95,6 +102,7 @@ protected O call( var callBuilder = ClientCall.builder(); callBuilder.input = input; + callBuilder.writer = writer; callBuilder.operation = operation; callBuilder.interceptor = callInterceptor; callBuilder.identityResolvers = callIdentityResolvers; diff --git a/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientCall.java b/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientCall.java index 976214844..ebf78bb8b 100644 --- a/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientCall.java +++ b/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientCall.java @@ -20,6 +20,8 @@ import software.amazon.smithy.java.core.schema.ApiOperation; import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.serde.TypeRegistry; +import software.amazon.smithy.java.core.serde.event.Frame; +import software.amazon.smithy.java.core.serde.event.ProtocolEventStreamWriter; import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.java.retries.api.RetryStrategy; import software.amazon.smithy.java.retries.api.RetryToken; @@ -37,13 +39,14 @@ final class ClientCall operation; + final Context context; final TypeRegistry typeRegistry; final ClientInterceptor interceptor; final AuthSchemeResolver authSchemeResolver; final Map> supportedAuthSchemes; final IdentityResolvers identityResolvers; - + final ProtocolEventStreamWriter> writer; final RetryStrategy retryStrategy; final String retryScope; RetryToken retryToken; @@ -52,6 +55,7 @@ final class ClientCall builder) { input = Objects.requireNonNull(builder.input, "input is null"); operation = Objects.requireNonNull(builder.operation, "operation is null"); + writer = builder.writer; context = Objects.requireNonNull(builder.context, "context is null"); typeRegistry = Objects.requireNonNull(builder.typeRegistry, "typeRegistry is null"); endpointResolver = Objects.requireNonNull(builder.endpointResolver, "endpointResolver is null"); @@ -94,6 +98,7 @@ static final class Builder operation; + ProtocolEventStreamWriter> writer; Context context; TypeRegistry typeRegistry; ClientInterceptor interceptor; diff --git a/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientPipeline.java b/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientPipeline.java index ebf811f60..326f857a3 100644 --- a/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientPipeline.java +++ b/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientPipeline.java @@ -11,6 +11,7 @@ import java.util.HashSet; import java.util.Objects; import java.util.StringJoiner; +import software.amazon.smithy.java.auth.api.SignResult; import software.amazon.smithy.java.auth.api.identity.Identity; import software.amazon.smithy.java.auth.api.identity.IdentityResolvers; import software.amazon.smithy.java.auth.api.identity.IdentityResult; @@ -28,6 +29,8 @@ import software.amazon.smithy.java.core.error.CallException; import software.amazon.smithy.java.core.schema.ApiOperation; import software.amazon.smithy.java.core.schema.SerializableStruct; +import software.amazon.smithy.java.core.serde.event.Frame; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; import software.amazon.smithy.java.logging.InternalLogger; import software.amazon.smithy.java.retries.api.AcquireInitialTokenRequest; import software.amazon.smithy.java.retries.api.RecordSuccessRequest; @@ -60,7 +63,7 @@ final class ClientPipeline { private final ClientTransport transport; /** - * @param protocol Protocol used to serialize requests and deserialize responses. + * @param protocol Protocol used to serialize requests and deserialize responses. * @param transport Transport used to send requests and return responses. */ ClientPipeline(ClientProtocol protocol, ClientTransport transport) { @@ -71,7 +74,7 @@ final class ClientPipeline { /** * Attempt to create a ClientTransport from the given protocol and transport. * - * @param protocol Protocol used to serialize requests and deserialize responses. + * @param protocol Protocol used to serialize requests and deserialize responses. * @param transport Transport used to send requests and return responses. * @throws IllegalStateException if the protocol and transport are incompatible. */ @@ -88,7 +91,7 @@ static ClientPipeline of( * Ensures that the given protocol and transport are compatible by comparing their {@link MessageExchange} class * for instance equality. * - * @param protocol Protocol to check. + * @param protocol Protocol to check. * @param transport Transport to check. * @throws IllegalStateException if the protocol and transport use different request or response classes. */ @@ -191,7 +194,12 @@ private O afterIden call.context.put(CallContext.ENDPOINT, endpoint); RequestT req = protocol.setServiceEndpoint(requestHook.request(), endpoint); - req = resolvedAuthScheme.sign(req); + var signResult = resolvedAuthScheme.sign(req); + req = signResult.signedRequest(); + if (call.writer != null) { + var eventSigner = resolvedAuthScheme.eventSigner(signResult); + call.writer.setSigner(eventSigner); + } var updatedHook = requestHook.withRequest(req); call.interceptor.readAfterSigning(updatedHook); @@ -278,13 +286,18 @@ private record ResolvedScheme( Context signerProperties, AuthScheme authScheme, IdentityResult identity) { - public RequestT sign(RequestT request) { + public SignResult sign(RequestT request) { // Throws when no identity is found. var resolvedIdentity = identity.unwrap(); try (var signer = authScheme.signer()) { return signer.sign(request, resolvedIdentity, signerProperties); } } + + public FrameProcessor> eventSigner(SignResult result) { + var resolvedIdentity = identity.unwrap(); + return authScheme.eventSigner(resolvedIdentity, signerProperties, result.signature()); + } } private Endpoint resolveEndpoint( diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpApiKeyAuthSigner.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpApiKeyAuthSigner.java index d0992b0ba..c7efb4c98 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpApiKeyAuthSigner.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpApiKeyAuthSigner.java @@ -7,6 +7,7 @@ import java.util.LinkedHashMap; import java.util.List; +import software.amazon.smithy.java.auth.api.SignResult; import software.amazon.smithy.java.auth.api.Signer; import software.amazon.smithy.java.auth.api.identity.ApiKeyIdentity; import software.amazon.smithy.java.context.Context; @@ -23,7 +24,7 @@ final class HttpApiKeyAuthSigner implements Signer private HttpApiKeyAuthSigner() {} @Override - public HttpRequest sign(HttpRequest request, ApiKeyIdentity identity, Context properties) { + public SignResult sign(HttpRequest request, ApiKeyIdentity identity, Context properties) { var name = properties.expect(HttpApiKeyAuthScheme.NAME); return switch (properties.expect(HttpApiKeyAuthScheme.IN)) { case HEADER -> { @@ -38,7 +39,7 @@ public HttpRequest sign(HttpRequest request, ApiKeyIdentity identity, Context pr if (existing != null) { LOGGER.debug("Replaced header value for {}", name); } - yield request.toBuilder().headers(HttpHeaders.of(updated)).build(); + yield new SignResult<>(request.toBuilder().headers(HttpHeaders.of(updated)).build()); } case QUERY -> { var uriBuilder = URIBuilder.of(request.uri()); @@ -48,7 +49,8 @@ public HttpRequest sign(HttpRequest request, ApiKeyIdentity identity, Context pr var existingQuery = request.uri().getQuery(); addExistingQueryParams(stringBuilder, existingQuery, name); queryBuilder.write(stringBuilder); - yield request.toBuilder().uri(uriBuilder.query(stringBuilder.toString()).build()).build(); + yield new SignResult<>( + request.toBuilder().uri(uriBuilder.query(stringBuilder.toString()).build()).build()); } }; } diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpBasicAuthSigner.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpBasicAuthSigner.java index 5d271e257..be166ca5d 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpBasicAuthSigner.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpBasicAuthSigner.java @@ -9,6 +9,7 @@ import java.util.Base64; import java.util.LinkedHashMap; import java.util.List; +import software.amazon.smithy.java.auth.api.SignResult; import software.amazon.smithy.java.auth.api.Signer; import software.amazon.smithy.java.auth.api.identity.LoginIdentity; import software.amazon.smithy.java.context.Context; @@ -25,7 +26,7 @@ final class HttpBasicAuthSigner implements Signer { private HttpBasicAuthSigner() {} @Override - public HttpRequest sign(HttpRequest request, LoginIdentity identity, Context properties) { + public SignResult sign(HttpRequest request, LoginIdentity identity, Context properties) { var identityString = identity.username() + ":" + identity.password(); var base64Value = Base64.getEncoder().encodeToString(identityString.getBytes(StandardCharsets.UTF_8)); var headers = new LinkedHashMap<>(request.headers().map()); @@ -33,6 +34,6 @@ public HttpRequest sign(HttpRequest request, LoginIdentity identity, Context pro if (existing != null) { LOGGER.debug("Replaced existing Authorization header value."); } - return request.toBuilder().headers(HttpHeaders.of(headers)).build(); + return new SignResult<>(request.toBuilder().headers(HttpHeaders.of(headers)).build()); } } diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpBearerAuthSigner.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpBearerAuthSigner.java index 54f28b92d..91b012176 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpBearerAuthSigner.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpBearerAuthSigner.java @@ -7,6 +7,7 @@ import java.util.LinkedHashMap; import java.util.List; +import software.amazon.smithy.java.auth.api.SignResult; import software.amazon.smithy.java.auth.api.Signer; import software.amazon.smithy.java.auth.api.identity.TokenIdentity; import software.amazon.smithy.java.context.Context; @@ -23,12 +24,12 @@ final class HttpBearerAuthSigner implements Signer { private HttpBearerAuthSigner() {} @Override - public HttpRequest sign(HttpRequest request, TokenIdentity identity, Context properties) { + public SignResult sign(HttpRequest request, TokenIdentity identity, Context properties) { var headers = new LinkedHashMap<>(request.headers().map()); var existing = headers.put(AUTHORIZATION_HEADER, List.of(SCHEME + " " + identity.token())); if (existing != null) { LOGGER.debug("Replaced existing Authorization header value."); } - return request.toBuilder().headers(HttpHeaders.of(headers)).build(); + return new SignResult<>(request.toBuilder().headers(HttpHeaders.of(headers)).build()); } } diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpDigestAuthSigner.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpDigestAuthSigner.java index d7beaca08..2e68ad824 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpDigestAuthSigner.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/auth/HttpDigestAuthSigner.java @@ -5,6 +5,7 @@ package software.amazon.smithy.java.client.http.auth; +import software.amazon.smithy.java.auth.api.SignResult; import software.amazon.smithy.java.auth.api.Signer; import software.amazon.smithy.java.auth.api.identity.LoginIdentity; import software.amazon.smithy.java.context.Context; @@ -19,7 +20,7 @@ final class HttpDigestAuthSigner implements Signer { private HttpDigestAuthSigner() {} @Override - public HttpRequest sign(HttpRequest request, LoginIdentity identity, Context properties) { + public SignResult sign(HttpRequest request, LoginIdentity identity, Context properties) { throw new UnsupportedOperationException(); } } diff --git a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpApiKeyAuthSignerTest.java b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpApiKeyAuthSignerTest.java index be16cb576..ba9b3cf5a 100644 --- a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpApiKeyAuthSignerTest.java +++ b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpApiKeyAuthSignerTest.java @@ -31,7 +31,8 @@ void testApiKeyAuthSignerAddsHeaderNoScheme() { authProperties.put(HttpApiKeyAuthScheme.IN, HttpApiKeyAuthTrait.Location.HEADER); authProperties.put(HttpApiKeyAuthScheme.NAME, "x-api-key"); - var signedRequest = HttpApiKeyAuthSigner.INSTANCE.sign(TEST_REQUEST, TEST_IDENTITY, authProperties); + var signedRequest = + HttpApiKeyAuthSigner.INSTANCE.sign(TEST_REQUEST, TEST_IDENTITY, authProperties).signedRequest(); var authHeader = signedRequest.headers().firstValue("x-api-key"); assertEquals(authHeader, API_KEY); } @@ -43,7 +44,8 @@ void testApiKeyAuthSignerAddsHeaderParamWithCustomScheme() { authProperties.put(HttpApiKeyAuthScheme.NAME, "x-api-key"); authProperties.put(HttpApiKeyAuthScheme.SCHEME, "SCHEME"); - var signedRequest = HttpApiKeyAuthSigner.INSTANCE.sign(TEST_REQUEST, TEST_IDENTITY, authProperties); + var signedRequest = + HttpApiKeyAuthSigner.INSTANCE.sign(TEST_REQUEST, TEST_IDENTITY, authProperties).signedRequest(); var authHeader = signedRequest.headers().firstValue("x-api-key"); assertEquals(authHeader, "SCHEME " + API_KEY); } @@ -56,7 +58,8 @@ void testOverwritesExistingHeader() { authProperties.put(HttpApiKeyAuthScheme.SCHEME, "SCHEME"); var updateRequest = TEST_REQUEST.toBuilder().withAddedHeader("x-api-key", "foo").build(); - var signedRequest = HttpApiKeyAuthSigner.INSTANCE.sign(updateRequest, TEST_IDENTITY, authProperties); + var signedRequest = + HttpApiKeyAuthSigner.INSTANCE.sign(updateRequest, TEST_IDENTITY, authProperties).signedRequest(); var authHeader = signedRequest.headers().firstValue("x-api-key"); assertEquals(authHeader, "SCHEME " + API_KEY); } @@ -67,7 +70,8 @@ void testApiKeyAuthSignerAddsQueryParam() { authProperties.put(HttpApiKeyAuthScheme.IN, HttpApiKeyAuthTrait.Location.QUERY); authProperties.put(HttpApiKeyAuthScheme.NAME, "apiKey"); - var signedRequest = HttpApiKeyAuthSigner.INSTANCE.sign(TEST_REQUEST, TEST_IDENTITY, authProperties); + var signedRequest = + HttpApiKeyAuthSigner.INSTANCE.sign(TEST_REQUEST, TEST_IDENTITY, authProperties).signedRequest(); var queryParam = signedRequest.uri().getQuery(); assertNotNull(queryParam); assertEquals(queryParam, "apiKey=my-api-key"); @@ -80,7 +84,8 @@ void testApiKeyAuthSignerAddsQueryParamIgnoresScheme() { authProperties.put(HttpApiKeyAuthScheme.NAME, "apiKey"); authProperties.put(HttpApiKeyAuthScheme.SCHEME, "SCHEME"); - var signedRequest = HttpApiKeyAuthSigner.INSTANCE.sign(TEST_REQUEST, TEST_IDENTITY, authProperties); + var signedRequest = + HttpApiKeyAuthSigner.INSTANCE.sign(TEST_REQUEST, TEST_IDENTITY, authProperties).signedRequest(); var queryParam = signedRequest.uri().getQuery(); assertNotNull(queryParam); assertEquals(queryParam, "apiKey=my-api-key"); @@ -94,7 +99,8 @@ void testApiKeyAuthSignerAddsQueryParamsAppendsToExisting() { var updatedRequest = TEST_REQUEST.toBuilder().uri(URI.create("https://www.example.com?x=1")).build(); - var signedRequest = HttpApiKeyAuthSigner.INSTANCE.sign(updatedRequest, TEST_IDENTITY, authProperties); + var signedRequest = + HttpApiKeyAuthSigner.INSTANCE.sign(updatedRequest, TEST_IDENTITY, authProperties).signedRequest(); var queryParam = signedRequest.uri().getQuery(); assertNotNull(queryParam); assertEquals(queryParam, "x=1&apiKey=my-api-key"); @@ -108,7 +114,8 @@ void testOverwritesExistingQuery() { var updatedRequest = TEST_REQUEST.toBuilder().uri(URI.create("https://www.example.com?x=1&apiKey=foo")).build(); - var signedRequest = HttpApiKeyAuthSigner.INSTANCE.sign(updatedRequest, TEST_IDENTITY, authProperties); + var signedRequest = + HttpApiKeyAuthSigner.INSTANCE.sign(updatedRequest, TEST_IDENTITY, authProperties).signedRequest(); var queryParam = signedRequest.uri().getQuery(); assertNotNull(queryParam); assertEquals(queryParam, "x=1&apiKey=my-api-key"); diff --git a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpBasicAuthSignerTest.java b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpBasicAuthSignerTest.java index fc483d1ce..a5ff5d508 100644 --- a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpBasicAuthSignerTest.java +++ b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpBasicAuthSignerTest.java @@ -33,7 +33,7 @@ void testBasicAuthSigner() { var expectedHeader = "Basic " + Base64.getEncoder() .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); - var signedRequest = HttpBasicAuthSigner.INSTANCE.sign(request, testIdentity, Context.empty()); + var signedRequest = HttpBasicAuthSigner.INSTANCE.sign(request, testIdentity, Context.empty()).signedRequest(); var authHeader = signedRequest.headers().firstValue("authorization"); assertEquals(authHeader, expectedHeader); } @@ -52,7 +52,7 @@ void overwritesExistingHeader() { var expectedHeader = "Basic " + Base64.getEncoder() .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); - var signedRequest = HttpBasicAuthSigner.INSTANCE.sign(request, testIdentity, Context.empty()); + var signedRequest = HttpBasicAuthSigner.INSTANCE.sign(request, testIdentity, Context.empty()).signedRequest(); var authHeader = signedRequest.headers().firstValue("authorization"); assertEquals(authHeader, expectedHeader); } diff --git a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpBearerAuthSignerTest.java b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpBearerAuthSignerTest.java index c79078253..7201fa909 100644 --- a/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpBearerAuthSignerTest.java +++ b/client/client-http/src/test/java/software/amazon/smithy/java/client/http/auth/HttpBearerAuthSignerTest.java @@ -27,7 +27,7 @@ void testBearerAuthSigner() { .uri(URI.create("https://www.example.com")) .build(); - var signedRequest = HttpBearerAuthSigner.INSTANCE.sign(request, tokenIdentity, Context.empty()); + var signedRequest = HttpBearerAuthSigner.INSTANCE.sign(request, tokenIdentity, Context.empty()).signedRequest(); var authHeader = signedRequest.headers().firstValue("authorization"); assertEquals(authHeader, "Bearer token"); } @@ -43,7 +43,7 @@ void overwritesExistingHeader() { .build(); var signedRequest = HttpBearerAuthSigner.INSTANCE.sign(request, tokenIdentity, Context.empty()); - var authHeader = signedRequest.headers().firstValue("authorization"); + var authHeader = signedRequest.signedRequest().headers().firstValue("authorization"); assertEquals(authHeader, "Bearer token"); } } diff --git a/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java b/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java index 5d19bfe21..3fcbc621d 100644 --- a/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java +++ b/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java @@ -33,7 +33,7 @@ import software.amazon.smithy.java.core.serde.event.EventDecoderFactory; import software.amazon.smithy.java.core.serde.event.EventEncoderFactory; import software.amazon.smithy.java.core.serde.event.EventStreamingException; -import software.amazon.smithy.java.core.serde.event.FrameTransformer; +import software.amazon.smithy.java.core.serde.event.FrameProcessor; import software.amazon.smithy.java.http.api.HttpHeaders; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.api.HttpResponse; @@ -83,6 +83,7 @@ public HttpRequest builder.headers(HttpHeaders.of(headersForEmptyBody())) .body(DataStream.ofEmpty()); } else if (operation instanceof InputEventStreamingApiOperation i) { + // set in the context the receiver (i'm interested in chunkSigner) // Event streaming var encoderFactory = getEventEncoderFactory(i); var body = RpcEventStreamsUtil.bodyForEventStreaming(encoderFactory, input); @@ -162,7 +163,7 @@ private EventEncoderFactory getEventEncoderFactory( return AwsEventEncoderFactory.forInputStream(inputOperation, payloadCodec(), PAYLOAD_MEDIA_TYPE, - FrameTransformer.identity(), + FrameProcessor.identity(), (e) -> new EventStreamingException("InternalServerException", "Internal Server Error")); } diff --git a/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/TestAuthScheme.java b/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/TestAuthScheme.java index c824c5b04..dc324a23f 100644 --- a/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/TestAuthScheme.java +++ b/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/TestAuthScheme.java @@ -5,6 +5,7 @@ package software.amazon.smithy.java.codegen.client; +import software.amazon.smithy.java.auth.api.SignResult; import software.amazon.smithy.java.auth.api.Signer; import software.amazon.smithy.java.auth.api.identity.Identity; import software.amazon.smithy.java.auth.api.identity.IdentityResolver; @@ -52,8 +53,9 @@ public Signer signer() { private static final class TestSigner implements Signer { @Override - public HttpRequest sign(HttpRequest request, Identity identity, Context properties) { - return request.toBuilder().withAddedHeader(SIGNATURE_HEADER, "smithy-test-signature").build(); + public SignResult sign(HttpRequest request, Identity identity, Context properties) { + return new SignResult<>( + request.toBuilder().withAddedHeader(SIGNATURE_HEADER, "smithy-test-signature").build()); } } diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/InputEventStreamingApiOperation.java b/core/src/main/java/software/amazon/smithy/java/core/schema/InputEventStreamingApiOperation.java index e2409f9e8..674d5328a 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/InputEventStreamingApiOperation.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/InputEventStreamingApiOperation.java @@ -10,8 +10,8 @@ /** * Represents a modeled Smithy operation. * - * @param Operation input shape type. - * @param Operation output shape type. + * @param Operation input shape type. + * @param Operation output shape type. * @param Operation input event shape type. */ public interface InputEventStreamingApiOperation> inputEventBuilderSupplier(); + + /** + * Returns the schema of the streaming member. + * + * @return the schema of the streaming member + */ + default Schema inputEventStreamMember() { + var schema = inputSchema(); + for (var member : schema.members()) { + if (member.isMember() && member.memberTarget().hasTrait(TraitKey.STREAMING_TRAIT)) { + return member; + } + } + throw new IllegalArgumentException("No streaming member found"); + + } } diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/event/DefaultEventStreamWriter.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/DefaultEventStreamWriter.java index 3c527bc4b..d4f6f8a22 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/event/DefaultEventStreamWriter.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/DefaultEventStreamWriter.java @@ -47,6 +47,8 @@ final class DefaultEventStreamWriter encoderFactory; + private IE initialEvent; private EventEncoder eventEncoder; private FrameEncoder frameEncoder; private volatile Throwable lastError; @@ -94,7 +96,14 @@ public void bootstrap(EventEncoderFactory encoderFactory, IE initialEvent) { if (readyLatch.getCount() == 0) { throw new IllegalStateException("bootstrap has been already called"); } - setEventStreamEncodingFactory(Objects.requireNonNull(encoderFactory, "encoderFactory")); + this.encoderFactory = encoderFactory; + this.initialEvent = initialEvent; + } + + @Override + public void setSigner(FrameProcessor eventSigner) { + this.encoderFactory = encoderFactory.withFrameProcessor(eventSigner); + setEventStreamEncodingFactory(); writeInitialEvent(initialEvent); } @@ -115,19 +124,20 @@ private void writeInitialEvent(SerializableStruct event) { } finally { // Always count down, even if write fails, to unblock waiting threads readyLatch.countDown(); + // allow initial event to be garbage collected. + initialEvent = null; } } /** - * Sets the event encoder factory used to get the event and frame encoders used + * Sets the event encoders from the factory used to get the event and frame encoders used * for encoding the events. - * - * @param factory the event encoder factory */ - private void setEventStreamEncodingFactory(EventEncoderFactory factory) { - Objects.requireNonNull(factory, "eventEncoderFactory"); - this.eventEncoder = factory.newEventEncoder(); - this.frameEncoder = factory.newFrameEncoder(); + private void setEventStreamEncodingFactory() { + this.eventEncoder = encoderFactory.newEventEncoder(); + this.frameEncoder = encoderFactory.newFrameEncoder(); + // No need to hold on to the reference, allow it to be garbage collected + this.encoderFactory = null; } /** @@ -190,6 +200,14 @@ public void closeWithError(Exception e) { @Override public void close() { if (closed.compareAndSet(false, true)) { + // Check if the event encoder requires to send a closing frame, and if so encode it and send + // it before closing the stream. The closing frame is defined by the protocol and/or the + // auth scheme used to sign the frames, e.g., SigV4 requires to send an empty trailing frame. + var frame = eventEncoder.closingFrame(); + if (frame != null) { + var encoded = frameEncoder.encode(frame); + pipeStream.write(encoded); + } pipeStream.complete(); } } diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/event/EventEncoder.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/EventEncoder.java index 4f06d47d3..962720372 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/event/EventEncoder.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/EventEncoder.java @@ -7,10 +7,36 @@ import software.amazon.smithy.java.core.schema.SerializableStruct; +/** + * Encodes serializable structs from the model to frames. + * + * @param the type of the frame. + */ public interface EventEncoder> { + /** + * Returns the frame containing the encoded item. + * + * @param item the item to encode + * @return the frame containing the encoded item + */ F encode(SerializableStruct item); + /** + * Returns the frame containing the encoded exception. + * + * @param exception the exception to encode + * @return the frame containing the encoded exception + */ F encodeFailure(Throwable exception); + /** + * Returns a closing frame to be sent when the stream ends as defined in the protocol and/or auth scheme + * signing the frames if any. + *

    + * This method will return null if no closing frame needs to be sent. + * + * @return the closing frame. + */ + F closingFrame(); } diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/event/EventEncoderFactory.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/EventEncoderFactory.java index 04a133f3e..f013191d1 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/event/EventEncoderFactory.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/EventEncoderFactory.java @@ -33,4 +33,11 @@ public interface EventEncoderFactory> { */ String contentType(); + /** + * Returns a new factory configured with the given frameProcessor. + * + * @param frameProcessor the frameProcessor for the factory + * @return a new factory configured with the given frameProcessor. + */ + EventEncoderFactory withFrameProcessor(FrameProcessor frameProcessor); } diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameEncoder.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameEncoder.java index f2597b266..f7014e4b8 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameEncoder.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameEncoder.java @@ -13,6 +13,7 @@ public interface FrameEncoder> { /** * Encode a frame into a buffer + * * @param frame the frame to encode. * @return a bytebuffer with the encoded frame's bytes */ diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameProcessor.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameProcessor.java new file mode 100644 index 000000000..18615d295 --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameProcessor.java @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.core.serde.event; + +/** + * Apply a transformation to a frame, such as signing the frame or validating the signature of a frame + * and unwrapping the embedded frame within. + *

    + * Null transformation results indicate that the frame should be dropped + * + * @param the frame type + */ +@FunctionalInterface +public interface FrameProcessor> { + + /** + * Applies a transformation to a frame. + * + * @param frame the frame to transform + * @return the transformed frame. + */ + F transformFrame(F frame); + + /** + * Returns a closing frame to be sent when the event stream is closed. + * This method returns null if no closing frame is needed. + *

    + * A closing empty frame is needed by some auth-schemes such as AWS SigV4. + * + * @return the closing frame. + */ + default F closingFrame() { + return null; + } + + /** + * An identity transformer that returns the same frame as given. + * + * @param the frame type + * @return an identity frame transformer + */ + static > FrameProcessor identity() { + return (F frame) -> frame; + } +} diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameTransformer.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameTransformer.java deleted file mode 100644 index 6cb1c15a9..000000000 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameTransformer.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.core.serde.event; - -import java.util.function.Function; - -/** - * Apply a transformation to a frame, such as validating the signature of a frame - * and unwrapping the embedded frame within. - *

    - * Null transformation results indicate that the frame should be dropped - * - * @param the frame type - */ -@FunctionalInterface -public interface FrameTransformer> extends Function { - - /** - * An identity transformer that returns the same frame as given. - * - * @param the frame type - * @return an identity frame transformer - */ - static > FrameTransformer identity() { - return (F frame) -> frame; - } -} diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProtocolEventStreamWriter.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProtocolEventStreamWriter.java index 8736ddc65..86269211c 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProtocolEventStreamWriter.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProtocolEventStreamWriter.java @@ -16,6 +16,7 @@ * * @param The event type * @param The initial event type + * // ProtocolEventStreamWriter */ public sealed interface ProtocolEventStreamWriter> extends EventStreamWriter permits DefaultEventStreamWriter { @@ -37,10 +38,17 @@ public sealed interface ProtocolEventStreamWriter encoderFactory, IE initialEvent); + /** + * Sets the signer to sign events in the stream + * + * @param eventSigner the signer to sign events in the stream + */ + void setSigner(FrameProcessor eventSigner); + /** * Utility method to convert a {@link EventStreamWriter} to a {@link ProtocolEventStreamWriter}. * diff --git a/core/src/test/java/software/amazon/smithy/java/core/serde/event/DefaultEventStreamReaderTest.java b/core/src/test/java/software/amazon/smithy/java/core/serde/event/DefaultEventStreamReaderTest.java index 909cfae3b..0a5b6b275 100644 --- a/core/src/test/java/software/amazon/smithy/java/core/serde/event/DefaultEventStreamReaderTest.java +++ b/core/src/test/java/software/amazon/smithy/java/core/serde/event/DefaultEventStreamReaderTest.java @@ -53,6 +53,7 @@ TestMessage.TestFrame> createWriter() { var writer = new DefaultEventStreamWriter(); writer.bootstrap(new TestMessage.TestEventEncoderFactory(), null); + writer.setSigner(FrameProcessor.identity()); return writer; } } diff --git a/core/src/test/java/software/amazon/smithy/java/core/serde/event/TestMessage.java b/core/src/test/java/software/amazon/smithy/java/core/serde/event/TestMessage.java index 4dc2aad73..49d39f4dc 100644 --- a/core/src/test/java/software/amazon/smithy/java/core/serde/event/TestMessage.java +++ b/core/src/test/java/software/amazon/smithy/java/core/serde/event/TestMessage.java @@ -100,6 +100,11 @@ public TestFrame encode(SerializableStruct item) { public TestFrame encodeFailure(Throwable exception) { return null; } + + @Override + public TestFrame closingFrame() { + return null; + } } static class TestEvent implements SerializableStruct { @@ -160,5 +165,10 @@ public FrameEncoder newFrameEncoder() { public String contentType() { return "text/plain"; } + + @Override + public EventEncoderFactory withFrameProcessor(FrameProcessor frameProcessor) { + return this; + } } } diff --git a/examples/event-streaming-client/build.gradle.kts b/examples/event-streaming-client/build.gradle.kts index 65954ca27..d5ec31ab8 100644 --- a/examples/event-streaming-client/build.gradle.kts +++ b/examples/event-streaming-client/build.gradle.kts @@ -16,6 +16,10 @@ dependencies { // Test dependencies testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation(project(":aws:aws-sigv4")) + testImplementation(project(":aws:sdkv2:aws-sdkv2-auth")) + testImplementation(libs.aws.sdk.auth) + testImplementation(libs.opentelemetry.test.api) } // Add generated Java sources to the main sourceset diff --git a/examples/event-streaming-client/model/main.smithy b/examples/event-streaming-client/model/main.smithy index c65f2ca88..b769eec3f 100644 --- a/examples/event-streaming-client/model/main.smithy +++ b/examples/event-streaming-client/model/main.smithy @@ -2,9 +2,11 @@ $version: "2.0" namespace smithy.example.eventstreaming +use aws.auth#sigv4 use aws.protocols#restJson1 @restJson1 +@sigv4(name: "tickservice") service FizzBuzzService { operations: [ FizzBuzz diff --git a/examples/event-streaming-client/src/it/java/software/amazon/smithy/java/example/eventstreaming/EventStreamTest.java b/examples/event-streaming-client/src/it/java/software/amazon/smithy/java/example/eventstreaming/EventStreamTest.java index 659de791b..23ea6efe0 100644 --- a/examples/event-streaming-client/src/it/java/software/amazon/smithy/java/example/eventstreaming/EventStreamTest.java +++ b/examples/event-streaming-client/src/it/java/software/amazon/smithy/java/example/eventstreaming/EventStreamTest.java @@ -13,7 +13,13 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.smithy.java.aws.client.auth.scheme.sigv4.SigV4AuthScheme; +import software.amazon.smithy.java.aws.client.core.settings.RegionSetting; +import software.amazon.smithy.java.aws.sdkv2.auth.SdkCredentialsResolver; import software.amazon.smithy.java.client.core.ProtocolSettings; +import software.amazon.smithy.java.client.core.auth.scheme.AuthSchemeResolver; import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; import software.amazon.smithy.java.client.rpcv2.RpcV2CborProtocol; import software.amazon.smithy.java.core.serde.event.EventStream; @@ -33,10 +39,11 @@ import static org.junit.jupiter.api.Assertions.fail; // TODO: Update the test to create and run the server in setup before the test -@Disabled("This test requires manually running a server locally and then verifies client behavior against it.") +//@Disabled("This test requires manually running a server locally and then verifies client behavior against it.") public class EventStreamTest { private static final InternalLogger LOGGER = InternalLogger.getLogger(EventStreamTest.class); + @ParameterizedTest @MethodSource("clients") public void fizzBuzz(FizzBuzzServiceClient client) { @@ -75,7 +82,7 @@ public void fizzBuzz(FizzBuzzServiceClient client) { break; case FizzBuzzStream.BuzzMember(var buzz): value = buzz.getValue(); - LOGGER.info("received buzz: {}", value); + LOGGER.info("received buzz: {}", value); assertEquals(0, value % 5); if (value % 3 == 0) { assertTrue(unbuzzed.remove(value), "No fizz for " + value); @@ -91,16 +98,23 @@ public void fizzBuzz(FizzBuzzServiceClient client) { } public static List clients() { + var credentials = AwsBasicCredentials.create("1REF0RPQBH0B9Q5T83R2", "ba1VyImxA8GbypNix6E1g1ar1ziwVsg2RxMo9sj8"); return List.of( + /* FizzBuzzServiceClient.builder() .endpointResolver(EndpointResolver.staticHost("http://localhost:8080")) .build(), + */ FizzBuzzServiceClient.builder() .protocol(new RpcV2CborProtocol.Factory() .createProtocol(ProtocolSettings.builder() .service(ShapeId.from("smithy.example#TickService")) .build(), Rpcv2CborTrait.builder().build())) - .endpointResolver(EndpointResolver.staticHost("http://localhost:8000")) + .endpointResolver(EndpointResolver.staticHost("http://localhost:8443")) + .putSupportedAuthSchemes(new SigV4AuthScheme("tickservice")) + .authSchemeResolver(AuthSchemeResolver.DEFAULT) + .putConfig(RegionSetting.REGION, "us-west-2") + .addIdentityResolver(new SdkCredentialsResolver(StaticCredentialsProvider.create(credentials))) .build() ); diff --git a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/HttpMcpProxy.java b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/HttpMcpProxy.java index 8e86d09b0..ec091b85e 100644 --- a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/HttpMcpProxy.java +++ b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/HttpMcpProxy.java @@ -134,7 +134,7 @@ public CompletableFuture rpc(JsonRpcRequest request) { context.put(HttpContext.HTTP_REQUEST_TIMEOUT, timeout); if (signer != null) { - httpRequest = signer.sign(httpRequest, null, context); + httpRequest = signer.sign(httpRequest, null, context).signedRequest(); } HttpResponse response = transport.send(context, httpRequest); @@ -149,7 +149,7 @@ public CompletableFuture rpc(JsonRpcRequest request) { } } - // "When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, + // "When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, // it MUST start a new session by sending a new InitializeRequest without a session ID attached." if (response.statusCode() == 404 && sessionId != null) { LOG.debug("Received 404 with active session ID. Clearing session to force restart."); diff --git a/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/HttpMcpProxyTest.java b/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/HttpMcpProxyTest.java index b242d553f..271eace22 100644 --- a/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/HttpMcpProxyTest.java +++ b/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/HttpMcpProxyTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import software.amazon.smithy.java.auth.api.SignResult; import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.json.JsonCodec; import software.amazon.smithy.java.mcp.model.JsonRpcRequest; @@ -86,7 +87,7 @@ void testBuilderWithHeaders() { var h = r.headers().toModifiable(); headers.forEach(h::setHeader); r.setHeaders(h); - return r; + return new SignResult<>(r); }) .build(); @@ -104,7 +105,7 @@ void testBuilderWithDynamicHeaders() { var h = r.headers().toModifiable(); h.setHeader("X-Request-Count", String.valueOf(++counter[0])); r.setHeaders(h); - return r; + return new SignResult<>(r); }) .build(); diff --git a/server/server-proxy/src/main/java/software/amazon/smithy/java/server/ProxyOperationTrait.java b/server/server-proxy/src/main/java/software/amazon/smithy/java/server/ProxyOperationTrait.java index 1a9c07403..6e57570a3 100644 --- a/server/server-proxy/src/main/java/software/amazon/smithy/java/server/ProxyOperationTrait.java +++ b/server/server-proxy/src/main/java/software/amazon/smithy/java/server/ProxyOperationTrait.java @@ -27,12 +27,12 @@ *

    Additional Input Access

    *

    The additional input data can be accessed in Dynamic client interceptors * using the {@code ProxyService.PROXY_INPUT} context key. This enables - * interceptors to process the extra data before the request is forwarded + * interceptors to process the extra data before the request is forwarded * to the delegate service.

    * *

    Input Stripping

    *

    The proxy service automatically strips out the additional input before - * sending the request to the delegate service, ensuring that only the + * sending the request to the delegate service, ensuring that only the * expected input parameters are forwarded to the original operation.

    * * @see ProxyService#PROXY_INPUT