From a4734c7551694dd90a71aa5d1e4818ec9ae7b47f Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:30:46 -0500 Subject: [PATCH 01/54] fix: create JMESPATH flattenIfPossible functions for Lists (#1169) --- .../smithy/kotlin/runtime/util/JMESPath.kt | 10 ++++ .../kotlin/runtime/util/JmesPathTest.kt | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt index 2a7cdd9791..9b691a79a2 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt @@ -66,6 +66,7 @@ public fun Any?.type(): String = when (this) { else -> throw Exception("Undetected type for: $this") } +// Collection `flattenIfPossible` functions @InternalApi @JvmName("noOpUnnestedCollection") public inline fun Collection.flattenIfPossible(): Collection = this @@ -73,3 +74,12 @@ public inline fun Collection.flattenIfPossible(): Collection = @InternalApi @JvmName("flattenNestedCollection") public inline fun Collection>.flattenIfPossible(): Collection = flatten() + +// List `flattenIfPossible` functions +@InternalApi +@JvmName("noOpUnnestedCollection") +public inline fun List.flattenIfPossible(): List = this + +@InternalApi +@JvmName("flattenNestedCollection") +public inline fun List>.flattenIfPossible(): List = flatten() diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt new file mode 100644 index 0000000000..e867f1e8a1 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt @@ -0,0 +1,49 @@ +package aws.smithy.kotlin.runtime.util + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JmesPathTest { + @Test + fun flattenNestedLists() { + val nestedList = listOf( + listOf(1, 2, 3), + listOf(4, 5), + listOf(6), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3, 4, 5, 6), flattenedList) + } + + @Test + fun flattenEmptyNestedLists() { + val nestedList = listOf( + listOf(), + listOf(), + listOf(), + ) + val flattenedList = nestedList.flattenIfPossible() + assertTrue(flattenedList.isEmpty()) + } + + @Test + fun flattenNestedEmptyAndNonEmptyNestedLists() { + val nestedList = listOf( + listOf(1, 2), + listOf(), + listOf(3, 4, 5), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3, 4, 5), flattenedList) + } + + @Test + fun flattenList() { + val nestedList = listOf( + listOf(1, 2, 3), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3), flattenedList) + } +} From 7911a94dd5f6ee27f2652aeb7c8031e2dce79215 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 16 Dec 2024 16:51:49 +0000 Subject: [PATCH 02/54] chore: release 1.3.30 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 892e2b1248..5633a30d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.30] - 12/16/2024 + ## [1.3.29] - 12/12/2024 ## [1.3.28] - 12/03/2024 diff --git a/gradle.properties b/gradle.properties index f35cfc1d5a..377ff6666b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.30-SNAPSHOT +sdkVersion=1.3.30 # codegen -codegenVersion=0.33.30-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.30 \ No newline at end of file From 7085c8ab0d4b34d3ddb77bbb31a2fd41c47c288c Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 16 Dec 2024 16:51:50 +0000 Subject: [PATCH 03/54] chore: bump snapshot version to 1.3.31-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 377ff6666b..3628520e5a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.30 +sdkVersion=1.3.31-SNAPSHOT # codegen -codegenVersion=0.33.30 \ No newline at end of file +codegenVersion=0.33.31-SNAPSHOT \ No newline at end of file From 45750232626bbe4e279142e486be8bbbfccab33f Mon Sep 17 00:00:00 2001 From: Matas Date: Wed, 18 Dec 2024 14:20:08 -0500 Subject: [PATCH 04/54] misc: enhance support for replayable instances of `InputStream` (#1197) --- .../2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json | 8 ++++++ .../kotlin/runtime/content/ByteStreamJVM.kt | 23 +++++++++++++-- .../runtime/content/ByteStreamJVMTest.kt | 28 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json diff --git a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json new file mode 100644 index 0000000000..fb16fa5843 --- /dev/null +++ b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json @@ -0,0 +1,8 @@ +{ + "id": "2de8162f-e5a0-4618-b00d-8da3cfdbc6a2", + "type": "feature", + "description": "Enhance support for replayable instances of `InputStream`", + "issues": [ + "https://github.com/awslabs/aws-sdk-kotlin/issues/1473" + ] +} \ No newline at end of file diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt index 1d6124357a..5647ac15af 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt @@ -114,11 +114,30 @@ public fun ByteStream.Companion.fromInputStream( * @param contentLength If specified, indicates how many bytes remain in this stream. Defaults to `null`. */ public fun InputStream.asByteStream(contentLength: Long? = null): ByteStream.SourceStream { - val source = source() + if (markSupported() && contentLength != null) { + mark(contentLength.toInt()) + } + return object : ByteStream.SourceStream() { override val contentLength: Long? = contentLength override val isOneShot: Boolean = !markSupported() - override fun readFrom(): SdkSource = source + override fun readFrom(): SdkSource { + if (markSupported() && contentLength != null) { + reset() + mark(contentLength.toInt()) + return object : SdkSource by source() { + /* + * This is a no-op close to prevent body hashing from closing the underlying InputStream, which causes + * `IOException: Stream closed` on subsequent reads. Consider making [ByteStream.ChannelStream]/[ByteStream.SourceStream] + * (or possibly even [ByteStream] itself) implement [Closeable] to better handle closing streams. + * This should allow us to clean up our usage of [ByteStream.cancel()]. + */ + override fun close() { } + } + } + + return source() + } } } diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt index 054387b2e5..e8324fb11f 100644 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt @@ -5,8 +5,11 @@ package aws.smithy.kotlin.runtime.content +import aws.smithy.kotlin.runtime.io.readToByteArray import aws.smithy.kotlin.runtime.testing.RandomTempFile import kotlinx.coroutines.test.runTest +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream import java.io.OutputStream @@ -228,6 +231,31 @@ class ByteStreamJVMTest { assertFalse(sos.closed) } + // https://github.com/awslabs/aws-sdk-kotlin/issues/1473 + @Test + fun testReplayableInputStreamAsByteStream() = runTest { + val content = "Hello, Bytes!".encodeToByteArray() + val byteArrayIns = ByteArrayInputStream(content) + val nonReplayableIns = NonReplayableInputStream(byteArrayIns) + + // buffer the non-replayable stream, making it replayable... + val bufferedIns = BufferedInputStream(nonReplayableIns) + + val byteStream = bufferedIns.asByteStream(content.size.toLong()) + + // Test that it can be read at least twice (e.g. once for hashing the body, once for transmitting the body) + assertContentEquals(content, byteStream.readFrom().use { it.readToByteArray() }) + assertContentEquals(content, byteStream.readFrom().use { it.readToByteArray() }) + } + + private class NonReplayableInputStream(val inputStream: InputStream) : InputStream() { + override fun markSupported(): Boolean = false // not replayable + + override fun read(): Int = inputStream.read() + override fun mark(readlimit: Int) = inputStream.mark(readlimit) + override fun reset() = inputStream.reset() + } + private class StatusTrackingOutputStream(val os: OutputStream) : OutputStream() { var closed: Boolean = false From b0a4bacc3626bf2c9e8cfe9607ef8d5ece03ee4e Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 18 Dec 2024 19:23:06 +0000 Subject: [PATCH 05/54] chore: release 1.3.31 --- .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json | 8 -------- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json diff --git a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json deleted file mode 100644 index fb16fa5843..0000000000 --- a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "2de8162f-e5a0-4618-b00d-8da3cfdbc6a2", - "type": "feature", - "description": "Enhance support for replayable instances of `InputStream`", - "issues": [ - "https://github.com/awslabs/aws-sdk-kotlin/issues/1473" - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5633a30d73..5473a56d69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.3.31] - 12/18/2024 + +### Features +* [#1473](https://github.com/awslabs/aws-sdk-kotlin/issues/1473) Enhance support for replayable instances of `InputStream` + ## [1.3.30] - 12/16/2024 ## [1.3.29] - 12/12/2024 diff --git a/gradle.properties b/gradle.properties index 3628520e5a..cf967edd0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.31-SNAPSHOT +sdkVersion=1.3.31 # codegen -codegenVersion=0.33.31-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.31 \ No newline at end of file From 80f453814d8b0c4e20ef7e729d47a364ca8b5753 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 18 Dec 2024 19:23:08 +0000 Subject: [PATCH 06/54] chore: bump snapshot version to 1.3.32-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index cf967edd0b..f902a36719 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.31 +sdkVersion=1.3.32-SNAPSHOT # codegen -codegenVersion=0.33.31 \ No newline at end of file +codegenVersion=0.33.32-SNAPSHOT \ No newline at end of file From e9d16a98b8030468e35a05f63c79cae2444e6759 Mon Sep 17 00:00:00 2001 From: Matas Date: Wed, 18 Dec 2024 16:28:30 -0500 Subject: [PATCH 07/54] fix: CBOR protocol test assertions / blob serialization (#1198) --- .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json | 5 +++++ .../protocol/HttpProtocolUnitTestRequestGenerator.kt | 4 ++-- .../codegen/rendering/serde/SerializeStructGenerator.kt | 1 - .../codegen/rendering/serde/SerializeStructGeneratorTest.kt | 2 +- .../src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json diff --git a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json new file mode 100644 index 0000000000..0fad5e0349 --- /dev/null +++ b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json @@ -0,0 +1,5 @@ +{ + "id": "1021e75a-45f3-4f3a-820c-700d9ec6e782", + "type": "bugfix", + "description": "Fix serialization of CBOR blobs" +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt index 74bd7db782..6fcc47c3e5 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt @@ -117,11 +117,11 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B write("return") } write("requireNotNull(expectedBytes) { #S }", "expected application/cbor body cannot be null") - write("requireNotNull(expectedBytes) { #S }", "actual application/cbor body cannot be null") + write("requireNotNull(actualBytes) { #S }", "actual application/cbor body cannot be null") write("") write("val expectedRequest = #L(#T(expectedBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) - write("val actualRequest = #L(#T(expectedBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) + write("val actualRequest = #L(#T(actualBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) write("assertEquals(expectedRequest, actualRequest)") } writer.write("") diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt index e7f2e261ef..d8810044a5 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt @@ -647,7 +647,6 @@ open class SerializeStructGenerator( val target = member.targetOrSelf(ctx.model) val encoded = when { - target.type == ShapeType.BLOB -> writer.format("#L.#T()", identifier, RuntimeTypes.Core.Text.Encoding.encodeBase64String) target.type == ShapeType.TIMESTAMP -> { writer.addImport(RuntimeTypes.Core.TimestampFormat) val tsFormat = member diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt index 40d9c9db7d..347da007cb 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt @@ -1822,7 +1822,7 @@ class SerializeStructGeneratorTest { val expected = """ serializer.serializeStruct(OBJ_DESCRIPTOR) { - input.fooBlob?.let { field(FOOBLOB_DESCRIPTOR, it.encodeBase64String()) } + input.fooBlob?.let { field(FOOBLOB_DESCRIPTOR, it) } } """.trimIndent() diff --git a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt index 35618cb49e..d1356f848c 100644 --- a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt +++ b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt @@ -134,7 +134,7 @@ public class XmlSerializer(private val xmlWriter: XmlStreamWriter = xmlStreamWri field(descriptor, value.format(format)) override fun field(descriptor: SdkFieldDescriptor, value: ByteArray): Unit = - field(descriptor, value) + field(descriptor, value.encodeBase64String()) override fun field(descriptor: SdkFieldDescriptor, value: Document?): Unit = throw SerializationException( "cannot serialize field ${descriptor.serialName}; Document type is not supported by xml encoding", From 48849a1208fe0bb2c42c06c49b54b9c419104378 Mon Sep 17 00:00:00 2001 From: Matas Date: Mon, 6 Jan 2025 16:34:11 -0500 Subject: [PATCH 08/54] fix: correctly serialize subset of shape's members when configured (#1199) --- .../core/QueryHttpBindingProtocolGenerator.kt | 2 +- .../codegen/aws/protocols/RpcV2CborTest.kt | 17 +++++++++++++++++ .../rendering/serde/CborSerializerGenerator.kt | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt index 62f8f7ebe1..db97349f84 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt @@ -145,7 +145,7 @@ abstract class AbstractQueryFormUrlSerializerGenerator( return shape.documentSerializer(ctx.settings, symbol, members) { writer -> writer.openBlock("internal fun #identifier.name:L(serializer: #T, input: #T) {", RuntimeTypes.Serde.Serializer, symbol) .call { - renderSerializerBody(ctx, shape, shape.members().toList(), writer) + renderSerializerBody(ctx, shape, members.toList(), writer) } .closeBlock("}") } diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt index 17a7c0f3d1..0114185004 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt @@ -4,6 +4,7 @@ */ package software.amazon.smithy.kotlin.codegen.aws.protocols +import io.kotest.matchers.string.shouldNotContain import software.amazon.smithy.kotlin.codegen.test.* import kotlin.test.Test @@ -145,4 +146,20 @@ class RpcV2CborTest { val serializeBody = serializer.lines(" override suspend fun serialize(context: ExecutionContext, input: PutFooStreamingRequest): HttpRequestBuilder {", "}") serializeBody.shouldContainOnlyOnceWithDiff("""builder.headers.setMissing("Content-Type", "application/vnd.amazon.eventstream")""") } + + @Test + fun testEventStreamInitialRequestDoesNotSerializeStreamMember() { + val ctx = model.newTestContext("CborExample") + + val generator = RpcV2Cbor() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val documentSerializer = ctx.manifest.expectFileString("/src/main/kotlin/com/test/serde/PutFooStreamingRequestDocumentSerializer.kt") + + val serializeBody = documentSerializer.lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", "}") + serializeBody.shouldNotContain("input.messages") // `messages` is the stream member and should not be serialized in the initial request + } } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt index 1fdb7850a7..545a484a90 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt @@ -108,7 +108,7 @@ class CborSerializerGenerator( val symbol = ctx.symbolProvider.toSymbol(shape) return shape.documentSerializer(ctx.settings, symbol, members) { writer -> writer.withBlock("internal fun #identifier.name:L(serializer: #T, input: #T) {", "}", RuntimeTypes.Serde.Serializer, symbol) { - call { renderSerializerBody(ctx, shape, shape.members().toList(), writer) } + call { renderSerializerBody(ctx, shape, members.toList(), writer) } } } } From ded3a4bb03060f28f3ce085fed3a9627f7297ae5 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 6 Jan 2025 21:45:24 +0000 Subject: [PATCH 09/54] chore: release 1.3.32 --- .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json diff --git a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json deleted file mode 100644 index 0fad5e0349..0000000000 --- a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "1021e75a-45f3-4f3a-820c-700d9ec6e782", - "type": "bugfix", - "description": "Fix serialization of CBOR blobs" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5473a56d69..a52fe756c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.3.32] - 01/06/2025 + +### Fixes +* Fix serialization of CBOR blobs + ## [1.3.31] - 12/18/2024 ### Features diff --git a/gradle.properties b/gradle.properties index f902a36719..eda623b285 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.32-SNAPSHOT +sdkVersion=1.3.32 # codegen -codegenVersion=0.33.32-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.32 \ No newline at end of file From d97e8ba056d5f0c9869226854638dd486e06f1fa Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 6 Jan 2025 21:45:25 +0000 Subject: [PATCH 10/54] chore: bump snapshot version to 1.3.33-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index eda623b285..b49ffff27f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.32 +sdkVersion=1.3.33-SNAPSHOT # codegen -codegenVersion=0.33.32 \ No newline at end of file +codegenVersion=0.33.33-SNAPSHOT \ No newline at end of file From 78f17f3dcb809e297669c9733e2f440da33985f2 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:23:33 -0800 Subject: [PATCH 11/54] chore: add *-main to branch workflows (#1209) --- .github/workflows/api-compat-verification.yml | 4 +++- .github/workflows/artifact-size-metrics.yml | 4 +++- .github/workflows/changelog-verification.yml | 4 +++- .github/workflows/continuous-integration.yml | 4 +++- .github/workflows/dependabot.yml | 4 +++- .github/workflows/kat-transform.yml | 4 +++- .github/workflows/lint.yml | 5 ++++- 7 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/api-compat-verification.yml b/.github/workflows/api-compat-verification.yml index 26c7ef3901..2d4a22452a 100644 --- a/.github/workflows/api-compat-verification.yml +++ b/.github/workflows/api-compat-verification.yml @@ -3,7 +3,9 @@ name: API compatibility verification on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' jobs: api-compat-verification: diff --git a/.github/workflows/artifact-size-metrics.yml b/.github/workflows/artifact-size-metrics.yml index bd1c521e16..0591b2b6aa 100644 --- a/.github/workflows/artifact-size-metrics.yml +++ b/.github/workflows/artifact-size-metrics.yml @@ -2,7 +2,9 @@ name: Artifact Size Metrics on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' release: types: [published] diff --git a/.github/workflows/changelog-verification.yml b/.github/workflows/changelog-verification.yml index 547b155abf..ed6efef911 100644 --- a/.github/workflows/changelog-verification.yml +++ b/.github/workflows/changelog-verification.yml @@ -3,7 +3,9 @@ name: Changelog verification on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' jobs: changelog-verification: diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9b0dc1c880..599293cc06 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -2,7 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: + - main + - '*-main' pull_request: workflow_dispatch: diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 69fb0c9885..6588de3857 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -2,7 +2,9 @@ name: Dependabot Dependency Submission on: push: - branches: [ main ] + branches: + - main + - '*-main' permissions: contents: write diff --git a/.github/workflows/kat-transform.yml b/.github/workflows/kat-transform.yml index 859a904bc3..938e936ef9 100644 --- a/.github/workflows/kat-transform.yml +++ b/.github/workflows/kat-transform.yml @@ -3,7 +3,9 @@ name: Kat Transform on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' # Allow one instance of this workflow per pull request, and cancel older runs when new changes are pushed concurrency: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6693ed2aae..e95b98aded 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,8 +5,11 @@ on: branches: - '**' - '!main' + - '!*-main' pull_request: - branches: [ main ] + branches: + - main + - '*-main' workflow_dispatch: env: From d06ee0a604dd0750421c6d7dfe7c47dfbcd062bf Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:23:43 -0800 Subject: [PATCH 12/54] chore: add .kotlin/ to .gitignore (#1208) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 225fe6070b..99ff20f122 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ out/ # Compiled class file *.class *.klib +.kotlin/ # Log file *.log From d2e7c918e7fc9ee9892fd6cd7d852db4d5b2f2db Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:16:58 -0500 Subject: [PATCH 13/54] chore: smithy version bump (#1213) --- .../model/error-correction-tests.smithy | 13 ++++++++++--- .../core/AwsHttpBindingProtocolGenerator.kt | 10 +++++++++- gradle/libs.versions.toml | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/codegen/protocol-tests/model/error-correction-tests.smithy b/codegen/protocol-tests/model/error-correction-tests.smithy index 74e614bdc5..201ffc877f 100644 --- a/codegen/protocol-tests/model/error-correction-tests.smithy +++ b/codegen/protocol-tests/model/error-correction-tests.smithy @@ -37,7 +37,12 @@ operation SayHello { output: TestOutputDocument, errors: [Error] } @http(method: "POST", uri: "/") operation SayHelloXml { output: TestOutput, errors: [Error] } -structure TestOutputDocument with [TestStruct] { innerField: Nested, @required document: Document } +structure TestOutputDocument with [TestStruct] { + innerField: Nested, + // FIXME: This trait fails smithy validator + // @required + document: Document +} structure TestOutput with [TestStruct] { innerField: Nested } @mixin @@ -60,7 +65,8 @@ structure TestStruct { @required nestedListValue: NestedList - @required + // FIXME: This trait fails smithy validator + // @required nested: Nested @required @@ -91,7 +97,8 @@ union MyUnion { } structure Nested { - @required + // FIXME: This trait fails smithy validator + // @required a: String } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt index 864c2a6c3c..6e5834f7d1 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt @@ -39,7 +39,15 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator() // The following can be used to generate only a specific test by name. // val targetedTest = TestMemberDelta(setOf("RestJsonComplexErrorWithNoMessage"), TestContainmentMode.RUN_TESTS) - val ignoredTests = TestMemberDelta(setOf()) + val ignoredTests = TestMemberDelta( + setOf( + "AwsJson10ClientErrorCorrectsWithDefaultValuesWhenServerFailsToSerializeRequiredValues", + "RestJsonNullAndEmptyHeaders", + "NullAndEmptyHeaders", + "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", + "RpcV2CborClientPopulatesDefaultValuesInInput", + ), + ) val requestTestBuilder = HttpProtocolUnitTestRequestGenerator.Builder() val responseTestBuilder = HttpProtocolUnitTestResponseGenerator.Builder() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0683bfc2e1..14232f4241 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ crt-kotlin-version = "0.8.10" micrometer-version = "1.13.6" # codegen -smithy-version = "1.51.0" +smithy-version = "1.53.0" smithy-gradle-version = "0.9.0" # testing From 5a2df8f171250e150c685c9389f80e2a6b0778eb Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 14:36:46 +0000 Subject: [PATCH 14/54] chore: release 1.3.33 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52fe756c1..2c327d4f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.33] - 01/10/2025 + ## [1.3.32] - 01/06/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index b49ffff27f..b6869a974b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.33-SNAPSHOT +sdkVersion=1.3.33 # codegen -codegenVersion=0.33.33-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.33 \ No newline at end of file From 003633bad25b5b3e75112da2d1de5f2eeffd1471 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 14:36:47 +0000 Subject: [PATCH 15/54] chore: bump snapshot version to 1.3.34-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index b6869a974b..6c237cf8df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.33 +sdkVersion=1.3.34-SNAPSHOT # codegen -codegenVersion=0.33.33 \ No newline at end of file +codegenVersion=0.33.34-SNAPSHOT \ No newline at end of file From 5f5ec8f16b4db571bb9c83f520e26f9927d8ecf2 Mon Sep 17 00:00:00 2001 From: Matas Date: Fri, 10 Jan 2025 11:13:29 -0500 Subject: [PATCH 16/54] feat: add `AuthTokenGenerator` (#1212) --- .../api/aws-signing-common.api | 10 +++ .../auth/awssigning/AuthTokenGenerator.kt | 44 ++++++++++++ .../auth/awssigning/AuthTokenGeneratorTest.kt | 71 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt create mode 100644 runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt diff --git a/runtime/auth/aws-signing-common/api/aws-signing-common.api b/runtime/auth/aws-signing-common/api/aws-signing-common.api index 0c96486679..d128b0bd6f 100644 --- a/runtime/auth/aws-signing-common/api/aws-signing-common.api +++ b/runtime/auth/aws-signing-common/api/aws-signing-common.api @@ -1,3 +1,13 @@ +public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator { + public fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;)V + public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun generateAuthToken-exY8QGI (Laws/smithy/kotlin/runtime/net/url/Url;Ljava/lang/String;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getClock ()Laws/smithy/kotlin/runtime/time/Clock; + public final fun getCredentialsProvider ()Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider; + public final fun getService ()Ljava/lang/String; + public final fun getSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; +} + public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsChunkedByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel { public fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;)V public synthetic fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt new file mode 100644 index 0000000000..e92ac1f72c --- /dev/null +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -0,0 +1,44 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Clock +import kotlin.time.Duration + +/** + * Generates an authentication token, which is a SigV4-signed URL with the HTTP scheme removed. + * @param service The name of the service the token is being generated for + * @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token + * @param signer The [AwsSigner] implementation to use when creating the authentication token + * @param clock The [Clock] implementation to use + */ +public class AuthTokenGenerator( + public val service: String, + public val credentialsProvider: CredentialsProvider, + public val signer: AwsSigner, + public val clock: Clock = Clock.System, +) { + private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") + + public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { + val req = HttpRequest(HttpMethod.GET, endpoint) + + val config = AwsSigningConfig { + credentials = credentialsProvider.resolve() + this.region = region + service = this@AuthTokenGenerator.service + signingDate = clock.now() + expiresAfter = expiration + signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS + } + + return signer.sign(req, config).output.url.trimScheme() + } +} diff --git a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt new file mode 100644 index 0000000000..87fec1a74e --- /dev/null +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -0,0 +1,71 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.http.Headers +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.net.Host +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.time.ManualClock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit + +class AuthTokenGeneratorTest { + @Test + fun testGenerateAuthToken() = runTest { + val credentials = Credentials("akid", "secret") + + val credentialsProvider = object : CredentialsProvider { + var credentialsResolved = false + override suspend fun resolve(attributes: Attributes): Credentials { + credentialsResolved = true + return credentials + } + } + + val clock = ManualClock(Instant.fromEpochSeconds(0)) + + val generator = AuthTokenGenerator("foo", credentialsProvider, TEST_SIGNER, clock = clock) + + val endpoint = Url { host = Host.parse("foo.bar.us-east-1.baz") } + val token = generator.generateAuthToken(endpoint, "us-east-1", 333.seconds) + + assertContains(token, "foo.bar.us-east-1.baz") + assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked + assertContains(token, "X-Amz-Expires=333") // expiration + assertContains(token, "X-Amz-SigningDate=0") // clock + + assertTrue(credentialsProvider.credentialsResolved) + } +} + +private val TEST_SIGNER = object : AwsSigner { + override suspend fun sign( + request: HttpRequest, + config: AwsSigningConfig, + ): AwsSigningResult { + val builder = request.toBuilder() + builder.url.parameters.decodedParameters.apply { + put("X-Amz-Credential", "signature") + put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString()) + put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString()) + } + + return AwsSigningResult(builder.build(), "signature".encodeToByteArray()) + } + + override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult = throw IllegalStateException("signChunk unexpectedly invoked") + + override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult = throw IllegalStateException("signChunkTrailer unexpectedly invoked") +} From 3fe1b5e7763bd4ed386f30d3f4bfcbb0ad4aa352 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 16:45:39 +0000 Subject: [PATCH 17/54] chore: release 1.3.34 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c327d4f40..c1758b4332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.34] - 01/10/2025 + ## [1.3.33] - 01/10/2025 ## [1.3.32] - 01/06/2025 diff --git a/gradle.properties b/gradle.properties index 6c237cf8df..193406ed69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.34-SNAPSHOT +sdkVersion=1.3.34 # codegen -codegenVersion=0.33.34-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.34 \ No newline at end of file From 0bba3084ebb3160abe2a21fc86f4223c7bf37c23 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 16:45:40 +0000 Subject: [PATCH 18/54] chore: bump snapshot version to 1.3.35-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 193406ed69..d76819491b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.34 +sdkVersion=1.3.35-SNAPSHOT # codegen -codegenVersion=0.33.34 \ No newline at end of file +codegenVersion=0.33.35-SNAPSHOT \ No newline at end of file From 52e4439088b388c5f4d8c9b1e6fec6588d37a1a4 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:33:57 -0500 Subject: [PATCH 19/54] misc: merge v1.4 into main (#1218) --- .brazil.json | 2 +- .../0857b6f0-0444-479f-be6b-06ef71d482a0.json | 9 ++++ .../1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json | 6 +++ .../3456d00f-1b29-4d88-ab02-045db1b1ebce.json | 8 +++ .../kotlin/codegen/core/KotlinDependency.kt | 2 +- .../rendering/waiters/WaiterGenerator.kt | 15 +++--- .../waiters/ServiceWaitersGeneratorTest.kt | 6 +-- .../rendering/waiters/WaiterGeneratorTest.kt | 4 +- gradle/libs.versions.toml | 17 ++++--- .../tests/SigningSuiteTestBaseJVM.kt | 3 +- runtime/runtime-core/api/runtime-core.api | 15 +++++- .../aws/smithy/kotlin/runtime/Exceptions.kt | 49 +++++++++++++++++- .../kotlin/runtime/collections/Attributes.kt | 11 ++++ .../kotlin/runtime/retries/Exceptions.kt | 23 ++++++++- .../runtime/retries/StandardRetryStrategy.kt | 30 +++++++++-- .../retries/delay/StandardRetryTokenBucket.kt | 2 +- .../smithy/kotlin/runtime/ExceptionsTest.kt | 34 +++++++++++++ .../impl/StandardRetryIntegrationTest.kt | 50 +++++++++++++++---- .../smithy/kotlin/codegen/util/TestUtils.kt | 7 +-- 19 files changed, 250 insertions(+), 43 deletions(-) create mode 100644 .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json create mode 100644 .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json create mode 100644 .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json diff --git a/.brazil.json b/.brazil.json index cb2e43713a..5a1d830abd 100644 --- a/.brazil.json +++ b/.brazil.json @@ -1,6 +1,6 @@ { "dependencies": { - "org.jetbrains.kotlin:kotlin-stdlib:2.0.*": "KotlinStdlib-2.x", + "org.jetbrains.kotlin:kotlin-stdlib:2.*": "KotlinStdlib-2.x", "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.*": "KotlinxCoroutinesCoreJvm-1.x", "com.squareup.okhttp3:okhttp-coroutines:5.*": "OkHttp3Coroutines-5.x", diff --git a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json new file mode 100644 index 0000000000..d89da89103 --- /dev/null +++ b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json @@ -0,0 +1,9 @@ +{ + "id": "0857b6f0-0444-479f-be6b-06ef71d482a0", + "type": "feature", + "description": "⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters", + "issues": [ + "https://github.com/awslabs/aws-sdk-kotlin/issues/1431" + ], + "requiresMinorVersionBump": true +} \ No newline at end of file diff --git a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json new file mode 100644 index 0000000000..10ef502204 --- /dev/null +++ b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json @@ -0,0 +1,6 @@ +{ + "id": "1a68d0b7-00e7-45c0-88f6-95e5c39c9c61", + "type": "misc", + "description": "⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0", + "requiresMinorVersionBump": true +} \ No newline at end of file diff --git a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json new file mode 100644 index 0000000000..767e48a02e --- /dev/null +++ b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json @@ -0,0 +1,8 @@ +{ + "id": "3456d00f-1b29-4d88-ab02-045db1b1ebce", + "type": "bugfix", + "description": "Include more information when retry strategy halts early due to token bucket capacity errors", + "issues": [ + "awslabs/aws-sdk-kotlin#1321" + ] +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt index 17faf0ad48..b575efae60 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt @@ -37,7 +37,7 @@ private fun getDefaultRuntimeVersion(): String { // publishing info const val RUNTIME_GROUP: String = "aws.smithy.kotlin" val RUNTIME_VERSION: String = System.getProperty("smithy.kotlin.codegen.clientRuntimeVersion", getDefaultRuntimeVersion()) -val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.0.10") +val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.1.0") enum class SourceSet { CommonMain, diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt index 552a352a1a..f150cbc3d2 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt @@ -17,7 +17,7 @@ import java.text.DecimalFormatSymbols * Renders the top-level retry strategy for a waiter. */ private fun KotlinWriter.renderRetryStrategy(wi: WaiterInfo, asValName: String) { - withBlock("val #L = #T {", "}", asValName, RuntimeTypes.Core.Retries.StandardRetryStrategy) { + withBlock("val #L = retryStrategy ?: #T {", "}", asValName, RuntimeTypes.Core.Retries.StandardRetryStrategy) { write("maxAttempts = 20") write("tokenBucket = #T", RuntimeTypes.Core.Retries.Delay.InfiniteTokenBucket) withBlock("delayProvider {", "}") { @@ -35,18 +35,21 @@ private fun KotlinWriter.renderRetryStrategy(wi: WaiterInfo, asValName: String) internal fun KotlinWriter.renderWaiter(wi: WaiterInfo) { write("") wi.waiter.documentation.ifPresent(::dokka) - val inputParameter = if (wi.input.hasAllOptionalMembers) { - format("request: #1T = #1T { }", wi.inputSymbol) + + val requestType = if (wi.input.hasAllOptionalMembers) { + format("#1T = #1T { }", wi.inputSymbol) } else { - format("request: #T", wi.inputSymbol) + format("#T", wi.inputSymbol) } + withBlock( - "#L suspend fun #T.#L(#L): #T<#T> {", + "#L suspend fun #T.#L(request: #L, retryStrategy: #T? = null): #T<#T> {", "}", wi.ctx.settings.api.visibility, wi.serviceSymbol, wi.methodName, - inputParameter, + requestType, + RuntimeTypes.Core.Retries.RetryStrategy, RuntimeTypes.Core.Retries.Outcome, wi.outputSymbol, ) { diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt index c83c29661d..49ee90f484 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt @@ -43,7 +43,7 @@ class ServiceWaitersGeneratorTest { /** * Wait until a foo exists with optional input */ - public suspend fun TestClient.waitUntilFooOptionalExists(request: DescribeFooOptionalRequest = DescribeFooOptionalRequest { }): Outcome { + public suspend fun TestClient.waitUntilFooOptionalExists(request: DescribeFooOptionalRequest = DescribeFooOptionalRequest { }, retryStrategy: RetryStrategy? = null): Outcome { """.trimIndent() val methodFooter = """ val policy = AcceptorRetryPolicy(request, acceptors) @@ -59,7 +59,7 @@ class ServiceWaitersGeneratorTest { /** * Wait until a foo exists with required input */ - public suspend fun TestClient.waitUntilFooRequiredExists(request: DescribeFooRequiredRequest): Outcome { + public suspend fun TestClient.waitUntilFooRequiredExists(request: DescribeFooRequiredRequest, retryStrategy: RetryStrategy? = null): Outcome { """.trimIndent() listOf( generateService("simple-service-with-operation-waiter.smithy"), @@ -105,7 +105,7 @@ class ServiceWaitersGeneratorTest { @Test fun testRetryStrategy() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt index c8f6ae3cfc..705b3b8b16 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt @@ -28,7 +28,7 @@ class WaiterGeneratorTest { @Test fun testDefaultDelays() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { @@ -45,7 +45,7 @@ class WaiterGeneratorTest { @Test fun testCustomDelays() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 14232f4241..6ef5b35d57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -kotlin-version = "2.0.21" +kotlin-version = "2.1.0" dokka-version = "1.9.10" -aws-kotlin-repo-tools-version = "0.4.16" +aws-kotlin-repo-tools-version = "0.4.17" # libs coroutines-version = "1.9.0" @@ -10,11 +10,12 @@ atomicfu-version = "0.25.0" okhttp-version = "5.0.0-alpha.14" okhttp4-version = "4.12.0" okio-version = "3.9.1" -otel-version = "1.43.0" +otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" -crt-kotlin-version = "0.8.10" -micrometer-version = "1.13.6" +crt-kotlin-version = "0.9.0" +micrometer-version = "1.14.2" +binary-compatibility-validator-version = "0.16.3" # codegen smithy-version = "1.53.0" @@ -23,7 +24,7 @@ smithy-gradle-version = "0.9.0" # testing junit-version = "5.10.5" kotest-version = "5.9.1" -kotlin-compile-testing-version = "1.6.0" +kotlin-compile-testing-version = "0.7.0" kotlinx-benchmark-version = "0.4.12" kotlinx-serialization-version = "1.7.3" docker-java-version = "3.4.0" @@ -80,7 +81,7 @@ smithy-smoke-test-traits = { module = "software.amazon.smithy:smithy-smoke-test- junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-version" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-version" } -kotlin-compile-testing = {module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kotlin-compile-testing-version" } +kotlin-compile-testing = {module = "dev.zacsweers.kctfork:core", version.ref = "kotlin-compile-testing-version" } kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest-version" } kotest-assertions-core-jvm = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest-version" } kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark-version" } @@ -104,7 +105,7 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka-version"} kotlin-jvm = {id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" } kotlin-multiplatform = {id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-version" } kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinx-benchmark-version" } -kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.13.2" } +kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator-version" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-version"} aws-kotlin-repo-tools-kmp = { id = "aws.sdk.kotlin.gradle.kmp", version.ref = "aws-kotlin-repo-tools-version" } aws-kotlin-repo-tools-smithybuild = { id = "aws.sdk.kotlin.gradle.smithybuild", version.ref = "aws-kotlin-repo-tools-version" } diff --git a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt b/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt index 904aef564b..b3c4202d26 100644 --- a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt +++ b/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt @@ -28,6 +28,7 @@ import io.ktor.util.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* import kotlinx.coroutines.runBlocking +import kotlinx.io.readByteArray import kotlinx.serialization.json.* import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Test @@ -383,7 +384,7 @@ public actual abstract class SigningSuiteTestBase : HasSigner { } if (hasBody) { - val bytes = runBlocking { chan.readRemaining().readBytes() } + val bytes = runBlocking { chan.readRemaining().readByteArray() } builder.body = HttpBody.fromBytes(bytes) } diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 942f99e348..8c42034110 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -1,3 +1,13 @@ +public final class aws/smithy/kotlin/runtime/ClientErrorContext { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getFormatted ()Ljava/lang/String; + public final fun getKey ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public class aws/smithy/kotlin/runtime/ClientException : aws/smithy/kotlin/runtime/SdkBaseException { public fun ()V public fun (Ljava/lang/String;)V @@ -9,11 +19,13 @@ public class aws/smithy/kotlin/runtime/ErrorMetadata { public static final field Companion Laws/smithy/kotlin/runtime/ErrorMetadata$Companion; public fun ()V public final fun getAttributes ()Laws/smithy/kotlin/runtime/collections/MutableAttributes; + public final fun getClientContext ()Ljava/util/List; public final fun isRetryable ()Z public final fun isThrottling ()Z } public final class aws/smithy/kotlin/runtime/ErrorMetadata$Companion { + public final fun getClientContext ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getRetryable ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getThrottlingError ()Laws/smithy/kotlin/runtime/collections/AttributeKey; } @@ -62,7 +74,6 @@ public class aws/smithy/kotlin/runtime/ServiceException : aws/smithy/kotlin/runt public fun (Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/lang/Throwable;)V public fun (Ljava/lang/Throwable;)V - protected fun getDisplayMetadata ()Ljava/util/List; public fun getMessage ()Ljava/lang/String; public synthetic fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ErrorMetadata; public fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ServiceErrorMetadata; @@ -134,6 +145,7 @@ public final class aws/smithy/kotlin/runtime/collections/AttributesBuilder { } public final class aws/smithy/kotlin/runtime/collections/AttributesKt { + public static final fun appendValue (Laws/smithy/kotlin/runtime/collections/MutableAttributes;Laws/smithy/kotlin/runtime/collections/AttributeKey;Ljava/lang/Object;)V public static final fun attributesOf (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/collections/Attributes; public static final fun emptyAttributes ()Laws/smithy/kotlin/runtime/collections/Attributes; public static final fun get (Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/collections/AttributeKey;)Ljava/lang/Object; @@ -1685,6 +1697,7 @@ public abstract class aws/smithy/kotlin/runtime/retries/RetryException : aws/smi public final fun getAttempts ()I public final fun getLastException ()Ljava/lang/Throwable; public final fun getLastResponse ()Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/smithy/kotlin/runtime/retries/RetryFailureException : aws/smithy/kotlin/runtime/retries/RetryException { diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt index 87697ec7d7..661d855a5b 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt @@ -8,6 +8,41 @@ import aws.smithy.kotlin.runtime.collections.AttributeKey import aws.smithy.kotlin.runtime.collections.MutableAttributes import aws.smithy.kotlin.runtime.collections.mutableAttributes +/** + * Describes additional context about an error which may be useful in client-side debugging. This information will be + * included in exception messages. This contrasts with [ErrorMetadata] which is not _necessarily_ included in messages + * and not _necessarily_ client-related. + * @param key A header or key for the information + * @param value A value for the information + */ +public class ClientErrorContext(public val key: String, public val value: String) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ClientErrorContext + + if (key != other.key) return false + if (value != other.value) return false + + return true + } + + /** + * Gets a formatted representation of this error context suitable for inclusion in a message. This format is + * generally `"$key: $value"`. + */ + public val formatted: String = "$key: $value" + + override fun hashCode(): Int { + var result = key.hashCode() + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String = "ClientErrorContext(key='$key', value='$value')" +} + /** * Additional metadata about an error */ @@ -16,6 +51,12 @@ public open class ErrorMetadata { public val attributes: MutableAttributes = mutableAttributes() public companion object { + /** + * Set if there are additional context elements about the error + */ + public val ClientContext: AttributeKey> = + AttributeKey("aws.smithy.kotlin#ClientContext") + /** * Set if an error is retryable */ @@ -32,6 +73,9 @@ public open class ErrorMetadata { public val isThrottling: Boolean get() = attributes.getOrNull(ThrottlingError) ?: false + + public val clientContext: List + get() = attributes.getOrNull(ClientContext).orEmpty() } /** @@ -156,7 +200,7 @@ public open class ServiceException : SdkBaseException { public constructor(cause: Throwable?) : super(cause) - protected open val displayMetadata: List + private val displayMetadata: List get() = buildList { val serviceProvidedMessage = super.message ?: sdkErrorMetadata.errorMessage if (serviceProvidedMessage == null) { @@ -166,7 +210,10 @@ public open class ServiceException : SdkBaseException { } else { add(serviceProvidedMessage) } + sdkErrorMetadata.requestId?.let { add("Request ID: $it") } + + sdkErrorMetadata.clientContext.mapTo(this@buildList) { it.formatted } } override val message: String diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt index 1fffcc82a2..77c2507394 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt @@ -115,6 +115,17 @@ public fun MutableAttributes.merge(other: Attributes) { } } +/** + * Appends a value to a list-typed attribute. If the attribute does not exist, it will be created. + * @param key The key for the attribute + * @param element The element to append to the existing (or new) list value of the attribute + */ +public fun MutableAttributes.appendValue(key: AttributeKey>, element: E) { + val existingList = getOrNull(key).orEmpty() + val newList = existingList + element + set(key, newList) +} + private class AttributesImpl constructor(seed: Attributes) : MutableAttributes { private val map: MutableMap, Any> = mutableMapOf() constructor() : this(emptyAttributes()) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt index dbc2de0ffa..a81c830da2 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt @@ -22,7 +22,28 @@ public sealed class RetryException( public val attempts: Int, public val lastResponse: Any?, public val lastException: Throwable?, -) : ClientException(message, cause) +) : ClientException(message, cause) { + override fun toString(): String = buildString { + append(this@RetryException::class.simpleName) + append("(") + + append("message=") + append(message) + + append(",attempts=") + append(attempts) + + if (lastException != null) { + append(",lastException=") + append(lastException) + } else if (lastResponse != null) { + append(",lastResponse=") + append(lastResponse) + } + + append(")") + } +} /** * Indicates that retrying has failed because too many attempts have completed unsuccessfully. diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt index 95fd16faee..fb223aec68 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt @@ -5,6 +5,10 @@ package aws.smithy.kotlin.runtime.retries +import aws.smithy.kotlin.runtime.ClientErrorContext +import aws.smithy.kotlin.runtime.ErrorMetadata +import aws.smithy.kotlin.runtime.ServiceException +import aws.smithy.kotlin.runtime.collections.appendValue import aws.smithy.kotlin.runtime.retries.delay.* import aws.smithy.kotlin.runtime.retries.policy.RetryDirective import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy @@ -129,17 +133,35 @@ public open class StandardRetryStrategy(override val config: Config = Config.def } } - private fun throwCapacityExceeded(cause: Throwable, attempt: Int, result: Result?): Nothing = - when (val ex = result?.exceptionOrNull()) { + private fun throwCapacityExceeded( + cause: RetryCapacityExceededException, + attempt: Int, + result: Result?, + ): Nothing { + val capacityMessage = buildString { + append("Insufficient client capacity to attempt retry, halting on attempt ") + append(attempt) + append(" of ") + append(config.maxAttempts) + } + + throw when (val retryableException = result?.exceptionOrNull()) { null -> throw TooManyAttemptsException( - cause.message!!, + capacityMessage, cause, attempt, result?.getOrNull(), result?.exceptionOrNull(), ) - else -> throw ex + + is ServiceException -> retryableException.apply { + val addCtx = ClientErrorContext("Early retry termination", capacityMessage) + sdkErrorMetadata.attributes.appendValue(ErrorMetadata.ClientContext, addCtx) + } + + else -> retryableException } + } /** * Handles the termination of the retry loop because of a non-retryable failure by throwing a diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt index 4bd2c23766..b2c4c70588 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt @@ -60,7 +60,7 @@ public class StandardRetryTokenBucket internal constructor( capacity -= size } else { if (config.useCircuitBreakerMode) { - throw RetryCapacityExceededException("Insufficient capacity to attempt another retry") + throw RetryCapacityExceededException("Insufficient capacity to attempt retry") } val extraRequiredCapacity = size - capacity diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt index 551718c36e..0ba2d1639f 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt @@ -5,9 +5,14 @@ package aws.smithy.kotlin.runtime import aws.smithy.kotlin.runtime.collections.MutableAttributes +import aws.smithy.kotlin.runtime.collections.appendValue import kotlin.test.Test import kotlin.test.assertEquals +private const val CTX_KEY_1 = "Color" +private const val CTX_VALUE_1 = "blue" +private const val CTX_KEY_2 = "Shape" +private const val CTX_VALUE_2 = "square" private const val ERROR_CODE = "ErrorWithNoMessage" private const val METADATA_MESSAGE = "This is a message included in metadata but not the regular response" private const val PROTOCOL_RESPONSE_SUMMARY = "HTTP 418 I'm a teapot" @@ -104,6 +109,35 @@ class ExceptionsTest { e.message, ) } + + @Test + fun testNoMessageWithClientContext() { + val e = FooServiceException { + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_1, CTX_VALUE_1)) + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_2, CTX_VALUE_2)) + } + assertEquals( + buildList { + add("Error type: Unknown") + add("Protocol response: (empty response)") + add("$CTX_KEY_1: $CTX_VALUE_1") + add("$CTX_KEY_2: $CTX_VALUE_2") + }.joinToString(), + e.message, + ) + } + + @Test + fun testMessageWithClientContext() { + val e = FooServiceException(SERVICE_MESSAGE) { + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_1, CTX_VALUE_1)) + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_2, CTX_VALUE_2)) + } + assertEquals( + "$SERVICE_MESSAGE, $CTX_KEY_1: $CTX_VALUE_1, $CTX_KEY_2: $CTX_VALUE_2", + e.message, + ) + } } private class FooServiceException( diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt index f2c08e47eb..2bd3bc7e6a 100644 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt @@ -5,8 +5,8 @@ package aws.smithy.kotlin.runtime.retries.impl +import aws.smithy.kotlin.runtime.ServiceException import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy -import aws.smithy.kotlin.runtime.retries.TooManyAttemptsException import aws.smithy.kotlin.runtime.retries.delay.StandardRetryTokenBucket import aws.smithy.kotlin.runtime.retries.getOrThrow import aws.smithy.kotlin.runtime.retries.policy.RetryDirective @@ -15,6 +15,7 @@ import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.assertThrows import kotlin.test.* import kotlin.time.Duration.Companion.milliseconds @@ -38,7 +39,7 @@ class StandardRetryIntegrationTest { val block = object { var index = 0 - suspend fun doIt() = tc.responses[index++].response.statusCode + suspend fun doIt() = tc.responses[index++].response.getOrThrow() }::doIt val startTimeMs = currentTime @@ -47,9 +48,29 @@ class StandardRetryIntegrationTest { val finalState = tc.responses.last().expected when (finalState.outcome) { - TestOutcome.Success -> assertEquals(200, result.getOrNull()?.getOrThrow(), "Unexpected outcome for $name") - TestOutcome.MaxAttemptsExceeded -> assertIs(result.exceptionOrNull()) - TestOutcome.RetryQuotaExceeded -> assertIs(result.exceptionOrNull()) + TestOutcome.Success -> + assertEquals(Ok, result.getOrThrow().getOrThrow(), "Unexpected outcome for $name") + + TestOutcome.MaxAttemptsExceeded -> { + val e = assertThrows("Expected exception for $name") { + result.getOrThrow() + } + + assertEquals(tc.responses.last().response.statusCode, e.code, "Unexpected error code for $name") + } + + TestOutcome.RetryQuotaExceeded -> { + val e = assertThrows("Expected exception for $name") { + result.getOrThrow() + } + + assertEquals(tc.responses.last().response.statusCode, e.code, "Unexpected error code for $name") + + assertTrue("Expected retry capacity message in exception for $name") { + "Insufficient client capacity to attempt retry" in e.message + } + } + else -> fail("Unexpected outcome for $name: ${finalState.outcome}") } @@ -72,10 +93,19 @@ class StandardRetryIntegrationTest { } } -object IntegrationTestPolicy : RetryPolicy { - override fun evaluate(result: Result): RetryDirective = when (val code = result.getOrNull()!!) { - 200 -> RetryDirective.TerminateAndSucceed - 500, 502 -> RetryDirective.RetryError(RetryErrorType.ServerSide) - else -> fail("Unexpected status code: $code") +object IntegrationTestPolicy : RetryPolicy { + override fun evaluate(result: Result): RetryDirective = when { + result.isSuccess -> RetryDirective.TerminateAndSucceed + result.isFailure -> RetryDirective.RetryError(RetryErrorType.ServerSide) + else -> fail("Unexpected result condition") } } + +data object Ok + +class HttpCodeException(val code: Int) : ServiceException() + +fun Response.getOrThrow() = when (statusCode) { + 200 -> Ok + else -> throw HttpCodeException(statusCode) +} diff --git a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt index 02bf209f3c..2e64c9d4fe 100644 --- a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt +++ b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt @@ -6,6 +6,7 @@ package software.amazon.smithy.kotlin.codegen.util +import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi @@ -36,8 +37,8 @@ private fun String.slashEscape(char: Char) = this.replace(char.toString(), """\$ * Captures the result of a model transformation test */ data class ModelChangeTestResult( - val originalModelCompilationResult: KotlinCompilation.Result, - val updatedModelCompilationResult: KotlinCompilation.Result, + val originalModelCompilationResult: JvmCompilationResult, + val updatedModelCompilationResult: JvmCompilationResult, val compileSuccess: Boolean, val compileOutput: String, ) @@ -88,7 +89,7 @@ fun compileSdkAndTest( testSource: String? = null, outputSink: OutputStream = System.out, emitSourcesToTmp: Boolean = false, -): KotlinCompilation.Result { +): JvmCompilationResult { val sdkFileManifest = generateSdk(model) if (emitSourcesToTmp) { From e0c25d654adde3124f50733e0bc66fde2ee8d058 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:35:50 -0500 Subject: [PATCH 20/54] feat: support default checksums (#1191) --- .../kotlin/codegen/core/RuntimeTypes.kt | 7 +- .../HttpChecksumRequiredIntegration.kt | 66 +++++ .../protocol/HttpProtocolClientGenerator.kt | 25 -- ...tlin.codegen.integration.KotlinIntegration | 1 + .../protocol/http-client/api/http-client.api | 20 +- ...eptor.kt => CachingChecksumInterceptor.kt} | 14 +- .../interceptors/ChecksumInterceptorUtils.kt | 44 +++ .../FlexibleChecksumsRequestInterceptor.kt | 277 ++++++++---------- .../FlexibleChecksumsResponseInterceptor.kt | 94 +++--- .../HttpChecksumRequiredInterceptor.kt | 98 +++++++ .../interceptors/Md5ChecksumInterceptor.kt | 54 ---- .../http/operation/HttpOperationContext.kt | 4 +- ...t.kt => CachingChecksumInterceptorTest.kt} | 12 +- ...FlexibleChecksumsRequestInterceptorTest.kt | 115 +++++--- ...lexibleChecksumsResponseInterceptorTest.kt | 84 +++++- ...=> HttpChecksumRequiredInterceptorTest.kt} | 46 ++- runtime/protocol/http/api/http.api | 18 ++ .../smithy/kotlin/runtime/http/HttpBody.kt | 52 ++++ runtime/runtime-core/api/runtime-core.api | 15 + .../businessmetrics/BusinessMetricsUtils.kt | 8 + .../kotlin/runtime/hashing/HashFunction.kt | 55 ++++ .../kotlin/runtime/io/SdkByteReadChannel.kt | 16 + runtime/smithy-client/api/smithy-client.api | 28 ++ .../client/config/HttpChecksumConfig.kt | 63 ++++ 24 files changed, 865 insertions(+), 351 deletions(-) create mode 100644 codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt rename runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/{AbstractChecksumInterceptor.kt => CachingChecksumInterceptor.kt} (67%) create mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt create mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt delete mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt rename runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/{AbstractChecksumInterceptorTest.kt => CachingChecksumInterceptorTest.kt} (88%) rename runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/{Md5ChecksumInterceptorTest.kt => HttpChecksumRequiredInterceptorTest.kt} (62%) create mode 100644 runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt index e42612b0b5..b6341e0443 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt @@ -81,7 +81,7 @@ object RuntimeTypes { object Interceptors : RuntimeTypePackage(KotlinDependency.HTTP, "interceptors") { val ContinueInterceptor = symbol("ContinueInterceptor") val HttpInterceptor = symbol("HttpInterceptor") - val Md5ChecksumInterceptor = symbol("Md5ChecksumInterceptor") + val HttpChecksumRequiredInterceptor = symbol("HttpChecksumRequiredInterceptor") val FlexibleChecksumsRequestInterceptor = symbol("FlexibleChecksumsRequestInterceptor") val FlexibleChecksumsResponseInterceptor = symbol("FlexibleChecksumsResponseInterceptor") val ResponseLengthValidationInterceptor = symbol("ResponseLengthValidationInterceptor") @@ -231,6 +231,9 @@ object RuntimeTypes { object Config : RuntimeTypePackage(KotlinDependency.SMITHY_CLIENT, "config") { val RequestCompressionConfig = symbol("RequestCompressionConfig") val CompressionClientConfig = symbol("CompressionClientConfig") + val HttpChecksumConfig = symbol("HttpChecksumConfig") + val RequestHttpChecksumConfig = symbol("RequestHttpChecksumConfig") + val ResponseHttpChecksumConfig = symbol("ResponseHttpChecksumConfig") } object Endpoints : RuntimeTypePackage(KotlinDependency.SMITHY_CLIENT, "endpoints") { @@ -395,6 +398,7 @@ object RuntimeTypes { val TelemetryContextElement = symbol("TelemetryContextElement", "context") val TraceSpan = symbol("TraceSpan", "trace") val withSpan = symbol("withSpan", "trace") + val warn = symbol("warn", "logging") } object TelemetryDefaults : RuntimeTypePackage(KotlinDependency.TELEMETRY_DEFAULTS) { val Global = symbol("Global") @@ -409,6 +413,7 @@ object RuntimeTypes { val CompletableDeferred = "kotlinx.coroutines.CompletableDeferred".toSymbol() val job = "kotlinx.coroutines.job".toSymbol() + val runBlocking = "kotlinx.coroutines.runBlocking".toSymbol() object Flow { // NOTE: smithy-kotlin core has an API dependency on this already diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt new file mode 100644 index 0000000000..61432a7b12 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt @@ -0,0 +1,66 @@ +package software.amazon.smithy.kotlin.codegen.rendering.checksums + +import software.amazon.smithy.aws.traits.HttpChecksumTrait +import software.amazon.smithy.kotlin.codegen.KotlinSettings +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.model.hasTrait +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait + +/** + * Handles the `httpChecksumRequired` trait. + * See: https://smithy.io/2.0/spec/http-bindings.html#httpchecksumrequired-trait + */ +class HttpChecksumRequiredIntegration : KotlinIntegration { + override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = + model.isTraitApplied(HttpChecksumRequiredTrait::class.java) + + override fun customizeMiddleware( + ctx: ProtocolGenerator.GenerationContext, + resolved: List, + ): List = resolved + httpChecksumRequiredDefaultAlgorithmMiddleware + httpChecksumRequiredMiddleware +} + +/** + * Adds default checksum algorithm to the execution context + */ +private val httpChecksumRequiredDefaultAlgorithmMiddleware = object : ProtocolMiddleware { + override val name: String = "httpChecksumRequiredDefaultAlgorithmMiddleware" + override val order: Byte = -2 // Before S3 Express (possibly) changes the default (-1) and before calculating checksum (0) + + override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean = + op.hasTrait() && !op.hasTrait() + + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.context[#T.DefaultChecksumAlgorithm] = #S", + RuntimeTypes.HttpClient.Operation.HttpOperationContext, + "MD5", + ) + } +} + +/** + * Adds interceptor to calculate request checksums. + * The `httpChecksum` trait supersedes the `httpChecksumRequired` trait. If both are applied to an operation use `httpChecksum`. + * + * See: https://smithy.io/2.0/aws/aws-core.html#behavior-with-httpchecksumrequired + */ +private val httpChecksumRequiredMiddleware = object : ProtocolMiddleware { + override val name: String = "httpChecksumRequiredMiddleware" + + override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean = + op.hasTrait() && !op.hasTrait() + + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.interceptors.add(#T())", + RuntimeTypes.HttpClient.Interceptors.HttpChecksumRequiredInterceptor, + ) + } +} diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt index 519957cbed..f12ab07a26 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt @@ -4,7 +4,6 @@ */ package software.amazon.smithy.kotlin.codegen.rendering.protocol -import software.amazon.smithy.aws.traits.HttpChecksumTrait import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.integration.SectionId @@ -22,7 +21,6 @@ import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.EndpointTrait -import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait /** * Renders an implementation of a service interface for HTTP protocol @@ -318,8 +316,6 @@ open class HttpProtocolClientGenerator( .forEach { middleware -> middleware.render(ctx, op, writer) } - - op.renderIsMd5ChecksumRequired(writer) } /** @@ -336,27 +332,6 @@ open class HttpProtocolClientGenerator( */ protected open fun renderAdditionalMethods(writer: KotlinWriter) { } - /** - * Render optionally installing Md5ChecksumMiddleware. - * The Md5 middleware will only be installed if the operation requires a checksum and the user has not opted-in to flexible checksums. - */ - private fun OperationShape.renderIsMd5ChecksumRequired(writer: KotlinWriter) { - val httpChecksumTrait = getTrait() - - // the checksum requirement can be modeled in either HttpChecksumTrait's `requestChecksumRequired` or the HttpChecksumRequired trait - if (!hasTrait() && httpChecksumTrait == null) { - return - } - - if (hasTrait() || httpChecksumTrait?.isRequestChecksumRequired == true) { - val interceptorSymbol = RuntimeTypes.HttpClient.Interceptors.Md5ChecksumInterceptor - val inputSymbol = ctx.symbolProvider.toSymbol(ctx.model.expectShape(inputShape)) - writer.withBlock("op.interceptors.add(#T<#T> {", "})", interceptorSymbol, inputSymbol) { - writer.write("op.context.getOrNull(#T.ChecksumAlgorithm) == null", RuntimeTypes.HttpClient.Operation.HttpOperationContext) - } - } - } - /** * render a utility function to populate an operation's ExecutionContext with defaults from service config, environment, etc */ diff --git a/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index 0d02b51187..2ab7fe506a 100644 --- a/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -13,3 +13,4 @@ software.amazon.smithy.kotlin.codegen.rendering.endpoints.SdkEndpointBuiltinInte software.amazon.smithy.kotlin.codegen.rendering.compression.RequestCompressionIntegration software.amazon.smithy.kotlin.codegen.rendering.auth.SigV4AsymmetricAuthSchemeIntegration software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestsIntegration +software.amazon.smithy.kotlin.codegen.rendering.checksums.HttpChecksumRequiredIntegration \ No newline at end of file diff --git a/runtime/protocol/http-client/api/http-client.api b/runtime/protocol/http-client/api/http-client.api index 6e91f528f2..e8527c2c2b 100644 --- a/runtime/protocol/http-client/api/http-client.api +++ b/runtime/protocol/http-client/api/http-client.api @@ -255,7 +255,7 @@ public final class aws/smithy/kotlin/runtime/http/engine/internal/ManagedHttpCli public static final fun manage (Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine;)Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine; } -public abstract class aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { +public abstract class aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public abstract fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public abstract fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -331,19 +331,17 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpoin public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor : aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor { - public fun ()V - public fun (Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor : aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor { + public fun (ZLaws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig;Ljava/lang/String;)V public fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { +public class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public static final field Companion Laws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor$Companion; - public fun (Lkotlin/jvm/functions/Function1;)V + public fun (ZLaws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig;)V + public fun ignoreChecksum (Ljava/lang/String;Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)Z public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -369,10 +367,8 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksums public final fun getChecksumHeaderValidated ()Laws/smithy/kotlin/runtime/collections/AttributeKey; } -public final class aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor : aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor { +public final class aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor : aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor { public fun ()V - public fun (Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -516,9 +512,9 @@ public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDes public final class aws/smithy/kotlin/runtime/http/operation/HttpOperationContext { public static final field INSTANCE Laws/smithy/kotlin/runtime/http/operation/HttpOperationContext; - public final fun getChecksumAlgorithm ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getClockSkew ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getClockSkewApproximateSigningTime ()Laws/smithy/kotlin/runtime/collections/AttributeKey; + public final fun getDefaultChecksumAlgorithm ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getHostPrefix ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getHttpCallList ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getOperationAttributes ()Laws/smithy/kotlin/runtime/collections/AttributeKey; diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt similarity index 67% rename from runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt rename to runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt index 3fa8406bf5..adad89f259 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt @@ -9,13 +9,21 @@ import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.http.request.HttpRequest +/** + * Enables inheriting [HttpInterceptor]s to use checksums caching + */ @InternalApi -public abstract class AbstractChecksumInterceptor : HttpInterceptor { +public abstract class CachingChecksumInterceptor : HttpInterceptor { private var cachedChecksum: String? = null override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - cachedChecksum ?: calculateChecksum(context).also { cachedChecksum = it } - return cachedChecksum?.let { applyChecksum(context, it) } ?: context.protocolRequest + cachedChecksum = cachedChecksum ?: calculateChecksum(context) + + return if (cachedChecksum != null) { + applyChecksum(context, cachedChecksum!!) + } else { + context.protocolRequest + } } public abstract suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt new file mode 100644 index 0000000000..b510ab14e5 --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt @@ -0,0 +1,44 @@ +package aws.smithy.kotlin.runtime.http.interceptors + +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.hashing.HashFunction +import aws.smithy.kotlin.runtime.hashing.resolveChecksumAlgorithmHeaderName +import aws.smithy.kotlin.runtime.hashing.toBusinessMetric +import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.http.toCompletingBody +import aws.smithy.kotlin.runtime.http.toHashingBody +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.job + +/** + * Configures [HttpRequest] with AWS chunked streaming to calculate checksum during transmission + * @return [HttpRequest] + */ +internal fun calculateAwsChunkedStreamingChecksum( + context: ProtocolRequestInterceptorContext, + checksumAlgorithm: HashFunction, +): HttpRequest { + val request = context.protocolRequest.toBuilder() + val deferredChecksum = CompletableDeferred(context.executionContext.coroutineContext.job) + val checksumHeader = checksumAlgorithm.resolveChecksumAlgorithmHeaderName() + + request.body = request.body + .toHashingBody(checksumAlgorithm, request.body.contentLength) + .toCompletingBody(deferredChecksum) + + request.headers.append("x-amz-trailer", checksumHeader) + request.trailingHeaders.append(checksumHeader, deferredChecksum) + + context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric()) + + return request.build() +} + +/** + * @return The default checksum algorithm name in the execution context, null if default checksums are disabled. + */ +internal val ProtocolRequestInterceptorContext.defaultChecksumAlgorithmName: String? + get() = executionContext.getOrNull(HttpOperationContext.DefaultChecksumAlgorithm) diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt index b29ede017f..936dead520 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt @@ -5,104 +5,117 @@ package aws.smithy.kotlin.runtime.http.interceptors -import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.client.config.RequestHttpChecksumConfig import aws.smithy.kotlin.runtime.hashing.* import aws.smithy.kotlin.runtime.http.* -import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext import aws.smithy.kotlin.runtime.http.request.HttpRequest -import aws.smithy.kotlin.runtime.http.request.header import aws.smithy.kotlin.runtime.http.request.toBuilder import aws.smithy.kotlin.runtime.io.* +import aws.smithy.kotlin.runtime.telemetry.logging.Logger import aws.smithy.kotlin.runtime.telemetry.logging.logger import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String -import aws.smithy.kotlin.runtime.util.LazyAsyncValue -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.job import kotlin.coroutines.coroutineContext /** - * Mutate a request to enable flexible checksums. + * Handles request checksums for operations with the [HttpChecksumTrait] applied. * - * If the checksum will be sent as a header, calculate the checksum. + * If a user supplies a checksum via an HTTP header no calculation will be done. The exception is MD5, if a user + * supplies an MD5 checksum header it will be ignored. * - * Otherwise, if it will be sent as a trailing header, calculate the checksum as asynchronously as the body is streamed. - * In this case, a [LazyAsyncValue] will be added to the execution context which allows the trailing checksum to be sent - * after the entire body has been streamed. + * If the request configuration and model requires checksum calculation: + * - Check if the user configured a checksum algorithm for the request and attempt to use that. + * - If no checksum is configured for the request then use the default checksum algorithm to calculate a checksum. * - * @param checksumAlgorithmNameInitializer an optional function which parses the input [I] to return the checksum algorithm name. - * if not set, then the [HttpOperationContext.ChecksumAlgorithm] execution context attribute will be used. + * If the request will be streamed: + * - The checksum calculation is done during transmission using a hashing & completing body. + * - The checksum will be sent in a trailing header, once the request is consumed. + * + * If the request will not be streamed: + * - The checksum calculation is done before transmission + * - The checksum will be sent in a header + * + * Business metrics MUST be emitted for the checksum algorithm used. + * + * @param requestChecksumRequired Model sourced flag indicating if checksum calculation is mandatory. + * @param requestChecksumCalculation Configuration option that determines when checksum calculation should be done. + * @param requestChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null. */ @InternalApi -public class FlexibleChecksumsRequestInterceptor( - private val checksumAlgorithmNameInitializer: ((I) -> String?)? = null, -) : AbstractChecksumInterceptor() { - private var checksumAlgorithmName: String? = null - - @Deprecated("readAfterSerialization is no longer used") - override fun readAfterSerialization(context: ProtocolRequestInterceptorContext) { } - +public class FlexibleChecksumsRequestInterceptor( + private val requestChecksumRequired: Boolean, + private val requestChecksumCalculation: RequestHttpChecksumConfig?, + private val requestChecksumAlgorithm: String?, +) : CachingChecksumInterceptor() { override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - val logger = coroutineContext.logger>() + val logger = coroutineContext.logger() - @Suppress("UNCHECKED_CAST") - val input = context.request as I - checksumAlgorithmName = checksumAlgorithmNameInitializer?.invoke(input) ?: context.executionContext.getOrNull(HttpOperationContext.ChecksumAlgorithm) + context.protocolRequest.userProvidedChecksumHeader(logger)?.let { + logger.debug { "Checksum was supplied via header: skipping checksum calculation" } - checksumAlgorithmName ?: run { - logger.debug { "no checksum algorithm specified, skipping flexible checksums processing" } + val request = context.protocolRequest.toBuilder() + request.headers.removeAllChecksumHeadersExcept(it) return context.protocolRequest } - val req = context.protocolRequest.toBuilder() - - check(context.protocolRequest.body !is HttpBody.Empty) { - "Can't calculate the checksum of an empty body" - } - - val headerName = "x-amz-checksum-$checksumAlgorithmName".lowercase() - logger.debug { "Resolved checksum header name: $headerName" } - - // remove all checksum headers except for $headerName - // this handles the case where a user inputs a precalculated checksum, but it doesn't match the input checksum algorithm - req.headers.removeAllChecksumHeadersExcept(headerName) - - val checksumAlgorithm = checksumAlgorithmName?.toHashFunction() ?: throw ClientException("Could not parse checksum algorithm $checksumAlgorithmName") - - if (!checksumAlgorithm.isSupported) { - throw ClientException("Checksum algorithm $checksumAlgorithmName is not supported for flexible checksums") - } - - if (req.body.isEligibleForAwsChunkedStreaming) { - req.header("x-amz-trailer", headerName) - - val deferredChecksum = CompletableDeferred(context.executionContext.coroutineContext.job) - - if (req.headers[headerName] != null) { - logger.debug { "User supplied a checksum, skipping asynchronous calculation" } - - val checksum = req.headers[headerName]!! - req.headers.remove(headerName) // remove the checksum header because it will be sent as a trailing header - - deferredChecksum.complete(checksum) + resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )?.let { checksumAlgorithm -> + return if (context.protocolRequest.body.isEligibleForAwsChunkedStreaming) { + logger.debug { "Calculating checksum during transmission using: ${checksumAlgorithm::class.simpleName}" } + calculateAwsChunkedStreamingChecksum(context, checksumAlgorithm) } else { - logger.debug { "Calculating checksum asynchronously" } - req.body = req.body - .toHashingBody(checksumAlgorithm, req.body.contentLength) - .toCompletingBody(deferredChecksum) + if (context.protocolRequest.body is HttpBody.Bytes) { + // Cache checksum + super.modifyBeforeSigning(context) + } else { + val checksum = calculateFlexibleChecksumsChecksum(context) + applyFlexibleChecksumsChecksum(context, checksum) + } } - - req.trailingHeaders.append(headerName, deferredChecksum) - return req.build() - } else { - return super.modifyBeforeSigning(context) } + + logger.debug { "Checksum wasn't provided, selected, or isn't required: skipping checksum calculation" } + return context.protocolRequest } - override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? { + /** + * Determines what checksum algorithm to use, null if none is required + */ + private fun resolveChecksumAlgorithm( + requestChecksumRequired: Boolean, + requestChecksumCalculation: RequestHttpChecksumConfig?, + requestChecksumAlgorithm: String?, + context: ProtocolRequestInterceptorContext, + ): HashFunction? = + requestChecksumAlgorithm + ?.toHashFunctionOrThrow() + ?.takeIf { it.isSupportedForFlexibleChecksums } + ?: context.defaultChecksumAlgorithmName + ?.toHashFunctionOrThrow() + ?.takeIf { + (requestChecksumRequired || requestChecksumCalculation == RequestHttpChecksumConfig.WHEN_SUPPORTED) && + it.isSupportedForFlexibleChecksums + } + + /** + * Calculates a checksum based on the requirements and limitations of [FlexibleChecksumsRequestInterceptor] + */ + private suspend fun calculateFlexibleChecksumsChecksum( + context: ProtocolRequestInterceptorContext, + ): String { val req = context.protocolRequest.toBuilder() - val checksumAlgorithm = checksumAlgorithmName?.toHashFunction() ?: return null + val checksumAlgorithm = resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )!! return when { req.body.contentLength == null && !req.body.isOneShot -> { @@ -110,104 +123,66 @@ public class FlexibleChecksumsRequestInterceptor( channel.rollingHash(checksumAlgorithm).encodeBase64String() } else -> { - val bodyBytes = req.body.readAll()!! - req.body = bodyBytes.toHttpBody() + val bodyBytes = req.body.readAll() ?: byteArrayOf() + if (req.body.isOneShot) req.body = bodyBytes.toHttpBody() bodyBytes.hash(checksumAlgorithm).encodeBase64String() } } } - override fun applyChecksum( + override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? = + calculateFlexibleChecksumsChecksum(context) + + /** + * Applies a checksum based on the requirements and limitations of [FlexibleChecksumsRequestInterceptor] + */ + private fun applyFlexibleChecksumsChecksum( context: ProtocolRequestInterceptorContext, checksum: String, ): HttpRequest { - val headerName = "x-amz-checksum-$checksumAlgorithmName".lowercase() - - val req = context.protocolRequest.toBuilder() - - if (!req.headers.contains(headerName)) { - req.header(headerName, checksum) - } - - return req.build() + val request = context.protocolRequest.toBuilder() + val checksumAlgorithm = resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )!! + val checksumHeader = checksumAlgorithm.resolveChecksumAlgorithmHeaderName() + + request.headers[checksumHeader] = checksum + request.headers.removeAllChecksumHeadersExcept(checksumHeader) + context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric()) + + return request.build() } - // FIXME this duplicates the logic from aws-signing-common, but can't import from there due to circular import. - private val HttpBody.isEligibleForAwsChunkedStreaming: Boolean - get() = (this is HttpBody.SourceContent || this is HttpBody.ChannelContent) && - contentLength != null && - (isOneShot || contentLength!! > 65536 * 16) + override fun applyChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest = applyFlexibleChecksumsChecksum(context, checksum) /** - * @return if the [HashFunction] is supported by flexible checksums + * Checks if a user provided a checksum for a request via an HTTP header. + * The header must start with "x-amz-checksum-" followed by the checksum algorithm's name. + * MD5 is not considered a supported checksum algorithm. */ - private val HashFunction.isSupported: Boolean get() = when (this) { - is Crc32, is Crc32c, is Sha256, is Sha1 -> true - else -> false - } + private fun HttpRequest.userProvidedChecksumHeader(logger: Logger) = headers + .names() + .firstOrNull { + it.startsWith("x-amz-checksum-", ignoreCase = true) && + !it.equals("x-amz-checksum-md5", ignoreCase = true).also { isMd5 -> + if (isMd5) { + logger.debug { "MD5 checksum was supplied via header, MD5 is not a supported algorithm, ignoring header" } + } + } + } /** * Removes all checksum headers except [headerName] * @param headerName the checksum header name to keep */ - private fun HeadersBuilder.removeAllChecksumHeadersExcept(headerName: String) { - names().forEach { name -> - if (name.startsWith("x-amz-checksum-") && name != headerName) { - remove(name) - } - } - } - - /** - * Convert an [HttpBody] with an underlying [HashingSource] or [HashingByteReadChannel] - * to a [CompletingSource] or [CompletingByteReadChannel], respectively. - */ - private fun HttpBody.toCompletingBody(deferred: CompletableDeferred) = when (this) { - is HttpBody.SourceContent -> CompletingSource(deferred, (readFrom() as HashingSource)).toHttpBody(contentLength) - is HttpBody.ChannelContent -> CompletingByteReadChannel(deferred, (readFrom() as HashingByteReadChannel)).toHttpBody(contentLength) - else -> throw ClientException("HttpBody type is not supported") - } - - /** - * An [SdkSource] which uses the underlying [hashingSource]'s checksum to complete a [CompletableDeferred] value. - */ - internal class CompletingSource( - private val deferred: CompletableDeferred, - private val hashingSource: HashingSource, - ) : SdkSource by hashingSource { - override fun read(sink: SdkBuffer, limit: Long): Long = hashingSource.read(sink, limit) - .also { - if (it == -1L) { - deferred.complete(hashingSource.digest().encodeBase64String()) - } - } - } - - /** - * An [SdkByteReadChannel] which uses the underlying [hashingChannel]'s checksum to complete a [CompletableDeferred] value. - */ - internal class CompletingByteReadChannel( - private val deferred: CompletableDeferred, - private val hashingChannel: HashingByteReadChannel, - ) : SdkByteReadChannel by hashingChannel { - override suspend fun read(sink: SdkBuffer, limit: Long): Long = hashingChannel.read(sink, limit) - .also { - if (it == -1L) { - deferred.complete(hashingChannel.digest().encodeBase64String()) - } - } - } - - /** - * Compute the rolling hash of an [SdkByteReadChannel] using [hashFunction], reading up-to [bufferSize] bytes into memory - * @return a ByteArray of the hash function's digest - */ - private suspend fun SdkByteReadChannel.rollingHash(hashFunction: HashFunction, bufferSize: Long = 8192): ByteArray { - val buffer = SdkBuffer() - while (!isClosedForRead) { - read(buffer, bufferSize) - hashFunction.update(buffer.readToByteArray()) - } - return hashFunction.digest() - } + private fun HeadersBuilder.removeAllChecksumHeadersExcept(headerName: String) = + names() + .filter { it.startsWith("x-amz-checksum-", ignoreCase = true) && !it.equals(headerName, ignoreCase = true) } + .forEach { remove(it) } } diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt index 2e43fe1469..eceb19e739 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt @@ -8,10 +8,11 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.client.ProtocolResponseInterceptorContext -import aws.smithy.kotlin.runtime.client.RequestInterceptorContext +import aws.smithy.kotlin.runtime.client.config.ResponseHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.AttributeKey -import aws.smithy.kotlin.runtime.hashing.toHashFunction +import aws.smithy.kotlin.runtime.hashing.toHashFunctionOrThrow import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.readAll import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.response.HttpResponse import aws.smithy.kotlin.runtime.http.response.copy @@ -31,60 +32,87 @@ internal val CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST: List = listOf( ) /** - * Validate a response's checksum. + * Handles response checksums. + * + * If it's a streaming response, it wraps the response in a hashing body, calculating the checksum as the response is + * streamed to the user. The checksum is validated after the user has consumed the entire body using a checksum validating body. + * Otherwise, the checksum if calculated all at once. * - * Wraps the response in a hashing body, calculating the checksum as the response is streamed to the user. - * The checksum is validated after the user has consumed the entire body using a checksum validating body. * Users can check which checksum was validated by referencing the `ResponseChecksumValidated` execution context variable. * - * @param shouldValidateResponseChecksumInitializer A function which uses the input [I] to return whether response checksum validation should occur + * @param responseValidationRequired Model sourced flag indicating if the checksum validation is mandatory. + * @param responseChecksumValidation Configuration option that determines when checksum validation should be done. */ - @InternalApi -public class FlexibleChecksumsResponseInterceptor( - private val shouldValidateResponseChecksumInitializer: (input: I) -> Boolean, +public open class FlexibleChecksumsResponseInterceptor( + private val responseValidationRequired: Boolean, + private val responseChecksumValidation: ResponseHttpChecksumConfig?, ) : HttpInterceptor { - - private var shouldValidateResponseChecksum: Boolean = false - @InternalApi public companion object { // The name of the checksum header which was validated. If `null`, validation was not performed. public val ChecksumHeaderValidated: AttributeKey = AttributeKey("ChecksumHeaderValidated") } - override fun readBeforeSerialization(context: RequestInterceptorContext) { - @Suppress("UNCHECKED_CAST") - val input = context.request as I - shouldValidateResponseChecksum = shouldValidateResponseChecksumInitializer(input) - } - override suspend fun modifyBeforeDeserialization(context: ProtocolResponseInterceptorContext): HttpResponse { - if (!shouldValidateResponseChecksum) { - return context.protocolResponse - } + val configuredToVerifyChecksum = responseValidationRequired || responseChecksumValidation == ResponseHttpChecksumConfig.WHEN_SUPPORTED + if (!configuredToVerifyChecksum) return context.protocolResponse - val logger = coroutineContext.logger>() + val logger = coroutineContext.logger() val checksumHeader = CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST .firstOrNull { context.protocolResponse.headers.contains(it) } ?: run { - logger.warn { "User requested checksum validation, but the response headers did not contain any valid checksums" } + logger.warn { "Checksum validation was requested but the response headers didn't contain a valid checksum." } return context.protocolResponse } - // let the user know which checksum will be validated - logger.debug { "Validating checksum from $checksumHeader" } - context.executionContext[ChecksumHeaderValidated] = checksumHeader + val serviceChecksumValue = context.protocolResponse.headers[checksumHeader]!! + if (ignoreChecksum(serviceChecksumValue, context)) { + return context.protocolResponse + } - val checksumAlgorithm = checksumHeader.removePrefix("x-amz-checksum-").toHashFunction() ?: throw ClientException("could not parse checksum algorithm from header $checksumHeader") + context.executionContext[ChecksumHeaderValidated] = checksumHeader - // Wrap the response body in a hashing body - return context.protocolResponse.copy( - body = context.protocolResponse.body - .toHashingBody(checksumAlgorithm, context.protocolResponse.body.contentLength) - .toChecksumValidatingBody(context.protocolResponse.headers[checksumHeader]!!), - ) + val checksumAlgorithm = checksumHeader + .removePrefix("x-amz-checksum-") + .toHashFunctionOrThrow() + + when (val bodyType = context.protocolResponse.body) { + is HttpBody.Bytes -> { + logger.debug { "Validating checksum before deserialization from $checksumHeader" } + + checksumAlgorithm.update( + context.protocolResponse.body.readAll() ?: byteArrayOf(), + ) + val sdkChecksumValue = checksumAlgorithm.digest().encodeBase64String() + + validateAndThrow( + serviceChecksumValue, + sdkChecksumValue, + ) + + return context.protocolResponse + } + is HttpBody.SourceContent, is HttpBody.ChannelContent -> { + logger.debug { "Validating checksum after deserialization from $checksumHeader" } + + return context.protocolResponse.copy( + body = context.protocolResponse.body + .toHashingBody(checksumAlgorithm, context.protocolResponse.body.contentLength) + .toChecksumValidatingBody(serviceChecksumValue), + ) + } + else -> throw IllegalStateException("HTTP body type '$bodyType' is not supported for flexible checksums.") + } } + + /** + * Additional check on the checksum itself to see if it should be validated + */ + public open fun ignoreChecksum( + checksum: String, + context: ProtocolResponseInterceptorContext, + ): Boolean = false } public class ChecksumMismatchException(message: String?) : ClientException(message) diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt new file mode 100644 index 0000000000..f422de7fa7 --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.http.interceptors + +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.hashing.* +import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.header +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.io.rollingHash +import aws.smithy.kotlin.runtime.telemetry.logging.logger +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String +import kotlin.coroutines.coroutineContext + +/** + * Handles checksum request calculation from the `httpChecksumRequired` trait. + */ +@InternalApi +public class HttpChecksumRequiredInterceptor : CachingChecksumInterceptor() { + override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { + if (context.defaultChecksumAlgorithmName == null) { + // Don't calculate checksum + return context.protocolRequest + } + + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumAlgorithm = checksumAlgorithmName.toHashFunctionOrThrow() + + return if (context.protocolRequest.body.isEligibleForAwsChunkedStreaming) { + coroutineContext.logger().debug { + "Calculating checksum during transmission using: ${checksumAlgorithm::class.simpleName}" + } + calculateAwsChunkedStreamingChecksum(context, checksumAlgorithm) + } else { + if (context.protocolRequest.body is HttpBody.Bytes) { + // Cache checksum + super.modifyBeforeSigning(context) + } else { + val checksum = calculateHttpChecksumRequiredChecksum(context) + applyHttpChecksumRequiredChecksum(context, checksum) + } + } + } + + /** + * Calculates a checksum based on the requirements and limitations of [HttpChecksumRequiredInterceptor] + */ + private suspend fun calculateHttpChecksumRequiredChecksum( + context: ProtocolRequestInterceptorContext, + ): String { + val req = context.protocolRequest.toBuilder() + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumAlgorithm = checksumAlgorithmName.toHashFunctionOrThrow() + + return when { + req.body.contentLength == null && !req.body.isOneShot -> { + val channel = req.body.toSdkByteReadChannel()!! + channel.rollingHash(checksumAlgorithm).encodeBase64String() + } + else -> { + val bodyBytes = req.body.readAll() ?: byteArrayOf() + if (req.body.isOneShot) req.body = bodyBytes.toHttpBody() + bodyBytes.hash(checksumAlgorithm).encodeBase64String() + } + } + } + + public override suspend fun calculateChecksum( + context: ProtocolRequestInterceptorContext, + ): String? = + calculateHttpChecksumRequiredChecksum(context) + + /** + * Applies a checksum based on the requirements and limitations of [HttpChecksumRequiredInterceptor] + */ + private fun applyHttpChecksumRequiredChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest { + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumHeader = checksumAlgorithmName.resolveChecksumAlgorithmHeaderName() + val request = context.protocolRequest.toBuilder() + + request.header(checksumHeader, checksum) + return request.build() + } + + public override fun applyChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest = + applyHttpChecksumRequiredChecksum(context, checksum) +} diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt deleted file mode 100644 index cbe6bcabe2..0000000000 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.smithy.kotlin.runtime.http.interceptors - -import aws.smithy.kotlin.runtime.InternalApi -import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext -import aws.smithy.kotlin.runtime.hashing.md5 -import aws.smithy.kotlin.runtime.http.HttpBody -import aws.smithy.kotlin.runtime.http.request.HttpRequest -import aws.smithy.kotlin.runtime.http.request.header -import aws.smithy.kotlin.runtime.http.request.toBuilder -import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String - -/** - * Set the `Content-MD5` header based on the current payload - * See: - * - https://awslabs.github.io/smithy/1.0/spec/core/behavior-traits.html#httpchecksumrequired-trait - * - https://datatracker.ietf.org/doc/html/rfc1864.html - * @param block An optional function which parses the input [I] to determine if the `Content-MD5` header should be set. - * If not provided, the default behavior will set the header. - */ -@InternalApi -public class Md5ChecksumInterceptor( - private val block: ((input: I) -> Boolean)? = null, -) : AbstractChecksumInterceptor() { - override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - @Suppress("UNCHECKED_CAST") - val input = context.request as I - - val injectMd5Header = block?.invoke(input) ?: true - if (!injectMd5Header) { - return context.protocolRequest - } - - return super.modifyBeforeSigning(context) - } - - public override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? = - when (val body = context.protocolRequest.body) { - is HttpBody.Bytes -> body.bytes().md5().encodeBase64String() - else -> null - } - - public override fun applyChecksum(context: ProtocolRequestInterceptorContext, checksum: String): HttpRequest { - val req = context.protocolRequest.toBuilder() - if (!req.headers.contains("Content-MD5")) { - req.header("Content-MD5", checksum) - } - return req.build() - } -} diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt index 524614a667..56444dca98 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt @@ -66,9 +66,9 @@ public object HttpOperationContext { public val ClockSkewApproximateSigningTime: AttributeKey = AttributeKey("aws.smithy.kotlin#ClockSkewApproximateSigningTime") /** - * The name of the algorithm to be used for computing a checksum of the request. + * The name of the default algorithm to be used for computing a checksum of the request. */ - public val ChecksumAlgorithm: AttributeKey = AttributeKey("aws.smithy.kotlin#ChecksumAlgorithm") + public val DefaultChecksumAlgorithm: AttributeKey = AttributeKey("aws.smithy.kotlin#DefaultChecksumAlgorithm") } internal val ExecutionContext.operationMetrics: OperationMetrics diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt similarity index 88% rename from runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt rename to runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt index 3de8e557c2..cee1db7766 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt @@ -6,7 +6,7 @@ import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.SdkHttpClient -import aws.smithy.kotlin.runtime.http.interceptors.AbstractChecksumInterceptor +import aws.smithy.kotlin.runtime.http.interceptors.CachingChecksumInterceptor import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext import aws.smithy.kotlin.runtime.http.operation.newTestOperation import aws.smithy.kotlin.runtime.http.operation.roundTrip @@ -21,7 +21,7 @@ import kotlin.test.assertEquals private val CHECKSUM_TEST_HEADER = "x-amz-kotlin-sdk-test-checksum-header" -class AbstractChecksumInterceptorTest { +class CachingChecksumInterceptorTest { private val client = SdkHttpClient(TestEngine()) @Test @@ -33,7 +33,7 @@ class AbstractChecksumInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add(TestAbstractChecksumInterceptor(expectedChecksumValue)) + op.interceptors.add(TestCachingChecksumInterceptor(expectedChecksumValue)) op.roundTrip(client, Unit) val call = op.context.attributes[HttpOperationContext.HttpCallList].first() @@ -49,16 +49,16 @@ class AbstractChecksumInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add(TestAbstractChecksumInterceptor(expectedChecksumValue)) + op.interceptors.add(TestCachingChecksumInterceptor(expectedChecksumValue)) // the TestAbstractChecksumInterceptor will throw an exception if calculateChecksum is called more than once. op.roundTrip(client, Unit) op.roundTrip(client, Unit) } - inner class TestAbstractChecksumInterceptor( + inner class TestCachingChecksumInterceptor( private val expectedChecksum: String?, - ) : AbstractChecksumInterceptor() { + ) : CachingChecksumInterceptor() { private var alreadyCalculatedChecksum = false override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? { diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt index c4c85de66b..37e5054865 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.ClientException +import aws.smithy.kotlin.runtime.client.config.RequestHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.hashing.toHashFunction import aws.smithy.kotlin.runtime.http.* @@ -41,9 +42,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -65,10 +68,13 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -87,13 +93,14 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - unsupportedChecksumAlgorithmName - }, - ) - assertFailsWith { + op.interceptors.add( + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = unsupportedChecksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), + ) op.roundTrip(client, Unit) } } @@ -115,9 +122,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -126,23 +135,6 @@ class FlexibleChecksumsRequestInterceptorTest { assertEquals(0, call.request.headers.getNumChecksumHeaders()) } - @Test - fun itSetsChecksumHeaderViaExecutionContext() = runTest { - checksums.forEach { (checksumAlgorithmName, expectedChecksumValue) -> - val req = HttpRequestBuilder().apply { - body = HttpBody.fromBytes("bar".encodeToByteArray()) - } - - val op = newTestOperation(req, Unit) - op.context[HttpOperationContext.ChecksumAlgorithm] = checksumAlgorithmName - op.interceptors.add(FlexibleChecksumsRequestInterceptor()) - - op.roundTrip(client, Unit) - val call = op.context.attributes[HttpOperationContext.HttpCallList].first() - assertEquals(expectedChecksumValue, call.request.headers["x-amz-checksum-$checksumAlgorithmName"]) - } - } - @Test fun testCompletingSource() = runTest { val hashFunctionName = "crc32" @@ -151,7 +143,7 @@ class FlexibleChecksumsRequestInterceptorTest { val source = byteArray.source() val completableDeferred = CompletableDeferred() val hashingSource = HashingSource(hashFunctionName.toHashFunction()!!, source) - val completingSource = FlexibleChecksumsRequestInterceptor.CompletingSource(completableDeferred, hashingSource) + val completingSource = CompletingSource(completableDeferred, hashingSource) completingSource.read(SdkBuffer(), 1L) assertFalse(completableDeferred.isCompleted) // deferred value should not be completed because the source is not exhausted @@ -172,7 +164,8 @@ class FlexibleChecksumsRequestInterceptorTest { val channel = SdkByteReadChannel(byteArray) val completableDeferred = CompletableDeferred() val hashingChannel = HashingByteReadChannel(hashFunctionName.toHashFunction()!!, channel) - val completingChannel = FlexibleChecksumsRequestInterceptor.CompletingByteReadChannel(completableDeferred, hashingChannel) + val completingChannel = + CompletingByteReadChannel(completableDeferred, hashingChannel) completingChannel.read(SdkBuffer(), 1L) assertFalse(completableDeferred.isCompleted) @@ -198,9 +191,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -210,5 +205,51 @@ class FlexibleChecksumsRequestInterceptorTest { assertEquals(precalculatedChecksumValue, call.request.headers["x-amz-checksum-sha256"]) } + @Test + fun testDefaultChecksumConfiguration() = runTest { + setOf( + DefaultChecksumTest(true, RequestHttpChecksumConfig.WHEN_SUPPORTED, true), + DefaultChecksumTest(true, RequestHttpChecksumConfig.WHEN_REQUIRED, true), + DefaultChecksumTest(false, RequestHttpChecksumConfig.WHEN_SUPPORTED, true), + DefaultChecksumTest(false, RequestHttpChecksumConfig.WHEN_REQUIRED, false), + ).forEach { runDefaultChecksumTest(it) } + } + private fun Headers.getNumChecksumHeaders(): Int = entries().count { (name, _) -> name.startsWith("x-amz-checksum-") } + + private data class DefaultChecksumTest( + val requestChecksumRequired: Boolean, + val requestChecksumCalculation: RequestHttpChecksumConfig, + val defaultChecksumExpected: Boolean, + ) + + private fun runDefaultChecksumTest( + testCase: DefaultChecksumTest, + ) = runTest { + val defaultChecksumAlgorithmName = "crc32" + val expectedChecksumValue = "WdqXHQ==" + + val req = HttpRequestBuilder().apply { + body = HttpBody.fromBytes("bar".encodeToByteArray()) + } + + val op = newTestOperation(req, Unit) + + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" + op.interceptors.add( + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = null, // See if default checksum is applied + requestChecksumRequired = testCase.requestChecksumRequired, + requestChecksumCalculation = testCase.requestChecksumCalculation, + ), + ) + + op.roundTrip(client, Unit) + val call = op.context.attributes[HttpOperationContext.HttpCallList].first() + + when (testCase.defaultChecksumExpected) { + true -> assertEquals(expectedChecksumValue, call.request.headers["x-amz-checksum-$defaultChecksumAlgorithmName"]) + false -> assertFalse { call.request.headers.contains("x-amz-checksum-$defaultChecksumAlgorithmName") } + } + } } diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt index 2c04ee680d..e465499ad1 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.interceptors +import aws.smithy.kotlin.runtime.client.config.ResponseHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.http.* import aws.smithy.kotlin.runtime.http.HttpCall @@ -73,9 +74,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" @@ -99,9 +101,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" @@ -126,9 +129,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseHeaders = Headers { @@ -150,9 +154,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseHeaders = Headers { @@ -170,9 +175,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - false - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = false, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_REQUIRED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-crc32" @@ -188,4 +194,52 @@ class FlexibleChecksumsResponseInterceptorTest { assertNull(op.context.getOrNull(ChecksumHeaderValidated)) } + + @Test + fun testResponseValidationConfiguration() = runTest { + setOf( + ResponseChecksumValidationTest(true, ResponseHttpChecksumConfig.WHEN_SUPPORTED, true), + ResponseChecksumValidationTest(true, ResponseHttpChecksumConfig.WHEN_REQUIRED, true), + ResponseChecksumValidationTest(false, ResponseHttpChecksumConfig.WHEN_SUPPORTED, true), + ResponseChecksumValidationTest(false, ResponseHttpChecksumConfig.WHEN_REQUIRED, false), + ).forEach { runResponseChecksumValidationTest(it) } + } + + private data class ResponseChecksumValidationTest( + val responseValidationRequired: Boolean, + val responseChecksumValidation: ResponseHttpChecksumConfig, + val checksumValidationExpected: Boolean, + ) + + private fun runResponseChecksumValidationTest( + testCase: ResponseChecksumValidationTest, + ) = runTest { + checksums.forEach { (checksumAlgorithmName, expectedChecksum) -> + val req = HttpRequestBuilder() + val op = newTestOperation(req) + + op.interceptors.add( + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = testCase.responseValidationRequired, + responseChecksumValidation = testCase.responseChecksumValidation, + ), + ) + + val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" + + val responseHeaders = Headers { + append(responseChecksumHeaderName, expectedChecksum) + } + + val client = getMockClient(response, responseHeaders) + + val output = op.roundTrip(client, TestInput("input")) + output.body.readAll() + + when (testCase.checksumValidationExpected) { + true -> assertEquals(responseChecksumHeaderName, op.context[ChecksumHeaderValidated]) + false -> assertNull(op.context.getOrNull(ChecksumHeaderValidated)) + } + } + } } diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt similarity index 62% rename from runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt rename to runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt index 109d2e3e8d..df2cf49f0e 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.collections.get +import aws.smithy.kotlin.runtime.hashing.Crc32 import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.SdkHttpClient import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext @@ -14,12 +15,13 @@ import aws.smithy.kotlin.runtime.http.operation.roundTrip import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder import aws.smithy.kotlin.runtime.httptest.TestEngine import aws.smithy.kotlin.runtime.io.SdkByteReadChannel +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -class Md5ChecksumInterceptorTest { +class HttpChecksumRequiredInterceptorTest { private val client = SdkHttpClient(TestEngine()) @Test @@ -29,10 +31,9 @@ class Md5ChecksumInterceptorTest { } val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "MD5" op.interceptors.add( - Md5ChecksumInterceptor { - true - }, + HttpChecksumRequiredInterceptor(), ) val expected = "RG22oBSZFmabBbkzVGRi4w==" @@ -42,7 +43,30 @@ class Md5ChecksumInterceptorTest { } @Test - fun itOnlySetsHeaderForBytesContent() = runTest { + fun itSetsContentCrc32Header() = runTest { + val testBody = "bar".encodeToByteArray() + + val req = HttpRequestBuilder().apply { + body = HttpBody.fromBytes(testBody) + } + val op = newTestOperation(req, Unit) + + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" + op.interceptors.add( + HttpChecksumRequiredInterceptor(), + ) + + val crc32 = Crc32() + crc32.update(testBody) + val expected = crc32.digest().encodeBase64String() + + op.roundTrip(client, Unit) + val call = op.context.attributes[HttpOperationContext.HttpCallList].first() + assertEquals(expected, call.request.headers["x-amz-checksum-crc32"]) + } + + @Test + fun itSetsHeaderForNonBytesContent() = runTest { val req = HttpRequestBuilder().apply { body = object : HttpBody.ChannelContent() { override fun readFrom(): SdkByteReadChannel = SdkByteReadChannel("fooey".encodeToByteArray()) @@ -50,15 +74,15 @@ class Md5ChecksumInterceptorTest { } val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "MD5" op.interceptors.add( - Md5ChecksumInterceptor { - true - }, + HttpChecksumRequiredInterceptor(), ) + val expected = "vJLiaOiNxaxdWfYAYzdzFQ==" op.roundTrip(client, Unit) val call = op.context.attributes[HttpOperationContext.HttpCallList].first() - assertNull(call.request.headers["Content-MD5"]) + assertEquals(expected, call.request.headers["Content-MD5"]) } @Test @@ -69,9 +93,7 @@ class Md5ChecksumInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - Md5ChecksumInterceptor { - false // interceptor disabled - }, + HttpChecksumRequiredInterceptor(), ) op.roundTrip(client, Unit) diff --git a/runtime/protocol/http/api/http.api b/runtime/protocol/http/api/http.api index 3f4c29414f..fb338e64d9 100644 --- a/runtime/protocol/http/api/http.api +++ b/runtime/protocol/http/api/http.api @@ -1,3 +1,19 @@ +public final class aws/smithy/kotlin/runtime/http/CompletingByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel { + public fun (Lkotlinx/coroutines/CompletableDeferred;Laws/smithy/kotlin/runtime/io/HashingByteReadChannel;)V + public fun cancel (Ljava/lang/Throwable;)Z + public fun getAvailableForRead ()I + public fun getClosedCause ()Ljava/lang/Throwable; + public fun isClosedForRead ()Z + public fun isClosedForWrite ()Z + public fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class aws/smithy/kotlin/runtime/http/CompletingSource : aws/smithy/kotlin/runtime/io/SdkSource { + public fun (Lkotlinx/coroutines/CompletableDeferred;Laws/smithy/kotlin/runtime/io/HashingSource;)V + public fun close ()V + public fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;J)J +} + public abstract interface class aws/smithy/kotlin/runtime/http/DeferredHeaders : aws/smithy/kotlin/runtime/collections/ValuesMap { public static final field Companion Laws/smithy/kotlin/runtime/http/DeferredHeaders$Companion; } @@ -86,8 +102,10 @@ public abstract class aws/smithy/kotlin/runtime/http/HttpBody$SourceContent : aw } public final class aws/smithy/kotlin/runtime/http/HttpBodyKt { + public static final fun isEligibleForAwsChunkedStreaming (Laws/smithy/kotlin/runtime/http/HttpBody;)Z public static final fun readAll (Laws/smithy/kotlin/runtime/http/HttpBody;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun toByteStream (Laws/smithy/kotlin/runtime/http/HttpBody;)Laws/smithy/kotlin/runtime/content/ByteStream; + public static final fun toCompletingBody (Laws/smithy/kotlin/runtime/http/HttpBody;Lkotlinx/coroutines/CompletableDeferred;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHashingBody (Laws/smithy/kotlin/runtime/http/HttpBody;Laws/smithy/kotlin/runtime/hashing/HashFunction;Ljava/lang/Long;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHttpBody (Laws/smithy/kotlin/runtime/content/ByteStream;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHttpBody (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Ljava/lang/Long;)Laws/smithy/kotlin/runtime/http/HttpBody; diff --git a/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt b/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt index b86c5c2199..a12626a656 100644 --- a/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt +++ b/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt @@ -10,6 +10,8 @@ import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.hashing.HashFunction import aws.smithy.kotlin.runtime.http.content.ByteArrayContent import aws.smithy.kotlin.runtime.io.* +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope /** @@ -191,6 +193,49 @@ public fun HttpBody.toHashingBody( else -> throw ClientException("HttpBody type is not supported") } +/** + * Convert an [HttpBody] with an underlying [HashingSource] or [HashingByteReadChannel] + * to a [CompletingSource] or [CompletingByteReadChannel], respectively. + */ +@InternalApi +public fun HttpBody.toCompletingBody(deferred: CompletableDeferred): HttpBody = when (this) { + is HttpBody.SourceContent -> CompletingSource(deferred, (readFrom() as HashingSource)).toHttpBody(contentLength) + is HttpBody.ChannelContent -> CompletingByteReadChannel(deferred, (readFrom() as HashingByteReadChannel)).toHttpBody(contentLength) + else -> throw ClientException("HttpBody type is not supported") +} + +/** + * An [SdkSource] which uses the underlying [hashingSource]'s checksum to complete a [CompletableDeferred] value. + */ +@InternalApi +public class CompletingSource( + private val deferred: CompletableDeferred, + private val hashingSource: HashingSource, +) : SdkSource by hashingSource { + override fun read(sink: SdkBuffer, limit: Long): Long = hashingSource.read(sink, limit) + .also { + if (it == -1L) { + deferred.complete(hashingSource.digest().encodeBase64String()) + } + } +} + +/** + * An [SdkByteReadChannel] which uses the underlying [hashingChannel]'s checksum to complete a [CompletableDeferred] value. + */ +@InternalApi +public class CompletingByteReadChannel( + private val deferred: CompletableDeferred, + private val hashingChannel: HashingByteReadChannel, +) : SdkByteReadChannel by hashingChannel { + override suspend fun read(sink: SdkBuffer, limit: Long): Long = hashingChannel.read(sink, limit) + .also { + if (it == -1L) { + deferred.complete(hashingChannel.digest().encodeBase64String()) + } + } +} + // FIXME - replace/move to reading to SdkBuffer instead /** * Consume the [HttpBody] and pull the entire contents into memory as a [ByteArray]. @@ -244,3 +289,10 @@ public fun HttpBody.toSdkByteReadChannel(scope: CoroutineScope? = null): SdkByte is HttpBody.ChannelContent -> body.readFrom() is HttpBody.SourceContent -> body.readFrom().toSdkByteReadChannel(scope) } + +// FIXME this duplicates the logic from aws-signing-common +@InternalApi +public val HttpBody.isEligibleForAwsChunkedStreaming: Boolean + get() = (this is HttpBody.SourceContent || this is HttpBody.ChannelContent) && + contentLength != null && + (isOneShot || contentLength!! > 65536 * 16) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 8c42034110..6d39c3b417 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -104,6 +104,14 @@ public final class aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtil public final class aws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { public static final field ACCOUNT_ID_BASED_ENDPOINT Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_CRC32 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_CRC32C Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_SHA1 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_SHA256 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field GZIP_REQUEST_COMPRESSION Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field PAGINATOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field PROTOCOL_RPC_V2_CBOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; @@ -699,7 +707,12 @@ public final class aws/smithy/kotlin/runtime/hashing/HashFunction$DefaultImpls { public final class aws/smithy/kotlin/runtime/hashing/HashFunctionKt { public static final fun hash ([BLaws/smithy/kotlin/runtime/hashing/HashFunction;)[B public static final fun hash ([BLkotlin/jvm/functions/Function0;)[B + public static final fun isSupportedForFlexibleChecksums (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Z + public static final fun resolveChecksumAlgorithmHeaderName (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Ljava/lang/String; + public static final fun resolveChecksumAlgorithmHeaderName (Ljava/lang/String;)Ljava/lang/String; + public static final fun toBusinessMetric (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Laws/smithy/kotlin/runtime/businessmetrics/BusinessMetric; public static final fun toHashFunction (Ljava/lang/String;)Laws/smithy/kotlin/runtime/hashing/HashFunction; + public static final fun toHashFunctionOrThrow (Ljava/lang/String;)Laws/smithy/kotlin/runtime/hashing/HashFunction; } public final class aws/smithy/kotlin/runtime/hashing/HmacKt { @@ -958,6 +971,8 @@ public final class aws/smithy/kotlin/runtime/io/SdkByteReadChannelKt { public static final fun readFully (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/io/SdkBuffer;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun readRemaining (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/io/SdkBuffer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun readToBuffer (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun rollingHash (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/hashing/HashFunction;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun rollingHash$default (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/hashing/HashFunction;JLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun writeAll (Laws/smithy/kotlin/runtime/io/SdkByteWriteChannel;Laws/smithy/kotlin/runtime/io/SdkSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt index fa9c41652a..0600752a6e 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt @@ -90,6 +90,14 @@ public enum class SmithyBusinessMetric(public override val identifier: String) : SERVICE_ENDPOINT_OVERRIDE("N"), ACCOUNT_ID_BASED_ENDPOINT("O"), SIGV4A_SIGNING("S"), + FLEXIBLE_CHECKSUMS_REQ_CRC32("U"), + FLEXIBLE_CHECKSUMS_REQ_CRC32C("V"), + FLEXIBLE_CHECKSUMS_REQ_SHA1("X"), + FLEXIBLE_CHECKSUMS_REQ_SHA256("Y"), + FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED("Z"), + FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED("a"), + FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED("b"), + FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED("c"), ; override fun toString(): String = identifier diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt index 50684d61d8..2ad5b6785f 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt @@ -4,7 +4,10 @@ */ package aws.smithy.kotlin.runtime.hashing +import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric /** * A cryptographic hash function (algorithm) @@ -70,3 +73,55 @@ public fun String.toHashFunction(): HashFunction? = when (this.lowercase()) { "md5" -> Md5() else -> null } + +/** + * @return The [HashFunction] which is represented by this string, or an exception if none match. + */ +@InternalApi +public fun String.toHashFunctionOrThrow(): HashFunction = + toHashFunction() ?: throw ClientException("Checksum algorithm is not supported: $this") + +/** + * @return If the [HashFunction] is supported by flexible checksums + */ +@InternalApi +public val HashFunction.isSupportedForFlexibleChecksums: Boolean + get() = when (this) { + is Crc32, is Crc32c, is Sha1, is Sha256 -> true + else -> false + } + +/** + * @return The checksum algorithm header used depending on the checksum algorithm name + */ +@InternalApi +public fun String.resolveChecksumAlgorithmHeaderName(): String = + this.toHashFunctionOrThrow().resolveChecksumAlgorithmHeaderName() + +/** + * @return The checksum algorithm header used depending on the checksum algorithm + */ +@InternalApi +public fun HashFunction.resolveChecksumAlgorithmHeaderName(): String { + val prefix = "x-amz-checksum-" + return when (this) { + is Crc32 -> prefix + "crc32" + is Crc32c -> prefix + "crc32c" + is Sha1 -> prefix + "sha1" + is Sha256 -> prefix + "sha256" + is Md5 -> "Content-MD5" + else -> throw ClientException("Checksum algorithm is not supported: ${this::class.simpleName}") + } +} + +/** + * Maps supported hash functions to business metrics. + */ +@InternalApi +public fun HashFunction.toBusinessMetric(): BusinessMetric = when (this) { + is Crc32 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_CRC32 + is Crc32c -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_CRC32C + is Sha1 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_SHA1 + is Sha256 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_SHA256 + else -> throw IllegalStateException("Checksum was calculated using an unsupported hash function: ${this::class.simpleName}") +} diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt index 87d5a531da..a50259e240 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt @@ -4,6 +4,8 @@ */ package aws.smithy.kotlin.runtime.io +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.hashing.HashFunction import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers import kotlinx.coroutines.withContext @@ -131,3 +133,17 @@ public suspend fun SdkByteWriteChannel.writeAll(source: SdkSource): Long = withC } totalRead } + +/** + * Compute the rolling hash of an [SdkByteReadChannel] using [hashFunction], reading up-to [bufferSize] bytes into memory + * @return a ByteArray of the hash function's digest + */ +@InternalApi +public suspend fun SdkByteReadChannel.rollingHash(hashFunction: HashFunction, bufferSize: Long = 8192): ByteArray { + val buffer = SdkBuffer() + while (!isClosedForRead) { + read(buffer, bufferSize) + hashFunction.update(buffer.readToByteArray()) + } + return hashFunction.digest() +} diff --git a/runtime/smithy-client/api/smithy-client.api b/runtime/smithy-client/api/smithy-client.api index c2a9190009..b6132b26b8 100644 --- a/runtime/smithy-client/api/smithy-client.api +++ b/runtime/smithy-client/api/smithy-client.api @@ -235,6 +235,18 @@ public final class aws/smithy/kotlin/runtime/client/config/CompressionClientConf public abstract interface annotation class aws/smithy/kotlin/runtime/client/config/CompressionClientConfigDsl : java/lang/annotation/Annotation { } +public abstract interface class aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig { + public abstract fun getRequestChecksumCalculation ()Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public abstract fun getResponseChecksumValidation ()Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; +} + +public abstract interface class aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig$Builder { + public abstract fun getRequestChecksumCalculation ()Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public abstract fun getResponseChecksumValidation ()Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public abstract fun setRequestChecksumCalculation (Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig;)V + public abstract fun setResponseChecksumValidation (Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig;)V +} + public final class aws/smithy/kotlin/runtime/client/config/RequestCompressionConfig { public static final field Companion Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig$Companion; public fun (Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig$Builder;)V @@ -258,6 +270,22 @@ public final class aws/smithy/kotlin/runtime/client/config/RequestCompressionCon public final fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig; } +public final class aws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig : java/lang/Enum { + public static final field WHEN_REQUIRED Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static final field WHEN_SUPPORTED Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static fun values ()[Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; +} + +public final class aws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig : java/lang/Enum { + public static final field WHEN_REQUIRED Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static final field WHEN_SUPPORTED Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static fun values ()[Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; +} + public final class aws/smithy/kotlin/runtime/client/config/RetryMode : java/lang/Enum { public static final field ADAPTIVE Laws/smithy/kotlin/runtime/client/config/RetryMode; public static final field LEGACY Laws/smithy/kotlin/runtime/client/config/RetryMode; diff --git a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt new file mode 100644 index 0000000000..fbe73b860e --- /dev/null +++ b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.config + +/** + * Client config for HTTP checksums + */ +public interface HttpChecksumConfig { + /** + * Configures request checksum calculation + */ + public val requestChecksumCalculation: RequestHttpChecksumConfig? + + /** + * Configures response checksum validation + */ + public val responseChecksumValidation: ResponseHttpChecksumConfig? + + public interface Builder { + /** + * Configures request checksum calculation + */ + public var requestChecksumCalculation: RequestHttpChecksumConfig? + + /** + * Configures response checksum validation + */ + public var responseChecksumValidation: ResponseHttpChecksumConfig? + } +} + +/** + * Configuration options for enabling and managing HTTP request checksums + */ +public enum class RequestHttpChecksumConfig { + /** + * SDK will calculate checksums if the service marks them as required or if the service offers optional checksums. + */ + WHEN_SUPPORTED, + + /** + * SDK will only calculate checksums if the service marks them as required. + */ + WHEN_REQUIRED, +} + +/** + * Configuration options for enabling and managing HTTP response checksums + */ +public enum class ResponseHttpChecksumConfig { + /** + * SDK will validate checksums if the service marks them as required or if the service offers optional checksums. + */ + WHEN_SUPPORTED, + + /** + * SDK will only validate checksums if the service marks them as required. + */ + WHEN_REQUIRED, +} From 14e0958a95a86016d1845c89019a06bc9a0f879c Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 15 Jan 2025 17:58:12 +0000 Subject: [PATCH 21/54] chore: release 1.4.0 --- .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json | 9 --------- .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json | 6 ------ .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json | 8 -------- CHANGELOG.md | 11 +++++++++++ gradle.properties | 4 ++-- 5 files changed, 13 insertions(+), 25 deletions(-) delete mode 100644 .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json delete mode 100644 .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json delete mode 100644 .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json diff --git a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json deleted file mode 100644 index d89da89103..0000000000 --- a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": "0857b6f0-0444-479f-be6b-06ef71d482a0", - "type": "feature", - "description": "⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters", - "issues": [ - "https://github.com/awslabs/aws-sdk-kotlin/issues/1431" - ], - "requiresMinorVersionBump": true -} \ No newline at end of file diff --git a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json deleted file mode 100644 index 10ef502204..0000000000 --- a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "1a68d0b7-00e7-45c0-88f6-95e5c39c9c61", - "type": "misc", - "description": "⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0", - "requiresMinorVersionBump": true -} \ No newline at end of file diff --git a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json deleted file mode 100644 index 767e48a02e..0000000000 --- a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "3456d00f-1b29-4d88-ab02-045db1b1ebce", - "type": "bugfix", - "description": "Include more information when retry strategy halts early due to token bucket capacity errors", - "issues": [ - "awslabs/aws-sdk-kotlin#1321" - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c1758b4332..c324c5e681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.4.0] - 01/15/2025 + +### Features +* [#1431](https://github.com/awslabs/aws-sdk-kotlin/issues/1431) ⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters + +### Fixes +* [#1321](https://github.com/awslabs/aws-sdk-kotlin/issues/1321) Include more information when retry strategy halts early due to token bucket capacity errors + +### Miscellaneous +* ⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0 + ## [1.3.34] - 01/10/2025 ## [1.3.33] - 01/10/2025 diff --git a/gradle.properties b/gradle.properties index d76819491b..d0e2da8b41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.35-SNAPSHOT +sdkVersion=1.4.0 # codegen -codegenVersion=0.33.35-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.0 \ No newline at end of file From ed95d7b5014ba9311d4a3fff16671971fc026e36 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 15 Jan 2025 17:58:13 +0000 Subject: [PATCH 22/54] chore: bump snapshot version to 1.4.1-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d0e2da8b41..dbe31b2197 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.0 +sdkVersion=1.4.1-SNAPSHOT # codegen -codegenVersion=0.34.0 \ No newline at end of file +codegenVersion=0.34.1-SNAPSHOT \ No newline at end of file From a4ace353ca9dc1517c00de345a833691c444841d Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 16 Jan 2025 11:50:34 -0500 Subject: [PATCH 23/54] fix: add 0.9.x aws-crt-kotlin transform (#1220) --- .brazil.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.brazil.json b/.brazil.json index 5a1d830abd..dbb2135d1b 100644 --- a/.brazil.json +++ b/.brazil.json @@ -8,6 +8,7 @@ "com.squareup.okio:okio-jvm:3.*": "OkioJvm-3.x", "io.opentelemetry:opentelemetry-api:1.*": "Maven-io-opentelemetry_opentelemetry-api-1.x", "org.slf4j:slf4j-api:2.*": "Maven-org-slf4j_slf4j-api-2.x", + "aws.sdk.kotlin.crt:aws-crt-kotlin:0.9.*": "AwsCrtKotlin-0.9.x", "aws.sdk.kotlin.crt:aws-crt-kotlin:0.8.*": "AwsCrtKotlin-0.8.x", "com.squareup.okhttp3:okhttp:4.*": "OkHttp3-4.x" }, From 15e5f0ce2c713f3e81f75a28e90b4175891e0575 Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 16 Jan 2025 14:57:24 -0500 Subject: [PATCH 24/54] fix: Ensure `Host` header is included when signing auth tokens (#1222) --- .github/workflows/continuous-integration.yml | 2 +- gradle/libs.versions.toml | 2 +- .../kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt | 9 ++++++++- .../runtime/auth/awssigning/AuthTokenGeneratorTest.kt | 2 ++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 599293cc06..ef2cc9db42 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -68,7 +68,7 @@ jobs: ./gradlew test jvmTest - name: Save Test Reports if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.os }} path: '**/build/reports' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ef5b35d57..6d8724016e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin-version = "2.1.0" dokka-version = "1.9.10" -aws-kotlin-repo-tools-version = "0.4.17" +aws-kotlin-repo-tools-version = "0.4.18" # libs coroutines-version = "1.9.0" diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt index e92ac1f72c..f80ee4fe7e 100644 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke +import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.HttpMethod import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.net.url.Url @@ -28,7 +29,13 @@ public class AuthTokenGenerator( private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { - val req = HttpRequest(HttpMethod.GET, endpoint) + val req = HttpRequest( + HttpMethod.GET, + endpoint, + headers = Headers { + append("Host", endpoint.hostAndPort) + }, + ) val config = AwsSigningConfig { credentials = credentialsProvider.resolve() diff --git a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt index 87fec1a74e..75b17fbab9 100644 --- a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -45,6 +45,7 @@ class AuthTokenGeneratorTest { assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked assertContains(token, "X-Amz-Expires=333") // expiration assertContains(token, "X-Amz-SigningDate=0") // clock + assertContains(token, "X-Amz-SignedHeaders=host") assertTrue(credentialsProvider.credentialsResolved) } @@ -60,6 +61,7 @@ private val TEST_SIGNER = object : AwsSigner { put("X-Amz-Credential", "signature") put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString()) put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString()) + put("X-Amz-SignedHeaders", request.headers.names().map { it.lowercase() }.joinToString()) } return AwsSigningResult(builder.build(), "signature".encodeToByteArray()) From 447ac10880ebe778471d9ad4bce97a424139550d Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 16 Jan 2025 19:59:39 +0000 Subject: [PATCH 25/54] chore: release 1.4.1 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c324c5e681..a4c4a65548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.4.1] - 01/16/2025 + ## [1.4.0] - 01/15/2025 ### Features diff --git a/gradle.properties b/gradle.properties index dbe31b2197..e005c08ee2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.1-SNAPSHOT +sdkVersion=1.4.1 # codegen -codegenVersion=0.34.1-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.1 \ No newline at end of file From 0f8db4452966822949bbe6616e3642b2063cfb63 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 16 Jan 2025 19:59:40 +0000 Subject: [PATCH 26/54] chore: bump snapshot version to 1.4.2-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e005c08ee2..df0e651ce1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.1 +sdkVersion=1.4.2-SNAPSHOT # codegen -codegenVersion=0.34.1 \ No newline at end of file +codegenVersion=0.34.2-SNAPSHOT \ No newline at end of file From 8b336937bb028bc3d0fcb1beafeabcf4c25fc51e Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 23 Jan 2025 14:03:54 -0500 Subject: [PATCH 27/54] fix: address various failing protocol tests (#1223) --- .../model/error-correction-tests.smithy | 12 ++++---- .../core/AwsHttpBindingProtocolGenerator.kt | 8 +---- .../codegen/core/KotlinSymbolProvider.kt | 30 ++++++++++++------- .../codegen/rendering/StructureGenerator.kt | 1 + .../protocol/HttpStringValuesMapSerializer.kt | 19 ++---------- .../kotlin/codegen/core/SymbolProviderTest.kt | 2 +- .../HttpBindingProtocolGeneratorTest.kt | 8 ++--- .../HttpStringValuesMapSerializerTest.kt | 16 +++++----- gradle/libs.versions.toml | 2 +- .../kotlin/runtime/text/encoding/Base64.kt | 11 +++++-- .../runtime/text/encoding/Base64Test.kt | 28 ++++++++++++----- 11 files changed, 74 insertions(+), 63 deletions(-) diff --git a/codegen/protocol-tests/model/error-correction-tests.smithy b/codegen/protocol-tests/model/error-correction-tests.smithy index 201ffc877f..4f246e1b99 100644 --- a/codegen/protocol-tests/model/error-correction-tests.smithy +++ b/codegen/protocol-tests/model/error-correction-tests.smithy @@ -39,8 +39,9 @@ operation SayHelloXml { output: TestOutput, errors: [Error] } structure TestOutputDocument with [TestStruct] { innerField: Nested, - // FIXME: This trait fails smithy validator - // @required + + // Note: This shape _should_ be @required, but causes Smithy httpResponseTests validation to fail. + // We expect `document` to be deserialized as `null` and enforce @required using a runtime check, but Smithy validator doesn't recognize / allow this. document: Document } structure TestOutput with [TestStruct] { innerField: Nested } @@ -65,8 +66,8 @@ structure TestStruct { @required nestedListValue: NestedList - // FIXME: This trait fails smithy validator - // @required + // Note: This shape _should_ be @required, but causes Smithy httpResponseTests validation to fail. + // We expect `nested` to be deserialized as `null` and enforce @required using a runtime check, but Smithy validator doesn't recognize / allow this. nested: Nested @required @@ -97,8 +98,7 @@ union MyUnion { } structure Nested { - // FIXME: This trait fails smithy validator - // @required + @required a: String } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt index 6e5834f7d1..bd41afcade 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt @@ -40,13 +40,7 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator() // val targetedTest = TestMemberDelta(setOf("RestJsonComplexErrorWithNoMessage"), TestContainmentMode.RUN_TESTS) val ignoredTests = TestMemberDelta( - setOf( - "AwsJson10ClientErrorCorrectsWithDefaultValuesWhenServerFailsToSerializeRequiredValues", - "RestJsonNullAndEmptyHeaders", - "NullAndEmptyHeaders", - "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", - "RpcV2CborClientPopulatesDefaultValuesInInput", - ), + setOf(), ) val requestTestBuilder = HttpProtocolUnitTestRequestGenerator.Builder() diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt index e6dbd0dca1..f35570dfeb 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt @@ -193,7 +193,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli } else { // only use @default if type is `T` shape.getTrait()?.let { - defaultValue(it.getDefaultValue(targetShape)) + setDefaultValue(it, targetShape) } } } @@ -219,9 +219,10 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli } } - private fun DefaultTrait.getDefaultValue(targetShape: Shape): String? { - val node = toNode() - return when { + private fun Symbol.Builder.setDefaultValue(defaultTrait: DefaultTrait, targetShape: Shape) { + val node = defaultTrait.toNode() + + val defaultValue = when { node.toString() == "null" -> null // Check if target is an enum before treating the default like a regular number/string @@ -235,13 +236,20 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli "${enumSymbol.fullName}.fromValue($arg)" } - targetShape.isBlobShape && targetShape.isStreaming -> - node - .toString() - .takeUnless { it.isEmpty() } - ?.let { "ByteStream.fromString(${it.dq()})" } + targetShape.isBlobShape -> { + addReferences(RuntimeTypes.Core.Text.Encoding.decodeBase64) - targetShape.isBlobShape -> "${node.toString().dq()}.encodeToByteArray()" + if (targetShape.isStreaming) { + node.toString() + .takeUnless { it.isEmpty() } + ?.let { + addReferences(RuntimeTypes.Core.Content.ByteStream) + "ByteStream.fromString(${it.dq()}.decodeBase64())" + } + } else { + "${node.toString().dq()}.decodeBase64().encodeToByteArray()" + } + } targetShape.isDocumentShape -> getDefaultValueForDocument(node) targetShape.isTimestampShape -> getDefaultValueForTimestamp(node.asNumberNode().get()) @@ -252,6 +260,8 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli node.isStringNode -> node.toString().dq() else -> node.toString() } + + defaultValue(defaultValue) } private fun getDefaultValueForTimestamp(node: NumberNode): String { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt index c34eb5ab9d..5d90f376d2 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt @@ -248,6 +248,7 @@ class StructureGenerator( } else { memberSymbol } + write("public var #L: #E", memberName, builderMemberSymbol) } write("") diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt index 932b2a796a..5dfc519962 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt @@ -157,9 +157,8 @@ class HttpStringValuesMapSerializer( val paramName = binding.locationName // addAll collection parameter 2 val param2 = if (mapFnContents.isEmpty()) "input.$memberName" else "input.$memberName.map { $mapFnContents }" - val nullCheck = if (memberSymbol.isNullable) "?" else "" writer.write( - "if (input.#L$nullCheck.isNotEmpty() == true) #L(#S, #L)", + "if (input.#L != null) #L(#S, #L)", memberName, binding.location.addAllFnName, paramName, @@ -174,8 +173,7 @@ class HttpStringValuesMapSerializer( val paramName = binding.locationName val memberSymbol = symbolProvider.toSymbol(binding.member) - // NOTE: query parameters are allowed to be empty, whereas headers should omit empty string - // values from serde + // NOTE: query parameters are allowed to be empty if ((location == HttpBinding.Location.QUERY || location == HttpBinding.Location.HEADER) && binding.member.hasTrait()) { // Call the idempotency token function if no supplied value. writer.addImport(RuntimeTypes.SmithyClient.IdempotencyTokenProviderExt) @@ -185,18 +183,7 @@ class HttpStringValuesMapSerializer( paramName, ) } else { - val nullCheck = - if (location == HttpBinding.Location.QUERY || - memberTarget.hasTrait< - @Suppress("DEPRECATION") - software.amazon.smithy.model.traits.EnumTrait, - >() - ) { - if (memberSymbol.isNullable) "input.$memberName != null" else "" - } else { - val nullCheck = if (memberSymbol.isNullable) "?" else "" - "input.$memberName$nullCheck.isNotEmpty() == true" - } + val nullCheck = if (memberSymbol.isNullable) "input.$memberName != null" else "" val cond = defaultCheck(binding.member) ?: nullCheck diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt index 0cc4d2cb14..bb90fbc0fb 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt @@ -182,7 +182,7 @@ class SymbolProviderTest { "double,2.71828,2.71828", "byte,10,10.toByte()", "string,\"hello\",\"hello\"", - "blob,\"abcdefg\",\"abcdefg\".encodeToByteArray()", + "blob,\"abcdefg\",\"abcdefg\".decodeBase64().encodeToByteArray()", "boolean,true,true", "bigInteger,5,5", "bigDecimal,9.0123456789,9.0123456789", diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt index 3c15579dab..6f8040e7dc 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt @@ -57,8 +57,8 @@ internal class SmokeTestOperationSerializer: HttpSerializer.NonStreaming( + "YQ" to "a", + "Yg" to "b", + "YWI" to "ab", + "YWJj" to "abc", + "SGVsbG8gd29ybGQ" to "Hello world", + ) + + inputs.forEach { (encoded, expected) -> + val actual = encoded.decodeBase64() + assertEquals(expected, actual) + } + } } From 03badf913813df5481b330e9eeff80bcea5586ec Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 23 Jan 2025 14:04:09 -0500 Subject: [PATCH 28/54] misc: re-enable `kotlinWarningsAsErrors=true` (#1224) --- .github/workflows/continuous-integration.yml | 3 +-- .../kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt | 2 +- .../smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt | 2 ++ .../smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt | 2 +- .../smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt | 2 +- .../common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt | 3 +-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ef2cc9db42..d47020ea5d 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -62,8 +62,7 @@ jobs: - name: Test shell: bash run: | - # FIXME K2. Re-enable warnings as errors after this warning is removed: https://youtrack.jetbrains.com/issue/KT-68532 - # echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties + echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties ./gradlew apiCheck ./gradlew test jvmTest - name: Save Test Reports diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt index 0ea8ae5e66..8047b870c3 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt @@ -30,7 +30,7 @@ public object MetricsInterceptor : Interceptor { } val originalResponse = chain.proceed(request) - val response = if (originalResponse.body == null || originalResponse.body?.contentLength() == 0L) { + val response = if (originalResponse.body.contentLength() == 0L) { originalResponse } else { originalResponse.newBuilder() diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt index 061c22a13d..a20387d0a0 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt @@ -15,6 +15,7 @@ import aws.smithy.kotlin.runtime.net.TlsVersion import aws.smithy.kotlin.runtime.operation.ExecutionContext import aws.smithy.kotlin.runtime.time.Instant import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.job import okhttp3.* import okhttp3.ConnectionPool @@ -47,6 +48,7 @@ public class OkHttpEngine( private val connectionIdleMonitor = config.connectionIdlePollingInterval?.let { ConnectionIdleMonitor(it) } private val client = config.buildClientWithConnectionListener(metrics, connectionIdleMonitor) + @OptIn(ExperimentalCoroutinesApi::class) override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { val callContext = callContext() diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt index c287228fdf..181ed400c3 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt @@ -104,7 +104,7 @@ public fun Headers.toOkHttpHeaders(): OkHttpHeaders = OkHttpHeaders.Builder().al @InternalApi public fun OkHttpResponse.toSdkResponse(): HttpResponse { val sdkHeaders = OkHttpHeadersAdapter(headers) - val httpBody = if (body == null || body!!.contentLength() == 0L) { + val httpBody = if (body.contentLength() == 0L) { HttpBody.Empty } else { object : HttpBody.SourceContent() { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt index ff59823201..026bbd942c 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt @@ -99,7 +99,7 @@ private suspend fun Call.executeAsync(): Response = call: Call, response: Response, ) { - continuation.resume(response) { + continuation.resume(response) { cause, _, _ -> response.closeQuietly() } } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt index 9b691a79a2..c8437b2b33 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt @@ -62,8 +62,7 @@ public fun Any?.type(): String = when (this) { is List<*>, is Array<*> -> "array" is Number -> "number" is Any -> "object" - null -> "null" - else -> throw Exception("Undetected type for: $this") + else -> "null" } // Collection `flattenIfPossible` functions From 9f44cdbd82adc1dd30eaf4bc4290b61eee09bbd4 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:33:23 -0800 Subject: [PATCH 29/54] fix: ignore hop-by-hop headers when signing requests (#1227) --- .../b0646077-397d-48a8-94f2-ea2db40b1dea.json | 5 +++++ gradle/libs.versions.toml | 2 +- .../runtime/auth/awssigning/Canonicalizer.kt | 17 ++++++++++++++--- .../auth/awssigning/DefaultCanonicalizerTest.kt | 9 +++++++-- .../runtime/http/auth/AwsHttpSignerTestBase.kt | 16 +++++++++------- 5 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json diff --git a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json new file mode 100644 index 0000000000..96e13a4a82 --- /dev/null +++ b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json @@ -0,0 +1,5 @@ +{ + "id": "b0646077-397d-48a8-94f2-ea2db40b1dea", + "type": "bugfix", + "description": "Ignore hop-by-hop headers when signing requests" +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31f5d17afa..bdc0c8e98b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ okio-version = "3.9.1" otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" -crt-kotlin-version = "0.9.0" +crt-kotlin-version = "0.9.1" micrometer-version = "1.14.2" binary-compatibility-validator-version = "0.16.3" diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt index 69d742de79..c7c47fe5a2 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt @@ -58,16 +58,27 @@ internal interface Canonicalizer { ): CanonicalRequest } -// Taken from https://github.com/awslabs/aws-c-auth/blob/dd505b55fd46222834f35c6e54165d8cbebbfaaa/source/aws_signing.c#L118-L156 private val skipHeaders = setOf( - "connection", "expect", // https://github.com/awslabs/aws-sdk-kotlin/issues/862 + + // Taken from https://github.com/awslabs/aws-c-auth/blob/274a1d21330731cc51bb742794adc70ada5f4380/source/aws_signing.c#L121-L164 "sec-websocket-key", "sec-websocket-protocol", "sec-websocket-version", - "upgrade", "user-agent", "x-amzn-trace-id", + + // Taken from https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1. These are "hop-by-hop" headers which may + // be modified/removed by intervening proxies or caches. These are unsafe to sign because if they change they render + // the signature invalid. + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", ) internal class DefaultCanonicalizer(private val sha256Supplier: HashSupplier = ::Sha256) : Canonicalizer { diff --git a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt index 7fc20ddb83..2dfbdf48e1 100644 --- a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt +++ b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt @@ -6,8 +6,12 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awssigning.tests.DEFAULT_TEST_CREDENTIALS -import aws.smithy.kotlin.runtime.http.* -import aws.smithy.kotlin.runtime.http.request.* +import aws.smithy.kotlin.runtime.http.Headers +import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.headers +import aws.smithy.kotlin.runtime.http.request.url import aws.smithy.kotlin.runtime.net.Host import aws.smithy.kotlin.runtime.net.url.Url import aws.smithy.kotlin.runtime.time.Instant @@ -136,6 +140,7 @@ class DefaultCanonicalizerTest { // These should not be signed set("Expect", "100-continue") set("X-Amzn-Trace-Id", "qux") + set("Transfer-Encoding", "chunked") } body = HttpBody.Empty } diff --git a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt index 2ce308df8f..7d10b35e14 100644 --- a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt +++ b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt @@ -12,7 +12,9 @@ import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner import aws.smithy.kotlin.runtime.auth.awssigning.internal.AWS_CHUNKED_THRESHOLD import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.collections.get -import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.SdkHttpClient import aws.smithy.kotlin.runtime.http.operation.* import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder @@ -149,8 +151,8 @@ public abstract class AwsHttpSignerTestBase( val op = buildOperation(streaming = true, replayable = false, requestBody = "a".repeat(AWS_CHUNKED_THRESHOLD + 1)) val expectedDate = "20201016T195600Z" val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=ac341b9b248a0b23d2fcd9f7e805f4eb0b8a1b789bb23a8ec6adc6c48dd084ad" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=ef06c95647c4d2daa6c89ac90274f1c780777cba8eaab772df6d8009def3eb8f" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) @@ -162,8 +164,8 @@ public abstract class AwsHttpSignerTestBase( val op = buildOperation(streaming = true, replayable = true, requestBody = "a".repeat(AWS_CHUNKED_THRESHOLD + 1)) val expectedDate = "20201016T195600Z" val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=ac341b9b248a0b23d2fcd9f7e805f4eb0b8a1b789bb23a8ec6adc6c48dd084ad" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=ef06c95647c4d2daa6c89ac90274f1c780777cba8eaab772df6d8009def3eb8f" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) @@ -176,8 +178,8 @@ public abstract class AwsHttpSignerTestBase( val expectedDate = "20201016T195600Z" // should have same signature as testSignAwsChunkedStreamNonReplayable(), except for the hash, since the body is different val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=3f0277123c9ed8a8858f793886a0ac0fcb457bc54401ffc22d470f373397cff0" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=a902702b57057a864bf41cc22ee846a1b7bd047e22784367ec6a459f6791330e" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) From 97ac44792df8016d27988bcde1d112c69b46ab29 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 28 Jan 2025 18:35:54 +0000 Subject: [PATCH 30/54] chore: release 1.4.2 --- .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json diff --git a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json deleted file mode 100644 index 96e13a4a82..0000000000 --- a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "b0646077-397d-48a8-94f2-ea2db40b1dea", - "type": "bugfix", - "description": "Ignore hop-by-hop headers when signing requests" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c4a65548..2e925d0943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.2] - 01/28/2025 + +### Fixes +* Ignore hop-by-hop headers when signing requests + ## [1.4.1] - 01/16/2025 ## [1.4.0] - 01/15/2025 diff --git a/gradle.properties b/gradle.properties index df0e651ce1..1b11779bb6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.2-SNAPSHOT +sdkVersion=1.4.2 # codegen -codegenVersion=0.34.2-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.2 \ No newline at end of file From b7e50e3c66a073e5696b363aae29823284beef09 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 28 Jan 2025 18:35:55 +0000 Subject: [PATCH 31/54] chore: bump snapshot version to 1.4.3-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1b11779bb6..9e53dbcaf0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.2 +sdkVersion=1.4.3-SNAPSHOT # codegen -codegenVersion=0.34.2 \ No newline at end of file +codegenVersion=0.34.3-SNAPSHOT \ No newline at end of file From 35ef3a1efc4f389bdba9b175a60b1349455a8cbe Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 12:35:06 -0500 Subject: [PATCH 32/54] Implement PlatformNative (except for getAllEnvVars), add `deleteFile` to `Filesystem` interface --- .../smithy/kotlin/runtime/util/Filesystem.kt | 8 ++ .../smithy/kotlin/runtime/util/Platform.kt | 1 + .../kotlin/runtime/util/PlatformTest.kt | 45 ++++++++ .../smithy/kotlin/runtime/util/PlatformJVM.kt | 6 ++ .../kotlin/runtime/util/PlatformNative.kt | 102 ++++++++++++------ 5 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/PlatformTest.kt diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt index 143d2410ca..4ca1e1b31c 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt @@ -31,6 +31,12 @@ public interface Filesystem { */ public suspend fun writeFile(path: String, data: ByteArray) + /** + * Delete the file, if it exists. + * @param path fully qualified path encoded specifically to the target platform's filesystem + */ + public suspend fun deleteFile(path: String) + /** * Check if a file exists at the [path]. * @param path fully qualified path encoded specifically to the target platform's filesystem @@ -54,5 +60,7 @@ internal class MapFilesystem( override suspend fun writeFile(path: String, data: ByteArray) { memFs[path] = data } + + override suspend fun deleteFile(path: String) { memFs.remove(path) } override fun fileExists(path: String): Boolean = memFs[path] != null } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt index 684949943a..d0f2cc8585 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt @@ -43,6 +43,7 @@ internal expect object SystemDefaultProvider : PlatformProvider { override fun getenv(key: String): String? override suspend fun readFileOrNull(path: String): ByteArray? override suspend fun writeFile(path: String, data: ByteArray) + override suspend fun deleteFile(path: String) override val isJvm: Boolean override val isAndroid: Boolean override val isBrowser: Boolean diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/PlatformTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/PlatformTest.kt new file mode 100644 index 0000000000..3de848a060 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/PlatformTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.util + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class PlatformTest { + @Test + fun testReadWriteFile() = runTest { + val ps = PlatformProvider.System + val path = "file.txt" + val expected = "Hello, File!".encodeToByteArray() + + try { + ps.writeFile(path, expected) + + assertTrue(ps.fileExists(path)) + + val actual = ps.readFileOrNull(path) + assertContentEquals(expected, actual) + } finally { + ps.deleteFile(path) + } + } + + @Test + fun testGetEnv() = runTest { + assertNotNull(PlatformProvider.System.getenv("PATH")) + assertNull(PlatformProvider.System.getenv("THIS_ENV_VAR_IS_NOT_SET")) + } + + @Test + fun testOsInfo() = runTest { + val osInfo = PlatformProvider.System.osInfo() + assertNotEquals(OsFamily.Unknown, osInfo.family) + } +} diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt index 2495d8dd0c..eaf3a3a413 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt @@ -48,6 +48,12 @@ internal actual object SystemDefaultProvider : PlatformProvider { } } + actual override suspend fun deleteFile(path: String) { + withContext(Dispatchers.IO) { + File(path).delete() + } + } + actual override fun fileExists(path: String): Boolean = File(path).exists() public suspend fun readFileOrNull(path: Path): ByteArray? = readFileOrNull(path.toAbsolutePath().toString()) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index bb435ccaea..58138cb1a9 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -4,50 +4,90 @@ */ package aws.smithy.kotlin.runtime.util +import aws.smithy.kotlin.runtime.io.IOException +import kotlinx.cinterop.* +import platform.posix.* + internal actual object SystemDefaultProvider : PlatformProvider { - actual override fun getAllEnvVars(): Map { - TODO("Not yet implemented") - } + // FIXME How to get all environment variables on Native? + // See if it's possible to get extern char **environ loaded... is not available by default in platform.posix.* + // https://man7.org/linux/man-pages/man7/environ.7.html + actual override fun getAllEnvVars(): Map = mapOf() - actual override fun getenv(key: String): String? { - TODO("Not yet implemented") - } + actual override fun getenv(key: String): String? = platform.posix.getenv(key)?.toKString() actual override val filePathSeparator: String - get() = TODO("Not yet implemented") + get() = "/" // Unix-style separator is used in Native actual override suspend fun readFileOrNull(path: String): ByteArray? { - TODO("Not yet implemented") + return try { + val file = fopen(path, "rb") ?: return null + + try { + // Get file size + fseek(file, 0L, SEEK_END) + val size = ftell(file) + fseek(file, 0L, SEEK_SET) + + // Read file content + val buffer = ByteArray(size.toInt()).pin() + val rc = fread(buffer.addressOf(0), 1.toULong(), size.toULong(), file) + if (rc == size.toULong()) buffer.get() else null + } finally { + fclose(file) + } + } catch (e: Exception) { + null + } } actual override suspend fun writeFile(path: String, data: ByteArray) { - TODO("Not yet implemented") + val file = fopen(path, "wb") ?: throw IOException("Cannot open file for writing: $path") + try { + val wc = fwrite(data.refTo(0), 1.toULong(), data.size.toULong(), file) + if (wc != data.size.toULong()) { + throw IOException("Failed to write all bytes to file $path, expected ${data.size.toLong()}, wrote $wc") + } + } finally { + fclose(file) + } } - actual override fun fileExists(path: String): Boolean { - TODO("Not yet implemented") - } + actual override suspend fun deleteFile(path: String) { remove(path) } - actual override fun osInfo(): OperatingSystem { - TODO("Not yet implemented") - } + actual override fun fileExists(path: String): Boolean = access(path, F_OK) == 0 - actual override val isJvm: Boolean - get() = TODO("Not yet implemented") - actual override val isAndroid: Boolean - get() = TODO("Not yet implemented") - actual override val isBrowser: Boolean - get() = TODO("Not yet implemented") - actual override val isNode: Boolean - get() = TODO("Not yet implemented") - actual override val isNative: Boolean - get() = TODO("Not yet implemented") - - actual override fun getAllProperties(): Map { - TODO("Not yet implemented") - } + actual override fun osInfo(): OperatingSystem = memScoped { + val utsname = alloc() + uname(utsname.ptr) + + val sysName = utsname.sysname.toKString().lowercase() + val version = utsname.release.toKString() + val machine = utsname.machine.toKString() // Helps differentiate iOS/macOS - actual override fun getProperty(key: String): String? { - TODO("Not yet implemented") + val family = when { + sysName.contains("darwin") -> { + if (machine.startsWith("iPhone") || machine.startsWith("iPad")) { + OsFamily.Ios + } else { + OsFamily.MacOs + } + } + sysName.contains("linux") -> OsFamily.Linux + sysName.contains("windows") -> OsFamily.Windows + else -> OsFamily.Unknown + } + + return OperatingSystem(family, version) } + + actual override val isJvm: Boolean = false + actual override val isAndroid: Boolean = false + actual override val isBrowser: Boolean = false + actual override val isNode: Boolean = false + actual override val isNative: Boolean = true + + // Kotlin/Native doesn't have system properties + actual override fun getAllProperties(): Map = emptyMap() + actual override fun getProperty(key: String): String? = null } From 002099bd565c89a07bb164e89bd9176be1566bc3 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 12:35:41 -0500 Subject: [PATCH 33/54] ktlintFormat --- .../common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt | 4 +++- .../src/aws/smithy/kotlin/runtime/util/PlatformNative.kt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt index 4ca1e1b31c..f6f8a67290 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt @@ -61,6 +61,8 @@ internal class MapFilesystem( memFs[path] = data } - override suspend fun deleteFile(path: String) { memFs.remove(path) } + override suspend fun deleteFile(path: String) { + memFs.remove(path) + } override fun fileExists(path: String): Boolean = memFs[path] != null } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index 58138cb1a9..4aa4667704 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -53,7 +53,9 @@ internal actual object SystemDefaultProvider : PlatformProvider { } } - actual override suspend fun deleteFile(path: String) { remove(path) } + actual override suspend fun deleteFile(path: String) { + remove(path) + } actual override fun fileExists(path: String): Boolean = access(path, F_OK) == 0 From f2572f63effd0d8d455c6db20512cbe043b8a03f Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 12:37:21 -0500 Subject: [PATCH 34/54] Use upload-artifact@v4 --- .github/workflows/continuous-integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 3187339faf..365a70b22b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -73,7 +73,7 @@ jobs: - name: Save Test Reports if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.os }} path: '**/build/reports' @@ -107,7 +107,7 @@ jobs: - name: Save Test Reports if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.os }} path: '**/build/reports' From 9c201e2ada9afc1e9a255d7b4154832437daefe3 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 12:48:46 -0500 Subject: [PATCH 35/54] apiDump --- runtime/runtime-core/api/runtime-core.api | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 835afc0e39..f27a64cf03 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -2325,6 +2325,7 @@ public final class aws/smithy/kotlin/runtime/util/ExpiringValue { public abstract interface class aws/smithy/kotlin/runtime/util/Filesystem { public static final field Companion Laws/smithy/kotlin/runtime/util/Filesystem$Companion; + public abstract fun deleteFile (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun fileExists (Ljava/lang/String;)Z public abstract fun getFilePathSeparator ()Ljava/lang/String; public abstract fun readFileOrNull (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -2423,6 +2424,7 @@ public final class aws/smithy/kotlin/runtime/util/TestPlatformProvider : aws/smi public fun ()V public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Laws/smithy/kotlin/runtime/util/OperatingSystem;)V public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Laws/smithy/kotlin/runtime/util/OperatingSystem;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun deleteFile (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun fileExists (Ljava/lang/String;)Z public fun getAllEnvVars ()Ljava/util/Map; public fun getAllProperties ()Ljava/util/Map; From c1f2eefab0d25a9c3418d1bec2bfdc0744023ff7 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 12:53:37 -0500 Subject: [PATCH 36/54] Rename test to `SystemPlatformProviderTest` --- .../util/{PlatformTest.kt => SystemPlatformProviderTest.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/{PlatformTest.kt => SystemPlatformProviderTest.kt} (97%) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/PlatformTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt similarity index 97% rename from runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/PlatformTest.kt rename to runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index 3de848a060..a546c22de4 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/PlatformTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -12,7 +12,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class PlatformTest { +class SystemPlatformProviderTest { @Test fun testReadWriteFile() = runTest { val ps = PlatformProvider.System From e6f0735eda60530d9dc6518f037e98bef9394fa1 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 13:22:59 -0500 Subject: [PATCH 37/54] Check USERPROFILE too --- .../smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index a546c22de4..16e3e7e840 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -33,7 +33,10 @@ class SystemPlatformProviderTest { @Test fun testGetEnv() = runTest { - assertNotNull(PlatformProvider.System.getenv("PATH")) + val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI... + val result: String? = envVarKeys.fold(null) { acc, curr -> acc ?: PlatformProvider.System.getenv(curr) } + assertNotNull(result) + assertNull(PlatformProvider.System.getenv("THIS_ENV_VAR_IS_NOT_SET")) } From e6357f92abd84dd2276a0f9ba27204be610d0068 Mon Sep 17 00:00:00 2001 From: Xinsong Cui Date: Thu, 30 Jan 2025 16:34:10 -0500 Subject: [PATCH 38/54] misc: add telemetry configuration to DefaultAwsSigner (#1226) * add telemetry provider configuration * lint * address pr reviews * add changelog --- .../d3ce7511-6fb2-4435-8f46-db724551b384.json | 5 ++++ .../api/aws-signing-default.api | 8 ++++++ .../auth/awssigning/DefaultAwsSigner.kt | 27 ++++++++++++++++--- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .changes/d3ce7511-6fb2-4435-8f46-db724551b384.json diff --git a/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json b/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json new file mode 100644 index 0000000000..aa89d9167e --- /dev/null +++ b/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json @@ -0,0 +1,5 @@ +{ + "id": "d3ce7511-6fb2-4435-8f46-db724551b384", + "type": "misc", + "description": "Add telemetry provider configuration to `DefaultAwsSigner`" +} \ No newline at end of file diff --git a/runtime/auth/aws-signing-default/api/aws-signing-default.api b/runtime/auth/aws-signing-default/api/aws-signing-default.api index 3b9535c403..e00e6d829a 100644 --- a/runtime/auth/aws-signing-default/api/aws-signing-default.api +++ b/runtime/auth/aws-signing-default/api/aws-signing-default.api @@ -1,4 +1,12 @@ +public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerBuilder { + public fun ()V + public final fun build ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; + public final fun getTelemetryProvider ()Laws/smithy/kotlin/runtime/telemetry/TelemetryProvider; + public final fun setTelemetryProvider (Laws/smithy/kotlin/runtime/telemetry/TelemetryProvider;)V +} + public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerKt { + public static final fun DefaultAwsSigner (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; public static final fun getDefaultAwsSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; } diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt index df613d31b0..791d8fce0a 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt @@ -4,8 +4,10 @@ */ package aws.smithy.kotlin.runtime.auth.awssigning +import aws.smithy.kotlin.runtime.ExperimentalApi import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.telemetry.TelemetryProvider import aws.smithy.kotlin.runtime.telemetry.logging.logger import aws.smithy.kotlin.runtime.time.TimestampFormat import kotlin.coroutines.coroutineContext @@ -13,13 +15,30 @@ import kotlin.coroutines.coroutineContext /** The default implementation of [AwsSigner] */ public val DefaultAwsSigner: AwsSigner = DefaultAwsSignerImpl() +/** Creates a customized instance of [AwsSigner] */ +@Suppress("ktlint:standard:function-naming") +public fun DefaultAwsSigner(block: DefaultAwsSignerBuilder.() -> Unit): AwsSigner = + DefaultAwsSignerBuilder().apply(block).build() + +/** A builder class for creating instances of [AwsSigner] using the default implementation */ +public class DefaultAwsSignerBuilder { + public var telemetryProvider: TelemetryProvider? = null + + public fun build(): AwsSigner = DefaultAwsSignerImpl( + telemetryProvider = telemetryProvider, + ) +} + +@OptIn(ExperimentalApi::class) internal class DefaultAwsSignerImpl( private val canonicalizer: Canonicalizer = Canonicalizer.Default, private val signatureCalculator: SignatureCalculator = SignatureCalculator.Default, private val requestMutator: RequestMutator = RequestMutator.Default, + private val telemetryProvider: TelemetryProvider? = null, ) : AwsSigner { override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() // TODO: implement SigV4a if (config.algorithm != AwsSigningAlgorithm.SIGV4) { @@ -52,7 +71,8 @@ internal class DefaultAwsSignerImpl( prevSignature: ByteArray, config: AwsSigningConfig, ): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() val stringToSign = signatureCalculator.chunkStringToSign(chunkBody, prevSignature, config) logger.trace { "Chunk string to sign:\n$stringToSign" } @@ -70,7 +90,8 @@ internal class DefaultAwsSignerImpl( prevSignature: ByteArray, config: AwsSigningConfig, ): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() // FIXME - can we share canonicalization code more than we are..., also this reduce is inefficient. // canonicalize the headers From 22886ed84f584eb418131d733a2c422c4ea501da Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 13:55:17 -0500 Subject: [PATCH 39/54] Remove `deleteFile`, use temp dir --- runtime/runtime-core/api/runtime-core.api | 2 -- .../src/aws/smithy/kotlin/runtime/util/Filesystem.kt | 10 ---------- .../src/aws/smithy/kotlin/runtime/util/Platform.kt | 1 - .../kotlin/runtime/util/SystemPlatformProviderTest.kt | 2 +- .../src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt | 6 ------ .../aws/smithy/kotlin/runtime/util/PlatformNative.kt | 4 ---- 6 files changed, 1 insertion(+), 24 deletions(-) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index f27a64cf03..835afc0e39 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -2325,7 +2325,6 @@ public final class aws/smithy/kotlin/runtime/util/ExpiringValue { public abstract interface class aws/smithy/kotlin/runtime/util/Filesystem { public static final field Companion Laws/smithy/kotlin/runtime/util/Filesystem$Companion; - public abstract fun deleteFile (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun fileExists (Ljava/lang/String;)Z public abstract fun getFilePathSeparator ()Ljava/lang/String; public abstract fun readFileOrNull (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -2424,7 +2423,6 @@ public final class aws/smithy/kotlin/runtime/util/TestPlatformProvider : aws/smi public fun ()V public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Laws/smithy/kotlin/runtime/util/OperatingSystem;)V public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Laws/smithy/kotlin/runtime/util/OperatingSystem;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun deleteFile (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun fileExists (Ljava/lang/String;)Z public fun getAllEnvVars ()Ljava/util/Map; public fun getAllProperties ()Ljava/util/Map; diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt index f6f8a67290..143d2410ca 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Filesystem.kt @@ -31,12 +31,6 @@ public interface Filesystem { */ public suspend fun writeFile(path: String, data: ByteArray) - /** - * Delete the file, if it exists. - * @param path fully qualified path encoded specifically to the target platform's filesystem - */ - public suspend fun deleteFile(path: String) - /** * Check if a file exists at the [path]. * @param path fully qualified path encoded specifically to the target platform's filesystem @@ -60,9 +54,5 @@ internal class MapFilesystem( override suspend fun writeFile(path: String, data: ByteArray) { memFs[path] = data } - - override suspend fun deleteFile(path: String) { - memFs.remove(path) - } override fun fileExists(path: String): Boolean = memFs[path] != null } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt index d0f2cc8585..684949943a 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt @@ -43,7 +43,6 @@ internal expect object SystemDefaultProvider : PlatformProvider { override fun getenv(key: String): String? override suspend fun readFileOrNull(path: String): ByteArray? override suspend fun writeFile(path: String, data: ByteArray) - override suspend fun deleteFile(path: String) override val isJvm: Boolean override val isAndroid: Boolean override val isBrowser: Boolean diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index 16e3e7e840..e941388a25 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -16,7 +16,7 @@ class SystemPlatformProviderTest { @Test fun testReadWriteFile() = runTest { val ps = PlatformProvider.System - val path = "file.txt" + val path = "/tmp/testReadWriteFile-${Uuid.random()}.txt" val expected = "Hello, File!".encodeToByteArray() try { diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt index eaf3a3a413..2495d8dd0c 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/util/PlatformJVM.kt @@ -48,12 +48,6 @@ internal actual object SystemDefaultProvider : PlatformProvider { } } - actual override suspend fun deleteFile(path: String) { - withContext(Dispatchers.IO) { - File(path).delete() - } - } - actual override fun fileExists(path: String): Boolean = File(path).exists() public suspend fun readFileOrNull(path: Path): ByteArray? = readFileOrNull(path.toAbsolutePath().toString()) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index 4aa4667704..fd491ce24e 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -53,10 +53,6 @@ internal actual object SystemDefaultProvider : PlatformProvider { } } - actual override suspend fun deleteFile(path: String) { - remove(path) - } - actual override fun fileExists(path: String): Boolean = access(path, F_OK) == 0 actual override fun osInfo(): OperatingSystem = memScoped { From 992efbc50b85d5cce187f30aa3aa93c19c8be792 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 14:36:08 -0500 Subject: [PATCH 40/54] Remove usage of deleteFile --- .../runtime/util/SystemPlatformProviderTest.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index e941388a25..b26289365e 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -19,16 +19,11 @@ class SystemPlatformProviderTest { val path = "/tmp/testReadWriteFile-${Uuid.random()}.txt" val expected = "Hello, File!".encodeToByteArray() - try { - ps.writeFile(path, expected) + ps.writeFile(path, expected) + assertTrue(ps.fileExists(path)) - assertTrue(ps.fileExists(path)) - - val actual = ps.readFileOrNull(path) - assertContentEquals(expected, actual) - } finally { - ps.deleteFile(path) - } + val actual = ps.readFileOrNull(path) + assertContentEquals(expected, actual) } @Test From a8a22da504c5997bbf2572752ec478826df02b69 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 15:48:48 -0500 Subject: [PATCH 41/54] Use correct tempdir on windows --- .../kotlin/runtime/util/SystemPlatformProviderTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index b26289365e..58f56e4ceb 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -16,7 +16,12 @@ class SystemPlatformProviderTest { @Test fun testReadWriteFile() = runTest { val ps = PlatformProvider.System - val path = "/tmp/testReadWriteFile-${Uuid.random()}.txt" + + val tempDir = if (ps.osInfo().family == OsFamily.Windows) { + ps.getenv("TEMP") ?: "C:\\Windows\\Temp" + } else { "/tmp" } + val path = "$tempDir/testReadWriteFile-${Uuid.random()}.txt" + val expected = "Hello, File!".encodeToByteArray() ps.writeFile(path, expected) From 7115a34d4c549094bb1462b46d1af7bd6aff4d2f Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 16:05:24 -0500 Subject: [PATCH 42/54] ktlint --- .../smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index 58f56e4ceb..465f4f0714 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -19,7 +19,9 @@ class SystemPlatformProviderTest { val tempDir = if (ps.osInfo().family == OsFamily.Windows) { ps.getenv("TEMP") ?: "C:\\Windows\\Temp" - } else { "/tmp" } + } else { + "/tmp" + } val path = "$tempDir/testReadWriteFile-${Uuid.random()}.txt" val expected = "Hello, File!".encodeToByteArray() From c5a138fc627fc0a82feefc90b034e83d373c6f73 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 16:35:10 -0500 Subject: [PATCH 43/54] Remove transfer-encoding from expected signed headers --- .../kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt index 86d5744a62..58c161da99 100644 --- a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt +++ b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt @@ -154,7 +154,7 @@ public abstract class AwsHttpSignerTestBase( val op = buildOperation(streaming = true, replayable = false, requestBody = "a".repeat(AWS_CHUNKED_THRESHOLD + 1)) val expectedDate = "20201016T195600Z" val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + "Signature=ac341b9b248a0b23d2fcd9f7e805f4eb0b8a1b789bb23a8ec6adc6c48dd084ad" val signed = getSignedRequest(op) @@ -168,7 +168,7 @@ public abstract class AwsHttpSignerTestBase( val op = buildOperation(streaming = true, replayable = true, requestBody = "a".repeat(AWS_CHUNKED_THRESHOLD + 1)) val expectedDate = "20201016T195600Z" val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + "Signature=ac341b9b248a0b23d2fcd9f7e805f4eb0b8a1b789bb23a8ec6adc6c48dd084ad" val signed = getSignedRequest(op) @@ -183,7 +183,7 @@ public abstract class AwsHttpSignerTestBase( val expectedDate = "20201016T195600Z" // should have same signature as testSignAwsChunkedStreamNonReplayable(), except for the hash, since the body is different val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + "Signature=3f0277123c9ed8a8858f793886a0ac0fcb457bc54401ffc22d470f373397cff0" val signed = getSignedRequest(op) From cda0523d0042038dcb469b9fac743894dc2ad320 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 10:24:00 -0500 Subject: [PATCH 44/54] Revert --- .github/workflows/continuous-integration.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 372fdb8cda..365a70b22b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -163,7 +163,8 @@ jobs: working-directory: ./smithy-kotlin shell: bash run: | - echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties + # FIXME K2. Re-enable warnings as errors after this warning is removed: https://youtrack.jetbrains.com/issue/KT-68532 + # echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties ./gradlew -Paws.kotlin.native=false apiCheck ./gradlew -Paws.kotlin.native=false test jvmTest From d0dc45da3a9305badedf4fdcf11986ddb0e4af60 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 16:43:55 -0500 Subject: [PATCH 45/54] Implement getAllEnvVars --- gradle.properties | 1 + runtime/runtime-core/build.gradle.kts | 13 +++++++++++++ .../runtime/util/SystemPlatformProviderTest.kt | 16 +++++++++++++++- .../smithy/kotlin/runtime/util/PlatformNative.kt | 16 ++++++++++++---- .../native/src/nativeInterop/cinterop/environ.h | 12 ++++++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 runtime/runtime-core/native/src/nativeInterop/cinterop/environ.h diff --git a/gradle.properties b/gradle.properties index 9e53dbcaf0..af79919e9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ kotlin.code.style=official kotlin.incremental.js=true kotlin.incremental.multiplatform=true kotlin.mpp.stability.nowarn=true +kotlin.mpp.enableCInteropCommonization=true kotlin.native.ignoreDisabledTargets=true # atomicfu diff --git a/runtime/runtime-core/build.gradle.kts b/runtime/runtime-core/build.gradle.kts index 8fac2377f8..23cc56662e 100644 --- a/runtime/runtime-core/build.gradle.kts +++ b/runtime/runtime-core/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 @@ -54,4 +56,15 @@ kotlin { languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") } } + + targets.withType { + compilations["main"].cinterops { + val interopDir = "$projectDir/native/src/nativeInterop/cinterop" + create("environ") { + includeDirs(interopDir) + packageName("aws.smithy.platform.posix") + headers(listOf("$interopDir/environ.h")) + } + } + } } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index 465f4f0714..f9635db43e 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -35,13 +35,27 @@ class SystemPlatformProviderTest { @Test fun testGetEnv() = runTest { - val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI... + val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI val result: String? = envVarKeys.fold(null) { acc, curr -> acc ?: PlatformProvider.System.getenv(curr) } assertNotNull(result) assertNull(PlatformProvider.System.getenv("THIS_ENV_VAR_IS_NOT_SET")) } + @Test + fun testGetAllEnvVars() = runTest { + val allEnv = PlatformProvider.System.getAllEnvVars() + assertTrue(allEnv.isNotEmpty()) + + val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI + + var envContainsKey = false + envVarKeys.forEach { key -> + envContainsKey = envContainsKey || allEnv.contains(key) + } + assertTrue(envContainsKey) + } + @Test fun testOsInfo() = runTest { val osInfo = PlatformProvider.System.osInfo() diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index fd491ce24e..9ef1701b7c 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -5,14 +5,22 @@ package aws.smithy.kotlin.runtime.util import aws.smithy.kotlin.runtime.io.IOException +import aws.smithy.platform.posix.get_environ_ptr import kotlinx.cinterop.* import platform.posix.* internal actual object SystemDefaultProvider : PlatformProvider { - // FIXME How to get all environment variables on Native? - // See if it's possible to get extern char **environ loaded... is not available by default in platform.posix.* - // https://man7.org/linux/man-pages/man7/environ.7.html - actual override fun getAllEnvVars(): Map = mapOf() + actual override fun getAllEnvVars(): Map = memScoped { + val environ = get_environ_ptr() + generateSequence(0) { it + 1 } + .map { idx -> environ?.get(idx)?.toKString() } + .takeWhile { it != null } + .associate { env -> + val parts = env?.split("=", limit = 2) + check(parts?.size == 2) { "Environment entry $env is malformed" } + parts[0] to parts[1] + } + } actual override fun getenv(key: String): String? = platform.posix.getenv(key)?.toKString() diff --git a/runtime/runtime-core/native/src/nativeInterop/cinterop/environ.h b/runtime/runtime-core/native/src/nativeInterop/cinterop/environ.h new file mode 100644 index 0000000000..6262167426 --- /dev/null +++ b/runtime/runtime-core/native/src/nativeInterop/cinterop/environ.h @@ -0,0 +1,12 @@ +#ifndef ENVIRON_H +#define ENVIRON_H + +// External declaration to get environment variables +extern char **environ; + +// Helper function to get the environ pointer +char** get_environ_ptr() { + return environ; +} + +#endif From a674cf4b990ff455916d2d8f3fd81260d8de2f9e Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 16:55:01 -0500 Subject: [PATCH 46/54] Update filePathSeparator for Windows --- .../src/aws/smithy/kotlin/runtime/util/PlatformNative.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index 9ef1701b7c..c78bb06258 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -25,7 +25,10 @@ internal actual object SystemDefaultProvider : PlatformProvider { actual override fun getenv(key: String): String? = platform.posix.getenv(key)?.toKString() actual override val filePathSeparator: String - get() = "/" // Unix-style separator is used in Native + get() = when (osInfo().family) { + OsFamily.Windows -> "\\" + else -> "/" + } actual override suspend fun readFileOrNull(path: String): ByteArray? { return try { From 426bcfe241fc2bbdf08fbb6a45dfc46a17b68b33 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 16:55:14 -0500 Subject: [PATCH 47/54] e -> _ --- .../native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index c78bb06258..36fab22cba 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -47,7 +47,7 @@ internal actual object SystemDefaultProvider : PlatformProvider { } finally { fclose(file) } - } catch (e: Exception) { + } catch (_: Exception) { null } } From aa4446157f95229d6defbb9c2a2ed2293c0906aa Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 4 Feb 2025 11:00:41 -0500 Subject: [PATCH 48/54] PR feedback --- runtime/runtime-core/build.gradle.kts | 4 ++-- .../runtime/util/SystemPlatformProviderTest.kt | 16 +++++++--------- .../util/SystemPlatformProviderTestLinux.kt | 17 +++++++++++++++++ .../kotlin/runtime/util/PlatformNative.kt | 16 +++++++++------- 4 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 runtime/runtime-core/linux/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTestLinux.kt diff --git a/runtime/runtime-core/build.gradle.kts b/runtime/runtime-core/build.gradle.kts index 23cc56662e..7a4bdd17fa 100644 --- a/runtime/runtime-core/build.gradle.kts +++ b/runtime/runtime-core/build.gradle.kts @@ -1,10 +1,10 @@ -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget - /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + plugins { alias(libs.plugins.kotlinx.serialization) } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index f9635db43e..144c1b9170 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -18,7 +18,7 @@ class SystemPlatformProviderTest { val ps = PlatformProvider.System val tempDir = if (ps.osInfo().family == OsFamily.Windows) { - ps.getenv("TEMP") ?: "C:\\Windows\\Temp" + requireNotNull(ps.getenv("TEMP")) { "%TEMP% unexpectedly null" } } else { "/tmp" } @@ -36,8 +36,9 @@ class SystemPlatformProviderTest { @Test fun testGetEnv() = runTest { val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI - val result: String? = envVarKeys.fold(null) { acc, curr -> acc ?: PlatformProvider.System.getenv(curr) } - assertNotNull(result) + assertNotNull( + envVarKeys.firstNotNullOfOrNull { PlatformProvider.System.getenv(it) } + ) assertNull(PlatformProvider.System.getenv("THIS_ENV_VAR_IS_NOT_SET")) } @@ -48,12 +49,9 @@ class SystemPlatformProviderTest { assertTrue(allEnv.isNotEmpty()) val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI - - var envContainsKey = false - envVarKeys.forEach { key -> - envContainsKey = envContainsKey || allEnv.contains(key) - } - assertTrue(envContainsKey) + assertTrue( + envVarKeys.any { allEnv.contains(it) } + ) } @Test diff --git a/runtime/runtime-core/linux/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTestLinux.kt b/runtime/runtime-core/linux/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTestLinux.kt new file mode 100644 index 0000000000..e0e1331fdb --- /dev/null +++ b/runtime/runtime-core/linux/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTestLinux.kt @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.util + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class SystemPlatformProviderTestLinux { + @Test + fun testOsInfo() = runTest { + val osInfo = PlatformProvider.System.osInfo() + assertEquals(OsFamily.Linux, osInfo.family) + } +} \ No newline at end of file diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index 36fab22cba..6edb57b2f7 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -5,8 +5,10 @@ package aws.smithy.kotlin.runtime.util import aws.smithy.kotlin.runtime.io.IOException +import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers import aws.smithy.platform.posix.get_environ_ptr import kotlinx.cinterop.* +import kotlinx.coroutines.withContext import platform.posix.* internal actual object SystemDefaultProvider : PlatformProvider { @@ -17,7 +19,7 @@ internal actual object SystemDefaultProvider : PlatformProvider { .takeWhile { it != null } .associate { env -> val parts = env?.split("=", limit = 2) - check(parts?.size == 2) { "Environment entry $env is malformed" } + check(parts?.size == 2) { "Environment entry \"$env\" is malformed" } parts[0] to parts[1] } } @@ -30,9 +32,9 @@ internal actual object SystemDefaultProvider : PlatformProvider { else -> "/" } - actual override suspend fun readFileOrNull(path: String): ByteArray? { - return try { - val file = fopen(path, "rb") ?: return null + actual override suspend fun readFileOrNull(path: String): ByteArray? = withContext(SdkDispatchers.IO) { + try { + val file = fopen(path, "rb") ?: return@withContext null try { // Get file size @@ -42,7 +44,7 @@ internal actual object SystemDefaultProvider : PlatformProvider { // Read file content val buffer = ByteArray(size.toInt()).pin() - val rc = fread(buffer.addressOf(0), 1.toULong(), size.toULong(), file) + val rc = fread(buffer.addressOf(0), 1uL, size.toULong(), file) if (rc == size.toULong()) buffer.get() else null } finally { fclose(file) @@ -52,10 +54,10 @@ internal actual object SystemDefaultProvider : PlatformProvider { } } - actual override suspend fun writeFile(path: String, data: ByteArray) { + actual override suspend fun writeFile(path: String, data: ByteArray) = withContext(SdkDispatchers.IO) { val file = fopen(path, "wb") ?: throw IOException("Cannot open file for writing: $path") try { - val wc = fwrite(data.refTo(0), 1.toULong(), data.size.toULong(), file) + val wc = fwrite(data.refTo(0), 1uL, data.size.toULong(), file) if (wc != data.size.toULong()) { throw IOException("Failed to write all bytes to file $path, expected ${data.size.toLong()}, wrote $wc") } From 6bc63cfa606aa80722400799e499eb4733b4a8e9 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 4 Feb 2025 11:53:31 -0500 Subject: [PATCH 49/54] Add LinuxX64 test --- .../runtime/util/SystemPlatformProviderLinuxX64Test.kt} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename runtime/runtime-core/{linux/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTestLinux.kt => linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt} (75%) diff --git a/runtime/runtime-core/linux/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTestLinux.kt b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt similarity index 75% rename from runtime/runtime-core/linux/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTestLinux.kt rename to runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt index e0e1331fdb..b98b0910e8 100644 --- a/runtime/runtime-core/linux/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTestLinux.kt +++ b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt @@ -8,10 +8,15 @@ import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals -class SystemPlatformProviderTestLinux { +class SystemPlatformProviderLinuxX64Test { @Test fun testOsInfo() = runTest { val osInfo = PlatformProvider.System.osInfo() assertEquals(OsFamily.Linux, osInfo.family) } + + @Test + fun definitelyShouldFail() = runTest { + assertEquals(1, 2) + } } \ No newline at end of file From 5e5c5411be7ff3678bb62c5a61bcb4af17dea425 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 4 Feb 2025 11:53:53 -0500 Subject: [PATCH 50/54] ktlint --- .../smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt | 4 ++-- .../kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt index 144c1b9170..734f788e49 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -37,7 +37,7 @@ class SystemPlatformProviderTest { fun testGetEnv() = runTest { val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI assertNotNull( - envVarKeys.firstNotNullOfOrNull { PlatformProvider.System.getenv(it) } + envVarKeys.firstNotNullOfOrNull { PlatformProvider.System.getenv(it) }, ) assertNull(PlatformProvider.System.getenv("THIS_ENV_VAR_IS_NOT_SET")) @@ -50,7 +50,7 @@ class SystemPlatformProviderTest { val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI assertTrue( - envVarKeys.any { allEnv.contains(it) } + envVarKeys.any { allEnv.contains(it) }, ) } diff --git a/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt index b98b0910e8..9d4d398cce 100644 --- a/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt +++ b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt @@ -19,4 +19,4 @@ class SystemPlatformProviderLinuxX64Test { fun definitelyShouldFail() = runTest { assertEquals(1, 2) } -} \ No newline at end of file +} From e282e5c3087607bfebd2821cc64367b358b95077 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 4 Feb 2025 12:36:30 -0500 Subject: [PATCH 51/54] Expand support of Apple platforms --- .../src/aws/smithy/kotlin/runtime/util/Platform.kt | 7 +++++++ .../smithy/kotlin/runtime/util/PlatformNative.kt | 13 ++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt index 684949943a..2837cce21e 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.util import aws.smithy.kotlin.runtime.InternalApi +import kotlinx.coroutines.awaitAll public interface PlatformEnvironProvider : EnvironmentProvider, @@ -58,6 +59,9 @@ public enum class OsFamily { Windows, Android, Ios, + IpadOs, + TvOs, + WatchOs, Unknown, ; @@ -67,6 +71,9 @@ public enum class OsFamily { Windows -> "windows" Android -> "android" Ios -> "ios" + IpadOs -> "ipados" + TvOs -> "tvos" + WatchOs -> "watchos" Unknown -> "unknown" } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index 6edb57b2f7..220e66fde5 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -74,14 +74,17 @@ internal actual object SystemDefaultProvider : PlatformProvider { val sysName = utsname.sysname.toKString().lowercase() val version = utsname.release.toKString() - val machine = utsname.machine.toKString() // Helps differentiate iOS/macOS + val machine = utsname.machine.toKString().lowercase() // Helps differentiate Apple platforms val family = when { sysName.contains("darwin") -> { - if (machine.startsWith("iPhone") || machine.startsWith("iPad")) { - OsFamily.Ios - } else { - OsFamily.MacOs + when { + machine.startsWith("iphone") -> OsFamily.Ios + // TODO Validate that iPadOS/tvOS/watchOS resolves correctly on each of these devices + machine.startsWith("ipad") -> OsFamily.IpadOs + machine.startsWith("tv") -> OsFamily.TvOs + machine.startsWith("watch") -> OsFamily.WatchOs + else -> OsFamily.MacOs } } sysName.contains("linux") -> OsFamily.Linux From ee85998ae72b2c76f4f046dd3c29b926ca6d6250 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 4 Feb 2025 12:36:54 -0500 Subject: [PATCH 52/54] Remove debug test --- .../runtime/util/SystemPlatformProviderLinuxX64Test.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt index 9d4d398cce..f6ca8da166 100644 --- a/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt +++ b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt @@ -14,9 +14,4 @@ class SystemPlatformProviderLinuxX64Test { val osInfo = PlatformProvider.System.osInfo() assertEquals(OsFamily.Linux, osInfo.family) } - - @Test - fun definitelyShouldFail() = runTest { - assertEquals(1, 2) - } } From 7dc3766d210f0b6e8fec3941f4f70143115ea3d6 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 4 Feb 2025 12:37:37 -0500 Subject: [PATCH 53/54] ktlint --- .../common/src/aws/smithy/kotlin/runtime/util/Platform.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt index 2837cce21e..b2b56a11b7 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt @@ -6,7 +6,6 @@ package aws.smithy.kotlin.runtime.util import aws.smithy.kotlin.runtime.InternalApi -import kotlinx.coroutines.awaitAll public interface PlatformEnvironProvider : EnvironmentProvider, From 8142608319b2bf9ad2a8323096a1b956534b42f1 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 4 Feb 2025 12:42:28 -0500 Subject: [PATCH 54/54] apiDump new OsFamily --- runtime/runtime-core/api/runtime-core.api | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index b864b68dce..6ccc7cb1e2 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -2383,9 +2383,12 @@ public final class aws/smithy/kotlin/runtime/util/OperatingSystem { public final class aws/smithy/kotlin/runtime/util/OsFamily : java/lang/Enum { public static final field Android Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Ios Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field IpadOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Linux Laws/smithy/kotlin/runtime/util/OsFamily; public static final field MacOs Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field TvOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Unknown Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field WatchOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Windows Laws/smithy/kotlin/runtime/util/OsFamily; public static fun getEntries ()Lkotlin/enums/EnumEntries; public fun toString ()Ljava/lang/String;