Skip to content

Commit f29e750

Browse files
authored
feat: interceptors (#775)
1 parent 5773afb commit f29e750

File tree

44 files changed

+2316
-302
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2316
-302
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "2a4ab4f2-bd6e-4de5-be7e-5bd1fe02b07c",
3+
"type": "feature",
4+
"description": "Add capability to intercept SDK operations",
5+
"issues": [
6+
"awslabs/smithy-kotlin#122"
7+
]
8+
}

docs/design/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ Start here for an overview:
2121
* [Retries](retries.md)
2222
* [Tracing](tracing.md)
2323
* [Waiters](waiters.md)
24+
* [Interceptors](interceptors.md)

docs/design/interceptors.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,12 @@ public interface Interceptor<
364364
/**
365365
* [Interceptor] context used for all phases that only have access to the operation input (request)
366366
*/
367-
public interface RequestInterceptorContext<I> : Attributes {
367+
public interface RequestInterceptorContext<I> {
368+
369+
/**
370+
* The [ExecutionContext] used to drive the execution of a single request/response
371+
*/
372+
public val executionContext: ExecutionContext
368373

369374
/**
370375
* Retrieve the modeled request for the operation being invoked
Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,35 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
package aws.smithy.kotlin.runtime.auth.awssigning.middleware
5+
package aws.smithy.kotlin.runtime.auth.awssigning
66

77
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
8-
import aws.smithy.kotlin.runtime.auth.awssigning.*
98
import aws.smithy.kotlin.runtime.auth.awssigning.internal.*
109
import aws.smithy.kotlin.runtime.auth.awssigning.internal.isEligibleForAwsChunkedStreaming
1110
import aws.smithy.kotlin.runtime.auth.awssigning.internal.setAwsChunkedBody
1211
import aws.smithy.kotlin.runtime.auth.awssigning.internal.setAwsChunkedHeaders
1312
import aws.smithy.kotlin.runtime.auth.awssigning.internal.useAwsChunkedEncoding
13+
import aws.smithy.kotlin.runtime.client.ExecutionContext
1414
import aws.smithy.kotlin.runtime.http.HttpBody
15-
import aws.smithy.kotlin.runtime.http.operation.*
15+
import aws.smithy.kotlin.runtime.http.auth.HttpSigner
1616
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1717
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
1818
import aws.smithy.kotlin.runtime.util.InternalApi
1919
import aws.smithy.kotlin.runtime.util.get
2020
import kotlin.time.Duration
2121

2222
/**
23-
* HTTP request pipeline middleware that signs outgoing requests
23+
* AWS SigV4/SigV4a [HttpSigner] that signs outgoing requests using the given [config]
2424
*/
2525
@InternalApi
26-
public class AwsSigningMiddleware(private val config: Config) : ModifyRequestMiddleware {
26+
public class AwsHttpSigner(private val config: Config) : HttpSigner {
2727
public companion object {
28-
public inline operator fun invoke(block: Config.() -> Unit): AwsSigningMiddleware {
28+
public inline operator fun invoke(block: Config.() -> Unit): AwsHttpSigner {
2929
val config = Config().apply(block)
3030
requireNotNull(config.credentialsProvider) { "A credentials provider must be specified for the middleware" }
3131
requireNotNull(config.service) { "A service must be specified for the middleware" }
3232
requireNotNull(config.signer) { "A signer must be specified for the middleware" }
33-
return AwsSigningMiddleware(config)
33+
return AwsHttpSigner(config)
3434
}
3535

3636
@InternalApi
@@ -105,24 +105,20 @@ public class AwsSigningMiddleware(private val config: Config) : ModifyRequestMid
105105
public var expiresAfter: Duration? = null
106106
}
107107

108-
override fun install(op: SdkHttpOperation<*, *>) {
109-
op.execution.finalize.register(this)
110-
}
111-
112-
override suspend fun modifyRequest(req: SdkHttpRequest): SdkHttpRequest {
113-
val body = req.subject.body
108+
override suspend fun sign(context: ExecutionContext, request: HttpRequestBuilder) {
109+
val body = request.body
114110

115111
// favor attributes from the current request context
116-
val contextHashSpecification = req.context.getOrNull(AwsSigningAttributes.HashSpecification)
117-
val contextSignedBodyHeader = req.context.getOrNull(AwsSigningAttributes.SignedBodyHeader)
112+
val contextHashSpecification = context.getOrNull(AwsSigningAttributes.HashSpecification)
113+
val contextSignedBodyHeader = context.getOrNull(AwsSigningAttributes.SignedBodyHeader)
118114

119115
// operation signing config is baseConfig + operation specific config/overrides
120116
val signingConfig = AwsSigningConfig {
121-
region = req.context[AwsSigningAttributes.SigningRegion]
122-
service = req.context.getOrNull(AwsSigningAttributes.SigningService) ?: checkNotNull(config.service)
117+
region = context[AwsSigningAttributes.SigningRegion]
118+
service = context.getOrNull(AwsSigningAttributes.SigningService) ?: checkNotNull(config.service)
123119
credentialsProvider = checkNotNull(config.credentialsProvider)
124120
algorithm = config.algorithm
125-
signingDate = req.context.getOrNull(AwsSigningAttributes.SigningDate)
121+
signingDate = context.getOrNull(AwsSigningAttributes.SigningDate)
126122

127123
signatureType = config.signatureType
128124
omitSessionToken = config.omitSessionToken
@@ -140,7 +136,7 @@ public class AwsSigningMiddleware(private val config: Config) : ModifyRequestMid
140136
config.isUnsignedPayload -> HashSpecification.UnsignedPayload
141137
body is HttpBody.Empty -> HashSpecification.EmptyBody
142138
body.isEligibleForAwsChunkedStreaming -> {
143-
if (req.subject.headers.contains("x-amz-trailer")) {
139+
if (request.headers.contains("x-amz-trailer")) {
144140
HashSpecification.StreamingAws4HmacSha256PayloadWithTrailers
145141
} else {
146142
HashSpecification.StreamingAws4HmacSha256Payload
@@ -152,21 +148,20 @@ public class AwsSigningMiddleware(private val config: Config) : ModifyRequestMid
152148
}
153149

154150
if (signingConfig.useAwsChunkedEncoding) {
155-
req.subject.setAwsChunkedHeaders()
151+
request.setAwsChunkedHeaders()
156152
}
157153

158-
val signingResult = checkNotNull(config.signer).sign(req.subject.build(), signingConfig)
154+
val signingResult = checkNotNull(config.signer).sign(request.build(), signingConfig)
159155
val signedRequest = signingResult.output
160156

161157
// Add the signature to the request context
162-
req.context[AwsSigningAttributes.RequestSignature] = signingResult.signature
158+
context[AwsSigningAttributes.RequestSignature] = signingResult.signature
163159

164-
req.subject.update(signedRequest)
160+
request.update(signedRequest)
165161

166162
if (signingConfig.useAwsChunkedEncoding) {
167-
req.subject.setAwsChunkedBody(checkNotNull(config.signer), signingConfig, signingResult.signature)
163+
request.setAwsChunkedBody(checkNotNull(config.signer), signingConfig, signingResult.signature)
168164
}
169-
return req
170165
}
171166
}
172167

runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/internal/AwsChunkedUtil.kt

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

66
package aws.smithy.kotlin.runtime.auth.awssigning.internal
77

8+
import aws.smithy.kotlin.runtime.auth.awssigning.AwsHttpSigner
89
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigner
910
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig
1011
import aws.smithy.kotlin.runtime.auth.awssigning.HashSpecification
11-
import aws.smithy.kotlin.runtime.auth.awssigning.middleware.AwsSigningMiddleware
1212
import aws.smithy.kotlin.runtime.http.Headers
1313
import aws.smithy.kotlin.runtime.http.HttpBody
1414
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
@@ -40,7 +40,7 @@ internal fun SdkBuffer.writeTrailers(
4040
*/
4141
internal val HttpBody.isEligibleForAwsChunkedStreaming: Boolean
4242
get() = (this is HttpBody.SourceContent || this is HttpBody.ChannelContent) && contentLength != null &&
43-
(isOneShot || contentLength!! > AwsSigningMiddleware.AWS_CHUNKED_THRESHOLD)
43+
(isOneShot || contentLength!! > AwsHttpSigner.AWS_CHUNKED_THRESHOLD)
4444

4545
/**
4646
* @return a boolean representing if the signing configuration is configured (via [HashSpecification]) for aws-chunked content encoding

runtime/auth/aws-signing-crt/common/src/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import aws.sdk.kotlin.crt.http.Headers as CrtHeaders
2424

2525
public object CrtAwsSigner : AwsSigner {
2626
override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult<HttpRequest> {
27-
val crtRequest = request.toSignableCrtRequest()
27+
val isUnsigned = config.hashSpecification is HashSpecification.UnsignedPayload
28+
val isAwsChunked = request.headers.contains("Content-Encoding", "aws-chunked")
29+
val crtRequest = request.toSignableCrtRequest(isUnsigned, isAwsChunked)
2830
val crtConfig = config.toCrtSigningConfig()
2931

3032
val crtResult = CrtSigner.sign(crtRequest, crtConfig)

runtime/auth/aws-signing-tests/common/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/BasicSigningTestBase.kt

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import kotlinx.coroutines.test.TestResult
2121
import kotlinx.coroutines.test.runTest
2222
import kotlin.test.Test
2323
import kotlin.test.assertEquals
24+
import kotlin.test.assertFalse
2425
import kotlin.test.assertTrue
2526

2627
// based on: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html#example-signature-calculations-streaming
@@ -47,6 +48,14 @@ private const val EXPECTED_FINAL_CHUNK_SIGNATURE = "b6c6ea8a5354eaf15b3cb7646744
4748
@Suppress("HttpUrlsUsage")
4849
@OptIn(ExperimentalCoroutinesApi::class)
4950
public abstract class BasicSigningTestBase : HasSigner {
51+
private val defaultSigningConfig = AwsSigningConfig {
52+
region = "us-east-1"
53+
service = "demo"
54+
signatureType = AwsSignatureType.HTTP_REQUEST_VIA_HEADERS
55+
signingDate = Instant.fromIso8601("2020-10-16T19:56:00Z")
56+
credentialsProvider = testCredentialsProvider
57+
}
58+
5059
@Test
5160
public fun testSignRequestSigV4(): TestResult = runTest {
5261
// sanity test
@@ -62,15 +71,7 @@ public abstract class BasicSigningTestBase : HasSigner {
6271
headers.append("Content-Length", body.contentLength?.toString() ?: "0")
6372
}.build()
6473

65-
val config = AwsSigningConfig {
66-
region = "us-east-1"
67-
service = "demo"
68-
signatureType = AwsSignatureType.HTTP_REQUEST_VIA_HEADERS
69-
signingDate = Instant.fromIso8601("2020-10-16T19:56:00Z")
70-
credentialsProvider = testCredentialsProvider
71-
}
72-
73-
val result = signer.sign(request, config)
74+
val result = signer.sign(request, defaultSigningConfig)
7475

7576
val expectedDate = "20201016T195600Z"
7677
val expectedSig = "e60a4adad4ae15e05c96a0d8ac2482fbcbd66c88647c4457db74e4dad1648608"
@@ -98,14 +99,12 @@ public abstract class BasicSigningTestBase : HasSigner {
9899
headers.append("Content-Length", body.contentLength?.toString() ?: "0")
99100
}.build()
100101

101-
val config = AwsSigningConfig {
102-
region = "us-east-1"
103-
service = "service"
104-
signatureType = AwsSignatureType.HTTP_REQUEST_VIA_HEADERS
105-
algorithm = AwsSigningAlgorithm.SIGV4_ASYMMETRIC
106-
signingDate = Instant.fromIso8601("2015-08-30T12:36:00Z")
107-
credentialsProvider = testCredentialsProvider
108-
}
102+
val config = defaultSigningConfig.toBuilder()
103+
.apply {
104+
service = "service"
105+
algorithm = AwsSigningAlgorithm.SIGV4_ASYMMETRIC
106+
signingDate = Instant.fromIso8601("2015-08-30T12:36:00Z")
107+
}.build()
109108

110109
val result = signer.sign(request, config)
111110

@@ -190,4 +189,42 @@ public abstract class BasicSigningTestBase : HasSigner {
190189
val finalChunkResult = signer.signChunk(ByteArray(0), prevSignature, chunkedSigningConfig)
191190
assertEquals(EXPECTED_FINAL_CHUNK_SIGNATURE, finalChunkResult.signature.decodeToString())
192191
}
192+
193+
@Test
194+
public fun testSigningCopiesInput(): TestResult = runTest {
195+
// sanity test the signer doesn't mutate the input and instead copies to a new request
196+
val requestBuilder = HttpRequestBuilder().apply {
197+
method = HttpMethod.POST
198+
url.scheme = Protocol.HTTP
199+
url.host = Host.Domain("test.amazonaws.com")
200+
url.path = "/"
201+
headers.append("Host", "test.amazonaws.com")
202+
headers.appendAll("x-amz-archive-description", listOf("test", "test"))
203+
body = ByteArrayContent("body".encodeToByteArray())
204+
headers.append("Content-Length", body.contentLength?.toString() ?: "0")
205+
}
206+
207+
val request = requestBuilder.build()
208+
209+
val result = signer.sign(request, defaultSigningConfig)
210+
211+
val originalHeaders = listOf("Content-Length", "Host", "x-amz-archive-description")
212+
val updatedHeaders = listOf("X-Amz-Date", "X-Amz-Security-Token", "Authorization")
213+
214+
assertEquals(originalHeaders.size, requestBuilder.headers.names().size)
215+
assertEquals(originalHeaders.size, request.headers.names().size)
216+
assertEquals(originalHeaders.size + updatedHeaders.size, result.output.headers.names().size)
217+
218+
originalHeaders.forEach { name ->
219+
assertTrue(requestBuilder.headers.contains(name), "${requestBuilder.headers} did not contain $name")
220+
assertTrue(request.headers.contains(name), "${request.headers} did not contain $name")
221+
assertTrue(result.output.headers.contains(name), "${result.output.headers} did not contain $name")
222+
}
223+
224+
updatedHeaders.forEach { name ->
225+
assertFalse(requestBuilder.headers.contains(name), "${requestBuilder.headers} contained $name")
226+
assertFalse(request.headers.contains(name), "${request.headers} contained $name")
227+
assertTrue(result.output.headers.contains(name), "${result.output.headers} did not contain $name")
228+
}
229+
}
193230
}

0 commit comments

Comments
 (0)