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: Add support for flexible checksums on streaming payloads #1329

Merged
merged 72 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
50a2c46
add support for Streaming flexible checksums
dayaffe Feb 7, 2024
6e08f16
fix rebase
dayaffe Feb 7, 2024
020603f
update crt version and S3 client codegen
dayaffe Feb 7, 2024
f8e116d
swiftlint
dayaffe Feb 7, 2024
e0c5fe8
fix merge conflict in test
dayaffe Feb 7, 2024
235d6ba
remove empty file
dayaffe Feb 7, 2024
6b2813f
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 8, 2024
3210f33
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 8, 2024
93068be
bump to crt 0.27.0
dayaffe Feb 9, 2024
0cae37b
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 9, 2024
9439af7
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 9, 2024
156046d
add integration tests for FC
dayaffe Feb 12, 2024
959fe54
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 12, 2024
eef3020
bump to crt 0.28.0
dayaffe Feb 12, 2024
f770b76
switch integration test from file to buffered stream
dayaffe Feb 12, 2024
37b9062
fix swiftlint
dayaffe Feb 12, 2024
0d83778
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 15, 2024
db21c76
Use xcodebuild to run integration tests
jbelkins Feb 15, 2024
92cc708
Put xcodebuild in right place
jbelkins Feb 15, 2024
bb0298a
Fix scheme name
jbelkins Feb 15, 2024
33186c9
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 16, 2024
811d4c6
go back to swift toolchain
dayaffe Feb 16, 2024
7c0910a
Test using xcode 14.3.1
dayaffe Feb 16, 2024
9698cac
test all different kinds of xcode versions with mac 14
dayaffe Feb 16, 2024
ebe51f1
revert xcode versions
dayaffe Feb 16, 2024
71ffb89
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 22, 2024
734734d
test xcode 14.3.1
dayaffe Feb 22, 2024
830601c
remove exclusions from test runner for xcode15.2
dayaffe Feb 22, 2024
396642d
checksumAlgorithm not needed to set chunked headers
dayaffe Feb 28, 2024
b8c849a
refactor for identity and auth
dayaffe Feb 28, 2024
5b9f7b1
merge/refactor for identity & auth
dayaffe Feb 29, 2024
eb9c76e
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 29, 2024
3807125
remove unused variable
dayaffe Feb 29, 2024
d26ec5e
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 29, 2024
5e32579
rename HashFunction to ChecksumAlgorithm for SRA
dayaffe Feb 29, 2024
434cbb6
fix name
dayaffe Feb 29, 2024
87c439e
Merge branch 'main' into day/aws-chunked-encoding
dayaffe Feb 29, 2024
d8a6c39
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 1, 2024
b544a13
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 1, 2024
bc0ae0c
Enable Intel Mac integration tests
jbelkins Mar 1, 2024
6b54549
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 4, 2024
c1d3b64
Get OS version in logs
jbelkins Mar 4, 2024
f70290d
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 5, 2024
ed0f60e
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 9, 2024
4f7ecc7
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 11, 2024
fc51f47
Refactor flexible checksum tests
Mar 11, 2024
7dd0ff9
More flexible checksum test refactors
Mar 12, 2024
3c3eef0
Refactor FC tests even more
jbelkins Mar 12, 2024
c9a3159
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 12, 2024
ae502f9
Exclude the Transfer-Encoding header from AWS request signature
jbelkins Mar 13, 2024
e5b32cf
Refactor FC integration tests
jbelkins Mar 13, 2024
9550710
Fix lint
jbelkins Mar 13, 2024
ae9dd19
Rename tests to match class under test
jbelkins Mar 13, 2024
9d17776
Run integration tests on Intel CI only
jbelkins Mar 13, 2024
1a246b7
Don't run integration tests in an environment
jbelkins Mar 13, 2024
e78129e
Don't run Linux integration tests, run 4 Intel Macs
jbelkins Mar 13, 2024
9f6f25e
Many runners
jbelkins Mar 14, 2024
3e4450a
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 14, 2024
7aba130
Re-enable Linux integration tests
jbelkins Mar 14, 2024
a42e403
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 15, 2024
e60ac8e
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 15, 2024
2a42286
Revert integration test CI workflow
jbelkins Mar 15, 2024
55a5d52
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 15, 2024
61a0a9c
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 15, 2024
9a47471
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 18, 2024
12aa861
Merge branch 'main' into day/aws-chunked-encoding
jbelkins Mar 18, 2024
87ffefa
Merge remote-tracking branch 'origin/main' into day/aws-chunked-encoding
jbelkins Mar 19, 2024
a462a2f
Run many integration tests
jbelkins Mar 20, 2024
8add54f
Quotes around Swift versions in CI workflow file
jbelkins Mar 20, 2024
e177ef1
Run integration tests on AL2
jbelkins Mar 21, 2024
4105b7a
Use AWS GH action v3
jbelkins Mar 21, 2024
4a694cf
Revert integration test CI workflow
jbelkins Mar 21, 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
@@ -0,0 +1,134 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
import AWSS3
@testable import ClientRuntime

final class S3FlexibleChecksumsTests: S3XCTestCase {
var originalData: Data!

override func setUp() {
super.setUp()
// Fill one MB with random data. Data is refreshed for each flexible checksums tests below.
originalData = Data((0..<(1024 * 1024)).map { _ in UInt8.random(in: UInt8.min...UInt8.max) })
}

// MARK: - Data uploads

func test_putGetObject_data_crc32() async throws {
try await _testPutGetObject(withChecksumAlgorithm: .crc32, objectNameSuffix: "crc32-data", upload: .data(originalData))
}

func test_putGetObject_data_crc32c() async throws {
try await _testPutGetObject(withChecksumAlgorithm: .crc32c, objectNameSuffix: "crc32c-data", upload: .data(originalData))
}

func test_putGetObject_data_sha1() async throws {
try await _testPutGetObject(withChecksumAlgorithm: .sha1, objectNameSuffix: "sha1-data", upload: .data(originalData))
}

func test_putGetObject_data_sha256() async throws {
try await _testPutGetObject(withChecksumAlgorithm: .sha256, objectNameSuffix: "sha256-data", upload: .data(originalData))
}

// MARK: - Streaming uploads

func test_putGetObject_streaming_crc32() async throws {
let bufferedStream = BufferedStream(data: originalData, isClosed: true)
try await _testPutGetObject(withChecksumAlgorithm: .crc32, objectNameSuffix: "crc32", upload: .stream(bufferedStream))
}

func test_putGetObject_streaming_crc32c() async throws {
let bufferedStream = BufferedStream(data: originalData, isClosed: true)
try await _testPutGetObject(withChecksumAlgorithm: .crc32c, objectNameSuffix: "crc32c", upload: .stream(bufferedStream))
}

func test_putGetObject_streaming_sha1() async throws {
let bufferedStream = BufferedStream(data: originalData, isClosed: true)
try await _testPutGetObject(withChecksumAlgorithm: .sha1, objectNameSuffix: "sha1", upload: .stream(bufferedStream))
}

func test_putGetObject_streaming_sha256() async throws {
let bufferedStream = BufferedStream(data: originalData, isClosed: true)
try await _testPutGetObject(withChecksumAlgorithm: .sha256, objectNameSuffix: "sha256", upload: .stream(bufferedStream))
}

// Streaming without checksum (chunked encoding)
func test_putGetObject_streaming_chunked() async throws {
let bufferedStream = BufferedStream(data: originalData, isClosed: true)
let objectName = "flexible-checksums-s3-test-chunked"

let putObjectInput = PutObjectInput(
body: .stream(bufferedStream),
bucket: bucketName,
key: objectName
)

_ = try await client.putObject(input: putObjectInput)

let getObjectInput = GetObjectInput(bucket: bucketName, key: objectName)
let getObjectOutput = try await client.getObject(input: getObjectInput)
let body = try XCTUnwrap(getObjectOutput.body)
let data = try await body.readData()
XCTAssertEqual(data, originalData)
}

// MARK: - Private methods

private func _testPutGetObject(
withChecksumAlgorithm algorithm: S3ClientTypes.ChecksumAlgorithm,
objectNameSuffix: String, upload: ByteStream, file: StaticString = #filePath, line: UInt = #line
) async throws {
let objectName = "flexible-checksums-s3-test-\(objectNameSuffix)"

let input = PutObjectInput(
body: upload,
bucket: bucketName,
checksumAlgorithm: algorithm,
key: objectName
)

let output = try await client.putObject(input: input)

// Verify the checksum response based on the algorithm used.
let checksumResponse = try XCTUnwrap(getChecksumResponse(from: output, with: algorithm), file: file, line: line)
XCTAssertNotNil(checksumResponse, file: file, line: line)

let getInput = GetObjectInput(bucket: bucketName, checksumMode: S3ClientTypes.ChecksumMode.enabled, key: objectName)
let getOutput = try await client.getObject(input: getInput) // will error for normal payloads if checksum mismatch
XCTAssertNotNil(getOutput.body, file: file, line: line) // Ensure there's a body in the response.

// Additional step for stream: Validate stream and read data.
if case .stream = upload {
let streamingBody = try XCTUnwrap(getOutput.body, file: file, line: line)
if case .stream(let stream) = streamingBody {
XCTAssert(stream is ValidatingBufferedStream, "Expected ValidatingBufferedStream for streaming upload", file: file, line: line)
let data = try await streamingBody.readData() // will error if checksum mismatch
XCTAssertEqual(data, originalData, file: file, line: line)
} else {
XCTFail("Did not receive a stream when expected for checksum validation!", file: file, line: line)
}
}
}

private func getChecksumResponse(from response: PutObjectOutput, with algorithm: S3ClientTypes.ChecksumAlgorithm) throws -> String? {
switch algorithm {
case .crc32:
return response.checksumCRC32
case .crc32c:
return response.checksumCRC32C
case .sha1:
return response.checksumSHA1
case .sha256:
return response.checksumSHA256
default:
XCTFail("Unsupported checksum algorithm")
return nil
}
}
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func addResolvedTargets() {

addDependencies(
clientRuntimeVersion: "0.44.0",
crtVersion: "0.26.0"
crtVersion: "0.28.0"
)

// Uncomment this line to exclude runtime unit tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ public struct SigV4AuthScheme: ClientRuntime.AuthScheme {
updatedSigningProperties.set(key: AttributeKeys.shouldNormalizeURIPath, value: true)
updatedSigningProperties.set(key: AttributeKeys.omitSessionToken, value: false)

// Copy checksum from middleware context to signing properties
updatedSigningProperties.set(key: AttributeKeys.checksum, value: context.getChecksum())

// Copy chunked streaming eligiblity from middleware context to signing properties
updatedSigningProperties.set(
key: AttributeKeys.isChunkedEligibleStream,
value: context.getIsChunkedEligibleStream()
)

// Set service-specific signing properties if needed.
try CustomSigningPropertiesSetter().setServiceSpecificSigningProperties(
signingProperties: &updatedSigningProperties,
Expand Down
53 changes: 50 additions & 3 deletions Sources/Core/AWSClientRuntime/Signing/AWSSigV4Signer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,31 @@ public class AWSSigV4Signer: ClientRuntime.Signer {
try unsignedRequest.toHttp2Request() :
try unsignedRequest.toHttpRequest()

let crtSigningConfig = try signingConfig.toCRTType()

let crtSignedRequest = try await Signer.signRequest(
request: crtUnsignedRequest,
config: signingConfig.toCRTType()
config: crtSigningConfig
)

let sdkSignedRequest = requestBuilder.update(from: crtSignedRequest, originalRequest: unsignedRequest)

if crtSigningConfig.useAwsChunkedEncoding {
guard let requestSignature = crtSignedRequest.signature else {
throw ClientError.dataNotFound("Could not get request signature!")
}

// Set streaming body to an AwsChunked wrapped type
try sdkSignedRequest.setAwsChunkedBody(
signingConfig: crtSigningConfig,
signature: requestSignature,
trailingHeaders: unsignedRequest.trailingHeaders,
checksumAlgorithm: signingProperties.get(key: AttributeKeys.checksum)
)
}

// Return signed request
return requestBuilder.update(from: crtSignedRequest, originalRequest: unsignedRequest)
return sdkSignedRequest
}

private func constructSigningConfig(
Expand Down Expand Up @@ -77,7 +95,17 @@ public class AWSSigV4Signer: ClientRuntime.Signer {

let expiration: TimeInterval = signingProperties.get(key: AttributeKeys.expiration) ?? 0
let signedBodyHeader: AWSSignedBodyHeader = signingProperties.get(key: AttributeKeys.signedBodyHeader) ?? .none
let signedBodyValue: AWSSignedBodyValue = unsignedBody ? .unsignedPayload : .empty

// Determine signed body value
let checksum = signingProperties.get(key: AttributeKeys.checksum)
let isChunkedEligibleStream = signingProperties.get(key: AttributeKeys.isChunkedEligibleStream) ?? false

let signedBodyValue: AWSSignedBodyValue = determineSignedBodyValue(
checksum: checksum,
isChunkedEligbleStream: isChunkedEligibleStream,
isUnsignedBody: unsignedBody
)

let flags: SigningFlags = SigningFlags(
useDoubleURIEncode: signingProperties.get(key: AttributeKeys.useDoubleURIEncode) ?? true,
shouldNormalizeURIPath: signingProperties.get(key: AttributeKeys.shouldNormalizeURIPath) ?? true,
Expand Down Expand Up @@ -216,4 +244,23 @@ public class AWSSigV4Signer: ClientRuntime.Signer {
signingAlgorithm: signingConfig.signingAlgorithm
)
}

private func determineSignedBodyValue(
checksum: ChecksumAlgorithm?,
isChunkedEligbleStream: Bool,
isUnsignedBody: Bool
) -> AWSSignedBodyValue {
if !isChunkedEligbleStream {
// Normal Payloads, Event Streams, etc.
return isUnsignedBody ? .unsignedPayload : .empty
}

// streaming + eligible for chunked transfer
if checksum == nil {
return isUnsignedBody ? .unsignedPayload : .streamingSha256Payload
} else {
// checksum is present
return isUnsignedBody ? .streamingUnsignedPayloadTrailer : .streamingSha256PayloadTrailer
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public enum AWSSignedBodyValue {
case unsignedPayload
case streamingSha256Payload
case streamingSha256Events
case streamingSha256PayloadTrailer
case streamingUnsignedPayloadTrailer
}

extension AWSSignedBodyValue {
Expand All @@ -23,6 +25,8 @@ extension AWSSignedBodyValue {
case .unsignedPayload: return .unsignedPayload
case .streamingSha256Payload: return .streamingSha256Payload
case .streamingSha256Events: return .streamingSha256Events
case .streamingSha256PayloadTrailer: return .streamingSha256PayloadTrailer
case .streamingUnsignedPayloadTrailer: return .streamingUnSignedPayloadTrailer
}
}
}
20 changes: 18 additions & 2 deletions Sources/Core/AWSClientRuntime/Signing/AWSSigningConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,23 @@ public struct AWSSigningConfig {

extension AWSSigningConfig {
func toCRTType() throws -> SigningConfig {
SigningConfig(
// Never include the Transfer-Encoding header in the signature,
// since older versions of URLSession will modify this header's value
// prior to sending a request.
//
// The Transfer-Encoding header does not affect the AWS operation being
// performed and is just there to coordinate the flow of data to the server.
//
// For all other headers, use the shouldSignHeaders block that was passed to
// determine if the header should be included in the signature. If the
// shouldSignHeaders block was not provided, then include all headers other
// than Transfer-Encoding.
let modifiedShouldSignHeader = { (name: String) in
guard name.lowercased(with: Locale(identifier: "en_US_POSIX")) != "transfer-encoding" else { return false }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

what's the reason for checking en_US_POSIX locale?

Copy link
Contributor

Choose a reason for hiding this comment

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

If you don't specify the locale for the date & string operations that are locale-dependent, then you get the locale that the user sets as default on their device. And that may not result in lowercasing like you expected.

(I have had this happen with datetime stamps in the past, not with lowercasing, but the same risk is present)

return shouldSignHeader?(name) ?? true
}

return SigningConfig(
algorithm: signingAlgorithm.toCRTType(),
signatureType: signatureType.toCRTType(),
service: service,
Expand All @@ -67,7 +83,7 @@ extension AWSSigningConfig {
expiration: expiration,
signedBodyHeader: signedBodyHeader.toCRTType(),
signedBodyValue: signedBodyValue.toCRTType(),
shouldSignHeader: shouldSignHeader,
shouldSignHeader: modifiedShouldSignHeader,
useDoubleURIEncode: flags.useDoubleURIEncode,
shouldNormalizeURIPath: flags.shouldNormalizeURIPath,
omitSessionToken: flags.omitSessionToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ private object FlexibleChecksumRequestMiddleware : MiddlewareRenderable {
val httpChecksumTrait = op.getTrait(HttpChecksumTrait::class.java).orElse(null)
val inputMemberName = httpChecksumTrait?.requestAlgorithmMember?.get()?.lowercaseFirstLetter()

// Convert algorithmNames list to a Swift array representation
val middlewareInit = "${ClientRuntimeTypes.Middleware.FlexibleChecksumsRequestMiddleware}<$inputShapeName, $outputShapeName>(checksumAlgorithm: input.$inputMemberName?.rawValue)"

writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: $middlewareInit)")
Expand Down
2 changes: 1 addition & 1 deletion packageDependencies.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<key>awsCRTSwiftBranch</key>
<string>main</string>
<key>awsCRTSwiftVersion</key>
<string>0.26.0</string>
<string>0.28.0</string>
<key>clientRuntimeBranch</key>
<string>main</string>
<key>clientRuntimeVersion</key>
Expand Down
11 changes: 11 additions & 0 deletions scripts/ci_steps/log_tool_versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ set -e

echo

# Log OS version (sw_vers on Mac, uname -a on Linux)

if command -v sw_vers &> /dev/null
then
sw_vers
else
uname -a
fi



# Log CPU for hardware in use, if running on Mac

if [[ "$OSTYPE" == "darwin"* ]];
Expand Down
Loading