Skip to content

Commit 6e9b206

Browse files
committed
Make composite checksum check S3 specific
1 parent f676b7b commit 6e9b206

File tree

8 files changed

+94
-53
lines changed

8 files changed

+94
-53
lines changed

codegen/protocol-tests/model/error-correction-tests.smithy

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ operation SayHello { output: TestOutputDocument, errors: [Error] }
3737
@http(method: "POST", uri: "/")
3838
operation SayHelloXml { output: TestOutput, errors: [Error] }
3939

40-
structure TestOutputDocument with [TestStruct] { innerField: Nested, @required document: Document }
40+
structure TestOutputDocument with [TestStruct] {
41+
innerField: Nested,
42+
// @required
43+
document: Document
44+
}
4145
structure TestOutput with [TestStruct] { innerField: Nested }
4246

4347
@mixin
@@ -60,7 +64,7 @@ structure TestStruct {
6064
@required
6165
nestedListValue: NestedList
6266

63-
@required
67+
// @required
6468
nested: Nested
6569

6670
@required
@@ -91,7 +95,7 @@ union MyUnion {
9195
}
9296

9397
structure Nested {
94-
@required
98+
// @required
9599
a: String
96100
}
97101

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksums
340340

341341
public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor : aws/smithy/kotlin/runtime/client/Interceptor {
342342
public static final field Companion Laws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor$Companion;
343-
public fun <init> (ZLaws/smithy/kotlin/runtime/client/config/HttpChecksumConfigOption;)V
343+
public fun <init> (ZLaws/smithy/kotlin/runtime/client/config/HttpChecksumConfigOption;Z)V
344344
public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
345345
public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
346346
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: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import kotlinx.coroutines.job
2626
import kotlin.coroutines.coroutineContext
2727

2828
/**
29-
* Calculates a request's checksum.
29+
* Handles request checksums.
3030
*
3131
* If a user supplies a checksum via an HTTP header no calculation will be done. The exception is MD5, if a user
3232
* supplies an MD5 checksum header it will be ignored.
@@ -36,65 +36,62 @@ import kotlin.coroutines.coroutineContext
3636
* - If no checksum is configured for the request then use the default checksum algorithm to calculate a checksum.
3737
*
3838
* If the request will be streamed:
39-
* - The checksum calculation is done asynchronously using a hashing & completing body.
39+
* - The checksum calculation is done during transmission using a hashing & completing body.
4040
* - The checksum will be sent in a trailing header, once the request is consumed.
4141
*
4242
* If the request will not be streamed:
43-
* - The checksum calculation is done synchronously
43+
* - The checksum calculation is done before transmission
4444
* - The checksum will be sent in a header
4545
*
4646
* Business metrics MUST be emitted for the checksum algorithm used.
4747
*
4848
* @param requestChecksumRequired Model sourced flag indicating if checksum calculation is mandatory.
4949
* @param requestChecksumCalculation Configuration option that determines when checksum calculation should be done.
50-
* @param userSelectedChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null.
50+
* @param requestChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null.
5151
*/
5252
@InternalApi
5353
public class FlexibleChecksumsRequestInterceptor(
5454
requestChecksumRequired: Boolean,
5555
requestChecksumCalculation: HttpChecksumConfigOption?,
56-
userSelectedChecksumAlgorithm: String?,
56+
requestChecksumAlgorithm: String?,
5757
) : AbstractChecksumInterceptor() {
58-
private val forcedToCalculateChecksum = requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED
59-
private val checksumHeader = StringBuilder("x-amz-checksum-")
60-
private val defaultChecksumAlgorithm = lazy { Crc32() }
61-
private val defaultChecksumAlgorithmHeaderPostfix = "crc32"
62-
63-
private val checksumAlgorithm = userSelectedChecksumAlgorithm?.let {
64-
val hashFunction = userSelectedChecksumAlgorithm.toHashFunction()
58+
private val checksumHeader = buildString {
59+
append("x-amz-checksum-")
60+
append(requestChecksumAlgorithm?.lowercase() ?: "crc32")
61+
}
62+
private val checksumAlgorithm = requestChecksumAlgorithm?.let {
63+
val hashFunction = requestChecksumAlgorithm.toHashFunction()
6564
if (hashFunction == null || !hashFunction.isSupported) {
66-
throw ClientException("Checksum algorithm '$userSelectedChecksumAlgorithm' is not supported for flexible checksums")
65+
throw ClientException("Checksum algorithm '$requestChecksumAlgorithm' is not supported for flexible checksums")
6766
}
68-
checksumHeader.append(userSelectedChecksumAlgorithm.lowercase())
6967
hashFunction
70-
} ?: if (forcedToCalculateChecksum) {
71-
checksumHeader.append(defaultChecksumAlgorithmHeaderPostfix)
72-
defaultChecksumAlgorithm.value
68+
} ?: if (requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED) {
69+
Crc32()
7370
} else {
7471
null
7572
}
7673

7774
override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): HttpRequest {
7875
val logger = coroutineContext.logger<FlexibleChecksumsRequestInterceptor>()
7976

80-
userProviderChecksumHeader(context.protocolRequest, logger)?.let {
81-
logger.debug { "User supplied a checksum via header, skipping checksum calculation" }
77+
context.protocolRequest.userProvidedChecksumHeader(logger)?.let {
78+
logger.debug { "Checksum was supplied via header, skipping checksum calculation" }
8279

8380
val request = context.protocolRequest.toBuilder()
8481
request.headers.removeAllChecksumHeadersExcept(it)
8582
return request.build()
8683
}
8784

8885
if (checksumAlgorithm == null) {
89-
logger.debug { "User didn't select a checksum algorithm and checksum calculation isn't required, skipping checksum calculation" }
86+
logger.debug { "A checksum algorithm isn't selected and checksum calculation isn't required, skipping checksum calculation" }
9087
return context.protocolRequest
9188
}
9289

93-
logger.debug { "Calculating checksum using '$checksumAlgorithm'" }
94-
9590
val request = context.protocolRequest.toBuilder()
9691

9792
if (request.body.isEligibleForAwsChunkedStreaming) {
93+
logger.debug { "Calculating checksum during transmission using '$checksumAlgorithm'" }
94+
9895
val deferredChecksum = CompletableDeferred<String>(context.executionContext.coroutineContext.job)
9996

10097
request.body = request.body
@@ -106,26 +103,28 @@ public class FlexibleChecksumsRequestInterceptor(
106103
deferredChecksum,
107104
)
108105

109-
request.headers.append("x-amz-trailer", checksumHeader.toString())
110-
request.trailingHeaders.append(checksumHeader.toString(), deferredChecksum)
106+
request.headers.append("x-amz-trailer", checksumHeader)
107+
request.trailingHeaders.append(checksumHeader, deferredChecksum)
111108
} else {
109+
logger.debug { "Calculating checksum before transmission using '$checksumAlgorithm'" }
110+
112111
checksumAlgorithm.update(
113112
request.body.readAll() ?: byteArrayOf(),
114113
)
115-
request.headers[checksumHeader.toString()] = checksumAlgorithm.digest().encodeBase64String()
114+
request.headers[checksumHeader] = checksumAlgorithm.digest().encodeBase64String()
116115
}
117116

118117
context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric())
119-
request.headers.removeAllChecksumHeadersExcept(checksumHeader.toString())
118+
request.headers.removeAllChecksumHeadersExcept(checksumHeader)
120119

121120
return request.build()
122121
}
123122

124123
override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): String? {
125-
val req = context.protocolRequest.toBuilder()
126-
127124
if (checksumAlgorithm == null) return null
128125

126+
val req = context.protocolRequest.toBuilder()
127+
129128
return when {
130129
req.body.contentLength == null && !req.body.isOneShot -> {
131130
val channel = req.body.toSdkByteReadChannel()!!
@@ -145,8 +144,8 @@ public class FlexibleChecksumsRequestInterceptor(
145144
): HttpRequest {
146145
val req = context.protocolRequest.toBuilder()
147146

148-
if (!req.headers.contains(checksumHeader.toString())) {
149-
req.header(checksumHeader.toString(), checksum)
147+
if (!req.headers.contains(checksumHeader)) {
148+
req.header(checksumHeader, checksum)
150149
}
151150

152151
return req.build()
@@ -234,15 +233,15 @@ public class FlexibleChecksumsRequestInterceptor(
234233
/**
235234
* Checks if a user provided a checksum for a request via an HTTP header.
236235
* The header must start with "x-amz-checksum-" followed by the checksum algorithm's name.
237-
* MD5 is not considered a valid checksum algorithm.
236+
* MD5 is not considered a supported checksum algorithm.
238237
*/
239-
private fun userProviderChecksumHeader(request: HttpRequest, logger: Logger): String? {
240-
request.headers.entries().forEach { header ->
238+
private fun HttpRequest.userProvidedChecksumHeader(logger: Logger): String? {
239+
this.headers.entries().forEach { header ->
241240
val headerName = header.key.lowercase()
242241
if (headerName.startsWith("x-amz-checksum-")) {
243-
if (headerName.endsWith("md5")) {
242+
if (headerName == "x-amz-checksum-md5") {
244243
logger.debug {
245-
"User provided md5 request checksum via headers, md5 is not a valid algorithm, ignoring header"
244+
"MD5 checksum was supplied via header, MD5 is not a supported algorithm, ignoring header"
246245
}
247246
} else {
248247
return headerName

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,22 @@ internal val CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST: List<String> = listOf(
3232
)
3333

3434
/**
35-
* Validate a response's checksum.
35+
* Handles response checksums.
3636
*
3737
* If it's a streaming response, it wraps the response in a hashing body, calculating the checksum as the response is
3838
* streamed to the user. The checksum is validated after the user has consumed the entire body using a checksum validating body.
3939
* Otherwise, the checksum if calculated all at once.
4040
*
4141
* Users can check which checksum was validated by referencing the `ResponseChecksumValidated` execution context variable.
4242
*
43-
* @param responseValidationRequired Flag indicating if the checksum validation is mandatory.
43+
* @param responseValidationRequired Model sourced flag indicating if the checksum validation is mandatory.
4444
* @param responseChecksumValidation Configuration option that determines when checksum validation should be done.
4545
*/
4646
@InternalApi
4747
public class FlexibleChecksumsResponseInterceptor(
4848
private val responseValidationRequired: Boolean,
4949
private val responseChecksumValidation: HttpChecksumConfigOption?,
50+
private val serviceUsesCompositeChecksums: Boolean,
5051
) : HttpInterceptor {
5152
@InternalApi
5253
public companion object {
@@ -62,24 +63,25 @@ public class FlexibleChecksumsResponseInterceptor(
6263

6364
val checksumHeader = CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST
6465
.firstOrNull { context.protocolResponse.headers.contains(it) } ?: run {
65-
logger.warn { "User requested checksum validation, but the response headers did not contain any valid checksums" }
66+
logger.warn { "Checksum validation was requested but the response headers didn't contain a valid checksum." }
6667
return context.protocolResponse
6768
}
6869
val serviceChecksumValue = context.protocolResponse.headers[checksumHeader]!!
6970

70-
if (serviceChecksumValue.isCompositeChecksum()) {
71+
if (serviceUsesCompositeChecksums && serviceChecksumValue.isCompositeChecksum()) {
7172
logger.debug { "Service returned composite checksum. Skipping validation." }
7273
return context.protocolResponse
7374
}
7475

75-
logger.debug { "Validating checksum from $checksumHeader" }
7676
context.executionContext[ChecksumHeaderValidated] = checksumHeader
7777

7878
val checksumAlgorithm = checksumHeader
7979
.removePrefix("x-amz-checksum-")
8080
.toHashFunction() ?: throw ClientException("Could not parse checksum algorithm from header $checksumHeader")
8181

8282
if (context.protocolResponse.body is HttpBody.Bytes) {
83+
logger.debug { "Validating checksum before deserialization from $checksumHeader" }
84+
8385
// Calculate checksum
8486
checksumAlgorithm.update(
8587
context.protocolResponse.body.readAll() ?: byteArrayOf(),
@@ -93,6 +95,8 @@ public class FlexibleChecksumsResponseInterceptor(
9395

9496
return context.protocolResponse
9597
} else {
98+
logger.debug { "Validating checksum during deserialization from $checksumHeader" }
99+
96100
// Wrap the response body in a hashing body
97101
return context.protocolResponse.copy(
98102
body = context.protocolResponse.body
@@ -157,7 +161,7 @@ private fun validateAndThrow(expected: String, actual: String) {
157161
* Composite checksums are used for multipart uploads.
158162
*/
159163
private fun String.isCompositeChecksum(): Boolean {
160-
// Ends with "-#" where "#" is a number between 1-1000
161-
val regex = Regex("-([1-9][0-9]{0,2}|1000)$")
164+
// Ends with "-#" where "#" is a number
165+
val regex = Regex("-(\\d)+$")
162166
return regex.containsMatchIn(this)
163167
}

runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class FlexibleChecksumsRequestInterceptorTest {
4343

4444
op.interceptors.add(
4545
FlexibleChecksumsRequestInterceptor(
46-
userSelectedChecksumAlgorithm = checksumAlgorithmName,
46+
requestChecksumAlgorithm = checksumAlgorithmName,
4747
requestChecksumRequired = true,
4848
requestChecksumCalculation = HttpChecksumConfigOption.WHEN_SUPPORTED,
4949
),
@@ -70,7 +70,7 @@ class FlexibleChecksumsRequestInterceptorTest {
7070

7171
op.interceptors.add(
7272
FlexibleChecksumsRequestInterceptor(
73-
userSelectedChecksumAlgorithm = checksumAlgorithmName,
73+
requestChecksumAlgorithm = checksumAlgorithmName,
7474
requestChecksumRequired = true,
7575
requestChecksumCalculation = HttpChecksumConfigOption.WHEN_SUPPORTED,
7676
),
@@ -95,7 +95,7 @@ class FlexibleChecksumsRequestInterceptorTest {
9595
assertFailsWith<ClientException> {
9696
op.interceptors.add(
9797
FlexibleChecksumsRequestInterceptor(
98-
userSelectedChecksumAlgorithm = unsupportedChecksumAlgorithmName,
98+
requestChecksumAlgorithm = unsupportedChecksumAlgorithmName,
9999
requestChecksumRequired = true,
100100
requestChecksumCalculation = HttpChecksumConfigOption.WHEN_SUPPORTED,
101101
),
@@ -121,7 +121,7 @@ class FlexibleChecksumsRequestInterceptorTest {
121121

122122
op.interceptors.add(
123123
FlexibleChecksumsRequestInterceptor(
124-
userSelectedChecksumAlgorithm = checksumAlgorithmName,
124+
requestChecksumAlgorithm = checksumAlgorithmName,
125125
requestChecksumRequired = true,
126126
requestChecksumCalculation = HttpChecksumConfigOption.WHEN_SUPPORTED,
127127
),
@@ -189,7 +189,7 @@ class FlexibleChecksumsRequestInterceptorTest {
189189

190190
op.interceptors.add(
191191
FlexibleChecksumsRequestInterceptor(
192-
userSelectedChecksumAlgorithm = checksumAlgorithmName,
192+
requestChecksumAlgorithm = checksumAlgorithmName,
193193
requestChecksumRequired = true,
194194
requestChecksumCalculation = HttpChecksumConfigOption.WHEN_SUPPORTED,
195195
),

runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class FlexibleChecksumsResponseInterceptorTest {
7777
FlexibleChecksumsResponseInterceptor(
7878
responseValidationRequired = true,
7979
responseChecksumValidation = HttpChecksumConfigOption.WHEN_SUPPORTED,
80+
serviceUsesCompositeChecksums = false,
8081
),
8182
)
8283

@@ -104,6 +105,7 @@ class FlexibleChecksumsResponseInterceptorTest {
104105
FlexibleChecksumsResponseInterceptor(
105106
responseValidationRequired = true,
106107
responseChecksumValidation = HttpChecksumConfigOption.WHEN_SUPPORTED,
108+
serviceUsesCompositeChecksums = false,
107109
),
108110
)
109111

@@ -132,6 +134,7 @@ class FlexibleChecksumsResponseInterceptorTest {
132134
FlexibleChecksumsResponseInterceptor(
133135
responseValidationRequired = true,
134136
responseChecksumValidation = HttpChecksumConfigOption.WHEN_SUPPORTED,
137+
serviceUsesCompositeChecksums = false,
135138
),
136139
)
137140

@@ -157,6 +160,7 @@ class FlexibleChecksumsResponseInterceptorTest {
157160
FlexibleChecksumsResponseInterceptor(
158161
responseValidationRequired = true,
159162
responseChecksumValidation = HttpChecksumConfigOption.WHEN_SUPPORTED,
163+
serviceUsesCompositeChecksums = false,
160164
),
161165
)
162166

@@ -168,4 +172,31 @@ class FlexibleChecksumsResponseInterceptorTest {
168172

169173
op.roundTrip(client, TestInput("input"))
170174
}
175+
176+
@Test
177+
fun testSkipsValidationWhenDisabled() = runTest {
178+
val req = HttpRequestBuilder()
179+
val op = newTestOperation<TestInput>(req)
180+
181+
op.interceptors.add(
182+
FlexibleChecksumsResponseInterceptor (
183+
responseValidationRequired = false,
184+
responseChecksumValidation = HttpChecksumConfigOption.WHEN_REQUIRED,
185+
serviceUsesCompositeChecksums = false,
186+
)
187+
)
188+
189+
val responseChecksumHeaderName = "x-amz-checksum-crc32"
190+
191+
val responseHeaders = Headers {
192+
append(responseChecksumHeaderName, "incorrect-checksum-would-throw-if-validated")
193+
}
194+
195+
val client = getMockClient(response, responseHeaders)
196+
197+
val output = op.roundTrip(client, TestInput("input"))
198+
output.body.readAll()
199+
200+
assertNull(op.context.getOrNull(ChecksumHeaderValidated))
201+
}
171202
}

runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,7 @@ public enum class SmithyBusinessMetric(public override val identifier: String) :
9898
FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED("a"),
9999
FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED("b"),
100100
FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED("c"),
101+
;
102+
103+
override fun toString(): String = identifier
101104
}

0 commit comments

Comments
 (0)