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..639b4731c 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 @@ -7,8 +7,7 @@ import java.util.Objects; import java.util.function.Supplier; -import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; -import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; +import software.amazon.smithy.java.core.schema.ApiOperation; import software.amazon.smithy.java.core.schema.Schema; import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.ShapeBuilder; @@ -16,7 +15,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 +31,7 @@ public final class AwsEventDecoderFactory> eventBuilder; - private final FrameTransformer transformer; + private final FrameProcessor frameProcessor; private AwsEventDecoderFactory( InitialEventType initialEventType, @@ -40,7 +39,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,7 +47,7 @@ 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"); } /** @@ -56,22 +55,22 @@ private AwsEventDecoderFactory( * * @param operation The input operation for the factory * @param codec The protocol codec to decode the payload - * @param transformer The frame transformer + * @param frameProcessor The frame transformer * @param The output event type * @return A new event decoder factory */ public static AwsEventDecoderFactory forInputStream( - InputEventStreamingApiOperation operation, + ApiOperation operation, Codec codec, - FrameTransformer transformer + FrameProcessor frameProcessor ) { return new AwsEventDecoderFactory<>( InitialEventType.INITIAL_REQUEST, operation::inputBuilder, operation.inputStreamMember(), codec, - operation.inputEventBuilderSupplier(), - transformer); + (Supplier>) (Supplier) operation.inputEventBuilderSupplier(), + frameProcessor); } /** @@ -79,22 +78,22 @@ private AwsEventDecoderFactory( * * @param operation The output operation for the factory * @param codec The protocol codec to decode the payload - * @param transformer The frame transformer + * @param frameProcessor The frame transformer * @param The output event type * @return A new event decoder factory */ public static AwsEventDecoderFactory forOutputStream( - OutputEventStreamingApiOperation operation, + ApiOperation operation, Codec codec, - FrameTransformer transformer + FrameProcessor frameProcessor ) { return new AwsEventDecoderFactory<>( InitialEventType.INITIAL_RESPONSE, operation::outputBuilder, operation.outputStreamMember(), codec, - operation.outputEventBuilderSupplier(), - transformer); + (Supplier>) (Supplier) operation.outputEventBuilderSupplier(), + frameProcessor); } @Override @@ -104,6 +103,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..56dc7059c 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 @@ -7,15 +7,13 @@ import java.util.Objects; import java.util.function.Function; -import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; -import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; +import software.amazon.smithy.java.core.schema.ApiOperation; import software.amazon.smithy.java.core.schema.Schema; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.event.EventEncoder; 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; /** * A {@link EventEncoderFactory} for AWS events. @@ -25,7 +23,6 @@ public final class AwsEventEncoderFactory implements EventEncoderFactory transformer; private final Function exceptionHandler; private AwsEventEncoderFactory( @@ -33,14 +30,12 @@ private AwsEventEncoderFactory( Schema schema, Codec codec, String payloadMediaType, - FrameTransformer transformer, 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.exceptionHandler = Objects.requireNonNull(exceptionHandler, "exceptionHandler"); } @@ -54,17 +49,15 @@ private AwsEventEncoderFactory( * @return A new event encoder factory */ public static AwsEventEncoderFactory forInputStream( - InputEventStreamingApiOperation operation, + ApiOperation operation, Codec codec, String payloadMediaType, - FrameTransformer transformer, Function exceptionHandler ) { return new AwsEventEncoderFactory(InitialEventType.INITIAL_REQUEST, operation.inputStreamMember(), codec, payloadMediaType, - transformer, exceptionHandler); } @@ -78,17 +71,15 @@ public static AwsEventEncoderFactory forInputStream( * @return A new event encoder factory */ public static AwsEventEncoderFactory forOutputStream( - OutputEventStreamingApiOperation operation, + ApiOperation operation, Codec codec, String payloadMediaType, - FrameTransformer transformer, Function exceptionHandler ) { return new AwsEventEncoderFactory(InitialEventType.INITIAL_RESPONSE, operation.outputStreamMember(), codec, payloadMediaType, - transformer, exceptionHandler); } @@ -98,7 +89,6 @@ public EventEncoder newEventEncoder() { schema, codec, payloadMediaType, - transformer, 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..7f1a8e042 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,6 @@ 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.model.shapes.ShapeId; public final class AwsEventShapeEncoder implements EventEncoder { @@ -36,7 +35,6 @@ 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 Function exceptionHandler; public AwsEventShapeEncoder( @@ -44,7 +42,6 @@ public AwsEventShapeEncoder( Schema eventSchema, Codec codec, String payloadMediaType, - FrameTransformer frameTransformer, Function exceptionHandler ) { this.initialEventType = Objects.requireNonNull(initialEventType, "initialEventType"); @@ -54,7 +51,6 @@ public AwsEventShapeEncoder( codec, initialEventType.value()); this.possibleExceptions = possibleExceptions(Objects.requireNonNull(eventSchema, "eventSchema")); - this.frameTransformer = Objects.requireNonNull(frameTransformer, "frameTransformer"); this.exceptionHandler = Objects.requireNonNull(exceptionHandler, "exceptionHandler"); } @@ -66,8 +62,7 @@ public AwsEventFrame encode(SerializableStruct item) { headers.put(":message-type", HeaderValue.fromString("event")); 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 new AwsEventFrame(new Message(headers, payload)); } private byte[] encodeInput( 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/AwsEventShapeDecoderTest.java b/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/AwsEventShapeDecoderTest.java index 6a47fff86..66ce7d80c 100644 --- a/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/AwsEventShapeDecoderTest.java +++ b/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/AwsEventShapeDecoderTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import java.nio.charset.StandardCharsets; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; import software.amazon.eventstream.Message; import software.amazon.smithy.java.aws.events.model.BodyAndHeaderEvent; @@ -143,10 +144,11 @@ public void testDecodeStringMember() { assertEquals(expected, actual); } + @SuppressWarnings("unchecked") static AwsEventShapeDecoder createDecoder() { return new AwsEventShapeDecoder<>(InitialEventType.INITIAL_RESPONSE, () -> TestOperation.instance().outputBuilder(), // output builder - TestOperation.instance().outputEventBuilderSupplier(), + (Supplier) TestOperation.instance().outputEventBuilderSupplier(), TestOperation.instance().outputStreamMember(), createJsonCodec()); } 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..fb6ba54ea 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,6 @@ 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.json.JsonCodec; class AwsEventShapeEncoderTest { @@ -136,7 +135,6 @@ static AwsEventShapeEncoder createEncoder() { TestOperation.instance().inputStreamMember(), // event schema createJsonCodec(), // codec "text/json", - FrameTransformer.identity(), (e) -> new EventStreamingException("InternalServerException", "Internal Server Error")); } diff --git a/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/model/TestOperation.java b/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/model/TestOperation.java index c9c2a8fdd..e128f3791 100644 --- a/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/model/TestOperation.java +++ b/aws/aws-event-streams/src/test/java/software/amazon/smithy/java/aws/events/model/TestOperation.java @@ -7,10 +7,10 @@ import java.util.List; import java.util.function.Supplier; +import software.amazon.smithy.java.core.schema.ApiOperation; import software.amazon.smithy.java.core.schema.ApiService; -import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; -import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.ShapeBuilder; import software.amazon.smithy.java.core.serde.TypeRegistry; import software.amazon.smithy.model.shapes.ShapeId; @@ -18,8 +18,7 @@ @SmithyGenerated public final class TestOperation - implements InputEventStreamingApiOperation, - OutputEventStreamingApiOperation { + implements ApiOperation { private static final TestOperation $INSTANCE = new TestOperation(); @@ -51,7 +50,7 @@ public ShapeBuilder inputBuilder() { } @Override - public Supplier> inputEventBuilderSupplier() { + public Supplier> inputEventBuilderSupplier() { return () -> TestEventStream.builder(); } @@ -61,7 +60,7 @@ public ShapeBuilder outputBuilder() { } @Override - public Supplier> outputEventBuilderSupplier() { + public Supplier> outputEventBuilderSupplier() { return () -> TestEventStream.builder(); } 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..29f09d03f 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 @@ -19,15 +19,12 @@ import software.amazon.smithy.java.client.http.binding.HttpBindingErrorFactory; 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.OutputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.TraitKey; import software.amazon.smithy.java.core.serde.Codec; 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.http.api.HttpRequest; import software.amazon.smithy.java.http.binding.RequestSerializer; import software.amazon.smithy.java.json.JsonCodec; @@ -79,8 +76,8 @@ public HttpRequest .omitEmptyPayload(omitEmptyPayload()) .allowEmptyStructPayload(hasStructPayload(input)); - if (operation instanceof InputEventStreamingApiOperation i) { - serializer.eventEncoderFactory(getEventEncoderFactory(i)); + if (operation.inputEventBuilderSupplier() != null) { + serializer.eventEncoderFactory(getEventEncoderFactory(operation)); } return serializer.serializeRequest(); @@ -107,23 +104,18 @@ protected boolean omitEmptyPayload() { } @Override - protected EventEncoderFactory getEventEncoderFactory( - InputEventStreamingApiOperation inputOperation - ) { + protected EventEncoderFactory getEventEncoderFactory(ApiOperation operation) { // TODO: this is where you'd plumb through Sigv4 support, another frame transformer? return AwsEventEncoderFactory.forInputStream( - inputOperation, + operation, payloadCodec(), payloadMediaType(), - FrameTransformer.identity(), (e) -> new EventStreamingException("InternalServerException", "Internal Server Error")); } @Override - protected EventDecoderFactory getEventDecoderFactory( - OutputEventStreamingApiOperation outputOperation - ) { - return AwsEventDecoderFactory.forOutputStream(outputOperation, payloadCodec(), f -> f); + protected EventDecoderFactory getEventDecoderFactory(ApiOperation operation) { + return AwsEventDecoderFactory.forOutputStream(operation, payloadCodec(), f -> f); } private boolean hasStructPayload(I input) { 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..66c698d42 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 @@ -21,15 +21,12 @@ import software.amazon.smithy.java.core.error.CallException; import software.amazon.smithy.java.core.error.ModeledException; import software.amazon.smithy.java.core.schema.ApiOperation; -import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; -import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.TypeRegistry; import software.amazon.smithy.java.core.serde.document.Document; 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.http.api.HttpResponse; import software.amazon.smithy.java.xml.XmlCodec; import software.amazon.smithy.java.xml.XmlUtil; @@ -81,23 +78,17 @@ protected HttpErrorDeserializer getErrorDeserializer(Context context) { } @Override - protected EventEncoderFactory getEventEncoderFactory( - InputEventStreamingApiOperation inputOperation - ) { - // TODO: this is where you'd plumb through Sigv4 support, another frame transformer? + protected EventEncoderFactory getEventEncoderFactory(ApiOperation operation) { return AwsEventEncoderFactory.forInputStream( - inputOperation, + operation, payloadCodec(), payloadMediaType(), - FrameTransformer.identity(), (e) -> new EventStreamingException("InternalServerException", "Internal Server Error")); } @Override - protected EventDecoderFactory getEventDecoderFactory( - OutputEventStreamingApiOperation outputOperation - ) { - return AwsEventDecoderFactory.forOutputStream(outputOperation, payloadCodec(), f -> f); + protected EventDecoderFactory getEventDecoderFactory(ApiOperation operation) { + return AwsEventDecoderFactory.forOutputStream(operation, payloadCodec(), f -> f); } private static final HttpErrorDeserializer.ErrorPayloadParser XML_ERROR_PAYLOAD_PARSER = 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..1dc652157 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 @@ -21,6 +21,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.retries.api.RetryStrategy; import software.amazon.smithy.utils.SmithyInternalApi; @@ -67,6 +69,10 @@ protected O call( ApiOperation operation, RequestOverrideConfig overrideConfig ) { + ProtocolEventStreamWriter> eventStreamWriter = null; + if (operation.inputEventBuilderSupplier() != null) { + eventStreamWriter = ProtocolEventStreamWriter.of(input.getMemberValue(operation.inputStreamMember())); + } ClientPipeline callPipeline = pipeline; IdentityResolvers callIdentityResolvers = identityResolvers; ClientInterceptor callInterceptor = interceptor; @@ -95,6 +101,7 @@ protected O call( var callBuilder = ClientCall.builder(); callBuilder.input = input; + callBuilder.eventStreamWriter = eventStreamWriter; 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..bf17d8edc 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> eventStreamWriter; 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"); + eventStreamWriter = builder.eventStreamWriter; 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> eventStreamWriter; 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..bbf36ce07 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.eventStreamWriter != null) { + var eventSigner = resolvedAuthScheme.eventSigner(signResult); + call.eventStreamWriter.setFrameAuthorizer(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-binding/src/main/java/software/amazon/smithy/java/client/http/binding/HttpBindingClientProtocol.java b/client/client-http-binding/src/main/java/software/amazon/smithy/java/client/http/binding/HttpBindingClientProtocol.java index b8bec0b69..03c4cb569 100644 --- a/client/client-http-binding/src/main/java/software/amazon/smithy/java/client/http/binding/HttpBindingClientProtocol.java +++ b/client/client-http-binding/src/main/java/software/amazon/smithy/java/client/http/binding/HttpBindingClientProtocol.java @@ -11,8 +11,6 @@ import software.amazon.smithy.java.context.Context; import software.amazon.smithy.java.core.error.CallException; import software.amazon.smithy.java.core.schema.ApiOperation; -import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; -import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.serde.Codec; import software.amazon.smithy.java.core.serde.TypeRegistry; @@ -55,11 +53,11 @@ protected final HttpBinding httpBinding() { return httpBinding; } - protected EventEncoderFactory getEventEncoderFactory(InputEventStreamingApiOperation inputOperation) { + protected EventEncoderFactory getEventEncoderFactory(ApiOperation operation) { throw new UnsupportedOperationException("This protocol does not support event streaming"); } - protected EventDecoderFactory getEventDecoderFactory(OutputEventStreamingApiOperation outputOperation) { + protected EventDecoderFactory getEventDecoderFactory(ApiOperation operation) { throw new UnsupportedOperationException("This protocol does not support event streaming"); } @@ -78,8 +76,8 @@ public HttpRequest .endpoint(endpoint) .omitEmptyPayload(omitEmptyPayload()); - if (operation instanceof InputEventStreamingApiOperation i) { - serializer.eventEncoderFactory(getEventEncoderFactory(i)); + if (operation.inputEventBuilderSupplier() != null) { + serializer.eventEncoderFactory(getEventEncoderFactory(operation)); } return serializer.serializeRequest(); @@ -106,8 +104,8 @@ public O deserializ .outputShapeBuilder(outputBuilder) .response(response); - if (operation instanceof OutputEventStreamingApiOperation o) { - deser.eventDecoderFactory(getEventDecoderFactory(o)); + if (operation.outputEventBuilderSupplier() != null) { + deser.eventDecoderFactory(getEventDecoderFactory(operation)); } deser.deserialize(); 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..2e732cd2a 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 @@ -22,8 +22,6 @@ import software.amazon.smithy.java.client.http.HttpErrorDeserializer; 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.OutputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.Unit; import software.amazon.smithy.java.core.serde.Codec; @@ -33,7 +31,6 @@ 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.http.api.HttpHeaders; import software.amazon.smithy.java.http.api.HttpRequest; import software.amazon.smithy.java.http.api.HttpResponse; @@ -82,9 +79,9 @@ public HttpRequest // Top-level Unit types do not get serialized builder.headers(HttpHeaders.of(headersForEmptyBody())) .body(DataStream.ofEmpty()); - } else if (operation instanceof InputEventStreamingApiOperation i) { + } else if (operation.inputEventBuilderSupplier() != null) { // Event streaming - var encoderFactory = getEventEncoderFactory(i); + var encoderFactory = getEventEncoderFactory(operation); var body = RpcEventStreamsUtil.bodyForEventStreaming(encoderFactory, input); builder.headers(HttpHeaders.of(headersForEventStreaming())) .body(body); @@ -108,8 +105,8 @@ public O deserializ throw errorDeserializer.createError(context, operation, typeRegistry, response); } - if (operation instanceof OutputEventStreamingApiOperation o) { - var eventDecoderFactory = getEventDecoderFactory(o); + if (operation.outputEventBuilderSupplier() != null) { + var eventDecoderFactory = getEventDecoderFactory(operation); return RpcEventStreamsUtil.deserializeResponse(eventDecoderFactory, bodyDataStream(response)); } @@ -154,22 +151,15 @@ private Map> headersForEventStreaming() { CONTENT_TYPE); } - private EventEncoderFactory getEventEncoderFactory( - InputEventStreamingApiOperation inputOperation - ) { - - // TODO: this is where you'd plumb through Sigv4 support, another frame transformer? - return AwsEventEncoderFactory.forInputStream(inputOperation, + private EventEncoderFactory getEventEncoderFactory(ApiOperation operation) { + return AwsEventEncoderFactory.forInputStream(operation, payloadCodec(), PAYLOAD_MEDIA_TYPE, - FrameTransformer.identity(), (e) -> new EventStreamingException("InternalServerException", "Internal Server Error")); } - private EventDecoderFactory getEventDecoderFactory( - OutputEventStreamingApiOperation outputOperation - ) { - return AwsEventDecoderFactory.forOutputStream(outputOperation, payloadCodec(), f -> f); + private EventDecoderFactory getEventDecoderFactory(ApiOperation operation) { + return AwsEventDecoderFactory.forOutputStream(operation, payloadCodec(), f -> f); } private static ShapeId extractErrorType(Document document, String namespace) { diff --git a/client/dynamic-client/src/main/java/software/amazon/smithy/java/dynamicclient/DynamicOperation.java b/client/dynamic-client/src/main/java/software/amazon/smithy/java/dynamicclient/DynamicOperation.java index 3bd57d068..ad5ed95ca 100644 --- a/client/dynamic-client/src/main/java/software/amazon/smithy/java/dynamicclient/DynamicOperation.java +++ b/client/dynamic-client/src/main/java/software/amazon/smithy/java/dynamicclient/DynamicOperation.java @@ -11,9 +11,11 @@ import java.util.List; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Supplier; import software.amazon.smithy.java.core.schema.ApiOperation; import software.amazon.smithy.java.core.schema.ApiService; import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.ShapeBuilder; import software.amazon.smithy.java.core.schema.TraitKey; import software.amazon.smithy.java.core.serde.TypeRegistry; @@ -24,6 +26,7 @@ import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; public final class DynamicOperation implements ApiOperation { @@ -34,6 +37,8 @@ public final class DynamicOperation implements ApiOperation errorSchemas; private final TypeRegistry typeRegistry; private final List effectiveAuthSchemes; + private final Supplier> inputEventBuilderSupplier; + private final Supplier> outputEventBuilderSupplier; DynamicOperation( ApiService service, @@ -42,7 +47,9 @@ public final class DynamicOperation implements ApiOperation errorSchemas, TypeRegistry typeRegistry, - List effectiveAuthSchemes + List effectiveAuthSchemes, + Supplier> inputEventBuilderSupplier, + Supplier> outputEventBuilderSupplier ) { this.service = service; this.effectiveAuthSchemes = effectiveAuthSchemes; @@ -51,16 +58,8 @@ public final class DynamicOperation implements ApiOperation errorSchemas() { return errorSchemas; } + @Override + public Supplier> inputEventBuilderSupplier() { + return inputEventBuilderSupplier; + } + + @Override + public Supplier> outputEventBuilderSupplier() { + return outputEventBuilderSupplier; + } + public static DynamicOperation create( OperationShape shape, SchemaConverter schemaConverter, @@ -155,6 +164,22 @@ public static DynamicOperation create( registry = TypeRegistry.compose(registryBuilder.build(), serviceErrorRegistry); } + // Detect event stream members for builder suppliers. + Supplier> inputEventSupplier = null; + Supplier> outputEventSupplier = null; + for (var member : inputSchema.members()) { + if (member.hasTrait(TraitKey.STREAMING_TRAIT) && member.type() == ShapeType.UNION) { + inputEventSupplier = () -> SchemaConverter.createDocumentBuilder(member, service.getId()); + break; + } + } + for (var member : outputSchema.members()) { + if (member.hasTrait(TraitKey.STREAMING_TRAIT) && member.type() == ShapeType.UNION) { + outputEventSupplier = () -> SchemaConverter.createDocumentBuilder(member, service.getId()); + break; + } + } + return new DynamicOperation( apiService, operationSchema, @@ -162,6 +187,8 @@ public static DynamicOperation create( outputSchema, Collections.unmodifiableSet(errorSchemas), registry, - authSchemes); + authSchemes, + inputEventSupplier, + outputEventSupplier); } } diff --git a/client/dynamic-client/src/test/java/software/amazon/smithy/java/dynamicclient/DynamicOperationTest.java b/client/dynamic-client/src/test/java/software/amazon/smithy/java/dynamicclient/DynamicOperationTest.java index 4e49e5dd4..7742795f0 100644 --- a/client/dynamic-client/src/test/java/software/amazon/smithy/java/dynamicclient/DynamicOperationTest.java +++ b/client/dynamic-client/src/test/java/software/amazon/smithy/java/dynamicclient/DynamicOperationTest.java @@ -6,14 +6,14 @@ package software.amazon.smithy.java.dynamicclient; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import java.util.List; import java.util.Set; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import software.amazon.smithy.java.core.schema.ApiService; import software.amazon.smithy.java.core.schema.Schema; @@ -22,17 +22,22 @@ import software.amazon.smithy.java.dynamicschemas.SchemaConverter; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.traits.DeprecatedTrait; public class DynamicOperationTest { @Test - public void doesNotCurrentlySupportStreaming() { + public void supportsDataStreamOperations() { Model model = Model.assembler() .addUnparsedModel("test.smithy", """ $version: "2" namespace smithy.example + service S { + operations: [PutFoo] + } + operation PutFoo { input := { @required @@ -47,26 +52,174 @@ public void doesNotCurrentlySupportStreaming() { .assemble() .unwrap(); var converter = new SchemaConverter(model); - var registry = TypeRegistry.empty(); - var operationSchema = converter.getSchema(model.expectShape(ShapeId.from("smithy.example#PutFoo"))); - var input = converter.getSchema(model.expectShape(ShapeId.from("smithy.example#PutFooInput"))); - var output = converter.getSchema(model.expectShape(ShapeId.from("smithy.example#PutFooOutput"))); + var service = model.expectShape(ShapeId.from("smithy.example#S")).asServiceShape().get(); + var operation = model.expectShape(ShapeId.from("smithy.example#PutFoo")).asOperationShape().get(); - var serviceSchema = Schema.createService(ShapeId.from("smithy.example#S")); - ApiService apiService = () -> serviceSchema; + var op = DynamicOperation.create( + operation, + converter, + model, + service, + TypeRegistry.empty(), + (id, b) -> {}); + + // Data streams don't need event builder suppliers + assertThat(op.inputEventBuilderSupplier(), is(nullValue())); + assertThat(op.outputEventBuilderSupplier(), is(nullValue())); + assertThat(op.inputStreamMember(), is(op.inputSchema().member("someStream"))); + } + + @Test + public void createsInputEventStreamingOperation() { + Model model = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + + namespace smithy.example + + service S { + operations: [PutFoo] + } + + operation PutFoo { + input := { + stream: Events + } + output := {} + } + + @streaming + union Events { + data: Data + } + + structure Data { + value: String + } + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(model); + var service = model.expectShape(ShapeId.from("smithy.example#S")).asServiceShape().get(); + var operation = model.expectShape(ShapeId.from("smithy.example#PutFoo")).asOperationShape().get(); + + var op = DynamicOperation.create( + operation, + converter, + model, + service, + TypeRegistry.empty(), + (id, b) -> {}); + + assertThat(op.inputEventBuilderSupplier(), is(notNullValue())); + assertThat(op.outputEventBuilderSupplier(), is(nullValue())); + assertThat(op.inputEventBuilderSupplier().get().schema().type(), + equalTo(ShapeType.UNION)); + } + + @Test + public void createsOutputEventStreamingOperation() { + Model model = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + + namespace smithy.example + + service S { + operations: [GetFoo] + } + + operation GetFoo { + input := {} + output := { + stream: Events + } + } + + @streaming + union Events { + data: Data + } + + structure Data { + value: String + } + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(model); + var service = model.expectShape(ShapeId.from("smithy.example#S")).asServiceShape().get(); + var operation = model.expectShape(ShapeId.from("smithy.example#GetFoo")).asOperationShape().get(); + + var op = DynamicOperation.create( + operation, + converter, + model, + service, + TypeRegistry.empty(), + (id, b) -> {}); + + assertThat(op.inputEventBuilderSupplier(), is(nullValue())); + assertThat(op.outputEventBuilderSupplier(), is(notNullValue())); + assertThat(op.outputEventBuilderSupplier().get().schema().type(), + equalTo(ShapeType.UNION)); + } + + @Test + public void createsBidirectionalEventStreamingOperation() { + Model model = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + + namespace smithy.example + + service S { + operations: [Chat] + } + + operation Chat { + input := { + inputStream: InputEvents + } + output := { + outputStream: OutputEvents + } + } + + @streaming + union InputEvents { + message: Message + } + + @streaming + union OutputEvents { + reply: Reply + } + + structure Message { + text: String + } + + structure Reply { + text: String + } + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(model); + var service = model.expectShape(ShapeId.from("smithy.example#S")).asServiceShape().get(); + var operation = model.expectShape(ShapeId.from("smithy.example#Chat")).asOperationShape().get(); + + var op = DynamicOperation.create( + operation, + converter, + model, + service, + TypeRegistry.empty(), + (id, b) -> {}); - var e = Assertions.assertThrows(UnsupportedOperationException.class, () -> { - DynamicOperation o = new DynamicOperation( - apiService, - operationSchema, - input, - output, - Set.of(), - registry, - List.of()); - }); - - assertThat(e.getMessage(), containsString("does not support streaming")); + assertThat(op.inputEventBuilderSupplier(), is(notNullValue())); + assertThat(op.outputEventBuilderSupplier(), is(notNullValue())); } @Test @@ -101,7 +254,9 @@ public void convertsSchemas() { output, Set.of(), registry, - List.of()); + List.of(), + null, + null); assertThat(o.schema().id(), equalTo(ShapeId.from("smithy.example#PutFoo"))); assertThat(o.schema().hasTrait(TraitKey.get(DeprecatedTrait.class)), is(true)); diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/OperationGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/OperationGenerator.java index 98221c73b..022a14e9e 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/OperationGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/OperationGenerator.java @@ -23,9 +23,8 @@ import software.amazon.smithy.java.core.schema.ApiOperation; import software.amazon.smithy.java.core.schema.ApiResource; import software.amazon.smithy.java.core.schema.ApiService; -import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; -import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.ShapeBuilder; import software.amazon.smithy.java.core.serde.TypeRegistry; import software.amazon.smithy.model.Model; @@ -92,7 +91,7 @@ public final class ${shape:T} implements ${operationType:C} { ${?hasInputEventStream} @Override - public ${supplier:T}<${sdkShapeBuilder:T}<${inputEventType:T}>> inputEventBuilderSupplier() { + public ${supplier:T}<${sdkShapeBuilder:T}> inputEventBuilderSupplier() { return () -> ${inputEventType:T}.builder(); } ${/hasInputEventStream} @@ -104,7 +103,7 @@ public final class ${shape:T} implements ${operationType:C} { ${?hasOutputEventStream} @Override - public ${supplier:T}<${sdkShapeBuilder:T}<${outputEventType:T}>> outputEventBuilderSupplier() { + public ${supplier:T}<${sdkShapeBuilder:T}> outputEventBuilderSupplier() { return () -> ${outputEventType:T}.builder(); } ${/hasOutputEventStream} @@ -210,6 +209,7 @@ public final class ${shape:T} implements ${operationType:C} { var inputShape = directive.model().expectShape(shape.getInputShape()); eventStreamIndex.getInputInfo(shape).ifPresentOrElse(info -> { writer.putContext("supplier", Supplier.class); + writer.putContext("serializableStruct", SerializableStruct.class); writer.putContext("hasInputEventStream", true); writer.putContext("inputStreamMember", info.getEventStreamMember().getMemberName()); writer.putContext( @@ -226,6 +226,7 @@ public final class ${shape:T} implements ${operationType:C} { eventStreamIndex.getOutputInfo(shape).ifPresentOrElse(info -> { writer.putContext("supplier", Supplier.class); + writer.putContext("serializableStruct", SerializableStruct.class); writer.putContext("hasOutputEventStream", true); writer.putContext("outputStreamMember", info.getEventStreamMember().getMemberName()); writer.putContext( @@ -295,30 +296,7 @@ public void run() { var outputShape = model.expectShape(shape.getOutputShape()); var output = symbolProvider.toSymbol(outputShape); - var inputEventStreamInfo = index.getInputInfo(shape); - var outputEventStreamInfo = index.getOutputInfo(shape); - inputEventStreamInfo.ifPresent( - info -> writer.writeInline( - "$1T<$2T, $3T, $4T>", - InputEventStreamingApiOperation.class, - input, - output, - symbolProvider.toSymbol(info.getEventStreamTarget()))); - outputEventStreamInfo.ifPresent(info -> { - if (inputEventStreamInfo.isPresent()) { - writer.writeInline(", "); - } - writer.writeInline( - "$1T<$2T, $3T, $4T>", - OutputEventStreamingApiOperation.class, - input, - output, - symbolProvider.toSymbol(info.getEventStreamTarget())); - }); - - if (inputEventStreamInfo.isEmpty() && outputEventStreamInfo.isEmpty()) { - writer.writeInline("$1T<$2T, $3T>", ApiOperation.class, input, output); - } + writer.writeInline("$1T<$2T, $3T>", ApiOperation.class, input, output); } } } 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/ApiOperation.java b/core/src/main/java/software/amazon/smithy/java/core/schema/ApiOperation.java index 983a58a69..b19335468 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/ApiOperation.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/ApiOperation.java @@ -6,6 +6,7 @@ package software.amazon.smithy.java.core.schema; import java.util.List; +import java.util.function.Supplier; import software.amazon.smithy.java.core.serde.TypeRegistry; import software.amazon.smithy.model.shapes.ShapeId; @@ -107,6 +108,24 @@ default Schema outputStreamMember() { return null; } + /** + * Get a supplier of builders for input event stream shapes. + * + * @return a supplier of input event shape builders, or null if the operation has no input event stream. + */ + default Supplier> inputEventBuilderSupplier() { + return null; + } + + /** + * Get a supplier of builders for output event stream shapes. + * + * @return a supplier of output event shape builders, or null if the operation has no output event stream. + */ + default Supplier> outputEventBuilderSupplier() { + return null; + } + /** * Get the schemas of all errors that can be thrown by the operation. * 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 deleted file mode 100644 index e2409f9e8..000000000 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/InputEventStreamingApiOperation.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.core.schema; - -import java.util.function.Supplier; - -/** - * Represents a modeled Smithy operation. - * - * @param Operation input shape type. - * @param Operation output shape type. - * @param Operation input event shape type. - */ -public interface InputEventStreamingApiOperation - extends ApiOperation { - /** - * Retrieves a supplier of builders for input events. - * - * @return Returns a supplier of input event shape builders. - */ - Supplier> inputEventBuilderSupplier(); -} diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/OutputEventStreamingApiOperation.java b/core/src/main/java/software/amazon/smithy/java/core/schema/OutputEventStreamingApiOperation.java deleted file mode 100644 index c63cc8931..000000000 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/OutputEventStreamingApiOperation.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.core.schema; - -import java.util.function.Supplier; - -/** - * Represents a modeled Smithy operation. - * - * @param Operation input shape type. - * @param Operation output shape type. - * @param Operation output event shape type. - */ -public interface OutputEventStreamingApiOperation - extends ApiOperation { - /** - * Retrieves a supplier of builders for output events. - * - * @return Returns a supplier of output event shape builders. - */ - Supplier> outputEventBuilderSupplier(); -} diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/DataStreamDocument.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/DataStreamDocument.java new file mode 100644 index 000000000..59c726677 --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/DataStreamDocument.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.core.serde.document; + +import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.core.serde.ShapeSerializer; +import software.amazon.smithy.java.io.datastream.DataStream; +import software.amazon.smithy.model.shapes.ShapeType; + +/** + * A document wrapper around a {@link DataStream} for streaming blob members. + */ +record DataStreamDocument(Schema schema, DataStream dataStream) implements Document { + + @Override + public ShapeType type() { + return ShapeType.BLOB; + } + + @Override + public DataStream asDataStream() { + return dataStream; + } + + @Override + public Object asObject() { + return dataStream; + } + + @Override + public void serialize(ShapeSerializer serializer) { + serializer.writeDataStream(schema, dataStream); + } + + @Override + public void serializeContents(ShapeSerializer serializer) { + serializer.writeDataStream(schema, dataStream); + } +} diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java index 69aaa7ad2..04b7eccd8 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java @@ -18,10 +18,13 @@ import software.amazon.smithy.java.core.schema.PreludeSchemas; import software.amazon.smithy.java.core.schema.Schema; import software.amazon.smithy.java.core.schema.SerializableShape; +import software.amazon.smithy.java.core.schema.SerializableStruct; import software.amazon.smithy.java.core.schema.ShapeBuilder; import software.amazon.smithy.java.core.serde.SerializationException; import software.amazon.smithy.java.core.serde.ShapeDeserializer; import software.amazon.smithy.java.core.serde.ShapeSerializer; +import software.amazon.smithy.java.core.serde.event.EventStream; +import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; @@ -323,6 +326,48 @@ default Instant asTimestamp() { throw new SerializationException("Expected a timestamp document, but found " + type()); } + /** + * Get the Document as a {@link DataStream} if it wraps a streaming blob. + * + * @return the data stream. + * @throws SerializationException if the Document does not wrap a data stream. + */ + default DataStream asDataStream() { + throw new SerializationException("Expected a data stream document, but found " + type()); + } + + /** + * Get the Document as an {@link EventStream} if it wraps a streaming union. + * + * @return the event stream. + * @throws SerializationException if the Document does not wrap an event stream. + */ + default EventStream asEventStream() { + throw new SerializationException("Expected an event stream document, but found " + type()); + } + + /** + * Create a Document wrapping a {@link DataStream} with a specific schema. + * + * @param schema Schema to associate with the stream. + * @param dataStream The data stream to wrap. + * @return the Document type. + */ + static Document of(Schema schema, DataStream dataStream) { + return new DataStreamDocument(schema, dataStream); + } + + /** + * Create a Document wrapping an {@link EventStream} with a specific schema. + * + * @param schema Schema to associate with the stream. + * @param eventStream The event stream to wrap. + * @return the Document type. + */ + static Document of(Schema schema, EventStream eventStream) { + return new EventStreamDocument(schema, eventStream); + } + /** * Get the number of elements in an array document, or the number of key value pairs in a map document. * @@ -626,6 +671,8 @@ static Document of(Map members) { *

      *
    • {@link Document}
    • *
    • {@link SerializableShape}
    • + *
    • {@link DataStream} to streaming blob
    • + *
    • {@link EventStream} to streaming union
    • *
    • {@link String}
    • *
    • {@code byte[]} to blob
    • *
    • {@link Instant} to timestamp
    • @@ -650,6 +697,8 @@ static Document ofObject(Object o) { return switch (o) { case Document d -> d; case SerializableShape s -> of(s); + case DataStream ds -> new DataStreamDocument(PreludeSchemas.BLOB, ds); + case EventStream es -> new EventStreamDocument(PreludeSchemas.DOCUMENT, es); case String s -> of(s); case Boolean b -> of(b); case Number n -> ofNumber(n); diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/EventStreamDocument.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/EventStreamDocument.java new file mode 100644 index 000000000..82fe0c0cb --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/EventStreamDocument.java @@ -0,0 +1,43 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.core.serde.document; + +import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.core.schema.SerializableStruct; +import software.amazon.smithy.java.core.serde.ShapeSerializer; +import software.amazon.smithy.java.core.serde.event.EventStream; +import software.amazon.smithy.model.shapes.ShapeType; + +/** + * A document wrapper around an {@link EventStream} for streaming union members. + */ +record EventStreamDocument(Schema schema, EventStream eventStream) implements Document { + + @Override + public ShapeType type() { + return ShapeType.UNION; + } + + @Override + public EventStream asEventStream() { + return eventStream; + } + + @Override + public Object asObject() { + return eventStream; + } + + @Override + public void serialize(ShapeSerializer serializer) { + serializer.writeEventStream(schema, eventStream); + } + + @Override + public void serializeContents(ShapeSerializer serializer) { + serializer.writeEventStream(schema, eventStream); + } +} 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..f18b9ce6e 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 setFrameAuthorizer(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..e5410ac77 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,38 @@ 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. + */ + default F closingFrame() { + return null; + } } 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..a68db1e7c 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,21 @@ public interface EventEncoderFactory> { */ String contentType(); + /** + * Composes the factory with the given frame processor. The processor is composed with other existing processors + * in the order those are created. For instance + * + * {@snippet java: + * // This factory will first apply the GzipFrameProcessor and then the SigningFrameProcessor. + * var factory = awsEncoderFactory.withFrameProcessor(new GzipFrameProcessor()) + * .withFrameProcessor(new SigningFrameProcessor()); + * + * } + * + * @param frameProcessor the frame processor + * @return the composed factory + */ + default EventEncoderFactory withFrameProcessor(FrameProcessor frameProcessor) { + return new ProcessingEventEncoderFactory<>(this, 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..f4f855788 --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/FrameProcessor.java @@ -0,0 +1,58 @@ +/* + * 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 + */ + @SuppressWarnings("unchecked") + static > FrameProcessor identity() { + return (FrameProcessor) IdentityFrameProcessor.INSTANCE; + } + + final class IdentityFrameProcessor> implements FrameProcessor { + static IdentityFrameProcessor INSTANCE = new IdentityFrameProcessor<>(); + + @Override + public F transformFrame(F frame) { + return 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/ProcessingEventEncoder.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProcessingEventEncoder.java new file mode 100644 index 000000000..4ce6dd8bb --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProcessingEventEncoder.java @@ -0,0 +1,44 @@ +/* + * 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 software.amazon.smithy.java.core.schema.SerializableStruct; + +/** + * Composes a event encoder with a frame processor. + * + * @param the type of the frame + */ +final class ProcessingEventEncoder> implements EventEncoder { + private final EventEncoder eventEncoder; + private final FrameProcessor frameProcessor; + + ProcessingEventEncoder(EventEncoder eventEncoder, FrameProcessor frameProcessor) { + this.eventEncoder = eventEncoder; + this.frameProcessor = frameProcessor; + } + + @Override + public F encode(SerializableStruct item) { + F frame = eventEncoder.encode(item); + return frameProcessor.transformFrame(frame); + } + + @Override + public F encodeFailure(Throwable exception) { + F frame = eventEncoder.encodeFailure(exception); + return frameProcessor.transformFrame(frame); + } + + @Override + public F closingFrame() { + F closingFrame = eventEncoder.closingFrame(); + if (closingFrame != null) { + return closingFrame; + } + return frameProcessor.closingFrame(); + } +} diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProcessingEventEncoderFactory.java b/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProcessingEventEncoderFactory.java new file mode 100644 index 000000000..474b3d5b4 --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/event/ProcessingEventEncoderFactory.java @@ -0,0 +1,39 @@ +/* + * 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.Objects; + +/** + * Wraps a factory with a frame processor. + * + * @param the type of the frame + */ +final class ProcessingEventEncoderFactory> implements EventEncoderFactory { + private final EventEncoderFactory eventDecoderFactory; + private final FrameProcessor frameProcessor; + + ProcessingEventEncoderFactory(EventEncoderFactory eventDecoderFactory, FrameProcessor frameProcessor) { + this.eventDecoderFactory = Objects.requireNonNull(eventDecoderFactory, "eventDecoderFactory"); + this.frameProcessor = Objects.requireNonNull(frameProcessor, "frameProcessor"); + } + + @Override + public EventEncoder newEventEncoder() { + var eventEncoder = eventDecoderFactory.newEventEncoder(); + return new ProcessingEventEncoder<>(eventEncoder, frameProcessor); + } + + @Override + public FrameEncoder newFrameEncoder() { + return eventDecoderFactory.newFrameEncoder(); + } + + @Override + public String contentType() { + return eventDecoderFactory.contentType(); + } +} 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..6d2552f8f 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 frame processor to add authorization information to events in the stream + * + * @param eventSigner the signer to sign events in the stream + */ + void setFrameAuthorizer(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/document/DocumentTest.java b/core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentTest.java index 1753ef2db..3562cf065 100644 --- a/core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentTest.java +++ b/core/src/test/java/software/amazon/smithy/java/core/serde/document/DocumentTest.java @@ -35,8 +35,10 @@ import software.amazon.smithy.java.core.serde.ShapeDeserializer; import software.amazon.smithy.java.core.serde.ShapeSerializer; import software.amazon.smithy.java.core.serde.ToStringSerializer; +import software.amazon.smithy.java.core.serde.event.EventStream; import software.amazon.smithy.java.core.testmodels.Bird; import software.amazon.smithy.java.core.testmodels.Person; +import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.model.shapes.ShapeType; public class DocumentTest { @@ -543,4 +545,45 @@ public void documentsDefaultToSizeNegativeOne() { assertThat(document.size(), is(-1)); } + + @Test + public void ofObjectCreatesDataStreamDocument() { + var ds = DataStream.ofString("hello"); + var document = Document.ofObject(ds); + + assertThat(document.type(), is(ShapeType.BLOB)); + assertThat(document.asDataStream(), is(ds)); + assertThat(document.asObject(), is(ds)); + } + + @Test + public void ofObjectCreatesEventStreamDocument() { + var es = EventStream.newWriter(); + var document = Document.ofObject(es); + + assertThat(document.type(), is(ShapeType.UNION)); + assertThat(document.asEventStream(), is(es)); + assertThat(document.asObject(), is(es)); + } + + @Test + public void ofObjectHandlesDataStreamInMap() { + var ds = DataStream.ofString("hello"); + var document = Document.ofObject(Map.of("body", ds)); + + assertThat(document.type(), is(ShapeType.MAP)); + assertThat(document.getMember("body").asDataStream(), is(ds)); + } + + @Test + public void asDataStreamThrowsForNonStreamDocument() { + var document = Document.of("hello"); + Assertions.assertThrows(SerializationException.class, document::asDataStream); + } + + @Test + public void asEventStreamThrowsForNonStreamDocument() { + var document = Document.of("hello"); + Assertions.assertThrows(SerializationException.class, document::asEventStream); + } } 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..b2e9965e9 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.setFrameAuthorizer(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..0384957e7 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 { diff --git a/dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/SchemaGuidedDocumentBuilder.java b/dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/SchemaGuidedDocumentBuilder.java index 48b580d76..ad6d90c4e 100644 --- a/dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/SchemaGuidedDocumentBuilder.java +++ b/dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/SchemaGuidedDocumentBuilder.java @@ -12,8 +12,11 @@ import software.amazon.smithy.java.core.schema.Schema; import software.amazon.smithy.java.core.schema.SchemaUtils; import software.amazon.smithy.java.core.schema.ShapeBuilder; +import software.amazon.smithy.java.core.schema.TraitKey; import software.amazon.smithy.java.core.serde.ShapeDeserializer; import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.java.core.serde.event.EventStream; +import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; @@ -54,16 +57,15 @@ public StructDocument build() { @Override public void setMemberValue(Schema member, Object value) { SchemaUtils.validateMemberInSchema(target, member, value); - Document convertedValue; - if (value instanceof Document d) { + Document convertedValue = switch (value) { // Convert the given document so it matches the required schema. - convertedValue = StructDocument.convertDocument(member, d, service); - } else { + case Document d -> StructDocument.convertDocument(member, d, service); + case DataStream ds -> Document.of(member, ds); + case EventStream es -> Document.of(member, es); // Convert the object to a document and then wrap it with the correct schema. - convertedValue = Document.ofObject(value); - convertedValue = StructDocument.convertDocument(member, convertedValue, service); - } + case null, default -> StructDocument.convertDocument(member, Document.ofObject(value), service); + }; map.put(member.memberName(), convertedValue); } @@ -82,7 +84,12 @@ public ShapeBuilder deserializeMember(ShapeDeserializer decoder, private Document deserialize(ShapeDeserializer decoder, Schema schema) { return switch (schema.type()) { - case BLOB -> new ContentDocument(Document.of(decoder.readBlob(schema)), schema); + case BLOB -> { + if (schema.hasTrait(TraitKey.STREAMING_TRAIT)) { + yield Document.of(schema, decoder.readDataStream(schema)); + } + yield new ContentDocument(Document.of(decoder.readBlob(schema)), schema); + } case BOOLEAN -> new ContentDocument(Document.of(decoder.readBoolean(schema)), schema); case STRING, ENUM -> new ContentDocument(Document.of(decoder.readString(schema)), schema); case TIMESTAMP -> new ContentDocument(Document.of(decoder.readTimestamp(schema)), schema); @@ -109,18 +116,25 @@ private Document deserialize(ShapeDeserializer decoder, Schema schema) { }); yield new ContentDocument(Document.of(map), schema); } - case STRUCTURE, UNION -> { - var map = new HashMap(); - decoder.readStruct(schema, map, (state, memberSchema, memberDeserializer) -> { - state.put(memberSchema.memberName(), deserialize(memberDeserializer, memberSchema)); - }); - // Use "new" here since we know all the nested shapes have the correct schemas. - yield new StructDocument(schema, map, service); + case STRUCTURE -> createStructDocument(decoder, schema); + case UNION -> { + if (schema.hasTrait(TraitKey.STREAMING_TRAIT)) { + yield Document.of(schema, decoder.readEventStream(schema)); + } + yield createStructDocument(decoder, schema); } default -> throw new UnsupportedOperationException("Unsupported target type: " + schema.type()); }; } + private StructDocument createStructDocument(ShapeDeserializer decoder, Schema schema) { + var map = new LinkedHashMap(); + decoder.readStruct(schema, map, (state, memberSchema, memberDeserializer) -> { + state.put(memberSchema.memberName(), deserialize(memberDeserializer, memberSchema)); + }); + return new StructDocument(schema, map, service); + } + @Override public ShapeBuilder errorCorrection() { // TODO: fill in defaults. diff --git a/dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/StructDocument.java b/dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/StructDocument.java index 90be2fb7d..fb595ed40 100644 --- a/dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/StructDocument.java +++ b/dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/StructDocument.java @@ -12,6 +12,7 @@ import java.util.Set; import software.amazon.smithy.java.core.schema.Schema; import software.amazon.smithy.java.core.schema.SerializableStruct; +import software.amazon.smithy.java.core.schema.TraitKey; import software.amazon.smithy.java.core.serde.ShapeSerializer; import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.core.serde.document.DocumentUtils; @@ -74,17 +75,25 @@ public static StructDocument of(Schema schema, Document delegate, ShapeId servic throw new IllegalArgumentException("Document must be a map, structure, or union, but got " + delegate.type()); } + private static Document convertStructureDocument(Schema schema, Document delegate, ShapeId service) { + Map result = new LinkedHashMap<>(); + for (var member : schema.members()) { + var value = delegate.getMember(member.memberName()); + if (value != null) { + result.put(member.memberName(), convertDocument(member, value, service)); + } + } + return new StructDocument(schema, result, service); + } + static Document convertDocument(Schema schema, Document delegate, ShapeId service) { return switch (schema.type()) { - case STRUCTURE, UNION -> { - Map result = new LinkedHashMap<>(); - for (var member : schema.members()) { - var value = delegate.getMember(member.memberName()); - if (value != null) { - result.put(member.memberName(), convertDocument(member, value, service)); - } + case STRUCTURE -> convertStructureDocument(schema, delegate, service); + case UNION -> { + if (schema.hasTrait(TraitKey.STREAMING_TRAIT)) { + yield Document.of(schema, delegate.asEventStream()); } - yield new StructDocument(schema, result, service); + yield convertStructureDocument(schema, delegate, service); } case MAP -> { Map result = new LinkedHashMap<>(); @@ -117,7 +126,12 @@ static Document convertDocument(Schema schema, Document delegate, ShapeId servic LONG, FLOAT, DOUBLE, BIG_INTEGER, BIG_DECIMAL -> new ContentDocument(Document.ofNumber(delegate.asNumber()), schema); case DOCUMENT -> new ContentDocument(delegate, schema); - case BLOB -> new ContentDocument(Document.of(delegate.asBlob()), schema); + case BLOB -> { + if (schema.hasTrait(TraitKey.STREAMING_TRAIT)) { + yield Document.of(schema, delegate.asDataStream()); + } + yield new ContentDocument(Document.of(delegate.asBlob()), schema); + } default -> throw new IllegalArgumentException("Unsupported schema type: " + schema); }; } diff --git a/dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/SchemaGuidedDocumentBuilderTest.java b/dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/SchemaGuidedDocumentBuilderTest.java index 2d1c07aa6..883f728f4 100644 --- a/dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/SchemaGuidedDocumentBuilderTest.java +++ b/dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/SchemaGuidedDocumentBuilderTest.java @@ -27,6 +27,8 @@ import software.amazon.smithy.java.core.serde.SpecificShapeSerializer; import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.core.serde.document.DocumentDeserializer; +import software.amazon.smithy.java.core.serde.event.EventStream; +import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.java.json.JsonCodec; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; @@ -429,4 +431,105 @@ public void usesStructDocumentForSetMember() { assertThat(StructDocumentTest.getDocumentSchema(result.getMember("foo")), equalTo(schema.member("foo"))); assertThat(StructDocumentTest.getDocumentSchema(result.getMember("baz")), equalTo(schema.member("baz"))); } + + @Test + public void setMemberValueHandlesDataStream() { + var testModel = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + namespace smithy.example + + structure TestShape { + @required + body: StreamBlob + } + + @streaming + blob StreamBlob + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(testModel); + var schema = converter.getSchema(testModel.expectShape(ShapeId.from("smithy.example#TestShape"))); + + var ds = DataStream.ofString("hello"); + var builder = SchemaConverter.createDocumentBuilder(schema); + builder.setMemberValue(schema.member("body"), ds); + + var result = builder.build(); + assertThat(result.getMember("body").asDataStream(), equalTo(ds)); + } + + @Test + public void setMemberValueHandlesEventStream() { + var testModel = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + namespace smithy.example + + structure TestShape { + events: Events + } + + @streaming + union Events { + data: Data + } + + structure Data { + value: String + } + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(testModel); + var schema = converter.getSchema(testModel.expectShape(ShapeId.from("smithy.example#TestShape"))); + + var es = EventStream.newWriter(); + var builder = SchemaConverter.createDocumentBuilder(schema); + builder.setMemberValue(schema.member("events"), es); + + var result = builder.build(); + assertThat(result.getMember("events").asEventStream(), equalTo(es)); + } + + @Test + public void deserializesDataStreamMember() { + var testModel = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + namespace smithy.example + + structure TestShape { + @required + body: StreamBlob + } + + @streaming + blob StreamBlob + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(testModel); + var schema = converter.getSchema(testModel.expectShape(ShapeId.from("smithy.example#TestShape"))); + + var ds = DataStream.ofString("hello"); + + // Simulate what the protocol layer does: provide a SpecificShapeDeserializer for the streaming member + var builder = SchemaConverter.createDocumentBuilder(schema); + builder.deserialize(new SpecificShapeDeserializer() { + @Override + public void readStruct(Schema s, T state, StructMemberConsumer consumer) { + consumer.accept(state, schema.member("body"), new SpecificShapeDeserializer() { + @Override + public DataStream readDataStream(Schema s2) { + return ds; + } + }); + } + }); + + var result = builder.build(); + assertThat(result.getMember("body").asDataStream(), equalTo(ds)); + } } diff --git a/dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/StructDocumentTest.java b/dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/StructDocumentTest.java index 98ad71a03..54dd0ca3d 100644 --- a/dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/StructDocumentTest.java +++ b/dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/StructDocumentTest.java @@ -33,6 +33,9 @@ import software.amazon.smithy.java.core.serde.SpecificShapeSerializer; import software.amazon.smithy.java.core.serde.document.DiscriminatorException; import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.java.core.serde.event.EventStream; +import software.amazon.smithy.java.io.datastream.DataStream; +import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; @@ -369,4 +372,98 @@ public void writeString(Schema schema, String value) { assertThat(set[0], equalTo(schema)); assertThat(set[1], equalTo(schema.member("a"))); } + + @Test + public void convertDocumentPassesThroughDataStream() { + var model = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + namespace smithy.example + + structure Foo { + @required + body: StreamBlob + } + + @streaming + blob StreamBlob + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(model); + var schema = converter.getSchema(model.expectShape(ShapeId.from("smithy.example#Foo"))); + + var ds = DataStream.ofString("hello"); + var input = Document.ofObject(Map.of("body", ds)); + var struct = StructDocument.of(schema, input); + + assertThat(struct.getMember("body").asDataStream(), is(ds)); + } + + @Test + public void convertDocumentPassesThroughEventStream() { + var model = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + namespace smithy.example + + structure Foo { + events: Events + } + + @streaming + union Events { + data: Data + } + + structure Data { + value: String + } + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(model); + var schema = converter.getSchema(model.expectShape(ShapeId.from("smithy.example#Foo"))); + + var es = EventStream.newWriter(); + var input = Document.ofObject(Map.of("events", es)); + var struct = StructDocument.of(schema, input); + + assertThat(struct.getMember("events").asEventStream(), is(es)); + } + + @Test + public void dataStreamSerializesMemberCorrectly() { + var model = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + namespace smithy.example + + structure Foo { + @required + body: StreamBlob + } + + @streaming + blob StreamBlob + """) + .assemble() + .unwrap(); + var converter = new SchemaConverter(model); + var schema = converter.getSchema(model.expectShape(ShapeId.from("smithy.example#Foo"))); + + var ds = DataStream.ofString("hello"); + var input = Document.ofObject(Map.of("body", ds)); + var struct = StructDocument.of(schema, input); + + DataStream[] captured = new DataStream[1]; + struct.serializeMembers(new SpecificShapeSerializer() { + @Override + public void writeDataStream(Schema s, DataStream value) { + captured[0] = value; + } + }); + + assertThat(captured[0], is(ds)); + } } diff --git a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/RequestSerializer.java b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/RequestSerializer.java index 6a5dd8f26..0b9b3da98 100644 --- a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/RequestSerializer.java +++ b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/RequestSerializer.java @@ -9,7 +9,6 @@ import java.util.Objects; import java.util.concurrent.ConcurrentMap; import software.amazon.smithy.java.core.schema.ApiOperation; -import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.Schema; import software.amazon.smithy.java.core.schema.SerializableShape; import software.amazon.smithy.java.core.schema.SerializableStruct; @@ -166,7 +165,7 @@ public HttpRequest serializeRequest() { .uri(targetEndpoint); var eventStream = serializer.getEventStream(); - if (eventStream != null && operation instanceof InputEventStreamingApiOperation) { + if (eventStream != null && operation.inputEventBuilderSupplier() != null) { ProtocolEventStreamWriter> writer = ProtocolEventStreamWriter.of(eventStream); writer.bootstrap((EventEncoderFactory) eventStreamEncodingFactory, null); diff --git a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/ResponseSerializer.java b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/ResponseSerializer.java index dda59ee1c..bd296b0f1 100644 --- a/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/ResponseSerializer.java +++ b/http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/ResponseSerializer.java @@ -8,7 +8,6 @@ import java.util.Objects; import java.util.concurrent.ConcurrentMap; import software.amazon.smithy.java.core.schema.ApiOperation; -import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation; import software.amazon.smithy.java.core.schema.Schema; import software.amazon.smithy.java.core.schema.SerializableShape; import software.amazon.smithy.java.core.schema.SerializableStruct; @@ -152,7 +151,7 @@ public HttpResponse serializeResponse() { .statusCode(serializer.getResponseStatus()); var eventStream = serializer.getEventStream(); - if (eventStream != null && operation instanceof OutputEventStreamingApiOperation) { + if (eventStream != null && operation.outputEventBuilderSupplier() != null) { ProtocolEventStreamWriter> writer = ProtocolEventStreamWriter.of(eventStream); writer.bootstrap(eventEncoderFactory, null); 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