Skip to content
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!: Flexible checksum v2 #1803

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
452351a
Add requestChecksumCalculation and responseChecksumValidation configs…
Oct 23, 2024
5e736d2
Add value resolvers for requestChecksumCalculation and responseChecks…
Oct 23, 2024
780e3f3
Add requestChecksumCalculation and responseChecksumValidation configs…
Oct 23, 2024
8fba8dd
Add context extension for getting and setting the new config options;…
Oct 23, 2024
6922099
Clean up flexchex request middleware conditionals into one logical fl…
Oct 23, 2024
d1ec299
Fix validation mode logic in flexchex response middleware; now it use…
Oct 23, 2024
5206a1e
Update flexchex request middleware codegen to pass in request checksu…
Oct 23, 2024
436757a
Update runtime tests for flexchex middlewares + add a test for no req…
Oct 23, 2024
095890c
Update codegen tests.
Oct 23, 2024
5e3a4b6
Merge branch 'main' into feat/flexible-checksum-v2
Oct 23, 2024
93264a4
Add CRC64NVME as one of the algorithms to check for in flexchex respo…
Oct 23, 2024
d8038c0
Add test case for no response validation when validation mode unset a…
Oct 23, 2024
798c31b
Address compile time errors in generated code.
Oct 23, 2024
8ce4364
Update codegen test
Oct 24, 2024
c6d69d2
Skip checksum flow if body is empty + ignore checksum of checksums th…
Oct 24, 2024
efc500b
Merge branch 'main' into feat/flexible-checksum-v2
Oct 24, 2024
1fd98a4
Add edge case handling for a stream body with size below chunked thre…
Oct 25, 2024
696dc72
Merge branch 'main' into feat/flexible-checksum-v2
Oct 25, 2024
3aa8ac0
Merge branch 'main' into feat/flexible-checksum-v2
Nov 8, 2024
5eb46b3
Add business metric feature ID tracking for flexible checksum v2.
Nov 8, 2024
a45ad36
Update initializer call in test.
Nov 9, 2024
7cf095e
Merge branch 'main' into feat/flexible-checksum-v2
Nov 12, 2024
2f61e67
Merge branch 'main' into feat/flexible-checksum-v2
Nov 15, 2024
92c45c9
Reflect context thread-safe changes upstream.
Nov 15, 2024
472111a
Fill impl gaps against SEP.
Nov 19, 2024
b9b7f18
Merge branch 'main' into feat/flexible-checksum-v2
Nov 19, 2024
eabc32e
Update codegen test & fix optional chaining.
Nov 19, 2024
bebffb1
Fill unit test gap for flex checksum middlewares
Nov 19, 2024
d6932b3
Temporarily comment out manual fill for requestAlgorithmMember http h…
Nov 19, 2024
fe87cd8
Merge branch 'main' into feat/flexible-checksum-v2
Nov 26, 2024
7be0fbd
Add PRESIGN_URL flow to flexchex request middleware.
Nov 26, 2024
ccc77e0
Merge branch 'main' into feat/flexible-checksum-v2
Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ private var runtimeTargets: [Target] {
.smithyEventStreamsAuthAPI,
.awsSDKCommon,
.awsSDKHTTPAuth,
.awsSDKIdentity
.awsSDKIdentity,
.awsSDKChecksums,
],
path: "Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime",
resources: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import enum ClientRuntime.ClientLogMode
import struct SmithyRetries.DefaultRetryStrategy
import struct SmithyRetries.ExponentialBackoffStrategy
import struct SmithyRetriesAPI.RetryStrategyOptions
import enum AWSSDKChecksums.AWSChecksumCalculationMode

typealias RuntimeConfigType = DefaultSDKRuntimeConfiguration<DefaultRetryStrategy, DefaultRetryErrorInfoProvider>

Expand Down Expand Up @@ -84,6 +85,40 @@ public class AWSClientConfigDefaultsProvider {
return resolvedAppID
}

public static func requestChecksumCalculation(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to add comment strings explaining what these functions do / how theyre used

_ requestChecksumCalculation: AWSChecksumCalculationMode? = nil
) throws -> AWSChecksumCalculationMode {
let fileBasedConfig = try CRTFileBasedConfiguration.make()
let resolvedRequestChecksumCalculation: AWSChecksumCalculationMode
if let requestChecksumCalculation {
resolvedRequestChecksumCalculation = requestChecksumCalculation
} else {
resolvedRequestChecksumCalculation = AWSChecksumsConfig.requestChecksumCalculation(
configValue: nil,
profileName: nil,
fileBasedConfig: fileBasedConfig
)
}
return resolvedRequestChecksumCalculation
}

public static func responseChecksumValidation(
_ responseChecksumValidation: AWSChecksumCalculationMode? = nil
) throws -> AWSChecksumCalculationMode {
let fileBasedConfig = try CRTFileBasedConfiguration.make()
let resolvedResponseChecksumValidation: AWSChecksumCalculationMode
if let responseChecksumValidation {
resolvedResponseChecksumValidation = responseChecksumValidation
} else {
resolvedResponseChecksumValidation = AWSChecksumsConfig.responseChecksumValidation(
configValue: nil,
profileName: nil,
fileBasedConfig: fileBasedConfig
)
}
return resolvedResponseChecksumValidation
}

public static func retryMode(_ retryMode: AWSRetryMode? = nil) throws -> AWSRetryMode {
let fileBasedConfig = try CRTFileBasedConfiguration.make()
let resolvedRetryMode: AWSRetryMode?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import enum AWSSDKChecksums.AWSChecksumCalculationMode
@_spi(FileBasedConfig) import AWSSDKCommon

public enum AWSChecksumsConfig {
static func requestChecksumCalculation(
configValue: AWSChecksumCalculationMode?,
profileName: String?,
fileBasedConfig: FileBasedConfiguration
) -> AWSChecksumCalculationMode {
return FieldResolver(
configValue: configValue,
envVarName: "AWS_REQUEST_CHECKSUM_CALCULATION",
configFieldName: "request_checksum_calculation",
fileBasedConfig: fileBasedConfig,
profileName: profileName,
converter: { AWSChecksumCalculationMode(caseInsensitiveRawValue: $0) }
).value ?? .whenSupported
}

static func responseChecksumValidation(
configValue: AWSChecksumCalculationMode?,
profileName: String?,
fileBasedConfig: FileBasedConfiguration
) -> AWSChecksumCalculationMode {
return FieldResolver(
configValue: configValue,
envVarName: "AWS_RESPONSE_CHECKSUM_VALIDATION",
configFieldName: "response_checksum_validation",
fileBasedConfig: fileBasedConfig,
profileName: profileName,
converter: { AWSChecksumCalculationMode(caseInsensitiveRawValue: $0) }
).value ?? .whenSupported
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import SmithyIdentity
import SmithyIdentityAPI
import enum AWSSDKChecksums.AWSChecksumCalculationMode

public protocol AWSDefaultClientConfiguration {
/// The AWS credential identity resolver to be used for AWS credentials.
Expand Down Expand Up @@ -45,4 +46,22 @@ public protocol AWSDefaultClientConfiguration {
///
/// If set, this value gets used when resolving max attempts value from the standard progression of potential sources. If no value could be resolved, the SDK uses max attempts value of 3 by default.
var maxAttempts: Int? { get set }

/// The AWS request checksum calculation mode to use.
///
/// If `.whenRequired`, the client calculates checksum for the request payload only if the operation requires it.
/// If `.whenSupported`, the client calculates checksum for the request payload if the operation supports it.
///
/// Default mode is `.whenSupported`.
///
/// If no algorithm was chosen and no checksum was provided, CRC32 checksum algorithm is used by default.
var requestChecksumCalculation: AWSChecksumCalculationMode { get set }

/// The AWS response checksum calculation mode to use.
///
/// If `.whenRequired`, the client validates checksum of the response only if the top-level input field for `requestValidationModeMember` is set to `.enabled` and SDK supports the checksum algorithm.
/// If `.whenSupported`, the client validates checksum of the response if the operation supports it and SDK supports at least one of the checksum algorithms returend by service.
///
/// Default mode is `.whenSupported`.
var responseChecksumValidation: AWSChecksumCalculationMode { get set }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import enum SmithyChecksumsAPI.ChecksumAlgorithm
import enum SmithyChecksums.ChecksumMismatchException
import enum Smithy.ClientError
import struct Smithy.URIQueryItem
import class Smithy.Context
import struct Foundation.Data
import AwsCommonRuntimeKit
import AWSSDKChecksums
import ClientRuntime
Expand All @@ -18,10 +20,18 @@

public let id: String = "FlexibleChecksumsRequestMiddleware"

let requestChecksumRequired: Bool
let checksumAlgorithm: String?

public init(checksumAlgorithm: String?) {
let checksumAlgoHeaderName: String?

public init(
requestChecksumRequired: Bool,
checksumAlgorithm: String?,
checksumAlgoHeaderName: String?
) {
self.requestChecksumRequired = requestChecksumRequired
self.checksumAlgorithm = checksumAlgorithm
self.checksumAlgoHeaderName = checksumAlgoHeaderName
}

private func addHeaders(builder: HTTPRequestBuilder, attributes: Context) async throws {
Expand All @@ -32,62 +42,127 @@
}
}

// Initialize logger
guard let logger = attributes.getLogger() else {
throw ClientError.unknownError("No logger found!")
}
let checksumHeaderPrefix = "x-amz-checksum-"

guard let checksumString = checksumAlgorithm else {
logger.info("No checksum provided! Skipping flexible checksums workflow...")
// If it's a PRESIGN_URL flow, add checksum headers present in the request to the request's query items. If only the algo header was configured in input, it's up to user to add the checksum hash query item along with the matching body when they send request using the presigned URL. If the checksum hash header was configured in input, it's up to user to provide a body whose checksum matches the value of the checksum hash header when they send request using the presigned URL.
if attributes.getFlowType() == .PRESIGN_URL {
let checksumAlgoHeader = builder.headers.headers.first(where: {
$0.name.lowercased() == checksumAlgoHeaderName?.lowercased()
})
if let checksumAlgoHeader {
builder.withQueryItem(URIQueryItem(
name: checksumAlgoHeader.name,
value: checksumAlgoHeader.value.first
))
}
let checksumHeader = builder.headers.headers.first(where: {
$0.name.lowercased().starts(with: checksumHeaderPrefix) &&
$0.name.lowercased() != checksumAlgoHeaderName?.lowercased()
})
if let checksumHeader {
builder.withQueryItem(URIQueryItem(
name: checksumHeader.name,
value: checksumHeader.value.first
))
}
// Skip default request checksum calculation logic.
return
}

guard let checksumHashFunction = ChecksumAlgorithm.from(string: checksumString) else {
logger.info("Found no supported checksums! Skipping flexible checksums workflow...")
return
// Initialize logger
guard let logger = attributes.getLogger() else {
throw ClientError.unknownError("No logger found!")
}

// Determine the header name
let headerName = "x-amz-checksum-\(checksumHashFunction)"
logger.debug("Resolved checksum header name: \(headerName)")

// Check if any checksum header is already provided by the user
let checksumHeaderPrefix = "x-amz-checksum-"
if builder.headers.headers.contains(where: {
$0.name.lowercased().starts(with: checksumHeaderPrefix) &&
$0.name.lowercased() != "x-amz-checksum-algorithm"
$0.name.lowercased() != checksumAlgoHeaderName?.lowercased()
}) {
logger.debug("Checksum header already provided by the user. Skipping calculation.")
return
}

var checksumHashFunction: ChecksumAlgorithm
if let checksumAlgorithm {
if let hashFunction = ChecksumAlgorithm.from(string: checksumAlgorithm) {
// If user chose supported algorithm, continue
checksumHashFunction = hashFunction
} else {
// If user chose unsupported algorithm, throw error
throw ClientError.invalidValue("Error: Checksum algorithm \(checksumAlgorithm) is not supported!")
}
} else {
// If user didn't choose an algorithm, then:
if requestChecksumRequired || (attributes.requestChecksumCalculation == .whenSupported) {
// If requestChecksumRequired == true OR RequestChecksumCalculation == when_supported, use CRC32 as default algorithm.
checksumHashFunction = ChecksumAlgorithm.from(string: "crc32")!
logger.info("No algorithm chosen by user. Defaulting to CRC32 checksum algorithm.")
// If the member specified by `requestAlgorithmMember` has `httpHeader` property set in model,
// set the header specified in `httpHeader` with SDK's default algorithm: crc32
// TODO: UNCOMMENT AFTER FEATURE RELEASE
// if let checksumAlgoHeaderName {
// builder.updateHeader(name: checksumAlgoHeaderName, value: "crc32")
// }
} else {
// If requestChecksumRequired == false AND RequestChecksumCalculation == when_required, skip calculation.
logger.info("Checksum not required for the operation.")
logger.info("Client config `requestChecksumCalculation` set to `.whenRequired`")
logger.info("No checksum algorithm chosen by the user. Skipping checksum calculation...")
return
}
}

// Save resolved ChecksumAlgorithm to interceptor context.
attributes.checksum = checksumHashFunction

// Determine the checksum header name
let checksumHashHeaderName = "x-amz-checksum-\(checksumHashFunction)"
logger.debug("Resolved checksum header name: \(checksumHashHeaderName)")

// Handle body vs handle stream
switch builder.body {
case .data(let data):
guard let data else {
throw ClientError.dataNotFound("Cannot calculate checksum of empty body!")
try await calculateAndAddChecksumHeader(data: data)
case .stream(let stream):
if stream.isEligibleForChunkedStreaming {
// Handle calculating and adding checksum header in ChunkedStream
builder.updateHeader(name: "x-amz-trailer", value: [checksumHashHeaderName])
} else {
// If not eligible for chunked streaming, calculate and add checksum to request header now
// instead of in a trailing header.
let streamBytes: Data?
let currentPosition = stream.position
if stream.isSeekable {
// Explicit seek to beginning for correct behavior of FileHandle
try stream.seek(toOffset: 0)
streamBytes = try await stream.readToEnd()

Check warning on line 139 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/FlexibleChecksumsRequestMiddleware.swift

View workflow job for this annotation

GitHub Actions / apple (macos-14, Xcode_15.2, platform=iOS Simulator,OS=17.2,name=iPhone 15)

no 'async' operations occur within 'await' expression

Check warning on line 139 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/FlexibleChecksumsRequestMiddleware.swift

View workflow job for this annotation

GitHub Actions / apple (macos-14, Xcode_15.2, platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (a...

no 'async' operations occur within 'await' expression

Check warning on line 139 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/FlexibleChecksumsRequestMiddleware.swift

View workflow job for this annotation

GitHub Actions / apple (macos-14, Xcode_15.2, platform=visionOS Simulator,OS=1.0,name=Apple Vision Pro)

no 'async' operations occur within 'await' expression

Check warning on line 139 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/FlexibleChecksumsRequestMiddleware.swift

View workflow job for this annotation

GitHub Actions / apple (macos-14, Xcode_15.2, platform=macOS)

no 'async' operations occur within 'await' expression

Check warning on line 139 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/FlexibleChecksumsRequestMiddleware.swift

View workflow job for this annotation

GitHub Actions / apple (macos-15, Xcode_16.1, platform=iOS Simulator,OS=18.1,name=iPhone 16)

no 'async' operations occur within 'await' expression

Check warning on line 139 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/FlexibleChecksumsRequestMiddleware.swift

View workflow job for this annotation

GitHub Actions / apple (macos-15, Xcode_16.1, platform=tvOS Simulator,OS=18.1,name=Apple TV 4K (3rd generation) (a...

no 'async' operations occur within 'await' expression

Check warning on line 139 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/FlexibleChecksumsRequestMiddleware.swift

View workflow job for this annotation

GitHub Actions / apple (macos-15, Xcode_16.1, platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro)

no 'async' operations occur within 'await' expression

Check warning on line 139 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/FlexibleChecksumsRequestMiddleware.swift

View workflow job for this annotation

GitHub Actions / apple (macos-15, Xcode_16.1, platform=macOS)

no 'async' operations occur within 'await' expression
try stream.seek(toOffset: currentPosition)
} else {
streamBytes = try await stream.readToEndAsync()
builder.withBody(.data(streamBytes))
}
try await calculateAndAddChecksumHeader(data: streamBytes)
}
case .noStream:
logger.info("Request body is empty. Skipping request checksum calculation...")
}

if builder.headers.value(for: headerName) == nil {
func calculateAndAddChecksumHeader(data: Data?) async throws {
guard let data else {
logger.info("Request body is empty. Skipping request checksum calculation...")
return
}
if builder.headers.value(for: checksumHashHeaderName) == nil {
logger.debug("Calculating checksum")
}

// Create checksum instance
let checksum = checksumHashFunction.createChecksum()

// Pass data to hash
try checksum.update(chunk: data)

// Retrieve the hash
let hash = try checksum.digest().toBase64String()

builder.updateHeader(name: headerName, value: [hash])
case .stream:
// Will handle calculating checksum and setting header later
attributes.checksum = checksumHashFunction
builder.updateHeader(name: "x-amz-trailer", value: [headerName])
case .noStream:
throw ClientError.dataNotFound("Cannot calculate the checksum of an empty body!")
builder.updateHeader(name: checksumHashHeaderName, value: [hash])
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ public struct FlexibleChecksumsResponseMiddleware<OperationStackInput, Operation
let CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST: [String] = [
ChecksumAlgorithm.crc32c,
.crc32,
.crc64nvme,
.sha1,
.sha256
].sorted().map { $0.toString() }

let validationMode: Bool
let validationMode: String
let priorityList: [String]

public init(validationMode: Bool, priorityList: [String] = []) {
public init(validationMode: String, priorityList: [String] = []) {
self.validationMode = validationMode
self.priorityList = !priorityList.isEmpty
? withPriority(checksums: priorityList)
Expand All @@ -31,13 +32,15 @@ public struct FlexibleChecksumsResponseMiddleware<OperationStackInput, Operation

private func validateChecksum(response: HTTPResponse, logger: any LogAgent, attributes: Context) async throws {
// Exit if validation should not be performed
if !validationMode {
if validationMode != "ENABLED" && attributes.responseChecksumValidation == .whenRequired {
logger.info("Checksum validation should not be performed! Skipping workflow...")
return
}

let checksumHeaderIsPresent = priorityList.first {
response.headers.value(for: "x-amz-checksum-\($0)") != nil
response.headers.value(for: "x-amz-checksum-\($0)") != nil &&
// Checksum of checksums has "-#" at the end and should be ignored.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a new requirement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily a new requirement, it was just something that was left out from V1 SEP. S3 can return checksum of checksum, which ends with -# and should be ignored when validating response checksum.

!(response.headers.value(for: "x-amz-checksum-\($0)")!.hasSuffix("-#"))
}

guard let checksumHeader = checksumHeaderIsPresent else {
Expand Down Expand Up @@ -65,7 +68,8 @@ public struct FlexibleChecksumsResponseMiddleware<OperationStackInput, Operation
switch response.body {
case .data(let data):
guard let data else {
throw ClientError.dataNotFound("Cannot calculate checksum of empty body!")
logger.info("Response body is empty. Skipping response checksum validation...")
return
}

let responseChecksumHasher = responseChecksum.createChecksum()
Expand All @@ -87,7 +91,8 @@ public struct FlexibleChecksumsResponseMiddleware<OperationStackInput, Operation
attributes.httpResponse = response
attributes.httpResponse?.body = validatingStream
case .noStream:
throw ClientError.dataNotFound("Cannot calculate the checksum of an empty body!")
logger.info("Response body is empty. Skipping response checksum validation...")
return
}
}
}
Expand Down
Loading
Loading