123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- import Foundation
- public protocol AuthenticationCredential {
-
-
-
-
-
-
-
- var requiresRefresh: Bool { get }
- }
- public protocol Authenticator: AnyObject {
-
- associatedtype Credential: AuthenticationCredential
-
-
-
-
-
-
-
-
- func apply(_ credential: Credential, to urlRequest: inout URLRequest)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool
- }
- public enum AuthenticationError: Error {
-
- case missingCredential
-
- case excessiveRefresh
- }
- public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator {
-
-
- public typealias Credential = AuthenticatorType.Credential
-
-
-
-
-
- public struct RefreshWindow {
-
-
-
-
- public let interval: TimeInterval
-
- public let maximumAttempts: Int
-
-
-
-
-
- public init(interval: TimeInterval = 30.0, maximumAttempts: Int = 5) {
- self.interval = interval
- self.maximumAttempts = maximumAttempts
- }
- }
- private struct AdaptOperation {
- let urlRequest: URLRequest
- let session: Session
- let completion: (Result<URLRequest, Error>) -> Void
- }
- private enum AdaptResult {
- case adapt(Credential)
- case doNotAdapt(AuthenticationError)
- case adaptDeferred
- }
- private struct MutableState {
- var credential: Credential?
- var isRefreshing = false
- var refreshTimestamps: [TimeInterval] = []
- var refreshWindow: RefreshWindow?
- var adaptOperations: [AdaptOperation] = []
- var requestsToRetry: [(RetryResult) -> Void] = []
- }
-
-
- public var credential: Credential? {
- get { mutableState.credential }
- set { mutableState.credential = newValue }
- }
- let authenticator: AuthenticatorType
- let queue = DispatchQueue(label: "org.alamofire.authentication.inspector")
- @Protected
- private var mutableState = MutableState()
-
-
-
-
-
-
-
-
-
-
- public init(authenticator: AuthenticatorType,
- credential: Credential? = nil,
- refreshWindow: RefreshWindow? = RefreshWindow()) {
- self.authenticator = authenticator
- mutableState.credential = credential
- mutableState.refreshWindow = refreshWindow
- }
-
- public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
- let adaptResult: AdaptResult = $mutableState.write { mutableState in
-
- guard !mutableState.isRefreshing else {
- let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
- mutableState.adaptOperations.append(operation)
- return .adaptDeferred
- }
-
- guard let credential = mutableState.credential else {
- let error = AuthenticationError.missingCredential
- return .doNotAdapt(error)
- }
-
- guard !credential.requiresRefresh else {
- let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
- mutableState.adaptOperations.append(operation)
- refresh(credential, for: session, insideLock: &mutableState)
- return .adaptDeferred
- }
- return .adapt(credential)
- }
- switch adaptResult {
- case let .adapt(credential):
- var authenticatedRequest = urlRequest
- authenticator.apply(credential, to: &authenticatedRequest)
- completion(.success(authenticatedRequest))
- case let .doNotAdapt(adaptError):
- completion(.failure(adaptError))
- case .adaptDeferred:
-
- break
- }
- }
-
- public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
-
- guard let urlRequest = request.request, let response = request.response else {
- completion(.doNotRetry)
- return
- }
-
- guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else {
- completion(.doNotRetry)
- return
- }
-
- guard let credential = credential else {
- let error = AuthenticationError.missingCredential
- completion(.doNotRetryWithError(error))
- return
- }
-
- guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else {
- completion(.retry)
- return
- }
- $mutableState.write { mutableState in
- mutableState.requestsToRetry.append(completion)
- guard !mutableState.isRefreshing else { return }
- refresh(credential, for: session, insideLock: &mutableState)
- }
- }
-
- private func refresh(_ credential: Credential, for session: Session, insideLock mutableState: inout MutableState) {
- guard !isRefreshExcessive(insideLock: &mutableState) else {
- let error = AuthenticationError.excessiveRefresh
- handleRefreshFailure(error, insideLock: &mutableState)
- return
- }
- mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime)
- mutableState.isRefreshing = true
-
- queue.async {
- self.authenticator.refresh(credential, for: session) { result in
- self.$mutableState.write { mutableState in
- switch result {
- case let .success(credential):
- self.handleRefreshSuccess(credential, insideLock: &mutableState)
- case let .failure(error):
- self.handleRefreshFailure(error, insideLock: &mutableState)
- }
- }
- }
- }
- }
- private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool {
- guard let refreshWindow = mutableState.refreshWindow else { return false }
- let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval
- let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in
- guard refreshWindowMin <= refreshTimestamp else { return }
- attempts += 1
- }
- let isRefreshExcessive = refreshAttemptsWithinWindow >= refreshWindow.maximumAttempts
- return isRefreshExcessive
- }
- private func handleRefreshSuccess(_ credential: Credential, insideLock mutableState: inout MutableState) {
- mutableState.credential = credential
- let adaptOperations = mutableState.adaptOperations
- let requestsToRetry = mutableState.requestsToRetry
- mutableState.adaptOperations.removeAll()
- mutableState.requestsToRetry.removeAll()
- mutableState.isRefreshing = false
-
- queue.async {
- adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) }
- requestsToRetry.forEach { $0(.retry) }
- }
- }
- private func handleRefreshFailure(_ error: Error, insideLock mutableState: inout MutableState) {
- let adaptOperations = mutableState.adaptOperations
- let requestsToRetry = mutableState.requestsToRetry
- mutableState.adaptOperations.removeAll()
- mutableState.requestsToRetry.removeAll()
- mutableState.isRefreshing = false
-
- queue.async {
- adaptOperations.forEach { $0.completion(.failure(error)) }
- requestsToRetry.forEach { $0(.doNotRetryWithError(error)) }
- }
- }
- }
|