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: modularize checksums #1568

Merged
merged 42 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4cc468e
Modularize I&A.
Jun 5, 2024
852a178
Merge branch 'main' into feat/modularize-identity-and-auth
Jun 5, 2024
cfb3866
ktlint
Jun 5, 2024
8f81e77
Update codegen test.
Jun 5, 2024
17e1b65
Dependency fix
Jun 5, 2024
767a28c
Fix integ test dependencies
Jun 5, 2024
638b3b8
Fix test dependency
Jun 5, 2024
89288f0
Fix integ test dependency
Jun 5, 2024
f4fd0b6
Add SmithyIdentity module dependency for services
Jun 5, 2024
0f63cd5
Merge branch 'main' into feat/modularize-identity-and-auth
Jun 6, 2024
3f9378a
Add SPI imports for AWSSDKCommon & AWSSDKIdentity to service clients
Jun 7, 2024
198c333
Add missing SPI tag
Jun 7, 2024
7ad1d84
Bruh
Jun 7, 2024
de1280a
Add dependency on AWSSDKCommon to serviceTargetDependency
Jun 8, 2024
927817e
Change SPI name in case conflict is causing issue
Jun 8, 2024
c43b24f
Imports in tests
Jun 8, 2024
70ac32e
Import changes
Jun 8, 2024
04dc1fe
Fix helper function
Jun 8, 2024
71f300c
Organize base package.swift
Jun 8, 2024
337ce08
Merge checksum separation work from David
Jun 10, 2024
d8a6390
Remove unnecessary import statement
Jun 11, 2024
1aea0e1
create AWSSDKChecksums
dayaffe Jun 11, 2024
01e636b
remove Package.swift changes
dayaffe Jun 12, 2024
4b5620e
Merge branch 'main' into day/modular-checksums
dayaffe Jun 12, 2024
d355c98
Merge branch 'main' into day/modular-checksums
dayaffe Jun 13, 2024
f60e12d
trim dependencies of awssdkchecksums
dayaffe Jun 13, 2024
6351e1a
try to get CI to pass
dayaffe Jun 13, 2024
86f31fb
Merge branch 'main' into day/modular-checksums
dayaffe Jun 13, 2024
18d71ad
more stuff
dayaffe Jun 14, 2024
12bc3d0
Merge branch 'main' into day/modular-checksums
dayaffe Jun 14, 2024
5474cd0
more stuff
dayaffe Jun 14, 2024
28f448b
fix integration import
dayaffe Jun 14, 2024
a2890f7
should pass nowww
dayaffe Jun 14, 2024
03866cb
Merge branch 'main' into day/modular-checksums
dayaffe Jun 14, 2024
19a4ccf
another update
dayaffe Jun 14, 2024
dfc3775
add missing dependency
dayaffe Jun 14, 2024
664715c
fix lint
dayaffe Jun 17, 2024
1760a63
add missing imports to integration tests
dayaffe Jun 17, 2024
778af3a
ktlint
dayaffe Jun 17, 2024
ad92966
ktlint
dayaffe Jun 17, 2024
05f2dbc
Merge branch 'main' into day/modular-checksums
dayaffe Jun 17, 2024
4dc3d73
Merge branch 'main' into day/modular-checksums
dayaffe Jun 19, 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
17 changes: 14 additions & 3 deletions AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Resources/Package.Base.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extension Target.Dependency {
static var awsSDKEventStreamsAuth: Self { "AWSSDKEventStreamsAuth" }
static var awsSDKHTTPAuth: Self { "AWSSDKHTTPAuth" }
static var awsSDKIdentity: Self { "AWSSDKIdentity" }
static var awsSDKChecksums: Self { "AWSSDKChecksums" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Is a separate module actually necessary for just a single extension with two methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wish more could be moved to AWSSDKChecksums but that's dependent on CRT HTTP Client being refactored in the future. I'd say even if its only two methods its good to have but I don't feel strongly. aws-chunked is pretty explicitly aws-only though and one of those methods adds it to the headers

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. We could keep it the way it is in this PR, or move it into AWSClientRuntime. For both cases we would revisit this down the line. I don't feel strongly about either option. Unless Josh has preference for one over the other I suppose we can keep AWSSDKChecksums.


// CRT module
static var crt: Self { .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift") }
Expand All @@ -30,6 +31,7 @@ extension Target.Dependency {
static var clientRuntime: Self { .product(name: "ClientRuntime", package: "smithy-swift") }
static var smithy: Self { .product(name: "Smithy", package: "smithy-swift") }
static var smithyChecksumsAPI: Self { .product(name: "SmithyChecksumsAPI", package: "smithy-swift") }
static var smithyChecksums: Self { .product(name: "SmithyChecksums", package: "smithy-swift") }
static var smithyEventStreams: Self { .product(name: "SmithyEventStreams", package: "smithy-swift") }
static var smithyEventStreamsAPI: Self { .product(name: "SmithyEventStreamsAPI", package: "smithy-swift") }
static var smithyEventStreamsAuthAPI: Self { .product(name: "SmithyEventStreamsAuthAPI", package: "smithy-swift") }
Expand All @@ -41,6 +43,7 @@ extension Target.Dependency {
static var smithyRetriesAPI: Self { .product(name: "SmithyRetriesAPI", package: "smithy-swift") }
static var smithyWaitersAPI: Self { .product(name: "SmithyWaitersAPI", package: "smithy-swift") }
static var smithyTestUtils: Self { .product(name: "SmithyTestUtil", package: "smithy-swift") }
static var smithyStreams: Self { .product(name: "SmithyStreams", package: "smithy-swift") }
}

// MARK: - Base Package
Expand All @@ -59,6 +62,7 @@ let package = Package(
.library(name: "AWSSDKEventStreamsAuth", targets: ["AWSSDKEventStreamsAuth"]),
.library(name: "AWSSDKHTTPAuth", targets: ["AWSSDKHTTPAuth"]),
.library(name: "AWSSDKIdentity", targets: ["AWSSDKIdentity"]),
.library(name: "AWSSDKChecksums", targets: ["AWSSDKChecksums"]),
],
targets: [
.target(
Expand Down Expand Up @@ -96,14 +100,19 @@ let package = Package(
),
.target(
name: "AWSSDKHTTPAuth",
dependencies: [.crt, .smithy, .clientRuntime, .smithyHTTPAuth, "AWSSDKIdentity"],
dependencies: [.crt, .smithy, .clientRuntime, .smithyHTTPAuth, "AWSSDKIdentity", "AWSSDKChecksums"],
path: "./Sources/Core/AWSSDKHTTPAuth"
),
.target(
name: "AWSSDKIdentity",
dependencies: [.crt, .smithy, .clientRuntime, .smithyIdentity, .smithyIdentityAPI, .smithyHTTPAPI, .awsSDKCommon],
path: "./Sources/Core/AWSSDKIdentity"
),
.target(
name: "AWSSDKChecksums",
dependencies: [.crt, .smithy, .clientRuntime, .smithyChecksumsAPI, .smithyChecksums, .smithyHTTPAPI],
path: "./Sources/Core/AWSSDKChecksums"
),
.testTarget(
name: "AWSClientRuntimeTests",
dependencies: [.awsClientRuntime, .clientRuntime, .smithyTestUtils, .awsSDKCommon],
Expand All @@ -112,7 +121,7 @@ let package = Package(
),
.testTarget(
name: "AWSSDKEventStreamsAuthTests",
dependencies: ["AWSClientRuntime", "AWSSDKEventStreamsAuth"],
dependencies: ["AWSClientRuntime", "AWSSDKEventStreamsAuth", .smithyStreams],
path: "./Tests/Core/AWSSDKEventStreamsAuthTests"
),
.testTarget(
Expand Down Expand Up @@ -186,11 +195,13 @@ let serviceTargetDependencies: [Target.Dependency] = [
.smithyEventStreamsAuthAPI,
.smithyEventStreams,
.smithyChecksumsAPI,
.smithyChecksums,
.smithyWaitersAPI,
.awsSDKCommon,
.awsSDKIdentity,
.awsSDKHTTPAuth,
.awsSDKEventStreamsAuth,
.awsSDKChecksums,
]

func addServiceTarget(_ name: String) {
Expand Down Expand Up @@ -325,7 +336,7 @@ func addProtocolTests() {
)
let testTarget = protocolTest.buildOnly ? nil : Target.testTarget(
name: "\(protocolTest.name)Tests",
dependencies: [.smithyTestUtils, .smithyWaitersAPI, .byNameItem(name: protocolTest.name, condition: nil)],
dependencies: [.smithyTestUtils, .smithyStreams, .smithyWaitersAPI, .byNameItem(name: protocolTest.name, condition: nil)],
path: "\(protocolTest.testPath ?? protocolTest.sourcePath)/swift-codegen/Tests/\(protocolTest.name)Tests"
)
package.targets += [target, testTarget].compactMap { $0 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Smithy
import XCTest
import AWSS3
@testable import ClientRuntime
import class SmithyStreams.BufferedStream
import class SmithyChecksums.ValidatingBufferedStream

final class S3FlexibleChecksumsTests: S3XCTestCase {
var originalData: Data!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Smithy
import XCTest
import AWSS3
@testable import ClientRuntime
import class SmithyStreams.FileStream

final class S3StreamTests: S3XCTestCase {
let objectName = "hello-world"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import enum SmithyChecksumsAPI.ChecksumAlgorithm
import enum SmithyChecksums.ChecksumMismatchException
import enum Smithy.ClientError
import class Smithy.Context
import AwsCommonRuntimeKit
import AWSSDKChecksums
import ClientRuntime
import SmithyHTTPAPI

public struct FlexibleChecksumsRequestMiddleware<OperationStackInput, OperationStackOutput>: Middleware {

public let id: String = "FlexibleChecksumsRequestMiddleware"

let checksumAlgorithm: String?

public init(checksumAlgorithm: String?) {
self.checksumAlgorithm = checksumAlgorithm
}

public func handle<H>(context: Context,
input: SerializeStepInput<OperationStackInput>,
next: H) async throws -> OperationOutput<OperationStackOutput>
where H: Handler,
Self.MInput == H.Input,
Self.MOutput == H.Output {
try await addHeaders(builder: input.builder, attributes: context)
return try await next.handle(context: context, input: input)
}

private func addHeaders(builder: SdkHttpRequestBuilder, attributes: Context) async throws {
if case(.stream(let stream)) = builder.body {
attributes.isChunkedEligibleStream = stream.isEligibleForChunkedStreaming
if stream.isEligibleForChunkedStreaming {
try builder.setAwsChunkedHeaders() // x-amz-decoded-content-length
}
}

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

guard let checksumString = checksumAlgorithm else {
logger.info("No checksum provided! Skipping flexible checksums workflow...")
return
}

guard let checksumHashFunction = ChecksumAlgorithm.from(string: checksumString) else {
logger.info("Found no supported checksums! Skipping flexible checksums workflow...")
return
}

// 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"
}) {
logger.debug("Checksum header already provided by the user. Skipping calculation.")
return
}

// 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!")
}

if builder.headers.value(for: headerName) == 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!")
}
}

public typealias MInput = SerializeStepInput<OperationStackInput>
public typealias MOutput = OperationOutput<OperationStackOutput>
}

extension FlexibleChecksumsRequestMiddleware: HttpInterceptor {
public typealias InputType = OperationStackInput
public typealias OutputType = OperationStackOutput

public func modifyBeforeRetryLoop(
context: some MutableRequest<InputType, RequestType, AttributesType>
) async throws {
let builder = context.getRequest().toBuilder()
try await addHeaders(builder: builder, attributes: context.getAttributes())
context.updateRequest(updated: builder.build())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0.

import Smithy
import SmithyHTTPAPI
import enum SmithyChecksumsAPI.ChecksumAlgorithm
import enum SmithyChecksums.ChecksumMismatchException
import ClientRuntime

public struct FlexibleChecksumsResponseMiddleware<OperationStackInput, OperationStackOutput>: Middleware {

public let id: String = "FlexibleChecksumsResponseMiddleware"

// The priority to validate response checksums, if multiple are present
let CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST: [String] = [
ChecksumAlgorithm.crc32c,
.crc32,
.sha1,
.sha256
].sorted().map { $0.toString() }

let validationMode: Bool
let priorityList: [String]

public init(validationMode: Bool, priorityList: [String] = []) {
self.validationMode = validationMode
self.priorityList = !priorityList.isEmpty
? withPriority(checksums: priorityList)
: CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST
}

public func handle<H>(context: Context,
input: SdkHttpRequest,
next: H) async throws -> OperationOutput<OperationStackOutput>
where H: Handler,
Self.MInput == H.Input,
Self.MOutput == H.Output {

// The name of the checksum header which was validated. If `null`, validation was not performed.
context.attributes.set(key: AttributeKey<String>(name: "ChecksumHeaderValidated"), value: nil)

// Initialize logger
guard let logger = context.getLogger() else { throw ClientError.unknownError("No logger found!") }

// Get the response
let output = try await next.handle(context: context, input: input)

try await validateChecksum(response: output.httpResponse, logger: logger, attributes: context)

return output
}

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

let checksumHeaderIsPresent = priorityList.first {
response.headers.value(for: "x-amz-checksum-\($0)") != nil
}

guard let checksumHeader = checksumHeaderIsPresent else {
let message =
"User requested checksum validation, but the response headers did not contain any valid checksums"
logger.warn(message)
return
}

let fullChecksumHeader = "x-amz-checksum-" + checksumHeader

logger.debug("Validating checksum from \(fullChecksumHeader)")
attributes.set(key: AttributeKey<String>(name: "ChecksumHeaderValidated"), value: fullChecksumHeader)

let checksumString = checksumHeader.removePrefix("x-amz-checksum-")
guard let responseChecksum = ChecksumAlgorithm.from(string: checksumString) else {
throw ClientError.dataNotFound("Checksum found in header is not supported!")
}

guard let expectedChecksum = response.headers.value(for: fullChecksumHeader) else {
throw ClientError.dataNotFound("Could not determine the expected checksum!")
}

// Handle body vs handle stream
switch response.body {
case .data(let data):
guard let data else {
throw ClientError.dataNotFound("Cannot calculate checksum of empty body!")
}

let responseChecksumHasher = responseChecksum.createChecksum()
try responseChecksumHasher.update(chunk: data)
let actualChecksum = try responseChecksumHasher.digest().toBase64String()

guard expectedChecksum == actualChecksum else {
let message = "Checksum mismatch. Expected \(expectedChecksum) but was \(actualChecksum)"
throw ChecksumMismatchException.message(message)
}
case .stream(let stream):
let validatingStream = ByteStream.getChecksumValidatingBody(
stream: stream,
expectedChecksum: expectedChecksum,
checksumAlgorithm: responseChecksum
)

// Set the response to a validating stream
attributes.httpResponse = response
attributes.httpResponse?.body = validatingStream
case .noStream:
throw ClientError.dataNotFound("Cannot calculate the checksum of an empty body!")
}
}

public typealias MInput = SdkHttpRequest
public typealias MOutput = OperationOutput<OperationStackOutput>
}

extension FlexibleChecksumsResponseMiddleware: HttpInterceptor {
public typealias InputType = OperationStackInput
public typealias OutputType = OperationStackOutput

public func modifyBeforeRetryLoop(
context: some MutableRequest<InputType, RequestType, AttributesType>
) async throws {
context.getAttributes().set(key: AttributeKey<String>(name: "ChecksumHeaderValidated"), value: nil)
}

public func modifyBeforeDeserialization(
context: some MutableResponse<InputType, RequestType, ResponseType, AttributesType>
) async throws {
guard let logger = context.getAttributes().getLogger() else {
throw ClientError.unknownError("No logger found!")
}

let response = context.getResponse()
try await validateChecksum(response: response, logger: logger, attributes: context.getAttributes())
context.updateResponse(updated: response)
}
}

private func withPriority(checksums: [String]) -> [String] {
let checksumsMap = checksums.compactMap { ChecksumAlgorithm.from(string: $0) }
return checksumsMap.sorted().map { $0.toString() }
}
Loading
Loading