Skip to content

Commit 5858bf1

Browse files
sichanyooSichan Yoo
andauthored
chore: Add SigV4 things to smithy-swift (#787)
* Add SigV4 things to smithy-swift. * swiftlint & compile errors * Sort out the module dependencies. * Dependency issue * Fix build issues * Fix dependency issues --------- Co-authored-by: Sichan Yoo <[email protected]>
1 parent c089796 commit 5858bf1

File tree

14 files changed

+303
-49
lines changed

14 files changed

+303
-49
lines changed

Package.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ let package = Package(
4040
.library(name: "SmithyIdentity", targets: ["SmithyIdentity"]),
4141
.library(name: "SmithyIdentityAPI", targets: ["SmithyIdentityAPI"]),
4242
.library(name: "SmithyHTTPAPI", targets: ["SmithyHTTPAPI"]),
43+
.library(name: "SmithyHTTPClient", targets: ["SmithyHTTPClient"]),
4344
.library(name: "SmithyHTTPAuth", targets: ["SmithyHTTPAuth"]),
4445
.library(name: "SmithyHTTPAuthAPI", targets: ["SmithyHTTPAuthAPI"]),
4546
.library(name: "SmithyEventStreamsAPI", targets: ["SmithyEventStreamsAPI"]),
@@ -74,6 +75,7 @@ let package = Package(
7475
"SmithyIdentity",
7576
"SmithyIdentityAPI",
7677
"SmithyHTTPAPI",
78+
"SmithyHTTPClient",
7779
"SmithyHTTPAuth",
7880
"SmithyHTTPAuthAPI",
7981
"SmithyEventStreamsAPI",
@@ -147,13 +149,24 @@ let package = Package(
147149
name: "SmithyHTTPAPI",
148150
dependencies: ["Smithy"]
149151
),
152+
.target(
153+
name: "SmithyHTTPClient",
154+
dependencies: [
155+
"Smithy",
156+
"SmithyHTTPAPI",
157+
"SmithyStreams",
158+
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift")
159+
]
160+
),
150161
.target(
151162
name: "SmithyHTTPAuth",
152163
dependencies: [
153164
"Smithy",
154165
"SmithyHTTPAuthAPI",
155166
"SmithyIdentity",
156167
"SmithyIdentityAPI",
168+
"SmithyChecksumsAPI",
169+
"SmithyHTTPClient",
157170
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift")
158171
]
159172
),
@@ -196,7 +209,7 @@ let package = Package(
196209
"Smithy",
197210
"SmithyChecksumsAPI",
198211
"SmithyStreams",
199-
"SmithyHTTPAuth",
212+
"SmithyHTTPClient",
200213
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift")
201214
]
202215
),
@@ -208,6 +221,16 @@ let package = Package(
208221
dependencies: ["ClientRuntime", "SmithyTestUtil", "SmithyStreams"],
209222
resources: [ .process("Resources") ]
210223
),
224+
.testTarget(
225+
name: "SmithyHTTPClientTests",
226+
dependencies: [
227+
"SmithyHTTPClient",
228+
"SmithyHTTPAPI",
229+
"Smithy",
230+
"SmithyTestUtil",
231+
.product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift")
232+
]
233+
),
211234
.testTarget(
212235
name: "SmithyXMLTests",
213236
dependencies: ["SmithyXML", "ClientRuntime"]

Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0.
33

4-
import class Smithy.Context
4+
import Smithy
55
import enum SmithyChecksumsAPI.ChecksumAlgorithm
66
import AwsCommonRuntimeKit
77
import SmithyChecksums

Sources/Smithy/Stream.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,27 @@ extension Stream {
7272
}
7373
}
7474
}
75+
76+
/*
77+
* Default chunk size
78+
*/
79+
public let CHUNK_SIZE_BYTES: Int = 65_536
80+
81+
/*
82+
* The minimum size of a streaming body before the SDK will chunk
83+
* (such as setting aws-chunked content encoding)
84+
*/
85+
public let CHUNKED_THRESHOLD = CHUNK_SIZE_BYTES * 16
86+
87+
public extension Stream {
88+
/*
89+
* Return a Bool representing if the ByteStream (request body) is large enough to send in chunks
90+
*/
91+
var isEligibleForChunkedStreaming: Bool {
92+
if let length = self.length, length >= CHUNKED_THRESHOLD {
93+
return true
94+
} else {
95+
return false
96+
}
97+
}
98+
}

Sources/SmithyChecksums/ChunkedReader.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77

88
import protocol SmithyChecksumsAPI.Checksum
99
import enum SmithyChecksumsAPI.ChecksumAlgorithm
10-
import protocol Smithy.ReadableStream
1110
import struct SmithyHTTPAPI.Headers
1211
import AwsCommonRuntimeKit
13-
import SmithyHTTPAuth
12+
import SmithyHTTPClient
1413
import struct Foundation.Data
14+
import Smithy
1515

1616
public class ChunkedReader {
1717
private var stream: ReadableStream
@@ -110,7 +110,7 @@ public class ChunkedReader {
110110
}
111111

112112
private func getUnsignedChunk(from stream: ReadableStream) async throws -> Data? {
113-
let chunk = try await stream.readAsync(upToCount: SmithyChecksums.CHUNK_SIZE_BYTES) ?? Data()
113+
let chunk = try await stream.readAsync(upToCount: CHUNK_SIZE_BYTES) ?? Data()
114114

115115
self.chunkBody = chunk
116116

Sources/SmithyChecksums/Stream+Chunks.swift

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import class Smithy.Context
9+
import enum SmithyHTTPAuthAPI.AWSSignedBodyHeader
10+
import enum SmithyHTTPAuthAPI.SigningPropertyKeys
11+
import protocol SmithyHTTPAuthAPI.AuthScheme
12+
import protocol SmithyHTTPAuthAPI.Signer
13+
import struct Smithy.Attributes
14+
import SmithyChecksumsAPI
15+
16+
public struct SigV4AuthScheme: AuthScheme {
17+
public let schemeID: String = "aws.auth#sigv4"
18+
public let signer: Signer = SigV4Signer()
19+
20+
public init() {}
21+
22+
public func customizeSigningProperties(
23+
signingProperties: Smithy.Attributes,
24+
context: Smithy.Context
25+
) throws -> Smithy.Attributes {
26+
var updatedSigningProperties = signingProperties
27+
28+
// Set signing algorithm flag
29+
updatedSigningProperties.set(key: SigningPropertyKeys.signingAlgorithm, value: .sigv4)
30+
31+
// Set bidirectional streaming flag
32+
updatedSigningProperties.set(
33+
key: SigningPropertyKeys.bidirectionalStreaming,
34+
value: context.isBidirectionalStreamingEnabled
35+
)
36+
37+
// Set signing name and signing region flags
38+
updatedSigningProperties.set(key: SigningPropertyKeys.signingName, value: context.signingName)
39+
updatedSigningProperties.set(key: SigningPropertyKeys.signingRegion, value: context.signingRegion)
40+
41+
// Set expiration flag
42+
//
43+
// Expiration is only used for presigning (presign request flow or presign URL flow).
44+
updatedSigningProperties.set(key: SigningPropertyKeys.expiration, value: context.expiration)
45+
46+
// Set signature type flag
47+
//
48+
// AWSSignatureType.requestQueryParams is only used for presign URL flow.
49+
// Out of the AWSSignatureType enum cases, only two are used. .requestHeaders and .requestQueryParams.
50+
// .requestHeaders is the deafult signing used for AWS operations.
51+
let isPresignURLFlow = context.getFlowType() == .PRESIGN_URL
52+
updatedSigningProperties.set(
53+
key: SigningPropertyKeys.signatureType,
54+
value: isPresignURLFlow ? .requestQueryParams : .requestHeaders
55+
)
56+
57+
// Set unsignedBody to true IFF operation had unsigned payload trait.
58+
let unsignedBody = context.hasUnsignedPayloadTrait()
59+
updatedSigningProperties.set(key: SigningPropertyKeys.unsignedBody, value: unsignedBody)
60+
61+
// Set default values.
62+
updatedSigningProperties.set(key: SigningPropertyKeys.signedBodyHeader, value: AWSSignedBodyHeader.none)
63+
updatedSigningProperties.set(key: SigningPropertyKeys.useDoubleURIEncode, value: true)
64+
updatedSigningProperties.set(key: SigningPropertyKeys.shouldNormalizeURIPath, value: true)
65+
updatedSigningProperties.set(key: SigningPropertyKeys.omitSessionToken, value: false)
66+
67+
// Copy checksum from middleware context to signing properties
68+
updatedSigningProperties.set(key: SigningPropertyKeys.checksum, value: context.checksumString)
69+
70+
return updatedSigningProperties
71+
}
72+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import class AwsCommonRuntimeKit.HTTPRequestBase
9+
import class AwsCommonRuntimeKit.Signer
10+
import class SmithyHTTPAPI.HTTPRequest
11+
import class SmithyHTTPAPI.HTTPRequestBuilder
12+
import enum AwsCommonRuntimeKit.CommonRunTimeError
13+
import enum Smithy.ClientError
14+
import enum SmithyHTTPAuthAPI.AWSSignedBodyHeader
15+
import enum SmithyHTTPAuthAPI.AWSSignedBodyValue
16+
import enum SmithyHTTPAuthAPI.AWSSignatureType
17+
import enum SmithyHTTPAuthAPI.SigningAlgorithm
18+
import enum SmithyHTTPAuthAPI.SigningPropertyKeys
19+
import protocol SmithyIdentity.AWSCredentialIdentityResolver
20+
import protocol SmithyIdentityAPI.Identity
21+
import protocol SmithyHTTPAuthAPI.Signer
22+
import struct AwsCommonRuntimeKit.SigningConfig
23+
import struct Smithy.AttributeKey
24+
import struct Smithy.Attributes
25+
import struct Smithy.SwiftLogger
26+
import struct SmithyIdentity.AWSCredentialIdentity
27+
import struct SmithyHTTPAuthAPI.SigningFlags
28+
import struct Foundation.Date
29+
import struct Foundation.TimeInterval
30+
import struct Foundation.URL
31+
import SmithyHTTPClient
32+
33+
public class SigV4Signer: SmithyHTTPAuthAPI.Signer {
34+
public init() {}
35+
36+
public func signRequest<IdentityT>(
37+
requestBuilder: SmithyHTTPAPI.HTTPRequestBuilder,
38+
identity: IdentityT,
39+
signingProperties: Smithy.Attributes
40+
) async throws -> SmithyHTTPAPI.HTTPRequestBuilder where IdentityT: SmithyIdentityAPI.Identity {
41+
guard let isBidirectionalStreamingEnabled = signingProperties.get(
42+
key: SigningPropertyKeys.bidirectionalStreaming
43+
) else {
44+
throw Smithy.ClientError.authError(
45+
"Signing properties passed to the AWSSigV4Signer must contain T/F flag for bidirectional streaming."
46+
)
47+
}
48+
49+
guard let identity = identity as? AWSCredentialIdentity else {
50+
throw Smithy.ClientError.authError(
51+
"Identity passed to the AWSSigV4Signer must be of type Credentials."
52+
)
53+
}
54+
55+
var signingConfig = try constructSigningConfig(identity: identity, signingProperties: signingProperties)
56+
57+
let unsignedRequest = requestBuilder.build()
58+
let crtUnsignedRequest: HTTPRequestBase = isBidirectionalStreamingEnabled ?
59+
try unsignedRequest.toHttp2Request() :
60+
try unsignedRequest.toHttpRequest()
61+
62+
let crtSigningConfig = try signingConfig.toCRTType()
63+
64+
let crtSignedRequest = try await Signer.signRequest(
65+
request: crtUnsignedRequest,
66+
config: crtSigningConfig
67+
)
68+
69+
let sdkSignedRequest = requestBuilder.update(from: crtSignedRequest, originalRequest: unsignedRequest)
70+
71+
// Return signed request
72+
return sdkSignedRequest
73+
}
74+
75+
private func constructSigningConfig(
76+
identity: AWSCredentialIdentity,
77+
signingProperties: Smithy.Attributes
78+
) throws -> AWSSigningConfig {
79+
guard let unsignedBody = signingProperties.get(key: SigningPropertyKeys.unsignedBody) else {
80+
throw Smithy.ClientError.authError(
81+
"Signing properties passed to the AWSSigV4Signer must contain T/F flag for unsigned body."
82+
)
83+
}
84+
guard let signingName = signingProperties.get(key: SigningPropertyKeys.signingName) else {
85+
throw Smithy.ClientError.authError(
86+
"Signing properties passed to the AWSSigV4Signer must contain signing name."
87+
)
88+
}
89+
guard let signingRegion = signingProperties.get(key: SigningPropertyKeys.signingRegion) else {
90+
throw Smithy.ClientError.authError(
91+
"Signing properties passed to the AWSSigV4Signer must contain signing region."
92+
)
93+
}
94+
guard let signingAlgorithm = signingProperties.get(key: SigningPropertyKeys.signingAlgorithm) else {
95+
throw Smithy.ClientError.authError(
96+
"Signing properties passed to the AWSSigV4Signer must contain signing algorithm."
97+
)
98+
}
99+
100+
let expiration: TimeInterval = signingProperties.get(key: SigningPropertyKeys.expiration) ?? 0
101+
let signedBodyHeader: AWSSignedBodyHeader =
102+
signingProperties.get(key: SigningPropertyKeys.signedBodyHeader) ?? .none
103+
104+
// Determine signed body value
105+
let checksumIsPresent = signingProperties.get(key: SigningPropertyKeys.checksum) != nil
106+
let isChunkedEligibleStream = signingProperties.get(key: SigningPropertyKeys.isChunkedEligibleStream) ?? false
107+
let preComputedSha256 = signingProperties.get(key: AttributeKey<String>(name: "SignedBodyValue"))
108+
109+
let signedBodyValue: AWSSignedBodyValue = determineSignedBodyValue(
110+
checksumIsPresent: checksumIsPresent,
111+
isChunkedEligbleStream: isChunkedEligibleStream,
112+
isUnsignedBody: unsignedBody,
113+
preComputedSha256: preComputedSha256
114+
)
115+
116+
let flags: SigningFlags = SigningFlags(
117+
useDoubleURIEncode: signingProperties.get(key: SigningPropertyKeys.useDoubleURIEncode) ?? true,
118+
shouldNormalizeURIPath: signingProperties.get(key: SigningPropertyKeys.shouldNormalizeURIPath) ?? true,
119+
omitSessionToken: signingProperties.get(key: SigningPropertyKeys.omitSessionToken) ?? false
120+
)
121+
let signatureType: AWSSignatureType =
122+
signingProperties.get(key: SigningPropertyKeys.signatureType) ?? .requestHeaders
123+
124+
return AWSSigningConfig(
125+
credentials: identity,
126+
expiration: expiration,
127+
signedBodyHeader: signedBodyHeader,
128+
signedBodyValue: signedBodyValue,
129+
flags: flags,
130+
date: Date(),
131+
service: signingName,
132+
region: signingRegion,
133+
signatureType: signatureType,
134+
signingAlgorithm: signingAlgorithm
135+
)
136+
}
137+
138+
private func determineSignedBodyValue(
139+
checksumIsPresent: Bool,
140+
isChunkedEligbleStream: Bool,
141+
isUnsignedBody: Bool,
142+
preComputedSha256: String?
143+
) -> AWSSignedBodyValue {
144+
if !isChunkedEligbleStream {
145+
// Normal Payloads, Event Streams, etc.
146+
if isUnsignedBody {
147+
return .unsignedPayload
148+
} else if let sha256 = preComputedSha256 {
149+
return .precomputed(sha256)
150+
} else {
151+
return .empty
152+
}
153+
}
154+
155+
// streaming + eligible for chunked transfer
156+
if !checksumIsPresent {
157+
return isUnsignedBody ? .unsignedPayload : .streamingSha256Payload
158+
} else {
159+
// checksum is present
160+
return isUnsignedBody ? .streamingUnsignedPayloadTrailer : .streamingSha256PayloadTrailer
161+
}
162+
}
163+
}

0 commit comments

Comments
 (0)