Skip to content

Commit e436482

Browse files
committed
Unit tests pass
1 parent e1dc616 commit e436482

File tree

13 files changed

+244
-209
lines changed

13 files changed

+244
-209
lines changed

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumIntegration.kt

Lines changed: 0 additions & 36 deletions
This file was deleted.

codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@ software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery.EndpointDisc
1212
software.amazon.smithy.kotlin.codegen.rendering.endpoints.SdkEndpointBuiltinIntegration
1313
software.amazon.smithy.kotlin.codegen.rendering.compression.RequestCompressionIntegration
1414
software.amazon.smithy.kotlin.codegen.rendering.auth.SigV4AsymmetricAuthSchemeIntegration
15-
software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestsIntegration
16-
software.amazon.smithy.kotlin.codegen.rendering.checksums.HttpChecksumIntegration
15+
software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestsIntegration

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ public fun HttpRequestBuilder.setAwsChunkedBody(signer: AwsSigner, signingConfig
9595
trailingHeaders,
9696
).toHttpBody(-1)
9797

98+
is HttpBody.Bytes -> this.body // TODO: Might need a bit more work here
99+
98100
else -> throw ClientException("HttpBody type is not supported")
99101
}
100102
}

runtime/auth/http-auth-aws/common/src/aws/smithy/kotlin/runtime/http/auth/AwsHttpSigner.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import aws.smithy.kotlin.runtime.auth.awssigning.internal.useAwsChunkedEncoding
1414
import aws.smithy.kotlin.runtime.client.LogMode
1515
import aws.smithy.kotlin.runtime.client.SdkClientOption
1616
import aws.smithy.kotlin.runtime.collections.get
17+
import aws.smithy.kotlin.runtime.hashing.HashingAttributes.ChecksumStreamingRequest
1718
import aws.smithy.kotlin.runtime.http.HttpBody
1819
import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext
1920
import aws.smithy.kotlin.runtime.http.request.HttpRequest
@@ -128,6 +129,7 @@ public class AwsHttpSigner(private val config: Config) : HttpSigner {
128129
val contextOmitSessionToken = attributes.getOrNull(AwsSigningAttributes.OmitSessionToken)
129130

130131
val enableAwsChunked = attributes.getOrNull(AwsSigningAttributes.EnableAwsChunked) ?: false
132+
val checksumStreamingRequest = attributes.getOrNull(ChecksumStreamingRequest) ?: false
131133

132134
// operation signing config is baseConfig + operation specific config/overrides
133135
val signingConfig = AwsSigningConfig {
@@ -164,7 +166,7 @@ public class AwsHttpSigner(private val config: Config) : HttpSigner {
164166
hashSpecification = when {
165167
contextHashSpecification != null -> contextHashSpecification
166168
body is HttpBody.Empty -> HashSpecification.EmptyBody
167-
body.isEligibleForAwsChunkedStreaming && enableAwsChunked -> {
169+
((body.isEligibleForAwsChunkedStreaming && enableAwsChunked) || checksumStreamingRequest) -> {
168170
if (request.headers.contains("x-amz-trailer")) {
169171
if (config.isUnsignedPayload) HashSpecification.StreamingUnsignedPayloadWithTrailers else HashSpecification.StreamingAws4HmacSha256PayloadWithTrailers
170172
} else {

runtime/protocol/http-client/api/http-client.api

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -332,18 +332,15 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpoin
332332
}
333333

334334
public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor : aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor {
335-
public fun <init> ()V
336-
public fun <init> (Lkotlin/jvm/functions/Function1;)V
337-
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
335+
public fun <init> (ZLaws/smithy/kotlin/runtime/client/config/ChecksumConfigOption;Ljava/lang/String;Z)V
338336
public fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest;
339337
public fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
340338
public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
341-
public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V
342339
}
343340

344341
public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor : aws/smithy/kotlin/runtime/client/Interceptor {
345342
public static final field Companion Laws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor$Companion;
346-
public fun <init> (Lkotlin/jvm/functions/Function1;)V
343+
public fun <init> (ZLaws/smithy/kotlin/runtime/client/config/ChecksumConfigOption;)V
347344
public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
348345
public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
349346
public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt

Lines changed: 100 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,103 +7,116 @@ package aws.smithy.kotlin.runtime.http.interceptors
77

88
import aws.smithy.kotlin.runtime.ClientException
99
import aws.smithy.kotlin.runtime.InternalApi
10+
import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric
11+
import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric
12+
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
1013
import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext
14+
import aws.smithy.kotlin.runtime.client.config.ChecksumConfigOption
15+
import aws.smithy.kotlin.runtime.collections.putIfAbsent
1116
import aws.smithy.kotlin.runtime.hashing.*
17+
import aws.smithy.kotlin.runtime.hashing.HashingAttributes.ChecksumStreamingRequest
1218
import aws.smithy.kotlin.runtime.http.*
13-
import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext
1419
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1520
import aws.smithy.kotlin.runtime.http.request.header
1621
import aws.smithy.kotlin.runtime.http.request.toBuilder
1722
import aws.smithy.kotlin.runtime.io.*
1823
import aws.smithy.kotlin.runtime.telemetry.logging.logger
1924
import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String
20-
import aws.smithy.kotlin.runtime.util.LazyAsyncValue
2125
import kotlinx.coroutines.CompletableDeferred
2226
import kotlinx.coroutines.job
2327
import kotlin.coroutines.coroutineContext
2428

2529
/**
26-
* Mutate a request to enable flexible checksums.
27-
*
28-
* If the checksum will be sent as a header, calculate the checksum.
29-
*
30-
* Otherwise, if it will be sent as a trailing header, calculate the checksum as asynchronously as the body is streamed.
31-
* In this case, a [LazyAsyncValue] will be added to the execution context which allows the trailing checksum to be sent
32-
* after the entire body has been streamed.
33-
*
34-
* @param checksumAlgorithmNameInitializer an optional function which parses the input [I] to return the checksum algorithm name.
35-
* if not set, then the [HttpOperationContext.ChecksumAlgorithm] execution context attribute will be used.
30+
* TODO -
3631
*/
3732
@InternalApi
38-
public class FlexibleChecksumsRequestInterceptor<I>(
39-
private val checksumAlgorithmNameInitializer: ((I) -> String?)? = null,
33+
public class FlexibleChecksumsRequestInterceptor(
34+
requestChecksumRequired: Boolean,
35+
requestChecksumCalculation: ChecksumConfigOption?,
36+
private val userSelectedChecksumAlgorithm: String?,
37+
private val streamingPayload: Boolean,
4038
) : AbstractChecksumInterceptor() {
41-
private var checksumAlgorithmName: String? = null
42-
43-
@Deprecated("readAfterSerialization is no longer used")
44-
override fun readAfterSerialization(context: ProtocolRequestInterceptorContext<Any, HttpRequest>) { }
39+
private val forcedToCalculateChecksum = requestChecksumRequired || requestChecksumCalculation == ChecksumConfigOption.WHEN_SUPPORTED
40+
private val checksumHeader = StringBuilder("x-amz-checksum-")
41+
private val defaultChecksumAlgorithm = lazy { Crc32() }
42+
private val defaultChecksumAlgorithmHeaderPostfix = "crc32"
43+
44+
private val checksumAlgorithm = userSelectedChecksumAlgorithm?.let {
45+
val hashFunction = userSelectedChecksumAlgorithm.toHashFunction()
46+
if (hashFunction == null || !hashFunction.isSupported) {
47+
throw ClientException("Checksum algorithm '$userSelectedChecksumAlgorithm' is not supported for flexible checksums")
48+
}
49+
checksumHeader.append(userSelectedChecksumAlgorithm.lowercase())
50+
hashFunction
51+
} ?: if (forcedToCalculateChecksum) {
52+
checksumHeader.append(defaultChecksumAlgorithmHeaderPostfix)
53+
defaultChecksumAlgorithm.value
54+
} else {
55+
null
56+
}
4557

4658
override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): HttpRequest {
47-
val logger = coroutineContext.logger<FlexibleChecksumsRequestInterceptor<I>>()
59+
val logger = coroutineContext.logger<FlexibleChecksumsRequestInterceptor>()
4860

49-
@Suppress("UNCHECKED_CAST")
50-
val input = context.request as I
51-
checksumAlgorithmName = checksumAlgorithmNameInitializer?.invoke(input) ?: context.executionContext.getOrNull(HttpOperationContext.ChecksumAlgorithm)
61+
userProviderChecksumHeader(context.protocolRequest)?.let {
62+
logger.debug { "User supplied a checksum via header, skipping checksum calculation" }
5263

53-
checksumAlgorithmName ?: run {
54-
logger.debug { "no checksum algorithm specified, skipping flexible checksums processing" }
55-
return context.protocolRequest
64+
val request = context.protocolRequest.toBuilder()
65+
request.headers.removeAllChecksumHeadersExcept(it)
66+
return request.build()
5667
}
5768

58-
val req = context.protocolRequest.toBuilder()
59-
60-
check(context.protocolRequest.body !is HttpBody.Empty) {
61-
"Can't calculate the checksum of an empty body"
69+
if (checksumAlgorithm == null) {
70+
logger.debug { "User didn't select a checksum algorithm and checksum calculation isn't required, skipping checksum calculation" }
71+
return context.protocolRequest
6272
}
6373

64-
val headerName = "x-amz-checksum-$checksumAlgorithmName".lowercase()
65-
logger.debug { "Resolved checksum header name: $headerName" }
74+
logger.debug { "Calculating checksum using '$checksumAlgorithm'" }
6675

67-
// remove all checksum headers except for $headerName
68-
// this handles the case where a user inputs a precalculated checksum, but it doesn't match the input checksum algorithm
69-
req.headers.removeAllChecksumHeadersExcept(headerName)
70-
71-
// TODO - business metric
72-
val checksumAlgorithm = checksumAlgorithmName?.toHashFunction() ?: throw ClientException("Could not parse checksum algorithm $checksumAlgorithmName")
73-
74-
if (!checksumAlgorithm.isSupported) {
75-
throw ClientException("Checksum algorithm $checksumAlgorithmName is not supported for flexible checksums")
76-
}
77-
78-
if (req.body.isEligibleForAwsChunkedStreaming) {
79-
req.header("x-amz-trailer", headerName)
76+
val request = context.protocolRequest.toBuilder()
8077

78+
if (request.body.isEligibleForAwsChunkedStreaming || streamingPayload) {
8179
val deferredChecksum = CompletableDeferred<String>(context.executionContext.coroutineContext.job)
8280

83-
if (req.headers[headerName] != null) {
84-
logger.debug { "User supplied a checksum, skipping asynchronous calculation" }
85-
86-
val checksum = req.headers[headerName]!!
87-
req.headers.remove(headerName) // remove the checksum header because it will be sent as a trailing header
88-
89-
deferredChecksum.complete(checksum)
81+
if (request.body is HttpBody.Bytes) {
82+
checksumAlgorithm.update(
83+
request.body.readAll() ?: byteArrayOf(),
84+
)
85+
deferredChecksum.complete(
86+
checksumAlgorithm.digest().encodeBase64String(),
87+
)
9088
} else {
91-
logger.debug { "Calculating checksum asynchronously" }
92-
req.body = req.body
93-
.toHashingBody(checksumAlgorithm, req.body.contentLength)
94-
.toCompletingBody(deferredChecksum)
89+
request.body = request.body
90+
.toHashingBody(
91+
checksumAlgorithm,
92+
request.body.contentLength,
93+
)
94+
.toCompletingBody(
95+
deferredChecksum,
96+
)
9597
}
9698

97-
req.trailingHeaders.append(headerName, deferredChecksum)
98-
return req.build()
99+
request.headers.append("x-amz-trailer", checksumHeader.toString())
100+
request.trailingHeaders.append(checksumHeader.toString(), deferredChecksum)
101+
102+
context.executionContext.putIfAbsent(ChecksumStreamingRequest, true)
99103
} else {
100-
return super.modifyBeforeSigning(context)
104+
checksumAlgorithm.update(
105+
request.body.readAll() ?: byteArrayOf(),
106+
)
107+
request.headers[checksumHeader.toString()] = checksumAlgorithm.digest().encodeBase64String()
101108
}
109+
110+
context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric())
111+
request.headers.removeAllChecksumHeadersExcept(checksumHeader.toString())
112+
113+
return request.build()
102114
}
103115

104116
override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): String? {
105117
val req = context.protocolRequest.toBuilder()
106-
val checksumAlgorithm = checksumAlgorithmName?.toHashFunction() ?: return null
118+
119+
if (checksumAlgorithm == null) return null
107120

108121
return when {
109122
req.body.contentLength == null && !req.body.isOneShot -> {
@@ -122,12 +135,10 @@ public class FlexibleChecksumsRequestInterceptor<I>(
122135
context: ProtocolRequestInterceptorContext<Any, HttpRequest>,
123136
checksum: String,
124137
): HttpRequest {
125-
val headerName = "x-amz-checksum-$checksumAlgorithmName".lowercase()
126-
127138
val req = context.protocolRequest.toBuilder()
128139

129-
if (!req.headers.contains(headerName)) {
130-
req.header(headerName, checksum)
140+
if (!req.headers.contains(checksumHeader.toString())) {
141+
req.header(checksumHeader.toString(), checksum)
131142
}
132143

133144
return req.build()
@@ -211,4 +222,30 @@ public class FlexibleChecksumsRequestInterceptor<I>(
211222
}
212223
return hashFunction.digest()
213224
}
225+
226+
/**
227+
* Checks if a user provided a checksum for a request via an HTTP header.
228+
* The header must start with "x-amz-checksum-" followed by the checksum algorithm's name.
229+
* MD5 is not considered a valid checksum algorithm.
230+
*/
231+
private fun userProviderChecksumHeader(request: HttpRequest): String? {
232+
request.headers.entries().forEach { header ->
233+
val headerName = header.key.lowercase()
234+
if (headerName.startsWith("x-amz-checksum-") && !headerName.endsWith("md5")) {
235+
return headerName
236+
}
237+
}
238+
return null
239+
}
240+
241+
/**
242+
* Maps supported hash functions to business metrics.
243+
*/
244+
private fun HashFunction.toBusinessMetric(): BusinessMetric = when (this) {
245+
is Crc32 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_CRC32
246+
is Crc32c -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_CRC32C
247+
is Sha1 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_SHA1
248+
is Sha256 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_SHA256
249+
else -> throw IllegalStateException("Checksum was calculated using an unsupported hash function: $this")
250+
}
214251
}

0 commit comments

Comments
 (0)