Skip to content
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
76 changes: 74 additions & 2 deletions Sources/OAuthKit/OAuth+URLProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ extension OAuth {

/// A custom `URLProtocol` that can be registered with any `URLSessionConfiguration` that will automatically inject
/// `Authorization: Bearer <<token>>` headers into outbound HTTP URLRequests based on ``Provider/authorizationPattern``.
public class URLProtocol: Foundation.URLProtocol {
public class URLProtocol: Foundation.URLProtocol, URLSessionDataDelegate, @unchecked Sendable {

private var session: URLSession?
private var sessionDataTask: URLSessionDataTask?

/// The lock that provides manual synchronization around access to authorization tokens.
private static let lock: NSLock = .init()
Expand All @@ -22,6 +25,18 @@ extension OAuth {
set { lock.withLock { _authorizations = newValue } }
}

/// Common Initializer.
/// - Parameters:
/// - request: the url requesy
/// - cachedResponse: the cached response
/// - client: the client
override public init(request: URLRequest, cachedResponse: CachedURLResponse?, client: (any URLProtocolClient)?) {
super.init(request: request, cachedResponse: cachedResponse, client: client)
if session == nil {
session = .init(configuration: .default, delegate: self, delegateQueue: nil)
}
}

/// Adds an authorization for the given provider that can be used inject `Authorization: Bearer <<token>>` headers into a request.
/// - Parameters:
/// - authorization: the authorization issued by the provider
Expand All @@ -47,7 +62,7 @@ extension OAuth {

/// Determines whether this protocol can handle the given request.
/// - Parameter request: the request to handle
/// - Returns: always true
/// - Returns: true if this protocl can handle the given request.
override public class func canInit(with request: URLRequest) -> Bool {
// Remove any expired authorizations
let expiredEntries = authorizations.filter{ $0.value.isExpired }
Expand All @@ -64,6 +79,14 @@ extension OAuth {
return false
}

/// Determines whether this protocol can handle the given task.
/// - Parameter task: the task to handle
/// - Returns: true if this protocl can handle the given task.
override public class func canInit(with task: URLSessionTask) -> Bool {
guard let request = task.originalRequest else { return false }
return canInit(with: request)
}

/// If an authorized provider matches the given request, then this method returns a canonical version
/// of the given request with an additional `Authorization: Bearer <<token>>` header field.
/// - Parameter request: the request
Expand All @@ -80,6 +103,55 @@ extension OAuth {
}
return request
}

/// Starts the loading of the current request.
override public func startLoading() {
sessionDataTask = session?.dataTask(with: request)
sessionDataTask?.resume()
}

/// Stops the loading of the current request.
override public func stopLoading() {
sessionDataTask?.cancel()
}

/// Called when data is available to consume.
/// - Parameters:
/// - session: the url session
/// - dataTask: the data task
/// - data: the data to consume
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let response = dataTask.response else { return }
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
}

/// Called when task is done loading data.
/// - Parameters:
/// - session: the url session
/// - task: the session task
/// - error: any error that may have occurred
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
if let error {
client?.urlProtocol(self, didFailWithError: error)
} else {
client?.urlProtocolDidFinishLoading(self)
}
}

public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
completionHandler(request)
}

public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: (any Error)?) {
guard let error = error else { return }
client?.urlProtocol(self, didFailWithError: error)
}

public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
client?.urlProtocolDidFinishLoading(self)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
// send a REST request to GitHub's API for a list of org repos
// The Authorization header will automatically get included
// in the request since it matches `api.github.com`
let url: URL = .init(string: "https://api.github.com/orgs/{ORG}/repos")
let url: URL = .init(string: "https://api.github.com/users/codefiesta/repos")
let request = URLRequest(url: url)
let (data, response) = try await urlSession.data(for: request)