Skip to content

Commit

Permalink
chore: Merge latest changes from main into SRA I&A (#662)
Browse files Browse the repository at this point in the history
* chore: Require Swift 5.7, fix deprecation warnings (#600)

* feat: support initial-response in RPC based event streams (#597)

* chore: Updates version to 0.32.0

* chore: Add newline to README.md (#602)

* feat: add limited support in smithy-swift for visionOS (#606)

* feat: add support for requiresLength trait and Transfer-Encoding: Chunked (#604)

* chore: Update to aws-crt-swift 0.15.0 (#607)

* fix: content-length middleware should not error on event streams (#608)

* chore: Updates version to 0.33.0

* chore: Improved downstream task (#568)

* chore: Convert idempotency token middleware from closure to reusable type (#610)

* fix: Update aws-crt-swift dependency to 0.17.0 (#612)

* chore: Updates version to 0.34.0

* fix: Endpoint url should be nil if host or scheme is missing (#614)

* fix: Pool HTTP connections based on scheme, host, and port (#615)

* add default log level to initialize method (#616)

* feat: add utility method for converting SdkHttpRequest to URLRequest. (#613)

* Add extension constructor to URLRequest to convert SDKHttpRequest

* Add preprocessor conditional import functionality to SwiftWriter.

---------

Co-authored-by: Sichan Yoo <[email protected]>

* chore: Updates version to 0.35.0

* fix: Add a header to operation doc comments (#621)

* remove unnecessary TODOs (#622)

* fix: Codegen issues re: recursion, Swift keywords in unions (#623)

* feat!: Replace the XML encoder with a custom Smithy implementation (#619)

* feat!: Use closures for processing HTTP response (#624)

* feat: add custom trait PaginationTruncationMember (#625)

* allow isTruncated to be optional bool (#626)

* chore: Updates version to 0.36.0

* chore: Run tvOS old & new in CI (#628)

* fix: Fix Package.swift warning on Mac (#629)

* chore: refactor HttpBody and ByteStream to be a single class ByteStream (#627)

* chore: remove sync read in unused data extension (#630)

* update smithy to 1.42.0 (#631)

* chore: Updates version to 0.37.0

* chore: Update to aws-crt-swift 0.20.0 (#633)

* fix: add back from method with fileHandle (#635)

* fix!: Add no-op behavior for initialize methods of logging system. (#637)

* Add no-op behavior for initialize methods if it isn't the first time being called.

* Make LockingSystem threadsafe.

* Make initialize methods async.

---------

Co-authored-by: Sichan Yoo <[email protected]>

* feat!: URLSession-based HTTP Client (#636)

* bump up CRT version to 0.22.0 (#639)

* chore: Update version to 0.38.0 (#641)

* chore: Empty commit (#643)

* feat: add wrapper for checksums + unit tests (#642)

* feat: Use the Foundation HTTP client by default on Mac (#646)

* chore: Updates version to 0.39.0

* chore: Change MyURLQueryItem to SDKURLQueryItem (#652)

* feat!: Provide HTTP request components by closure instead of protocol (#654)

* fix: Don't retry modeled errors by default (#653)

* feat!: Eliminate service client protocols (#655)

* feat: add support for flexible checksums on Normal payloads (#647)

* chore: Update aws-crt-swift to 0.26.0 (#661)

---------

Co-authored-by: Josh Elkins <[email protected]>
Co-authored-by: David Yaffe <[email protected]>
Co-authored-by: AWS SDK Swift Automation <[email protected]>
Co-authored-by: Cyprien Ricque <[email protected]>
Co-authored-by: Sichan Yoo <[email protected]>
  • Loading branch information
6 people authored Feb 9, 2024
1 parent ec8f4ea commit bb7045d
Show file tree
Hide file tree
Showing 20 changed files with 538 additions and 258 deletions.
75 changes: 71 additions & 4 deletions Sources/ClientRuntime/Networking/HashFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@

import AwsCommonRuntimeKit

enum HashResult {
public enum HashResult {
case data(Data)
case integer(UInt32)
}

enum HashError: Error {
public enum HashError: Error {
case invalidInput
case hashingFailed(reason: String)
}

enum HashFunction {
public enum HashFunction {
case crc32, crc32c, sha1, sha256, md5

static func from(string: String) -> HashFunction? {
Expand All @@ -29,7 +29,29 @@ enum HashFunction {
}
}

var isSupported: Bool {
static func fromList(_ stringArray: [String]) -> [HashFunction] {
var hashFunctions = [HashFunction]()

for string in stringArray {
if let hashFunction = HashFunction.from(string: string) {
hashFunctions.append(hashFunction)
}
}

return hashFunctions
}

func toString() -> String {
switch self {
case .crc32: return "crc32"
case .crc32c: return "crc32c"
case .sha1: return "sha1"
case .sha256: return "sha256"
case .md5: return "md5"
}
}

var isFlexibleChecksum: Bool {
switch self {
case .crc32, .crc32c, .sha256, .sha1:
return true
Expand Down Expand Up @@ -69,6 +91,41 @@ enum HashFunction {
}
}

extension HashFunction: Comparable {
/*
* Priority-order for validating checksum = [ CRC32C, CRC32, SHA1, SHA256 ]
* Order is determined by speed of the algorithm's implementation
* MD5 is not supported by list ordering
*/
public static func < (lhs: HashFunction, rhs: HashFunction) -> Bool {
let order: [HashFunction] = [.crc32c, .crc32, .sha1, .sha256]

let lhsIndex = order.firstIndex(of: lhs) ?? Int.max
let rhsIndex = order.firstIndex(of: rhs) ?? Int.max

return lhsIndex < rhsIndex
}
}

extension [HashFunction] {
func getPriorityOrderValidationList() -> [HashFunction] {
// Filter out .md5 if present and then sort the remaining hash functions
return self.filter { $0 != .md5 }.sorted()
}
}

extension UInt32 {
func toBase64EncodedString() -> String {
// Create a Data instance from the UInt32 value
var value = self
var bigEndianValue = value.bigEndian
var data = Data(bytes: &bigEndianValue, count: MemoryLayout<UInt32>.size)

// Base64 encode the data
return data.base64EncodedString()
}
}

extension HashResult {

// Convert a HashResult to a hexadecimal String
Expand All @@ -80,4 +137,14 @@ extension HashResult {
return String(format: "%08x", integer)
}
}

// Convert a HashResult to a base64-encoded String
func toBase64String() -> String {
switch self {
case .data(let data):
return data.base64EncodedString()
case .integer(let integer):
return integer.toBase64EncodedString()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public struct ContentMD5Middleware<OperationStackOutput>: Middleware {
Self.MOutput == H.Output,
Self.Context == H.Context {

// Skip MD5 hash if using checksums
if (input.headers.exists(name: "x-amz-sdk-checksum-algorithm")) {
return try await next.handle(context: context, input: input)
}

switch input.body {
case .data(let data):
guard let data else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0.

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,
Self.Context == H.Context {

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

guard let checksumString = checksumAlgorithm else {
logger.info("No checksum provided! Skipping flexible checksums workflow...")
return try await next.handle(context: context, input: input)
}

guard let checksumHashFunction = HashFunction.from(string: checksumString) else {
logger.info("Found no supported checksums! Skipping flexible checksums workflow...")
return try await next.handle(context: context, input: input)
}

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

// Get the request
let request = input.builder

func handleNormalPayload(_ data: Data?) throws {

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

guard let data else {
throw ClientError.dataNotFound("Cannot calculate checksum of empty body!")
}

if input.builder.headers.value(for: headerName) == nil {
logger.debug("Calculating checksum")
}

let checksum = try checksumHashFunction.computeHash(of: data).toBase64String()

request.updateHeader(name: headerName, value: [checksum])
}

func handleStreamPayload(_ stream: Stream) throws {
logger.error("Stream payloads are not yet supported with flexible checksums!")
return
}

// Handle body vs handle stream
switch request.body {
case .data(let data):
try handleNormalPayload(data)
case .stream(let stream):
try handleStreamPayload(stream)
case .noStream:
throw ClientError.dataNotFound("Cannot calculate the checksum of an empty body!")
}

return try await next.handle(context: context, input: input)
}

public typealias MInput = SerializeStepInput<OperationStackInput>
public typealias MOutput = OperationOutput<OperationStackOutput>
public typealias Context = HttpContext
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0.

public struct FlexibleChecksumsResponseMiddleware<OperationStackOutput>: Middleware {

public let id: String = "FlexibleChecksumsResponseMiddleware"

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

let validationMode: Bool

public init(validationMode: Bool) {
self.validationMode = validationMode
}

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,
Self.Context == H.Context {

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

// Exit if validation should not be performed
if !validationMode {
logger.info("Checksum validation should not be performed! Skipping workflow...")
return try await next.handle(context: context, input: input)
}

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

// Determine if any checksum headers are present
logger.debug("HEADERS: \(httpResponse.headers)")
let _checksumHeader = CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST.first {
httpResponse.headers.value(for: "x-amz-checksum-\($0)") != nil
}
guard let checksumHeader = _checksumHeader else {
logger.warn(
"User requested checksum validation, but the response headers did not contain any valid checksums"
)
return try await next.handle(context: context, input: input)
}

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

// let the user know which checksum will be validated
logger.debug("Validating checksum from \(fullChecksumHeader)")
context.attributes.set(key: AttributeKey<String>(name: "ChecksumHeaderValidated"), value: fullChecksumHeader)

let checksumString = checksumHeader.removePrefix("x-amz-checksum-")
guard let responseChecksum = HashFunction.from(string: checksumString) else {
throw ClientError.dataNotFound("Checksum found in header is not supported!")
}
guard let expectedChecksum = httpResponse.headers.value(for: fullChecksumHeader) else {
throw ClientError.dataNotFound("Could not determine the expected checksum!")
}

func handleNormalPayload(_ data: Data?) throws {

guard let data else {
throw ClientError.dataNotFound("Cannot calculate checksum of empty body!")
}

let calculatedChecksum = try responseChecksum.computeHash(of: data)

let actualChecksum = calculatedChecksum.toBase64String()

if expectedChecksum != actualChecksum {
throw ChecksumMismatchException.message(
"Checksum mismatch. Expected \(expectedChecksum) but was \(actualChecksum)"
)
}
}

func handleStreamPayload(_ stream: Stream) throws {
return
}

// Handle body vs handle stream
switch response.httpResponse.body {
case .data(let data):
try handleNormalPayload(data)
case .stream(let stream):
try handleStreamPayload(stream)
case .noStream:
throw ClientError.dataNotFound("Cannot calculate the checksum of an empty body!")
}

return try await next.handle(context: context, input: input)
}

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

enum ChecksumMismatchException: Error {
case message(String)
}
Loading

0 comments on commit bb7045d

Please sign in to comment.