From db2e1e653354bd19c60ff01d1ff304c4f216336c Mon Sep 17 00:00:00 2001 From: RuiyangSun Date: Sun, 21 Apr 2024 18:52:03 +0800 Subject: [PATCH 1/6] chore(package): add dependence `SyncStream` --- Package.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 3ae3f9d..215ccdc 100644 --- a/Package.swift +++ b/Package.swift @@ -21,13 +21,15 @@ let package = Package( ), ], dependencies: [ + .package(url: "https://github.com/rockmagma02/SyncStream.git", from: "1.1.0"), .package(url: "https://github.com/apple/swift-docc-plugin", branch: "main"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "HttpX" + name: "HttpX", + dependencies: ["SyncStream"] ), .testTarget( name: "HttpXTests", From 03aa0493cb2081ae544cf79e21540b01ffca7d26 Mon Sep 17 00:00:00 2001 From: RuiyangSun Date: Mon, 22 Apr 2024 13:55:34 +0800 Subject: [PATCH 2/6] chore(swiftlint): disable `no_grouping_extension` --- .swiftlint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swiftlint.yaml b/.swiftlint.yaml index d61e0b0..0aeaa41 100644 --- a/.swiftlint.yaml +++ b/.swiftlint.yaml @@ -71,7 +71,7 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in - multiline_parameters_brackets - nimble_operator # - no_extension_access_modifier - - no_grouping_extension + # - no_grouping_extension - no_magic_numbers - non_overridable_class_declaration - nslocalizedstring_key From 5b29026ee8857150dd67ad5357235d47b4e9549f Mon Sep 17 00:00:00 2001 From: RuiyangSun Date: Mon, 22 Apr 2024 13:56:27 +0800 Subject: [PATCH 3/6] chore(package): update SyncStream --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 215ccdc..d15a264 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/rockmagma02/SyncStream.git", from: "1.1.0"), + .package(url: "https://github.com/rockmagma02/SyncStream.git", from: "1.1.2"), .package(url: "https://github.com/apple/swift-docc-plugin", branch: "main"), ], targets: [ From a869ce46813c68d15046584c635f825f4a5739f4 Mon Sep 17 00:00:00 2001 From: RuiyangSun Date: Mon, 22 Apr 2024 14:00:19 +0800 Subject: [PATCH 4/6] refactor(Auth): refactor Auth, support stream --- Sources/HttpX/Auth/APIKeyAuth.swift | 30 +++-- Sources/HttpX/Auth/BaseAuth.swift | 172 ++++++++++++++++++-------- Sources/HttpX/Auth/BasicAuth.swift | 41 ++++-- Sources/HttpX/Auth/DigestAuth.swift | 166 +++++++++++++++++-------- Sources/HttpX/Auth/EmptyAuth.swift | 22 +++- Sources/HttpX/Auth/FunctionAuth.swift | 26 +++- Sources/HttpX/Auth/OAuth.swift | 34 +++-- 7 files changed, 341 insertions(+), 150 deletions(-) diff --git a/Sources/HttpX/Auth/APIKeyAuth.swift b/Sources/HttpX/Auth/APIKeyAuth.swift index daf0b30..0fe33e1 100644 --- a/Sources/HttpX/Auth/APIKeyAuth.swift +++ b/Sources/HttpX/Auth/APIKeyAuth.swift @@ -13,9 +13,9 @@ // limitations under the License. import Foundation +import SyncStream /// The APIKeyAuth class, user should provide the key. -@available(macOS 10.15, *) public class APIKeyAuth: BaseAuth { // MARK: Lifecycle @@ -32,18 +32,28 @@ public class APIKeyAuth: BaseAuth { /// default value is false public var needRequestBody: Bool { false } + /// default value is false public var needResponseBody: Bool { false } - public func authFlow(request: URLRequest?, lastResponse _: Response?) throws -> (URLRequest?, Bool) { - if var request { - request.setValue( - key, - forHTTPHeaderField: "x-api-key" - ) - return (request, true) - } - throw AuthError.invalidRequest(message: "Request is nil in \(APIKeyAuth.self)") + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalSyncStream.Continuation + ) { + var request = request + request.setValue(key, forHTTPHeaderField: "X-Api-Key") + continuation.yield(request) + continuation.return(NoneType()) + } + + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalAsyncStream.Continuation + ) async { + var request = request + request.setValue(key, forHTTPHeaderField: "X-Api-Key") + await continuation.yield(request) + await continuation.return(NoneType()) } // MARK: Private diff --git a/Sources/HttpX/Auth/BaseAuth.swift b/Sources/HttpX/Auth/BaseAuth.swift index 64fd515..5c728f3 100644 --- a/Sources/HttpX/Auth/BaseAuth.swift +++ b/Sources/HttpX/Auth/BaseAuth.swift @@ -13,82 +13,144 @@ // limitations under the License. import Foundation +import SyncStream // MARK: - BaseAuth +// swiftlint:disable closure_body_length + /// Protocol defining the base authentication flow. -@available(macOS 10.15, *) public protocol BaseAuth { /// Indicates if the request body is needed for authentication. var needRequestBody: Bool { get } /// Indicates if the response body is needed for authentication. var needResponseBody: Bool { get } - /// Handles the authentication flow for a given request and last response. + /// The authentication flow. Bidirectional communication with the Client. + /// /// - Parameters: - /// - request: The current URLRequest that needs authentication. - /// - lastResponse: The last URLResponse received from the server. - /// - Returns: A tuple containing the modified URLRequest and a Bool indicating if auth flow is done. + /// - request: The initial request to be authenticated. + /// - continuation: The continuation of the stream, which will communicate with + /// the client. Having method `yield(_:)`, `return(_:)`, `throw(_:)`. + /// + /// Basically, The Client will give the initial request to the flow, and the flow can + /// add auth header or do other something, than `yield(_:)` it back to the client. + /// The Client will send the request to the server, and fetch the response back, then + /// send it back to the flow, i.d. the `yield(_:)` will return the response. If the + /// auth progress is over, authFlow can invoke `return(NoneType())` to end the flow, + /// or continue the flow by `yield(_:)` the modified request back to the client. If + /// any error occurred, the flow can throw the error to the client by `throw(_:)`. + /// + /// When you creat a custom authentication method, The `authFlow(_:, continuation:)` + /// method should be implemented. + func authFlow( + _ request: URLRequest, + continuation: BidirectionalSyncStream.Continuation + ) + + /// The Async Version authentication flow. Bidirectional communication with the Client. /// - /// When you create a custom authentication method, you need to implement the `authFlow` method. - /// The method should take two parameters, a `URLRequest` and a `Response`, and return a tuple - /// containing the modified `URLRequest` and a `Bool` value indicating whether the auth is done. + /// - Parameters: + /// - request: The initial request to be authenticated. + /// - continuation: The continuation of the stream, which will communicate with + /// the client. Having method `yield(_:)`, `return(_:)`, `throw(_:)`. /// - /// In some situation, the auth need to use the response of the request to decide the next request. - /// Like the Digest Auth, after you send the first request, the server will return the - /// `WWW-Authenticate` header, and you need to use the header to generate the `Authorization` - /// header for the next request. In this case, you can return `false` in the second value of - /// the tuple to indicate the auth need to use the response. - func authFlow(request: URLRequest?, lastResponse: Response?) throws -> (URLRequest?, Bool) + /// Have the same behavior as the sync version, but the method is async, all interactions + /// with the `continuation` should be `await`. For the custom authentication method, you + /// can simplily copy the sync version `authFlow(_:, continuation:)` to the async version, + /// but add the `await` keyword before the `yield(_:)`, `return(_:)`, `throw(_:)`. + func authFlow( + _ request: URLRequest, + continuation: BidirectionalAsyncStream.Continuation + ) async } -@available(macOS 10.15, *) extension BaseAuth { - /// Synchronously handles the authentication flow for a given request and last response. - /// This method ensures that if the request body is needed, it is converted from a stream to data. - /// - Parameters: - /// - request: The current URLRequest that needs authentication. - /// - lastResponse: The last URLResponse received from the server. - /// - Returns: A tuple containing the modified URLRequest and a Bool indicating if auth flow is done. - func syncAuthFlow( // swiftlint:disable:this explicit_acl - request: URLRequest?, lastResponse: Response? - ) throws -> (URLRequest?, Bool) { - var request = request - let lastResponse = lastResponse - if needRequestBody, request != nil { - if let stream = request!.httpBodyStream { - // read all data from the stream - let data = stream.readAllData() - request!.httpBodyStream = nil - request!.httpBody = data + func authFlowAdapter( // swiftlint:disable:this explicit_acl + _ request: URLRequest + ) -> BidirectionalSyncStream { + BidirectionalSyncStream { continuation in + var request = request + var response: Response + if needRequestBody { + if let stream = request.httpBodyStream { + // read all data from the stream + let data = stream.readAllData() + request.httpBodyStream = nil + request.httpBody = data + } } + let streamAdapter = BidirectionalSyncStream { continuationAdapter in + self.authFlow(request, continuation: continuationAdapter) + } + do { + request = try streamAdapter.next() + } catch { + continuation.throw(error: error) + return + } + while true { + response = continuation.yield(request) + if needResponseBody { + _ = response.getData() + } + do { + request = try streamAdapter.send(response) + } catch { + if error is StopIteration { + break + } + continuation.throw(error: error) + return + } + } + continuation.return(NoneType()) } - - if needResponseBody, lastResponse != nil { - _ = lastResponse?.getData() - } - - return try authFlow(request: request, lastResponse: lastResponse) } - func asyncAuthFlow( // swiftlint:disable:this explicit_acl - request: URLRequest?, lastResponse: Response? - ) async throws -> (URLRequest?, Bool) { - var request = request - let lastResponse = lastResponse - if needRequestBody, request != nil { - if let stream = request!.httpBodyStream { - // read all data from the stream - let data = stream.readAllData() - request!.httpBodyStream = nil - request!.httpBody = data + func authFlowAdapter( // swiftlint:disable:this explicit_acl + _ request: URLRequest + ) async -> BidirectionalAsyncStream { + BidirectionalAsyncStream { continuation in + var request = request + var response: Response + if needRequestBody { + if let stream = request.httpBodyStream { + // read all data from the stream + let data = stream.readAllData() + request.httpBodyStream = nil + request.httpBody = data + } } + let streamAdapter = BidirectionalAsyncStream { continuaitonAdapter in + await self.authFlow(request, continuation: continuaitonAdapter) + } + do { + request = try await streamAdapter.next() + } catch { + await continuation.throw(error: error) + } + while true { + response = await continuation.yield(request) + if needResponseBody { + do { + _ = try await response.getData() + } catch { + await continuation.throw(error: error) + } + } + do { + request = try await streamAdapter.send(response) + } catch { + if error is StopIteration { + break + } + await continuation.throw(error: error) + } + } + await continuation.return(NoneType()) } - - if needResponseBody, lastResponse != nil { - _ = try await lastResponse?.getData() - } - - return try authFlow(request: request, lastResponse: lastResponse) } } + +// swiftlint:enable closure_body_length diff --git a/Sources/HttpX/Auth/BasicAuth.swift b/Sources/HttpX/Auth/BasicAuth.swift index d7dcff3..bf6b059 100644 --- a/Sources/HttpX/Auth/BasicAuth.swift +++ b/Sources/HttpX/Auth/BasicAuth.swift @@ -13,17 +13,16 @@ // limitations under the License. import Foundation +import SyncStream /// The BasicAuth class, user should provide the username and password. -@available(macOS 10.15, *) public class BasicAuth: BaseAuth { // MARK: Lifecycle /// Initialize the BasicAuth with username and password. - /// /// - Parameters: - /// - username: The username for the basic auth. - /// - password: The password for the basic auth. + /// - username: The username for the basic auth. + /// - password: The password for the basic auth. public init(username: String, password: String) { self.username = username self.password = password @@ -35,18 +34,34 @@ public class BasicAuth: BaseAuth { /// default value is false public var needRequestBody: Bool { false } + /// default value is false public var needResponseBody: Bool { false } - public func authFlow(request: URLRequest?, lastResponse _: Response?) throws -> (URLRequest?, Bool) { - if var request { - request.setValue( - buildAuthHeader(username: username, password: password), - forHTTPHeaderField: "Authorization" - ) - return (request, true) - } - throw AuthError.invalidRequest(message: "Request is nil in \(BasicAuth.self)") + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalSyncStream.Continuation + ) { + var request = request + request.setValue( + buildAuthHeader(username: username, password: password), + forHTTPHeaderField: "Authorization" + ) + continuation.yield(request) + continuation.return(NoneType()) + } + + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalAsyncStream.Continuation + ) async { + var request = request + request.setValue( + buildAuthHeader(username: username, password: password), + forHTTPHeaderField: "Authorization" + ) + await continuation.yield(request) + await continuation.return(NoneType()) } // MARK: Private diff --git a/Sources/HttpX/Auth/DigestAuth.swift b/Sources/HttpX/Auth/DigestAuth.swift index c383953..1a56f22 100644 --- a/Sources/HttpX/Auth/DigestAuth.swift +++ b/Sources/HttpX/Auth/DigestAuth.swift @@ -14,9 +14,11 @@ import CryptoKit import Foundation +import SyncStream + +// MARK: - DigestAuth /// The DigestAuth class, user should provide the username and password. -@available(macOS 10.15, *) public class DigestAuth: BaseAuth { // MARK: Lifecycle @@ -38,54 +40,138 @@ public class DigestAuth: BaseAuth { /// default value is false public var needRequestBody: Bool { false } + /// default value is false public var needResponseBody: Bool { false } - public func authFlow(request: URLRequest?, lastResponse: Response?) throws -> (URLRequest?, Bool) { - // First time, request is passed, but lastResponse is nil - if var request, lastResponse == nil { - if let lastChallenge { + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalSyncStream.Continuation + ) { + var request = request + if let lastChallenge { + do { try request.addValue( buildAuthHeader(request: request, challenge: lastChallenge), forHTTPHeaderField: "Authorization" ) + } catch { + continuation.throw(error: error) + return } + } - return (request, false) + let response = continuation.yield(request) + + if response.statusCode != needAuthStatusCode { + continuation.return(NoneType()) + return } - // Second time, the Request and the Response are both passed - if var request, let lastResponse { - if lastResponse.statusCode != needAuthStatusCode { - // If the response is not a 401 then we don't need to - // build an authenticated request - return (nil, true) - } + let authHeader = response.value(forHTTPHeaderField: "Www-Authenticate") + guard let authHeader, authHeader.lowercased().hasPrefix("digest ") else { + continuation.return(NoneType()) + return + } + + do { + lastChallenge = try parseChallenge(authHeader: authHeader) + } catch { + continuation.throw(error: error) + return + } + nonceCount = 1 + + do { + try request.addValue( + buildAuthHeader(request: request, challenge: lastChallenge!), + forHTTPHeaderField: "Authorization" + ) + } catch { + continuation.throw(error: error) + return + } + + continuation.yield(request) + continuation.return(NoneType()) + } - let authHeader = lastResponse.value(forHTTPHeaderField: "Www-Authenticate") - guard let authHeader, authHeader.lowercased().hasPrefix("digest ") else { - // If the response is not a digest auth challenge then we don't need to - // build an authenticated request - return (nil, true) + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalAsyncStream.Continuation + ) async { + var request = request + if let lastChallenge { + do { + try request.addValue( + buildAuthHeader(request: request, challenge: lastChallenge), + forHTTPHeaderField: "Authorization" + ) + } catch { + await continuation.throw(error: error) + return } + } + let response = await continuation.yield(request) + + if response.statusCode != needAuthStatusCode { + await continuation.return(NoneType()) + return + } + + let authHeader = response.value(forHTTPHeaderField: "Www-Authenticate") + guard let authHeader, authHeader.lowercased().hasPrefix("digest ") else { + await continuation.return(NoneType()) + return + } + + do { lastChallenge = try parseChallenge(authHeader: authHeader) - nonceCount = 1 + } catch { + await continuation.throw(error: error) + return + } + nonceCount = 1 + do { try request.addValue( buildAuthHeader(request: request, challenge: lastChallenge!), forHTTPHeaderField: "Authorization" ) - - return (request, true) + } catch { + await continuation.throw(error: error) + return } - throw AuthError.invalidRequest(message: "Request is nil in \(DigestAuth.self)") + await continuation.yield(request) + await continuation.return(NoneType()) } // MARK: Private - private enum HashAlgorithms { + private var username: String + private var password: String + + private var needAuthStatusCode = 401 + + private var nonceCount: Int + private var lastChallenge: DigestAuthChallenge? + + private var algorithmsToHashFunction: [String: (String) -> String] = [ + "MD5": HashAlgorithms.md5, + "MD5-SESS": HashAlgorithms.md5, + "SHA": HashAlgorithms.sha1, + "SHA-SESS": HashAlgorithms.sha1, + "SHA-256": HashAlgorithms.sha256, + "SHA-256-SESS": HashAlgorithms.sha256, + "SHA-512": HashAlgorithms.sha512, + "SHA-512-SESS": HashAlgorithms.sha512, + ] +} + +private extension DigestAuth { + enum HashAlgorithms { fileprivate static func md5(_ string: String) -> String { let digest = Insecure.MD5.hash(data: string.data(using: .utf8)!) return digest.map { String(format: "%02hhx", $0) }.joined() @@ -107,7 +193,7 @@ public class DigestAuth: BaseAuth { } } - private struct DigestAuthChallenge { + struct DigestAuthChallenge { fileprivate var realm: String fileprivate var nonce: String fileprivate var algorithm: String @@ -115,25 +201,7 @@ public class DigestAuth: BaseAuth { fileprivate var qop: String? } - private var needAuthStatusCode = 401 - - private var username: String - private var password: String - private var nonceCount: Int - private var lastChallenge: DigestAuthChallenge? - - private var algorithmsToHashFunction: [String: (String) -> String] = [ - "MD5": HashAlgorithms.md5, - "MD5-SESS": HashAlgorithms.md5, - "SHA": HashAlgorithms.sha1, - "SHA-SESS": HashAlgorithms.sha1, - "SHA-256": HashAlgorithms.sha256, - "SHA-256-SESS": HashAlgorithms.sha256, - "SHA-512": HashAlgorithms.sha512, - "SHA-512-SESS": HashAlgorithms.sha512, - ] - - private func buildAuthHeader(request: URLRequest, challenge: DigestAuthChallenge) throws -> String { + func buildAuthHeader(request: URLRequest, challenge: DigestAuthChallenge) throws -> String { let hashFunction = algorithmsToHashFunction[challenge.algorithm.uppercased()]! let a1 = [username, challenge.realm, password].joined(separator: ":") @@ -186,7 +254,7 @@ public class DigestAuth: BaseAuth { return "Digest \(getHeaderValue(headerFields: headersField))" } - private func parseChallenge(authHeader: String) throws -> DigestAuthChallenge { + func parseChallenge(authHeader: String) throws -> DigestAuthChallenge { var fields: String let parts = authHeader.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: false) fields = String(parts[1]) @@ -215,7 +283,7 @@ public class DigestAuth: BaseAuth { return DigestAuthChallenge(realm: realm, nonce: nonce, algorithm: algorithm, opaque: opaque, qop: qop) } - private func parseHttpList(_ string: String) -> [String] { + func parseHttpList(_ string: String) -> [String] { var result: [String] = [] var part = "" @@ -259,7 +327,7 @@ public class DigestAuth: BaseAuth { return result } - private func unquote(_ string: String) -> String { + func unquote(_ string: String) -> String { if string.first == "\"", string.last == "\"" { return String(string.dropFirst().dropLast()) } @@ -267,7 +335,7 @@ public class DigestAuth: BaseAuth { return string } - private func getClientNonce(nonceCount: Int, nonce: String) -> String { + func getClientNonce(nonceCount: Int, nonce: String) -> String { var string = String(nonceCount) + nonce string += String(Date().timeIntervalSince1970) var randomBytes = [UInt8](repeating: 0, count: 8) // swiftlint:disable:this no_magic_numbers @@ -280,7 +348,7 @@ public class DigestAuth: BaseAuth { return String(string[startIndex ..< endIndex]) } - private func resolveQop(qop: String?, request _: URLRequest) throws -> String? { + func resolveQop(qop: String?, request _: URLRequest) throws -> String? { guard let qop else { return nil } @@ -297,7 +365,7 @@ public class DigestAuth: BaseAuth { throw AuthError.invalidDigestAuth(message: "The qop is invalid") } - private func getHeaderValue(headerFields: [String: String]) -> String { + func getHeaderValue(headerFields: [String: String]) -> String { let nonQuotedFields = ["algorithms", "qop", "nc"] var headerValue = "" for (idx, (key, value)) in headerFields.enumerated() { diff --git a/Sources/HttpX/Auth/EmptyAuth.swift b/Sources/HttpX/Auth/EmptyAuth.swift index fd10f4d..9adb861 100644 --- a/Sources/HttpX/Auth/EmptyAuth.swift +++ b/Sources/HttpX/Auth/EmptyAuth.swift @@ -13,15 +13,14 @@ // limitations under the License. import Foundation +import SyncStream -/// The EmptyAuth class, default auth, do nothing. -@available(macOS 10.15, *) +/// /// The EmptyAuth class, default auth, do nothing. public class EmptyAuth: BaseAuth { // MARK: Lifecycle - /// Initialize the EmptyAuth. + /// /// Initialize the EmptyAuth. public init() {} - deinit {} // MARK: Public @@ -31,7 +30,18 @@ public class EmptyAuth: BaseAuth { /// default value is false public var needResponseBody: Bool { false } - public func authFlow(request: URLRequest?, lastResponse _: Response?) -> (URLRequest?, Bool) { - (request, true) + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalSyncStream.Continuation + ) { + continuation.yield(request) + continuation.return(NoneType()) + } + + public func authFlow( + _ request: URLRequest, continuation: BidirectionalAsyncStream.Continuation + ) async { + await continuation.yield(request) + await continuation.return(NoneType()) } } diff --git a/Sources/HttpX/Auth/FunctionAuth.swift b/Sources/HttpX/Auth/FunctionAuth.swift index 3b934e8..b58adf0 100644 --- a/Sources/HttpX/Auth/FunctionAuth.swift +++ b/Sources/HttpX/Auth/FunctionAuth.swift @@ -13,9 +13,9 @@ // limitations under the License. import Foundation +import SyncStream /// The FunctionAuth class, user should provide an auth function. -@available(macOS 10.15, *) public class FunctionAuth: BaseAuth { // MARK: Lifecycle @@ -23,7 +23,11 @@ public class FunctionAuth: BaseAuth { /// /// - Parameters: /// - authFunction: The auth function for the function auth. - public init(authFunction: @escaping (URLRequest?, Response?) -> (URLRequest, Bool)) { + /// + /// Functional authentication assumes that the authentication will simply modify + /// the request once. Simple authentication methods, such as adding a header or + /// changing the request method, can be implemented using this approach. + public init(authFunction: @escaping (URLRequest) -> URLRequest) { self.authFunction = authFunction } @@ -36,11 +40,23 @@ public class FunctionAuth: BaseAuth { /// default value is false public var needResponseBody: Bool { false } - public func authFlow(request: URLRequest?, lastResponse: Response?) -> (URLRequest?, Bool) { - authFunction(request, lastResponse) + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalSyncStream.Continuation + ) { + continuation.yield(authFunction(request)) + continuation.return(NoneType()) + } + + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalAsyncStream.Continuation + ) async { + await continuation.yield(authFunction(request)) + await continuation.return(NoneType()) } // MARK: Private - private var authFunction: (URLRequest?, Response?) -> (URLRequest, Bool) + private var authFunction: (URLRequest) -> URLRequest } diff --git a/Sources/HttpX/Auth/OAuth.swift b/Sources/HttpX/Auth/OAuth.swift index ae90b7e..bf15b68 100644 --- a/Sources/HttpX/Auth/OAuth.swift +++ b/Sources/HttpX/Auth/OAuth.swift @@ -13,9 +13,9 @@ // limitations under the License. import Foundation +import SyncStream -/// The OAuth class, user should provide the token. -@available(macOS 10.15, *) +/// /// The OAuth class, user should provide the token. public class OAuth: BaseAuth { // MARK: Lifecycle @@ -33,25 +33,35 @@ public class OAuth: BaseAuth { /// default value is false public var needRequestBody: Bool { false } + /// default value is false public var needResponseBody: Bool { false } - public func authFlow(request: URLRequest?, lastResponse _: Response?) throws -> (URLRequest?, Bool) { - if var request { - request.setValue( - buildAuthHeader(token: token), - forHTTPHeaderField: "Authorization" - ) - return (request, true) - } - throw AuthError.invalidRequest(message: "Request is nil in \(OAuth.self)") + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalSyncStream.Continuation + ) { + var request = request + request.addValue(buildAuthHeader(token), forHTTPHeaderField: "Authorization") + continuation.yield(request) + continuation.return(NoneType()) + } + + public func authFlow( + _ request: URLRequest, + continuation: BidirectionalAsyncStream.Continuation + ) async { + var request = request + request.addValue(buildAuthHeader(token), forHTTPHeaderField: "Authorization") + await continuation.yield(request) + await continuation.return(NoneType()) } // MARK: Private private var token: String - private func buildAuthHeader(token: String) -> String { + private func buildAuthHeader(_ token: String) -> String { "Bearer \(token)" } } From 8dd3ed9f90840f8ab80bd47e6dc8a4998d85ac47 Mon Sep 17 00:00:00 2001 From: RuiyangSun Date: Mon, 22 Apr 2024 14:00:48 +0800 Subject: [PATCH 5/6] refactor: adapt new Auth API --- Sources/HttpX/Client/AsyncClient.swift | 52 +++++++++++++------------- Sources/HttpX/Client/SyncClient.swift | 52 +++++++++++++------------- Sources/HttpX/Type.swift | 2 +- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Sources/HttpX/Client/AsyncClient.swift b/Sources/HttpX/Client/AsyncClient.swift index 3f23bfb..00283de 100644 --- a/Sources/HttpX/Client/AsyncClient.swift +++ b/Sources/HttpX/Client/AsyncClient.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +import SyncStream /// Synchronous HTTP client. @available(macOS 10.15, *) @@ -107,37 +108,36 @@ public class AsyncClient: BaseClient { history: [Response] = [], chunkSize: Int? = nil ) async throws -> Response { + var request = request var history = history - var (request, authStop) = try await auth.asyncAuthFlow(request: request, lastResponse: nil) - - guard request != nil else { - throw HttpXError.invalidRequest(message: "Auth flow did not return a request") - } - - var response = try await sendHandlingRedirect( - request: request!, - followRedirects: followRedirects, - history: history, - chunkSize: chunkSize - ) + var response: Response? + let authFlow = await auth.authFlowAdapter(request) + request = try await authFlow.next() - while !authStop { - (request, authStop) = try await auth.asyncAuthFlow(request: request, lastResponse: response) - if let request { - response = try await sendHandlingRedirect( - request: request, - followRedirects: followRedirects, - history: history, - chunkSize: chunkSize - ) - response.historyInternal = history - history += [response] - } else { - break + while true { + response = try await sendHandlingRedirect( + request: request, + followRedirects: followRedirects, + history: history, + chunkSize: chunkSize + ) + + let nextRequest: URLRequest + do { + nextRequest = try await authFlow.send(response!) + } catch { + if error is StopIteration { + break + } + throw error } + + response?.historyInternal = history + request = nextRequest + history += [response!] } - return response + return response! } internal func sendHandlingRedirect( diff --git a/Sources/HttpX/Client/SyncClient.swift b/Sources/HttpX/Client/SyncClient.swift index dc92dec..b737584 100644 --- a/Sources/HttpX/Client/SyncClient.swift +++ b/Sources/HttpX/Client/SyncClient.swift @@ -14,6 +14,7 @@ import Dispatch import Foundation +import SyncStream /// Synchronous HTTP client. @available(macOS 10.15, *) @@ -107,37 +108,36 @@ public class SyncClient: BaseClient { history: [Response] = [], chunkSize: Int? = nil ) throws -> Response { + var request = request var history = history - var (request, authStop) = try auth.syncAuthFlow(request: request, lastResponse: nil) - - guard request != nil else { - throw HttpXError.invalidRequest(message: "Auth flow did not return a request") - } - - var response = try sandHandlingRedirect( - request: request!, - followRedirects: followRedirects, - history: history, - chunkSize: chunkSize - ) + var response: Response? + let authFlow = auth.authFlowAdapter(request) + request = try authFlow.next() - while !authStop { - (request, authStop) = try auth.syncAuthFlow(request: request, lastResponse: response) - if let request { - response = try sandHandlingRedirect( - request: request, - followRedirects: followRedirects, - history: history, - chunkSize: chunkSize - ) - response.historyInternal = history - history += [response] - } else { - break + while true { + response = try sandHandlingRedirect( + request: request, + followRedirects: followRedirects, + history: history, + chunkSize: chunkSize + ) + + let nextRequest: URLRequest + do { + nextRequest = try authFlow.send(response!) + } catch { + if error is StopIteration { + break + } + throw error } + + response?.historyInternal = history + request = nextRequest + history += [response!] } - return response + return response! } internal func sandHandlingRedirect( diff --git a/Sources/HttpX/Type.swift b/Sources/HttpX/Type.swift index 3372374..0cb16f5 100644 --- a/Sources/HttpX/Type.swift +++ b/Sources/HttpX/Type.swift @@ -186,7 +186,7 @@ public enum AuthType { /// Authentication using a class that conforms to `BaseAuth`. case `class`(any BaseAuth) /// Authentication using a custom function. - case `func`((URLRequest?, Response?) -> (URLRequest, Bool)) + case `func`((URLRequest) -> URLRequest) /// Basic authentication using a username and password. case basic((String, String)) From 2e6e181a885abff578f5fe793f8882c209b9b39b Mon Sep 17 00:00:00 2001 From: RuiyangSun Date: Mon, 22 Apr 2024 14:01:14 +0800 Subject: [PATCH 6/6] Test: modify test for new Auth --- Tests/HttpXTests/Auth/APIKeyAuthTests.swift | 20 +- Tests/HttpXTests/Auth/BaseAuthTests.swift | 56 ++--- Tests/HttpXTests/Auth/BasicAuthTests.swift | 18 +- Tests/HttpXTests/Auth/DigestAuthTests.swift | 191 +++++++++++++----- Tests/HttpXTests/Auth/EmptyAuthTests.swift | 14 +- Tests/HttpXTests/Auth/FunctionAuthTests.swift | 26 ++- Tests/HttpXTests/Auth/OAuthTests.swift | 24 +-- .../HttpXTests/Client/AsyncClientTests.swift | 44 ---- Tests/HttpXTests/Client/SyncClientTests.swift | 41 ---- Tests/HttpXTests/TypeTests.swift | 5 +- 10 files changed, 225 insertions(+), 214 deletions(-) diff --git a/Tests/HttpXTests/Auth/APIKeyAuthTests.swift b/Tests/HttpXTests/Auth/APIKeyAuthTests.swift index cabb1dd..3422ae8 100644 --- a/Tests/HttpXTests/Auth/APIKeyAuthTests.swift +++ b/Tests/HttpXTests/Auth/APIKeyAuthTests.swift @@ -13,6 +13,7 @@ // limitations under the License. @testable import HttpX +import SyncStream import XCTest class APIKeyAuthTests: XCTestCase { @@ -21,22 +22,23 @@ class APIKeyAuthTests: XCTestCase { let apiKeyAuth = APIKeyAuth(key: "testKey") let request = URLRequest(url: URL(string: "https://example.com")!) - // When - let (modifiedRequest, shouldContinue) = try apiKeyAuth.authFlow(request: request, lastResponse: nil) + let authFlow = apiKeyAuth.authFlowAdapter(request) + let modifiedRequest = try authFlow.next() // Then - XCTAssertEqual(modifiedRequest?.value(forHTTPHeaderField: "x-api-key"), "testKey") - XCTAssertTrue(shouldContinue) + XCTAssertEqual(modifiedRequest.value(forHTTPHeaderField: "x-api-key"), "testKey") } - func testAuthFlow_withNilRequest_shouldReturnNilAndTrue() throws { + func testAuthFlow_withValidRequest_shouldSetAPIKeyHeaderAsync() async throws { // Given let apiKeyAuth = APIKeyAuth(key: "testKey") + let request = URLRequest(url: URL(string: "https://example.com")!) + + let authFlow = await apiKeyAuth.authFlowAdapter(request) + let modifiedRequest = try await authFlow.next() - // When // Then - XCTAssertThrowsError(try apiKeyAuth.authFlow(request: nil, lastResponse: nil)) { - XCTAssertEqual($0 as? AuthError, AuthError.invalidRequest()) - } + // Then + XCTAssertEqual(modifiedRequest.value(forHTTPHeaderField: "x-api-key"), "testKey") } func testProperty() { diff --git a/Tests/HttpXTests/Auth/BaseAuthTests.swift b/Tests/HttpXTests/Auth/BaseAuthTests.swift index 3d2baef..cc1f21c 100644 --- a/Tests/HttpXTests/Auth/BaseAuthTests.swift +++ b/Tests/HttpXTests/Auth/BaseAuthTests.swift @@ -13,6 +13,7 @@ // limitations under the License. @testable import HttpX +import SyncStream import XCTest // MARK: - BaseAuthTests @@ -23,12 +24,20 @@ final class BaseAuthTests: XCTestCase { let mockAuth = MockBaseAuth(needRequestBody: true, needResponseBody: false) var request = URLRequest(url: URL(string: "https://example.com")!) request.httpBodyStream = InputStream(data: "test body".data(using: .utf8)!) + let authFlow = mockAuth.authFlowAdapter(request) + let modifiedRequest = try authFlow.next() + XCTAssertNotNil(modifiedRequest.httpBody) + XCTAssertNil(modifiedRequest.httpBodyStream) + } - let (modifiedRequest, authDone) = try mockAuth.syncAuthFlow(request: request, lastResponse: nil) - - XCTAssertNotNil(modifiedRequest?.httpBody) - XCTAssertNil(modifiedRequest?.httpBodyStream) - XCTAssertFalse(authDone) + func testSyncAuthFlowWithRequestBodyAsync() async throws { + let mockAuth = MockBaseAuth(needRequestBody: true, needResponseBody: false) + var request = URLRequest(url: URL(string: "https://example.com")!) + request.httpBodyStream = InputStream(data: "test body".data(using: .utf8)!) + let authFlow = await mockAuth.authFlowAdapter(request) + let modifiedRequest = try await authFlow.next() + XCTAssertNotNil(modifiedRequest.httpBody) + XCTAssertNil(modifiedRequest.httpBodyStream) } func testSyncAuthFlowWithResponseBody() throws { @@ -36,33 +45,23 @@ final class BaseAuthTests: XCTestCase { let request = URLRequest(url: URL(string: "https://example.com")!) let lastResponse = MockResponse(url: request.url!, statusCode: 200)! - let (_, authDone) = try mockAuth.syncAuthFlow(request: request, lastResponse: lastResponse) + let authFlow = mockAuth.authFlowAdapter(request) + _ = try authFlow.next() + _ = try authFlow.send(lastResponse) XCTAssertTrue(lastResponse.didReadAllFormSyncStream) - XCTAssertFalse(authDone) - } - - func testAsyncAuthFlowWithRequestBody() async throws { - let mockAuth = MockBaseAuth(needRequestBody: true, needResponseBody: false) - var request = URLRequest(url: URL(string: "https://example.com")!) - request.httpBodyStream = InputStream(data: "test body".data(using: .utf8)!) - - let (modifiedRequest, authDone) = try await mockAuth.asyncAuthFlow(request: request, lastResponse: nil) - - XCTAssertNotNil(modifiedRequest?.httpBody) - XCTAssertNil(modifiedRequest?.httpBodyStream) - XCTAssertFalse(authDone) } - func testAsyncAuthFlowWithResponseBody() async throws { + func testSyncAuthFlowWithResponseBodyAsync() async throws { let mockAuth = MockBaseAuth(needRequestBody: false, needResponseBody: true) let request = URLRequest(url: URL(string: "https://example.com")!) let lastResponse = MockResponse(url: request.url!, statusCode: 200)! - let (_, authDone) = try await mockAuth.asyncAuthFlow(request: request, lastResponse: lastResponse) + let authFlow = await mockAuth.authFlowAdapter(request) + _ = try await authFlow.next() + _ = try await authFlow.send(lastResponse) XCTAssertTrue(lastResponse.didReadAllFormAsyncStream) - XCTAssertFalse(authDone) } } @@ -82,9 +81,16 @@ private class MockBaseAuth: BaseAuth { var needRequestBody: Bool var needResponseBody: Bool - func authFlow(request: URLRequest?, lastResponse _: Response?) throws -> (URLRequest?, Bool) { - // Mock implementation - (request, false) + func authFlow(_ request: URLRequest, continuation: BidirectionalSyncStream.Continuation) { + let _ = continuation.yield(request) + let _ = continuation.yield(request) + continuation.return(NoneType()) + } + + func authFlow(_ request: URLRequest, continuation: BidirectionalAsyncStream.Continuation) async { + let _ = await continuation.yield(request) + let _ = await continuation.yield(request) + await continuation.return(NoneType()) } } diff --git a/Tests/HttpXTests/Auth/BasicAuthTests.swift b/Tests/HttpXTests/Auth/BasicAuthTests.swift index 5f0e754..0c5b18e 100644 --- a/Tests/HttpXTests/Auth/BasicAuthTests.swift +++ b/Tests/HttpXTests/Auth/BasicAuthTests.swift @@ -22,20 +22,24 @@ final class BasicAuthTests: XCTestCase { let basicAuth = BasicAuth(username: "testUser", password: "testPass") let request = URLRequest(url: URL(string: "https://example.com")!) - let (modifiedRequest, didModify) = try basicAuth.authFlow(request: request, lastResponse: nil) + let authFlow = basicAuth.authFlowAdapter(request) + let modifiedRequest = try authFlow.next() XCTAssertNotNil(modifiedRequest) - XCTAssertTrue(didModify) let expectedAuthHeader = buildAuthHeader(username: "testUser", password: "testPass") - XCTAssertEqual(modifiedRequest?.value(forHTTPHeaderField: "Authorization"), expectedAuthHeader) + XCTAssertEqual(modifiedRequest.value(forHTTPHeaderField: "Authorization"), expectedAuthHeader) } - func testAuthFlowWithNilRequest() throws { + func testAuthFlowWithValidRequestAsync() async throws { let basicAuth = BasicAuth(username: "testUser", password: "testPass") + let request = URLRequest(url: URL(string: "https://example.com")!) + + let authFlow = await basicAuth.authFlowAdapter(request) + let modifiedRequest = try await authFlow.next() - XCTAssertThrowsError(try basicAuth.authFlow(request: nil, lastResponse: nil)) { error in - XCTAssertEqual(error as? AuthError, AuthError.invalidRequest()) - } + XCTAssertNotNil(modifiedRequest) + let expectedAuthHeader = buildAuthHeader(username: "testUser", password: "testPass") + XCTAssertEqual(modifiedRequest.value(forHTTPHeaderField: "Authorization"), expectedAuthHeader) } func testProperty() { diff --git a/Tests/HttpXTests/Auth/DigestAuthTests.swift b/Tests/HttpXTests/Auth/DigestAuthTests.swift index 4489540..7f0e18b 100644 --- a/Tests/HttpXTests/Auth/DigestAuthTests.swift +++ b/Tests/HttpXTests/Auth/DigestAuthTests.swift @@ -13,23 +13,44 @@ // limitations under the License. @testable import HttpX +import SyncStream import XCTest final class DigestAuthTests: XCTestCase { func testReUseChallenge() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com")!) + var authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() + let URLResponse = HTTPURLResponse( + url: URL(string: "http://example.com")!, + statusCode: 401, httpVersion: nil, + headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=MD5, stale=FALSE"] + ) + let response = Response(HTTPURLResponse: URLResponse!)! + _ = try authFlow.send(response) - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() + + // We don't test result here, just make sure the code can run without crash, and code coverage can be 100%. + } + + func testReUseChallengeAsync() async throws { + let auth = DigestAuth(username: "user", password: "pass") + var request: URLRequest? = URLRequest(url: URL(string: "http://example.com")!) + var authFlow = await auth.authFlowAdapter(request!) + request = try await authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=MD5, stale=FALSE"] ) let response = Response(HTTPURLResponse: URLResponse!)! + _ = try await authFlow.send(response) - let _ = try auth.authFlow(request: request, lastResponse: response).0 - let _ = try auth.authFlow(request: request, lastResponse: nil).0 + authFlow = await auth.authFlowAdapter(request!) + request = try await authFlow.next() // We don't test result here, just make sure the code can run without crash, and code coverage can be 100%. } @@ -37,156 +58,208 @@ final class DigestAuthTests: XCTestCase { func testNon401Response() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com")!) - - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + let authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 200, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"test\""] ) let response = Response(HTTPURLResponse: URLResponse!)! + do { + _ = try authFlow.send(response) + XCTFail("Should throw error") + } catch { + XCTAssertTrue(error is StopIteration) + } + } - let (authedRequest, authStop) = try auth.authFlow(request: request, lastResponse: response) - XCTAssertNil(authedRequest) - XCTAssertTrue(authStop) + func testNon401ResponseAsync() async throws { + let auth = DigestAuth(username: "user", password: "pass") + var request: URLRequest? = URLRequest(url: URL(string: "http://example.com")!) + let authFlow = await auth.authFlowAdapter(request!) + request = try await authFlow.next() + let URLResponse = HTTPURLResponse( + url: URL(string: "http://example.com")!, + statusCode: 200, httpVersion: nil, + headerFields: ["Www-Authenticate": "digest realm=\"test\""] + ) + let response = Response(HTTPURLResponse: URLResponse!)! + do { + _ = try await authFlow.send(response) + XCTFail("Should throw error") + } catch { + XCTAssertTrue(error is StopIteration) + } } - func testInvalidWwwAuthenticate() { + func testInvalidWwwAuthenticate() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com")!) - (request, _) = try! auth.authFlow(request: request, lastResponse: nil) + let authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "invalid"] ) let response = Response(HTTPURLResponse: URLResponse!)! - - let (authedRequest, authStop) = try! auth.authFlow(request: request, lastResponse: response) - XCTAssertNil(authedRequest) - XCTAssertTrue(authStop) + do { + _ = try authFlow.send(response) + XCTFail("Should throw error") + } catch { + XCTAssertTrue(error is StopIteration) + } } - func testInvalidRequest() { + func testInvalidWwwAuthenticateAsync() async throws { let auth = DigestAuth(username: "user", password: "pass") + var request: URLRequest? = URLRequest(url: URL(string: "http://example.com")!) - XCTAssertThrowsError(try auth.authFlow(request: nil, lastResponse: nil)) { error in - XCTAssertEqual(error as! AuthError, AuthError.invalidRequest()) + let authFlow = await auth.authFlowAdapter(request!) + request = try await authFlow.next() + let URLResponse = HTTPURLResponse( + url: URL(string: "http://example.com")!, + statusCode: 401, httpVersion: nil, + headerFields: ["Www-Authenticate": "invalid"] + ) + let response = Response(HTTPURLResponse: URLResponse!)! + do { + _ = try await authFlow.send(response) + XCTFail("Should throw error") + } catch { + XCTAssertTrue(error is StopIteration) } } func testMoreHashFunction() throws { - let auth = DigestAuth(username: "user", password: "pass") + var auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com")!) - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + var authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() var URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=MD5-SESS, stale=FALSE"] ) var response = Response(HTTPURLResponse: URLResponse!)! - let _ = try auth.authFlow(request: request, lastResponse: response).0 + _ = try authFlow.send(response) + auth = DigestAuth(username: "user", password: "pass") + authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=SHA, stale=FALSE"] ) response = Response(HTTPURLResponse: URLResponse!)! - let _ = try auth.authFlow(request: request, lastResponse: response).0 + _ = try authFlow.send(response) + auth = DigestAuth(username: "user", password: "pass") + authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=SHA-SESS, stale=FALSE"] ) response = Response(HTTPURLResponse: URLResponse!)! - let _ = try auth.authFlow(request: request, lastResponse: response).0 + _ = try authFlow.send(response) + auth = DigestAuth(username: "user", password: "pass") + authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=SHA-256, stale=FALSE"] ) response = Response(HTTPURLResponse: URLResponse!)! - let _ = try auth.authFlow(request: request, lastResponse: response).0 + _ = try authFlow.send(response) + auth = DigestAuth(username: "user", password: "pass") + authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=SHA-256-SESS, stale=FALSE"] ) response = Response(HTTPURLResponse: URLResponse!)! - let _ = try auth.authFlow(request: request, lastResponse: response).0 + _ = try authFlow.send(response) + auth = DigestAuth(username: "user", password: "pass") + authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=SHA-512, stale=FALSE"] ) response = Response(HTTPURLResponse: URLResponse!)! - let _ = try auth.authFlow(request: request, lastResponse: response).0 + _ = try authFlow.send(response) + auth = DigestAuth(username: "user", password: "pass") + authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=SHA-512-SESS, stale=FALSE"] ) response = Response(HTTPURLResponse: URLResponse!)! - let _ = try auth.authFlow(request: request, lastResponse: response).0 + _ = try authFlow.send(response) } func testComplexURL() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com/with/path?query=with")!) - - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + let authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", qop=\"auth\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=MD5, stale=FALSE"] ) let response = Response(HTTPURLResponse: URLResponse!)! - - let _ = try auth.authFlow(request: request, lastResponse: response).0 + let _ = try authFlow.send(response) } func testNoQop() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com/with/path?query=with")!) - - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + let authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", algorithm=MD5, stale=FALSE"] ) let response = Response(HTTPURLResponse: URLResponse!)! - - let _ = try auth.authFlow(request: request, lastResponse: response).0 + let _ = try authFlow.send(response) } func testNonAlgo() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com/with/path?query=with")!) - - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + let authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, headerFields: ["Www-Authenticate": "digest realm=\"me@kennethreitz.com\", nonce=\"f84b8428b38019eefd5dbdb8e72bf7d6\", opaque=\"ee1a7a03d8c7032f17dd33b3043db4c6\", stale=FALSE"] ) let response = Response(HTTPURLResponse: URLResponse!)! - - let _ = try auth.authFlow(request: request, lastResponse: response).0 + let _ = try authFlow.send(response) } func testInvalidAlgo() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com/with/path?query=with")!) - - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + let authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, @@ -194,16 +267,17 @@ final class DigestAuthTests: XCTestCase { ) let response = Response(HTTPURLResponse: URLResponse!)! - XCTAssertThrowsError(try auth.authFlow(request: request, lastResponse: response).0) { error in + XCTAssertThrowsError(try authFlow.send(response)) { error in + let error = (error as! Terminated).error XCTAssertEqual(error as? AuthError, AuthError.invalidDigestAuth()) } } func testInvalidQop() throws { - let auth = DigestAuth(username: "user", password: "pass") + var auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com/with/path?query=with")!) - - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + var authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() var URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, @@ -211,10 +285,15 @@ final class DigestAuthTests: XCTestCase { ) var response = Response(HTTPURLResponse: URLResponse!)! - XCTAssertThrowsError(try auth.authFlow(request: request, lastResponse: response)) { - XCTAssertEqual($0 as? AuthError, AuthError.qopNotSupported()) + XCTAssertThrowsError(try authFlow.send(response)) { error in + let error = (error as! Terminated).error + XCTAssertEqual(error as? AuthError, AuthError.qopNotSupported()) } + auth = DigestAuth(username: "user", password: "pass") + request = URLRequest(url: URL(string: "http://example.com/with/path?query=with")!) + authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, @@ -222,16 +301,17 @@ final class DigestAuthTests: XCTestCase { ) response = Response(HTTPURLResponse: URLResponse!)! - XCTAssertThrowsError(try auth.authFlow(request: request, lastResponse: response)) { - XCTAssertEqual($0 as? AuthError, AuthError.invalidDigestAuth()) + XCTAssertThrowsError(try authFlow.send(response)) { error in + let error = (error as! Terminated).error + XCTAssertEqual(error as? AuthError, AuthError.invalidDigestAuth()) } } func testInvalidDigestAuthString() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com/with/path?query=with")!) - - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + let authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, @@ -239,16 +319,17 @@ final class DigestAuthTests: XCTestCase { ) let response = Response(HTTPURLResponse: URLResponse!)! - XCTAssertThrowsError(try auth.authFlow(request: request, lastResponse: response)) { - XCTAssertEqual($0 as? AuthError, AuthError.invalidDigestAuth()) + XCTAssertThrowsError(try authFlow.send(response)) { error in + let error = (error as! Terminated).error + XCTAssertEqual(error as? AuthError, AuthError.invalidDigestAuth()) } } func testEscaped() throws { let auth = DigestAuth(username: "user", password: "pass") var request: URLRequest? = URLRequest(url: URL(string: "http://example.com/with/path?query=with")!) - - (request, _) = try auth.authFlow(request: request, lastResponse: nil) + let authFlow = auth.authFlowAdapter(request!) + request = try authFlow.next() let URLResponse = HTTPURLResponse( url: URL(string: "http://example.com")!, statusCode: 401, httpVersion: nil, @@ -256,7 +337,7 @@ final class DigestAuthTests: XCTestCase { ) let response = Response(HTTPURLResponse: URLResponse!)! - let _ = try auth.authFlow(request: request, lastResponse: response).0 + _ = try authFlow.send(response) } func testProperty() { diff --git a/Tests/HttpXTests/Auth/EmptyAuthTests.swift b/Tests/HttpXTests/Auth/EmptyAuthTests.swift index f68af28..24b581d 100644 --- a/Tests/HttpXTests/Auth/EmptyAuthTests.swift +++ b/Tests/HttpXTests/Auth/EmptyAuthTests.swift @@ -36,11 +36,19 @@ final class EmptyAuthTests: XCTestCase { XCTAssertFalse(emptyAuth.needResponseBody) } - func testAuthFlow() { + func testAuthFlow() throws { let request = URLRequest(url: URL(string: "https://example.com")!) - let (modifiedRequest, shouldProceed) = emptyAuth.authFlow(request: request, lastResponse: nil) + let authFlow = emptyAuth.authFlowAdapter(request) + let modifiedRequest = try authFlow.next() + + XCTAssertEqual(modifiedRequest, request) + } + + func testAuthFlowAsync() async throws { + let request = URLRequest(url: URL(string: "https://example.com")!) + let authFlow = await emptyAuth.authFlowAdapter(request) + let modifiedRequest = try await authFlow.next() XCTAssertEqual(modifiedRequest, request) - XCTAssertTrue(shouldProceed) } } diff --git a/Tests/HttpXTests/Auth/FunctionAuthTests.swift b/Tests/HttpXTests/Auth/FunctionAuthTests.swift index a2c7e3c..728bfe2 100644 --- a/Tests/HttpXTests/Auth/FunctionAuthTests.swift +++ b/Tests/HttpXTests/Auth/FunctionAuthTests.swift @@ -16,37 +16,35 @@ import XCTest class FunctionAuthTests: XCTestCase { - func testAuthFlow_withValidRequestAndResponse_returnsTrue() { + func testAuthFlow_withValidRequestAndResponse_returnsTrue() throws { // Given let expectedRequest = URLRequest(url: URL(string: "https://example.com")!) - let response = Response(url: expectedRequest.url!, statusCode: 200)! - let authFunction: (URLRequest?, Response?) -> (URLRequest, Bool) = { request, _ in - (request!, true) + let authFunction: (URLRequest) -> URLRequest = { request in + request } let functionAuth = FunctionAuth(authFunction: authFunction) // When - let (request, result) = functionAuth.authFlow(request: expectedRequest, lastResponse: response) + let authFlow = functionAuth.authFlowAdapter(expectedRequest) + let request = try authFlow.next() // Then - XCTAssertTrue(result) XCTAssertEqual(request, expectedRequest) } - func testAuthFlow_withNilRequest_returnsFalse() { + func testAuthFlow_withValidRequestAndResponse_returnsTrueAsync() async throws { // Given - let authFunction: (URLRequest?, Response?) -> (URLRequest, Bool) = { _, _ in - (URLRequest(url: URL(string: "https://example.com")!), false) + let expectedRequest = URLRequest(url: URL(string: "https://example.com")!) + let authFunction: (URLRequest) -> URLRequest = { request in + request } let functionAuth = FunctionAuth(authFunction: authFunction) // When - let (_, result) = functionAuth.authFlow(request: nil, lastResponse: nil) + let authFlow = await functionAuth.authFlowAdapter(expectedRequest) + let request = try await authFlow.next() // Then - XCTAssertFalse(result) - - XCTAssertEqual(functionAuth.needRequestBody, false) - XCTAssertEqual(functionAuth.needResponseBody, false) + XCTAssertEqual(request, expectedRequest) } } diff --git a/Tests/HttpXTests/Auth/OAuthTests.swift b/Tests/HttpXTests/Auth/OAuthTests.swift index 4be1529..913ca32 100644 --- a/Tests/HttpXTests/Auth/OAuthTests.swift +++ b/Tests/HttpXTests/Auth/OAuthTests.swift @@ -23,27 +23,25 @@ class OAuthTests: XCTestCase { let request = URLRequest(url: URL(string: "https://example.com")!) // When - let (modifiedRequest, result) = try oauth.authFlow(request: request, lastResponse: nil) + let authFlow = oauth.authFlowAdapter(request) + let modifiedRequest = try authFlow.next() // Then XCTAssertNotNil(modifiedRequest) - XCTAssertTrue(result) - XCTAssertEqual(modifiedRequest?.value(forHTTPHeaderField: "Authorization"), "Bearer testToken") + XCTAssertEqual(modifiedRequest.value(forHTTPHeaderField: "Authorization"), "Bearer testToken") } - func testAuthFlow_withNilRequest_returnsNilAndTrue() throws { + func testAuthFlow_withValidRequest_returnsModifiedRequestAndTrueAsync() async throws { // Given let oauth = OAuth(token: "testToken") + let request = URLRequest(url: URL(string: "https://example.com")!) - // When // Then - XCTAssertThrowsError(try oauth.authFlow(request: nil, lastResponse: nil)) { - XCTAssertEqual($0 as? AuthError, AuthError.invalidRequest()) - } - } + // When + let authFlow = await oauth.authFlowAdapter(request) + let modifiedRequest = try await authFlow.next() - func testProperty() { - let auth = OAuth(token: "testToken") - XCTAssertEqual(auth.needRequestBody, false) - XCTAssertEqual(auth.needResponseBody, false) + // Then + XCTAssertNotNil(modifiedRequest) + XCTAssertEqual(modifiedRequest.value(forHTTPHeaderField: "Authorization"), "Bearer testToken") } } diff --git a/Tests/HttpXTests/Client/AsyncClientTests.swift b/Tests/HttpXTests/Client/AsyncClientTests.swift index 76dc956..6f084dd 100644 --- a/Tests/HttpXTests/Client/AsyncClientTests.swift +++ b/Tests/HttpXTests/Client/AsyncClientTests.swift @@ -32,50 +32,6 @@ final class AsyncClientTests: XCTestCase { mock() } - func testRequest() async throws { - class WrongAuth: BaseAuth { - var needRequestBody = false - var needResponseBody = false - func authFlow(request _: URLRequest?, lastResponse _: Response?) throws -> (URLRequest?, Bool) { - (nil, true) - } - } - let expectation = expectation(description: "request") - do { - _ = try await client.request( - method: .get, - url: URLType.string("/get"), - auth: AuthType.class(WrongAuth()) - ) - } catch { - expectation.fulfill() - XCTAssertEqual(error as? HttpXError, HttpXError.invalidRequest()) - } - await fulfillment(of: [expectation], timeout: 5) - } - - func testNonAuth() async throws { - // This Auth will stop when need to send request body secondly - class NonAuth: BaseAuth { - var needRequestBody = false - var needResponseBody = false - func authFlow(request: URLRequest?, lastResponse: Response?) throws -> (URLRequest?, Bool) { - if let request, lastResponse == nil { - (request, false) - } else { - (nil, false) - } - } - } - - let response = try await client.request( - method: .get, - url: URLType.string("/get"), - auth: AuthType.class(NonAuth()) - ) - XCTAssertEqual(response.statusCode, 200) - } - func testMaxRedirect() async throws { let expectation = expectation(description: "maxRedirect") do { diff --git a/Tests/HttpXTests/Client/SyncClientTests.swift b/Tests/HttpXTests/Client/SyncClientTests.swift index 82ef9bd..cdf3942 100644 --- a/Tests/HttpXTests/Client/SyncClientTests.swift +++ b/Tests/HttpXTests/Client/SyncClientTests.swift @@ -32,47 +32,6 @@ final class SyncClientTests: XCTestCase { mock() } - func testRequest() throws { - class WrongAuth: BaseAuth { - var needRequestBody = false - var needResponseBody = false - func authFlow(request _: URLRequest?, lastResponse _: Response?) throws -> (URLRequest?, Bool) { - (nil, true) - } - } - - XCTAssertThrowsError(try client.request( - method: .get, - url: URLType.string("/get"), - auth: AuthType.class(WrongAuth()), - followRedirects: nil - )) { - XCTAssertEqual($0 as? HttpXError, HttpXError.invalidRequest()) - } - } - - func testNonAuth() throws { - // This Auth will stop when need to send request body secondly - class NonAuth: BaseAuth { - var needRequestBody = false - var needResponseBody = false - func authFlow(request: URLRequest?, lastResponse: Response?) throws -> (URLRequest?, Bool) { - if let request, lastResponse == nil { - (request, false) - } else { - (nil, false) - } - } - } - - let response = try client.request( - method: .get, - url: URLType.string("/get"), - auth: AuthType.class(NonAuth()) - ) - XCTAssertEqual(response.statusCode, 200) - } - func testMaxRedirect() throws { XCTAssertThrowsError( try client.request(method: .get, url: URLType.string("/absolute-redirect/3"), followRedirects: true) diff --git a/Tests/HttpXTests/TypeTests.swift b/Tests/HttpXTests/TypeTests.swift index 71a9b09..356e1cb 100644 --- a/Tests/HttpXTests/TypeTests.swift +++ b/Tests/HttpXTests/TypeTests.swift @@ -173,12 +173,11 @@ class AuthTypeTests: XCTestCase { } func testBuildAuthWithFunc() throws { - let authFunction: (URLRequest?, Response?) -> (URLRequest, Bool) = { _, _ in - (URLRequest(url: URL(string: "https://example.com")!), true) + let authFunction: (URLRequest) -> URLRequest = { _ in + URLRequest(url: URL(string: "https://example.com")!) } let authType = AuthType.func(authFunction) let result = authType.buildAuth() - _ = try result.authFlow(request: nil, lastResponse: nil) XCTAssertTrue(result is FunctionAuth) } }