-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support default checksums #1191
base: main
Are you sure you want to change the base?
Changes from 8 commits
2a9d104
205839c
7233b71
e1dc616
e436482
abdba02
3e4c891
9760ee1
f8b39b0
f676b7b
6e9b206
fb3a52a
40bb298
828adaa
e2068e7
08b4a37
91355d1
1fcd4b2
4edabfb
db65d74
b2e6f61
c63cf83
b68a9f5
0dd7886
d74c949
1157f26
d9f0659
dc3ce8b
3ef1c20
7116221
344f118
c689ff5
9beca23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,7 +37,11 @@ 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, | ||
// @required | ||
document: Document | ||
} | ||
structure TestOutput with [TestStruct] { innerField: Nested } | ||
|
||
@mixin | ||
|
@@ -60,7 +64,7 @@ structure TestStruct { | |
@required | ||
nestedListValue: NestedList | ||
|
||
@required | ||
// @required | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Were these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The required trait isn't supposed to be there, a smithy validator will throw an error if it is, even if I disable the protocol test |
||
nested: Nested | ||
|
||
@required | ||
|
@@ -91,7 +95,7 @@ union MyUnion { | |
} | ||
|
||
structure Nested { | ||
@required | ||
// @required | ||
a: String | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -338,21 +337,14 @@ open class HttpProtocolClientGenerator( | |
|
||
/** | ||
* 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. | ||
* The Md5 middleware will only be installed if the operation requires a checksum. | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Why is this installed for every operation now without consideration of flexible checksums? If we enable CRC32 by default, doesn't this also calculate the MD5 unnecessarily? |
||
private fun OperationShape.renderIsMd5ChecksumRequired(writer: KotlinWriter) { | ||
val httpChecksumTrait = getTrait<HttpChecksumTrait>() | ||
|
||
// the checksum requirement can be modeled in either HttpChecksumTrait's `requestChecksumRequired` or the HttpChecksumRequired trait | ||
if (!hasTrait<HttpChecksumRequiredTrait>() && httpChecksumTrait == null) { | ||
return | ||
} | ||
|
||
if (hasTrait<HttpChecksumRequiredTrait>() || httpChecksumTrait?.isRequestChecksumRequired == true) { | ||
if (hasTrait<HttpChecksumRequiredTrait>()) { | ||
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) | ||
writer.write("true") | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,7 @@ import kotlinx.coroutines.job | |
import kotlin.coroutines.coroutineContext | ||
|
||
/** | ||
* Calculates a request's checksum. | ||
* Handles request checksums. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. clarification: "Handles request checksums for operations with the [HttpChecksumTrait] applied" |
||
* | ||
* 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. | ||
|
@@ -36,65 +36,85 @@ import kotlin.coroutines.coroutineContext | |
* - If no checksum is configured for the request then use the default checksum algorithm to calculate a checksum. | ||
* | ||
* If the request will be streamed: | ||
* - The checksum calculation is done asynchronously using a hashing & completing body. | ||
* - 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 synchronously | ||
* - 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 userSelectedChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null. | ||
* @param requestChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null. | ||
*/ | ||
@InternalApi | ||
public class FlexibleChecksumsRequestInterceptor( | ||
public class FlexibleChecksumsRequestInterceptor<I>( | ||
requestChecksumRequired: Boolean, | ||
requestChecksumCalculation: HttpChecksumConfigOption?, | ||
userSelectedChecksumAlgorithm: String?, | ||
requestChecksumAlgorithm: String?, | ||
) : AbstractChecksumInterceptor() { | ||
private val forcedToCalculateChecksum = requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED | ||
private val checksumHeader = StringBuilder("x-amz-checksum-") | ||
private val defaultChecksumAlgorithm = lazy { Crc32() } | ||
private val defaultChecksumAlgorithmHeaderPostfix = "crc32" | ||
|
||
private val checksumAlgorithm = userSelectedChecksumAlgorithm?.let { | ||
val hashFunction = userSelectedChecksumAlgorithm.toHashFunction() | ||
// FIXME: Remove in next minor version bump | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add these to our internal ticket SDK-KT-385 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, SDK-KT-385 seems to be more for SDK changes. I can create a ticket for the smith kotlin version bump since it seems like we'll do a minor version bump for the SDK soon and one for smithy kotlin later at a different time There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure we will bump both at the same time There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, in that case I'll just fix the |
||
@Deprecated("Old constructor is no longer used but it's kept for backwards compatibility") | ||
public constructor() : this( | ||
false, | ||
HttpChecksumConfigOption.WHEN_REQUIRED, | ||
null, | ||
) | ||
|
||
// FIXME: Remove in next minor version bump | ||
@Deprecated("Old constructor is no longer used but it's kept for backwards compatibility") | ||
public constructor( | ||
checksumAlgorithmNameInitializer: ((I) -> String?)? = null, | ||
) : this( | ||
false, | ||
HttpChecksumConfigOption.WHEN_REQUIRED, | ||
null, | ||
) | ||
|
||
private val checksumHeader = buildString { | ||
append("x-amz-checksum-") | ||
append(requestChecksumAlgorithm?.lowercase() ?: "crc32") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: "crc32" could be lifted to a |
||
} | ||
private val checksumAlgorithm = requestChecksumAlgorithm?.let { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Missing vertical whitespace |
||
val hashFunction = requestChecksumAlgorithm.toHashFunction() | ||
if (hashFunction == null || !hashFunction.isSupported) { | ||
throw ClientException("Checksum algorithm '$userSelectedChecksumAlgorithm' is not supported for flexible checksums") | ||
throw ClientException("Checksum algorithm '$requestChecksumAlgorithm' is not supported for flexible checksums") | ||
} | ||
checksumHeader.append(userSelectedChecksumAlgorithm.lowercase()) | ||
hashFunction | ||
} ?: if (forcedToCalculateChecksum) { | ||
checksumHeader.append(defaultChecksumAlgorithmHeaderPostfix) | ||
defaultChecksumAlgorithm.value | ||
} ?: if (requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED) { | ||
Crc32() | ||
} else { | ||
null | ||
} | ||
|
||
// TODO: Remove in next minor version bump | ||
@Deprecated("readAfterSerialization is no longer used but can't be removed due to backwards incompatibility") | ||
override fun readAfterSerialization(context: ProtocolRequestInterceptorContext<Any, HttpRequest>) { } | ||
|
||
override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): HttpRequest { | ||
val logger = coroutineContext.logger<FlexibleChecksumsRequestInterceptor>() | ||
val logger = coroutineContext.logger<FlexibleChecksumsRequestInterceptor<I>>() | ||
|
||
userProviderChecksumHeader(context.protocolRequest, logger)?.let { | ||
logger.debug { "User supplied a checksum via header, skipping checksum calculation" } | ||
context.protocolRequest.userProvidedChecksumHeader(logger)?.let { | ||
logger.debug { "Checksum was supplied via header, skipping checksum calculation" } | ||
|
||
val request = context.protocolRequest.toBuilder() | ||
request.headers.removeAllChecksumHeadersExcept(it) | ||
return request.build() | ||
} | ||
|
||
if (checksumAlgorithm == null) { | ||
logger.debug { "User didn't select a checksum algorithm and checksum calculation isn't required, skipping checksum calculation" } | ||
logger.debug { "A checksum algorithm isn't selected and checksum calculation isn't required, skipping checksum calculation" } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could also fall into this case when a user does select a checksum algorithm but they also configure I think a better log would be: |
||
return context.protocolRequest | ||
} | ||
|
||
logger.debug { "Calculating checksum using '$checksumAlgorithm'" } | ||
|
||
val request = context.protocolRequest.toBuilder() | ||
|
||
if (request.body.isEligibleForAwsChunkedStreaming) { | ||
logger.debug { "Calculating checksum during transmission using '$checksumAlgorithm'" } | ||
|
||
val deferredChecksum = CompletableDeferred<String>(context.executionContext.coroutineContext.job) | ||
|
||
request.body = request.body | ||
|
@@ -106,26 +126,28 @@ public class FlexibleChecksumsRequestInterceptor( | |
deferredChecksum, | ||
) | ||
|
||
request.headers.append("x-amz-trailer", checksumHeader.toString()) | ||
request.trailingHeaders.append(checksumHeader.toString(), deferredChecksum) | ||
request.headers.append("x-amz-trailer", checksumHeader) | ||
request.trailingHeaders.append(checksumHeader, deferredChecksum) | ||
} else { | ||
logger.debug { "Calculating checksum before transmission using '$checksumAlgorithm'" } | ||
|
||
checksumAlgorithm.update( | ||
request.body.readAll() ?: byteArrayOf(), | ||
) | ||
request.headers[checksumHeader.toString()] = checksumAlgorithm.digest().encodeBase64String() | ||
request.headers[checksumHeader] = checksumAlgorithm.digest().encodeBase64String() | ||
} | ||
|
||
context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: I see here we're emitting the metric for the specific algorithm (e.g., There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
request.headers.removeAllChecksumHeadersExcept(checksumHeader.toString()) | ||
request.headers.removeAllChecksumHeadersExcept(checksumHeader) | ||
|
||
return request.build() | ||
} | ||
|
||
override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): String? { | ||
val req = context.protocolRequest.toBuilder() | ||
|
||
if (checksumAlgorithm == null) return null | ||
|
||
val req = context.protocolRequest.toBuilder() | ||
|
||
return when { | ||
req.body.contentLength == null && !req.body.isOneShot -> { | ||
val channel = req.body.toSdkByteReadChannel()!! | ||
|
@@ -145,8 +167,8 @@ public class FlexibleChecksumsRequestInterceptor( | |
): HttpRequest { | ||
val req = context.protocolRequest.toBuilder() | ||
|
||
if (!req.headers.contains(checksumHeader.toString())) { | ||
req.header(checksumHeader.toString(), checksum) | ||
if (!req.headers.contains(checksumHeader)) { | ||
req.header(checksumHeader, checksum) | ||
} | ||
|
||
return req.build() | ||
|
@@ -234,15 +256,15 @@ public class FlexibleChecksumsRequestInterceptor( | |
/** | ||
* 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 valid checksum algorithm. | ||
* MD5 is not considered a supported checksum algorithm. | ||
*/ | ||
private fun userProviderChecksumHeader(request: HttpRequest, logger: Logger): String? { | ||
request.headers.entries().forEach { header -> | ||
private fun HttpRequest.userProvidedChecksumHeader(logger: Logger): String? { | ||
this.headers.entries().forEach { header -> | ||
val headerName = header.key.lowercase() | ||
if (headerName.startsWith("x-amz-checksum-")) { | ||
if (headerName.endsWith("md5")) { | ||
if (headerName == "x-amz-checksum-md5") { | ||
logger.debug { | ||
"User provided md5 request checksum via headers, md5 is not a valid algorithm, ignoring header" | ||
"MD5 checksum was supplied via header, MD5 is not a supported algorithm, ignoring header" | ||
} | ||
} else { | ||
return headerName | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI I know we talked about disabling some tests that are failing in Smithy 1.53.0, but these are disabling the entire test suite, which we don't want to do