Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(auth): New more easy-use API for Auth #7

Merged
merged 6 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .swiftlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/rockmagma02/SyncStream.git", from: "1.1.2"),
.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",
Expand Down
30 changes: 20 additions & 10 deletions Sources/HttpX/Auth/APIKeyAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<URLRequest, Response, NoneType>.Continuation
) {
var request = request
request.setValue(key, forHTTPHeaderField: "X-Api-Key")
continuation.yield(request)
continuation.return(NoneType())
}

public func authFlow(
_ request: URLRequest,
continuation: BidirectionalAsyncStream<URLRequest, Response, NoneType>.Continuation
) async {
var request = request
request.setValue(key, forHTTPHeaderField: "X-Api-Key")
await continuation.yield(request)
await continuation.return(NoneType())
}

// MARK: Private
Expand Down
172 changes: 117 additions & 55 deletions Sources/HttpX/Auth/BaseAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<URLRequest, Response, NoneType>.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<URLRequest, Response, NoneType>.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<URLRequest, Response, NoneType> {
BidirectionalSyncStream<URLRequest, Response, NoneType> { 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<URLRequest, Response, NoneType> { 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<NoneType> {
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<URLRequest, Response, NoneType> {
BidirectionalAsyncStream<URLRequest, Response, NoneType> { 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<URLRequest, Response, NoneType> { 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<NoneType> {
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
41 changes: 28 additions & 13 deletions Sources/HttpX/Auth/BasicAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<URLRequest, Response, NoneType>.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<URLRequest, Response, NoneType>.Continuation
) async {
var request = request
request.setValue(
buildAuthHeader(username: username, password: password),
forHTTPHeaderField: "Authorization"
)
await continuation.yield(request)
await continuation.return(NoneType())
}

// MARK: Private
Expand Down
Loading