Skip to content

Commit 764701c

Browse files
authored
feat: bootstrap event streams (#545)
* Bootstrap event streams (#537) * fix event stream filtering * add parsing of common headers * refractor frame decoder into a Flow * remove event stream operation filter from customizations * refactore event stream parsing; implement rough deserialization codegen * fix warning * filter out event stream errors * render deserialization for exception event stream messages * inject http request signature into the execution context once known * add support for chunked signing * add encode transform for message stream * inline signing config builder * initial event stream serialize implementation * fix compile issues * disable wip integration tests * suppress test; cleanup codegen * Event Stream Codegen Tests (#542) * Checkpoint Event Streams (#544) * fix tests * increase windows runner memory
1 parent 82c1f1e commit 764701c

File tree

40 files changed

+1819
-130
lines changed

40 files changed

+1819
-130
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
# windows job runs out of memory with the defaults normally used
7373
shell: bash
7474
run: |
75-
sed -i 's/org\.gradle\.jvmargs=.*$/org.gradle.jvmargs=-Xmx4g/' gradle.properties
75+
sed -i 's/org\.gradle\.jvmargs=.*$/org.gradle.jvmargs=-Xmx5g/' gradle.properties
7676
cat gradle.properties
7777
- name: Build and Test ${{ env.PACKAGE_NAME }}
7878
run: |

aws-runtime/aws-core/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ kotlin {
1515
commonMain {
1616
dependencies {
1717
api("aws.smithy.kotlin:runtime-core:$smithyKotlinVersion")
18+
19+
// FIXME - should we just move these into core and get rid of aws-types at this point?
20+
api(project(":aws-runtime:aws-types"))
1821
implementation("aws.smithy.kotlin:logging:$smithyKotlinVersion")
1922
}
2023
}

aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/execution/AuthAttributes.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55

66
package aws.sdk.kotlin.runtime.execution
77

8+
import aws.sdk.kotlin.runtime.auth.credentials.CredentialsProvider
89
import aws.smithy.kotlin.runtime.client.ClientOption
910
import aws.smithy.kotlin.runtime.time.Instant
11+
import aws.smithy.kotlin.runtime.util.AttributeKey
12+
import aws.smithy.kotlin.runtime.util.InternalApi
1013

1114
/**
1215
* Operation (execution) options related to authorization
@@ -31,7 +34,36 @@ public object AuthAttributes {
3134

3235
/**
3336
* Override the date to complete the signing process with. Defaults to current time when not specified.
34-
* NOTE: This is not a common option.
37+
*
38+
* NOTE: This is an advanced configuration option that does not normally need to be set manually.
3539
*/
3640
public val SigningDate: ClientOption<Instant> = ClientOption("SigningDate")
41+
42+
/**
43+
* The [CredentialsProvider] to complete the signing process with. Defaults to the provider configured
44+
* on the service client.
45+
*
46+
* NOTE: This is an advanced configuration option that does not normally need to be set manually.
47+
*/
48+
public val CredentialsProvider: ClientOption<CredentialsProvider> = ClientOption("CredentialsProvider")
49+
50+
/**
51+
* The precomputed signed body value. See [aws.sdk.kotlin.runtime.auth.signing.AwsSigningConfig.signedBodyValue].
52+
*
53+
* NOTE: This is an advanced configuration option that does not normally need to be set manually.
54+
*/
55+
public val SignedBodyValue: ClientOption<String> = ClientOption("SignedBodyValue")
56+
57+
/**
58+
* The signed body header type. See [aws.sdk.kotlin.runtime.auth.signing.AwsSigningConfig.signedBodyHeaderType].
59+
*
60+
* NOTE: This is an advanced configuration option that does not normally need to be set manually.
61+
*/
62+
public val SignedBodyHeaderType: ClientOption<String> = ClientOption("SignedBodyHeaderType")
63+
64+
/**
65+
* The signature of the HTTP request. This will only exist after the request has been signed!
66+
*/
67+
@InternalApi
68+
public val RequestSignature: AttributeKey<ByteArray> = AttributeKey("AWS_HTTP_SIGNATURE")
3769
}

aws-runtime/aws-signing/common/src/aws/sdk/kotlin/runtime/auth/signing/AwsSigV4SigningMiddleware.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ public class AwsSigV4SigningMiddleware(private val config: Config) : ModifyReque
6969
*/
7070
public var omitSessionToken: Boolean = false
7171

72+
/**
73+
* Optional string to use as the canonical request's body public value.
74+
* If string is empty, a public value will be calculated from the payload during signing.
75+
* Typically, this is the SHA-256 of the (request/chunk/event) payload, written as lowercase hex.
76+
* If this has been precalculated, it can be set here. Special public values used by certain services can also be set
77+
* (e.g. "UNSIGNED-PAYLOAD" "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" "STREAMING-AWS4-HMAC-SHA256-EVENTS").
78+
*/
79+
public val signedBodyValue: String? = null
80+
7281
/**
7382
* Controls what body "hash" header, if any, should be added to the canonical request and the signed request.
7483
* Most services do not require this additional header.
@@ -117,6 +126,10 @@ public class AwsSigV4SigningMiddleware(private val config: Config) : ModifyReque
117126
// then we must decide how to compute the payload hash ourselves (defaults to unsigned payload)
118127
val isUnboundedStream = signableRequest.body == null && req.subject.body is HttpBody.Streaming
119128

129+
// favor attributes from the current request context
130+
val precomputedSignedBodyValue = req.context.getOrNull(AuthAttributes.SignedBodyValue)
131+
val precomputedHeaderType = req.context.getOrNull(AuthAttributes.SignedBodyHeaderType)?.let { AwsSignedBodyHeaderType.valueOf(it) }
132+
120133
// operation signing config is baseConfig + operation specific config/overrides
121134
val opSigningConfig = AwsSigningConfig {
122135
region = req.context[AuthAttributes.SigningRegion]
@@ -131,8 +144,9 @@ public class AwsSigV4SigningMiddleware(private val config: Config) : ModifyReque
131144
useDoubleUriEncode = config.useDoubleUriEncode
132145
expiresAfter = config.expiresAfter
133146

134-
signedBodyHeader = config.signedBodyHeaderType
147+
signedBodyHeader = precomputedHeaderType ?: config.signedBodyHeaderType
135148
signedBodyValue = when {
149+
precomputedSignedBodyValue != null -> precomputedSignedBodyValue
136150
isUnsignedRequest -> AwsSignedBodyValue.UNSIGNED_PAYLOAD
137151
req.subject.body is HttpBody.Empty -> AwsSignedBodyValue.EMPTY_SHA256
138152
isUnboundedStream -> {
@@ -144,7 +158,12 @@ public class AwsSigV4SigningMiddleware(private val config: Config) : ModifyReque
144158
}
145159
}
146160

147-
val signedRequest = AwsSigner.signRequest(signableRequest, opSigningConfig.toCrt())
161+
val signingResult = AwsSigner.sign(signableRequest, opSigningConfig.toCrt())
162+
val signedRequest = checkNotNull(signingResult.signedRequest) { "signing result must return a non-null HTTP request" }
163+
164+
// Add the signature to the request context
165+
req.context[AuthAttributes.RequestSignature] = signingResult.signature
166+
148167
req.subject.update(signedRequest)
149168
req.subject.body.resetStream()
150169

aws-runtime/aws-signing/common/src/aws/sdk/kotlin/runtime/auth/signing/AwsSigning.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package aws.sdk.kotlin.runtime.auth.signing
77

88
import aws.sdk.kotlin.crt.auth.signing.AwsSigner
9+
import aws.sdk.kotlin.runtime.InternalSdkApi
910
import aws.sdk.kotlin.runtime.crt.toSignableCrtRequest
1011
import aws.sdk.kotlin.runtime.crt.update
1112
import aws.smithy.kotlin.runtime.http.request.HttpRequest
@@ -61,3 +62,18 @@ public suspend fun sign(request: HttpRequest, config: AwsSigningConfig): Signing
6162
val output = builder.build()
6263
return SigningResult(output, crtResult.signature)
6364
}
65+
66+
/**
67+
* Sign a body [chunk] using the given signing [config]
68+
*
69+
* @param chunk the body chunk to sign
70+
* @param prevSignature the signature of the previous component of the request (either the initial request signature
71+
* itself for the first chunk or the previous chunk otherwise)
72+
* @param config the signing configuration to use
73+
* @return the signing result
74+
*/
75+
@InternalSdkApi
76+
public suspend fun sign(chunk: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): SigningResult<Unit> {
77+
val crtResult = AwsSigner.signChunk(chunk, prevSignature, config.toCrt())
78+
return SigningResult(Unit, crtResult.signature)
79+
}

aws-runtime/aws-signing/common/src/aws/sdk/kotlin/runtime/auth/signing/AwsSigningConfig.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package aws.sdk.kotlin.runtime.auth.signing
77

8+
import aws.sdk.kotlin.runtime.InternalSdkApi
89
import aws.sdk.kotlin.runtime.auth.credentials.Credentials
910
import aws.sdk.kotlin.runtime.auth.credentials.CredentialsProvider
1011
import aws.smithy.kotlin.runtime.time.Instant
@@ -21,7 +22,7 @@ public typealias ShouldSignHeaderFn = (String) -> Boolean
2122
*/
2223
public class AwsSigningConfig private constructor(builder: Builder) {
2324
public companion object {
24-
public operator fun invoke(block: Builder.() -> Unit): AwsSigningConfig = Builder().apply(block).build()
25+
public inline operator fun invoke(block: Builder.() -> Unit): AwsSigningConfig = Builder().apply(block).build()
2526
}
2627
/**
2728
* The region to sign against
@@ -119,6 +120,27 @@ public class AwsSigningConfig private constructor(builder: Builder) {
119120
*/
120121
public val expiresAfter: Duration? = builder.expiresAfter
121122

123+
@InternalSdkApi
124+
public fun toBuilder(): Builder {
125+
val config = this
126+
return Builder().apply {
127+
region = config.region
128+
service = config.service
129+
date = config.date
130+
algorithm = config.algorithm
131+
shouldSignHeader = config.shouldSignHeader
132+
signatureType = config.signatureType
133+
useDoubleUriEncode = config.useDoubleUriEncode
134+
normalizeUriPath = config.normalizeUriPath
135+
omitSessionToken = config.omitSessionToken
136+
signedBodyValue = config.signedBodyValue
137+
signedBodyHeader = config.signedBodyHeaderType
138+
credentials = config.credentials
139+
credentialsProvider = config.credentialsProvider
140+
expiresAfter = config.expiresAfter
141+
}
142+
}
143+
122144
public class Builder {
123145
public var region: String? = null
124146
public var service: String? = null
@@ -135,7 +157,8 @@ public class AwsSigningConfig private constructor(builder: Builder) {
135157
public var credentialsProvider: CredentialsProvider? = null
136158
public var expiresAfter: Duration? = null
137159

138-
internal fun build(): AwsSigningConfig = AwsSigningConfig(this)
160+
@InternalSdkApi
161+
public fun build(): AwsSigningConfig = AwsSigningConfig(this)
139162
}
140163
}
141164

aws-runtime/aws-signing/common/test/aws/sdk/kotlin/runtime/auth/signing/AwsSigningTest.kt

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55

66
package aws.sdk.kotlin.runtime.auth.signing
77

8+
import aws.sdk.kotlin.runtime.auth.credentials.Credentials
89
import aws.smithy.kotlin.runtime.http.HttpMethod
10+
import aws.smithy.kotlin.runtime.http.Url
911
import aws.smithy.kotlin.runtime.http.content.ByteArrayContent
12+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1013
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
14+
import aws.smithy.kotlin.runtime.http.request.headers
15+
import aws.smithy.kotlin.runtime.http.request.url
1116
import aws.smithy.kotlin.runtime.time.Instant
1217
import kotlinx.coroutines.ExperimentalCoroutinesApi
1318
import kotlinx.coroutines.test.runTest
@@ -83,4 +88,102 @@ class AwsSigningTest {
8388
val authHeader = result.output.headers["Authorization"]!!
8489
assertTrue(authHeader.contains(expectedPrefix), "Sigv4A auth header: $authHeader")
8590
}
91+
92+
// based on: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html#example-signature-calculations-streaming
93+
private val CHUNKED_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
94+
private val CHUNKED_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
95+
private val CHUNKED_TEST_CREDENTIALS = Credentials(CHUNKED_ACCESS_KEY_ID, CHUNKED_SECRET_ACCESS_KEY)
96+
private val CHUNKED_TEST_REGION = "us-east-1"
97+
private val CHUNKED_TEST_SERVICE = "s3"
98+
private val CHUNKED_TEST_SIGNING_TIME = "2013-05-24T00:00:00Z"
99+
private val CHUNK1_SIZE = 65536
100+
private val CHUNK2_SIZE = 1024
101+
102+
private val EXPECTED_CHUNK_REQUEST_AUTHORIZATION_HEADER =
103+
"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, " +
104+
"SignedHeaders=content-encoding;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-" +
105+
"amz-storage-class, Signature=4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9"
106+
107+
private val EXPECTED_REQUEST_SIGNATURE = "4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9"
108+
private val EXPECTED_FIRST_CHUNK_SIGNATURE = "ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648"
109+
private val EXPECTED_SECOND_CHUNK_SIGNATURE = "0055627c9e194cb4542bae2aa5492e3c1575bbb81b612b7d234b86a503ef5497"
110+
private val EXPECTED_FINAL_CHUNK_SIGNATURE = "b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9"
111+
private val EXPECTED_TRAILING_HEADERS_SIGNATURE = "df5735bd9f3295cd9386572292562fefc93ba94e80a0a1ddcbd652c4e0a75e6c"
112+
113+
private fun createChunkedRequestSigningConfig(): AwsSigningConfig = AwsSigningConfig {
114+
algorithm = AwsSigningAlgorithm.SIGV4
115+
signatureType = AwsSignatureType.HTTP_REQUEST_VIA_HEADERS
116+
region = CHUNKED_TEST_REGION
117+
service = CHUNKED_TEST_SERVICE
118+
date = Instant.fromIso8601(CHUNKED_TEST_SIGNING_TIME)
119+
useDoubleUriEncode = false
120+
normalizeUriPath = true
121+
signedBodyHeader = AwsSignedBodyHeaderType.X_AMZ_CONTENT_SHA256
122+
signedBodyValue = AwsSignedBodyValue.STREAMING_AWS4_HMAC_SHA256_PAYLOAD
123+
credentials = CHUNKED_TEST_CREDENTIALS
124+
}
125+
126+
private fun createChunkedSigningConfig(): AwsSigningConfig = AwsSigningConfig {
127+
algorithm = AwsSigningAlgorithm.SIGV4
128+
signatureType = AwsSignatureType.HTTP_REQUEST_CHUNK
129+
region = CHUNKED_TEST_REGION
130+
service = CHUNKED_TEST_SERVICE
131+
date = Instant.fromIso8601(CHUNKED_TEST_SIGNING_TIME)
132+
useDoubleUriEncode = false
133+
normalizeUriPath = true
134+
signedBodyHeader = AwsSignedBodyHeaderType.NONE
135+
credentials = CHUNKED_TEST_CREDENTIALS
136+
}
137+
138+
private fun createChunkedTestRequest() = HttpRequest {
139+
method = HttpMethod.PUT
140+
url(Url.parse("https://s3.amazonaws.com/examplebucket/chunkObject.txt"))
141+
headers {
142+
set("Host", url.host)
143+
set("x-amz-storage-class", "REDUCED_REDUNDANCY")
144+
set("Content-Encoding", "aws-chunked")
145+
set("x-amz-decoded-content-length", "66560")
146+
set("Content-Length", "66824")
147+
}
148+
}
149+
150+
private fun chunk1(): ByteArray {
151+
val chunk = ByteArray(CHUNK1_SIZE)
152+
for (i in chunk.indices) {
153+
chunk[i] = 'a'.code.toByte()
154+
}
155+
return chunk
156+
}
157+
158+
private fun chunk2(): ByteArray {
159+
val chunk = ByteArray(CHUNK2_SIZE)
160+
for (i in chunk.indices) {
161+
chunk[i] = 'a'.code.toByte()
162+
}
163+
return chunk
164+
}
165+
166+
@Test
167+
fun testSignChunks() = runTest {
168+
val request = createChunkedTestRequest()
169+
val chunkedRequestConfig = createChunkedRequestSigningConfig()
170+
val requestResult = sign(request, chunkedRequestConfig)
171+
assertEquals(EXPECTED_CHUNK_REQUEST_AUTHORIZATION_HEADER, requestResult.output.headers["Authorization"])
172+
assertEquals(EXPECTED_REQUEST_SIGNATURE, requestResult.signature.decodeToString())
173+
174+
var prevSignature = requestResult.signature
175+
176+
val chunkedSigningConfig = createChunkedSigningConfig()
177+
val chunk1Result = sign(chunk1(), prevSignature, chunkedSigningConfig)
178+
assertEquals(EXPECTED_FIRST_CHUNK_SIGNATURE, chunk1Result.signature.decodeToString())
179+
prevSignature = chunk1Result.signature
180+
181+
val chunk2Result = sign(chunk2(), prevSignature, chunkedSigningConfig)
182+
assertEquals(EXPECTED_SECOND_CHUNK_SIGNATURE, chunk2Result.signature.decodeToString())
183+
prevSignature = chunk2Result.signature
184+
185+
// TODO - do we want 0 byte data like this or just allow null?
186+
val finalChunkResult = sign(ByteArray(0), prevSignature, chunkedSigningConfig)
187+
assertEquals(EXPECTED_FINAL_CHUNK_SIGNATURE, finalChunkResult.signature.decodeToString())
188+
}
86189
}

aws-runtime/protocols/aws-event-stream/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,27 @@ description = "Support for the vnd.amazon.event-stream content type"
77
extra["displayName"] = "AWS :: SDK :: Kotlin :: Protocols :: Event Stream"
88
extra["moduleName"] = "aws.sdk.kotlin.runtime.protocol.eventstream"
99

10+
val smithyKotlinVersion: String by project
11+
val coroutinesVersion: String by project
1012
kotlin {
1113
sourceSets {
1214
commonMain {
1315
dependencies {
1416
api(project(":aws-runtime:aws-core"))
17+
// exposes Buffer/MutableBuffer and SdkByteReadChannel
18+
api("aws.smithy.kotlin:io:$smithyKotlinVersion")
19+
// exposes Flow<T>
20+
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
21+
22+
// exposes AwsSigningConfig
23+
api(project(":aws-runtime:aws-signing"))
1524
}
1625
}
1726

1827
commonTest {
1928
dependencies {
2029
implementation(project(":aws-runtime:testing"))
30+
api("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
2131
}
2232
}
2333

0 commit comments

Comments
 (0)