Initial commit

This commit is contained in:
oscarz
2024-08-12 10:49:20 +08:00
parent 3002510aaf
commit 00fd0adf89
331 changed files with 53210 additions and 130 deletions
+61
View File
@@ -0,0 +1,61 @@
//
// AlamofireExtended.swift
//
// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
/// Type that acts as a generic extension point for all `AlamofireExtended` types.
public struct AlamofireExtension<ExtendedType> {
/// Stores the type or meta-type of any extended type.
public private(set) var type: ExtendedType
/// Create an instance from the provided value.
///
/// - Parameter type: Instance being extended.
public init(_ type: ExtendedType) {
self.type = type
}
}
/// Protocol describing the `af` extension points for Alamofire extended types.
public protocol AlamofireExtended {
/// Type being extended.
associatedtype ExtendedType
/// Static Alamofire extension point.
static var af: AlamofireExtension<ExtendedType>.Type { get set }
/// Instance Alamofire extension point.
var af: AlamofireExtension<ExtendedType> { get set }
}
extension AlamofireExtended {
/// Static Alamofire extension point.
public static var af: AlamofireExtension<Self>.Type {
get { AlamofireExtension<Self>.self }
set {}
}
/// Instance Alamofire extension point.
public var af: AlamofireExtension<Self> {
get { AlamofireExtension(self) }
set {}
}
}
@@ -0,0 +1,402 @@
//
// AuthenticationInterceptor.swift
//
// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Types adopting the `AuthenticationCredential` protocol can be used to authenticate `URLRequest`s.
///
/// One common example of an `AuthenticationCredential` is an OAuth2 credential containing an access token used to
/// authenticate all requests on behalf of a user. The access token generally has an expiration window of 60 minutes
/// which will then require a refresh of the credential using the refresh token to generate a new access token.
public protocol AuthenticationCredential {
/// Whether the credential requires a refresh. This property should always return `true` when the credential is
/// expired. It is also wise to consider returning `true` when the credential will expire in several seconds or
/// minutes depending on the expiration window of the credential.
///
/// For example, if the credential is valid for 60 minutes, then it would be wise to return `true` when the
/// credential is only valid for 5 minutes or less. That ensures the credential will not expire as it is passed
/// around backend services.
var requiresRefresh: Bool { get }
}
// MARK: -
/// Types adopting the `Authenticator` protocol can be used to authenticate `URLRequest`s with an
/// `AuthenticationCredential` as well as refresh the `AuthenticationCredential` when required.
public protocol Authenticator: AnyObject {
/// The type of credential associated with the `Authenticator` instance.
associatedtype Credential: AuthenticationCredential
/// Applies the `Credential` to the `URLRequest`.
///
/// In the case of OAuth2, the access token of the `Credential` would be added to the `URLRequest` as a Bearer
/// token to the `Authorization` header.
///
/// - Parameters:
/// - credential: The `Credential`.
/// - urlRequest: The `URLRequest`.
func apply(_ credential: Credential, to urlRequest: inout URLRequest)
/// Refreshes the `Credential` and executes the `completion` closure with the `Result` once complete.
///
/// Refresh can be called in one of two ways. It can be called before the `Request` is actually executed due to
/// a `requiresRefresh` returning `true` during the adapt portion of the `Request` creation process. It can also
/// be triggered by a failed `Request` where the authentication server denied access due to an expired or
/// invalidated access token.
///
/// In the case of OAuth2, this method would use the refresh token of the `Credential` to generate a new
/// `Credential` using the authentication service. Once complete, the `completion` closure should be called with
/// the new `Credential`, or the error that occurred.
///
/// In general, if the refresh call fails with certain status codes from the authentication server (commonly a 401),
/// the refresh token in the `Credential` can no longer be used to generate a valid `Credential`. In these cases,
/// you will need to reauthenticate the user with their username / password.
///
/// Please note, these are just general examples of common use cases. They are not meant to solve your specific
/// authentication server challenges. Please work with your authentication server team to ensure your
/// `Authenticator` logic matches their expectations.
///
/// - Parameters:
/// - credential: The `Credential` to refresh.
/// - session: The `Session` requiring the refresh.
/// - completion: The closure to be executed once the refresh is complete.
func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void)
/// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`.
///
/// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `false`
/// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then you
/// will need to work with your authentication server team to understand how to identify when this occurs.
///
/// In the case of OAuth2, where an authentication server can invalidate credentials, you will need to inspect the
/// `HTTPURLResponse` or possibly the `Error` for when this occurs. This is commonly handled by the authentication
/// server returning a 401 status code and some additional header to indicate an OAuth2 failure occurred.
///
/// It is very important to understand how your authentication server works to be able to implement this correctly.
/// For example, if your authentication server returns a 401 when an OAuth2 error occurs, and your downstream
/// service also returns a 401 when you are not authorized to perform that operation, how do you know which layer
/// of the backend returned you a 401? You do not want to trigger a refresh unless you know your authentication
/// server is actually the layer rejecting the request. Again, work with your authentication server team to understand
/// how to identify an OAuth2 401 error vs. a downstream 401 error to avoid endless refresh loops.
///
/// - Parameters:
/// - urlRequest: The `URLRequest`.
/// - response: The `HTTPURLResponse`.
/// - error: The `Error`.
///
/// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise.
func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool
/// Determines whether the `URLRequest` is authenticated with the `Credential`.
///
/// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `true`
/// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then
/// read on.
///
/// When an authentication server can invalidate credentials, it means that you may have a non-expired credential
/// that appears to be valid, but will be rejected by the authentication server when used. Generally when this
/// happens, a number of requests are all sent when the application is foregrounded, and all of them will be
/// rejected by the authentication server in the order they are received. The first failed request will trigger a
/// refresh internally, which will update the credential, and then retry all the queued requests with the new
/// credential. However, it is possible that some of the original requests will not return from the authentication
/// server until the refresh has completed. This is where this method comes in.
///
/// When the authentication server rejects a credential, we need to check to make sure we haven't refreshed the
/// credential while the request was in flight. If it has already refreshed, then we don't need to trigger an
/// additional refresh. If it hasn't refreshed, then we need to refresh.
///
/// Now that it is understood how the result of this method is used in the refresh lifecyle, let's walk through how
/// to implement it. You should return `true` in this method if the `URLRequest` is authenticated in a way that
/// matches the values in the `Credential`. In the case of OAuth2, this would mean that the Bearer token in the
/// `Authorization` header of the `URLRequest` matches the access token in the `Credential`. If it matches, then we
/// know the `Credential` was used to authenticate the `URLRequest` and should return `true`. If the Bearer token
/// did not match the access token, then you should return `false`.
///
/// - Parameters:
/// - urlRequest: The `URLRequest`.
/// - credential: The `Credential`.
///
/// - Returns: `true` if the `URLRequest` is authenticated with the `Credential`, `false` otherwise.
func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool
}
// MARK: -
/// Represents various authentication failures that occur when using the `AuthenticationInterceptor`. All errors are
/// still vended from Alamofire as `AFError` types. The `AuthenticationError` instances will be embedded within
/// `AFError` `.requestAdaptationFailed` or `.requestRetryFailed` cases.
public enum AuthenticationError: Error {
/// The credential was missing so the request could not be authenticated.
case missingCredential
/// The credential was refreshed too many times within the `RefreshWindow`.
case excessiveRefresh
}
// MARK: -
/// The `AuthenticationInterceptor` class manages the queuing and threading complexity of authenticating requests.
/// It relies on an `Authenticator` type to handle the actual `URLRequest` authentication and `Credential` refresh.
public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator {
// MARK: Typealiases
/// Type of credential used to authenticate requests.
public typealias Credential = AuthenticatorType.Credential
// MARK: Helper Types
/// Type that defines a time window used to identify excessive refresh calls. When enabled, prior to executing a
/// refresh, the `AuthenticationInterceptor` compares the timestamp history of previous refresh calls against the
/// `RefreshWindow`. If more refreshes have occurred within the refresh window than allowed, the refresh is
/// cancelled and an `AuthorizationError.excessiveRefresh` error is thrown.
public struct RefreshWindow {
/// `TimeInterval` defining the duration of the time window before the current time in which the number of
/// refresh attempts is compared against `maximumAttempts`. For example, if `interval` is 30 seconds, then the
/// `RefreshWindow` represents the past 30 seconds. If more attempts occurred in the past 30 seconds than
/// `maximumAttempts`, an `.excessiveRefresh` error will be thrown.
public let interval: TimeInterval
/// Total refresh attempts allowed within `interval` before throwing an `.excessiveRefresh` error.
public let maximumAttempts: Int
/// Creates a `RefreshWindow` instance from the specified `interval` and `maximumAttempts`.
///
/// - Parameters:
/// - interval: `TimeInterval` defining the duration of the time window before the current time.
/// - maximumAttempts: The maximum attempts allowed within the `TimeInterval`.
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] = []
}
// MARK: Properties
/// The `Credential` used to authenticate requests.
public var credential: Credential? {
get { mutableState.credential }
set { mutableState.credential = newValue }
}
let authenticator: AuthenticatorType
let queue = DispatchQueue(label: "org.alamofire.authentication.inspector")
private let mutableState: Protected<MutableState>
// MARK: Initialization
/// Creates an `AuthenticationInterceptor` instance from the specified parameters.
///
/// A `nil` `RefreshWindow` will result in the `AuthenticationInterceptor` not checking for excessive refresh calls.
/// It is recommended to always use a `RefreshWindow` to avoid endless refresh cycles.
///
/// - Parameters:
/// - authenticator: The `Authenticator` type.
/// - credential: The `Credential` if it exists. `nil` by default.
/// - refreshWindow: The `RefreshWindow` used to identify excessive refresh calls. `RefreshWindow()` by default.
public init(authenticator: AuthenticatorType,
credential: Credential? = nil,
refreshWindow: RefreshWindow? = RefreshWindow()) {
self.authenticator = authenticator
mutableState = Protected(MutableState(credential: credential, refreshWindow: refreshWindow))
}
// MARK: Adapt
public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
let adaptResult: AdaptResult = mutableState.write { mutableState in
// Queue the adapt operation if a refresh is already in place.
guard !mutableState.isRefreshing else {
let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
mutableState.adaptOperations.append(operation)
return .adaptDeferred
}
// Throw missing credential error is the credential is missing.
guard let credential = mutableState.credential else {
let error = AuthenticationError.missingCredential
return .doNotAdapt(error)
}
// Queue the adapt operation and trigger refresh operation if credential requires refresh.
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:
// No-op: adapt operation captured during refresh.
break
}
}
// MARK: Retry
public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
// Do not attempt retry if there was not an original request and response from the server.
guard let urlRequest = request.request, let response = request.response else {
completion(.doNotRetry)
return
}
// Do not attempt retry unless the `Authenticator` verifies failure was due to authentication error (i.e. 401 status code).
guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else {
completion(.doNotRetry)
return
}
// Do not attempt retry if there is no credential.
guard let credential else {
let error = AuthenticationError.missingCredential
completion(.doNotRetryWithError(error))
return
}
// Retry the request if the `Authenticator` verifies it was authenticated with a previous credential.
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)
}
}
// MARK: Refresh
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
// Dispatch to queue to hop out of the lock in case authenticator.refresh is implemented synchronously.
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
// Dispatch to queue to hop out of the mutable state lock
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
// Dispatch to queue to hop out of the mutable state lock
queue.async {
adaptOperations.forEach { $0.completion(.failure(error)) }
requestsToRetry.forEach { $0(.doNotRetryWithError(error)) }
}
}
}
+107
View File
@@ -0,0 +1,107 @@
//
// CachedResponseHandler.swift
//
// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// A type that handles whether the data task should store the HTTP response in the cache.
public protocol CachedResponseHandler {
/// Determines whether the HTTP response should be stored in the cache.
///
/// The `completion` closure should be passed one of three possible options:
///
/// 1. The cached response provided by the server (this is the most common use case).
/// 2. A modified version of the cached response (you may want to modify it in some way before caching).
/// 3. A `nil` value to prevent the cached response from being stored in the cache.
///
/// - Parameters:
/// - task: The data task whose request resulted in the cached response.
/// - response: The cached response to potentially store in the cache.
/// - completion: The closure to execute containing cached response, a modified response, or `nil`.
func dataTask(_ task: URLSessionDataTask,
willCacheResponse response: CachedURLResponse,
completion: @escaping (CachedURLResponse?) -> Void)
}
// MARK: -
/// `ResponseCacher` is a convenience `CachedResponseHandler` making it easy to cache, not cache, or modify a cached
/// response.
public struct ResponseCacher {
/// Defines the behavior of the `ResponseCacher` type.
public enum Behavior {
/// Stores the cached response in the cache.
case cache
/// Prevents the cached response from being stored in the cache.
case doNotCache
/// Modifies the cached response before storing it in the cache.
case modify((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)
}
/// Returns a `ResponseCacher` with a `.cache` `Behavior`.
public static let cache = ResponseCacher(behavior: .cache)
/// Returns a `ResponseCacher` with a `.doNotCache` `Behavior`.
public static let doNotCache = ResponseCacher(behavior: .doNotCache)
/// The `Behavior` of the `ResponseCacher`.
public let behavior: Behavior
/// Creates a `ResponseCacher` instance from the `Behavior`.
///
/// - Parameter behavior: The `Behavior`.
public init(behavior: Behavior) {
self.behavior = behavior
}
}
extension ResponseCacher: CachedResponseHandler {
public func dataTask(_ task: URLSessionDataTask,
willCacheResponse response: CachedURLResponse,
completion: @escaping (CachedURLResponse?) -> Void) {
switch behavior {
case .cache:
completion(response)
case .doNotCache:
completion(nil)
case let .modify(closure):
let response = closure(task, response)
completion(response)
}
}
}
extension CachedResponseHandler where Self == ResponseCacher {
/// Provides a `ResponseCacher` which caches the response, if allowed. Equivalent to `ResponseCacher.cache`.
public static var cache: ResponseCacher { .cache }
/// Provides a `ResponseCacher` which does not cache the response. Equivalent to `ResponseCacher.doNotCache`.
public static var doNotCache: ResponseCacher { .doNotCache }
/// Creates a `ResponseCacher` which modifies the proposed `CachedURLResponse` using the provided closure.
///
/// - Parameter closure: Closure used to modify the `CachedURLResponse`.
/// - Returns: The `ResponseCacher`.
public static func modify(using closure: @escaping ((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)) -> ResponseCacher {
ResponseCacher(behavior: .modify(closure))
}
}
+652
View File
@@ -0,0 +1,652 @@
//
// Combine.swift
//
// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#if !((os(iOS) && (arch(i386) || arch(arm))) || os(Windows) || os(Linux) || os(Android))
import Combine
import Dispatch
import Foundation
// MARK: - DataRequest / UploadRequest
/// A Combine `Publisher` that publishes the `DataResponse<Value, AFError>` of the provided `DataRequest`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public struct DataResponsePublisher<Value>: Publisher {
public typealias Output = DataResponse<Value, AFError>
public typealias Failure = Never
private typealias Handler = (@escaping (_ response: DataResponse<Value, AFError>) -> Void) -> DataRequest
private let request: DataRequest
private let responseHandler: Handler
/// Creates an instance which will serialize responses using the provided `ResponseSerializer`.
///
/// - Parameters:
/// - request: `DataRequest` for which to publish the response.
/// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default.
/// - serializer: `ResponseSerializer` used to produce the published `DataResponse`.
public init<Serializer: ResponseSerializer>(_ request: DataRequest, queue: DispatchQueue, serializer: Serializer)
where Value == Serializer.SerializedObject {
self.request = request
responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) }
}
/// Creates an instance which will serialize responses using the provided `DataResponseSerializerProtocol`.
///
/// - Parameters:
/// - request: `DataRequest` for which to publish the response.
/// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default.
/// - serializer: `DataResponseSerializerProtocol` used to produce the published `DataResponse`.
public init<Serializer: DataResponseSerializerProtocol>(_ request: DataRequest,
queue: DispatchQueue,
serializer: Serializer)
where Value == Serializer.SerializedObject {
self.request = request
responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) }
}
/// Publishes only the `Result` of the `DataResponse` value.
///
/// - Returns: The `AnyPublisher` publishing the `Result<Value, AFError>` value.
public func result() -> AnyPublisher<Result<Value, AFError>, Never> {
map(\.result).eraseToAnyPublisher()
}
/// Publishes the `Result` of the `DataResponse` as a single `Value` or fail with the `AFError` instance.
///
/// - Returns: The `AnyPublisher<Value, AFError>` publishing the stream.
public func value() -> AnyPublisher<Value, AFError> {
setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher()
}
public func receive<S>(subscriber: S) where S: Subscriber, DataResponsePublisher.Failure == S.Failure, DataResponsePublisher.Output == S.Input {
subscriber.receive(subscription: Inner(request: request,
responseHandler: responseHandler,
downstream: subscriber))
}
private final class Inner<Downstream: Subscriber>: Subscription
where Downstream.Input == Output {
typealias Failure = Downstream.Failure
private let downstream: Protected<Downstream?>
private let request: DataRequest
private let responseHandler: Handler
init(request: DataRequest, responseHandler: @escaping Handler, downstream: Downstream) {
self.request = request
self.responseHandler = responseHandler
self.downstream = Protected(downstream)
}
func request(_ demand: Subscribers.Demand) {
assert(demand > 0)
guard let downstream = downstream.read({ $0 }) else { return }
self.downstream.write(nil)
responseHandler { response in
_ = downstream.receive(response)
downstream.receive(completion: .finished)
}.resume()
}
func cancel() {
request.cancel()
downstream.write(nil)
}
}
}
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
extension DataResponsePublisher where Value == Data? {
/// Creates an instance which publishes a `DataResponse<Data?, AFError>` value without serialization.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public init(_ request: DataRequest, queue: DispatchQueue) {
self.request = request
responseHandler = { request.response(queue: queue, completionHandler: $0) }
}
}
extension DataRequest {
/// Creates a `DataResponsePublisher` for this instance using the given `ResponseSerializer` and `DispatchQueue`.
///
/// - Parameters:
/// - serializer: `ResponseSerializer` used to serialize response `Data`.
/// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default.
///
/// - Returns: The `DataResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishResponse<Serializer: ResponseSerializer, T>(using serializer: Serializer, on queue: DispatchQueue = .main) -> DataResponsePublisher<T>
where Serializer.SerializedObject == T {
DataResponsePublisher(self, queue: queue, serializer: serializer)
}
/// Creates a `DataResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the
/// response.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default.
/// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()`
/// by default.
/// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
/// default.
/// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of
/// status code. `[.head]` by default.
/// - Returns: The `DataResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishData(queue: DispatchQueue = .main,
preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher<Data> {
publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
on: queue)
}
/// Creates a `DataResponsePublisher` for this instance and uses a `StringResponseSerializer` to serialize the
/// response.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default.
/// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()`
/// by default.
/// - encoding: `String.Encoding` to parse the response. `nil` by default, in which case the encoding
/// will be determined by the server response, falling back to the default HTTP character
/// set, `ISO-8859-1`.
/// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
/// default.
/// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of
/// status code. `[.head]` by default.
///
/// - Returns: The `DataResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishString(queue: DispatchQueue = .main,
preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil,
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher<String> {
publishResponse(using: StringResponseSerializer(dataPreprocessor: preprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
on: queue)
}
@_disfavoredOverload
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
@available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).")
public func publishDecodable<T: Decodable>(type: T.Type = T.self,
queue: DispatchQueue = .main,
preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyResponseMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DataResponsePublisher<T> {
publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyResponseMethods),
on: queue)
}
/// Creates a `DataResponsePublisher` for this instance and uses a `DecodableResponseSerializer` to serialize the
/// response.
///
/// - Parameters:
/// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by
/// default.
/// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default.
/// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization.
/// `PassthroughPreprocessor()` by default.
/// - decoder: `DataDecoder` instance used to decode response `Data`. `JSONDecoder()` by default.
/// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
/// default.
/// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of
/// status code. `[.head]` by default.
///
/// - Returns: The `DataResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishDecodable<T: Decodable>(type: T.Type = T.self,
queue: DispatchQueue = .main,
preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DataResponsePublisher<T> {
publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
on: queue)
}
/// Creates a `DataResponsePublisher` for this instance which does not serialize the response before publishing.
///
/// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default.
///
/// - Returns: The `DataResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishUnserialized(queue: DispatchQueue = .main) -> DataResponsePublisher<Data?> {
DataResponsePublisher(self, queue: queue)
}
}
// A Combine `Publisher` that publishes a sequence of `Stream<Value, AFError>` values received by the provided `DataStreamRequest`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public struct DataStreamPublisher<Value>: Publisher {
public typealias Output = DataStreamRequest.Stream<Value, AFError>
public typealias Failure = Never
private typealias Handler = (@escaping DataStreamRequest.Handler<Value, AFError>) -> DataStreamRequest
private let request: DataStreamRequest
private let streamHandler: Handler
/// Creates an instance which will serialize responses using the provided `DataStreamSerializer`.
///
/// - Parameters:
/// - request: `DataStreamRequest` for which to publish the response.
/// - queue: `DispatchQueue` on which the `Stream<Value, AFError>` values will be published. `.main` by
/// default.
/// - serializer: `DataStreamSerializer` used to produce the published `Stream<Value, AFError>` values.
public init<Serializer: DataStreamSerializer>(_ request: DataStreamRequest, queue: DispatchQueue, serializer: Serializer)
where Value == Serializer.SerializedObject {
self.request = request
streamHandler = { request.responseStream(using: serializer, on: queue, stream: $0) }
}
/// Publishes only the `Result` of the `DataStreamRequest.Stream`'s `Event`s.
///
/// - Returns: The `AnyPublisher` publishing the `Result<Value, AFError>` value.
public func result() -> AnyPublisher<Result<Value, AFError>, Never> {
compactMap { stream in
switch stream.event {
case let .stream(result):
return result
// If the stream has completed with an error, send the error value downstream as a `.failure`.
case let .complete(completion):
return completion.error.map(Result.failure)
}
}
.eraseToAnyPublisher()
}
/// Publishes the streamed values of the `DataStreamRequest.Stream` as a sequence of `Value` or fail with the
/// `AFError` instance.
///
/// - Returns: The `AnyPublisher<Value, AFError>` publishing the stream.
public func value() -> AnyPublisher<Value, AFError> {
result().setFailureType(to: AFError.self).flatMap(\.publisher).eraseToAnyPublisher()
}
public func receive<S>(subscriber: S) where S: Subscriber, DataStreamPublisher.Failure == S.Failure, DataStreamPublisher.Output == S.Input {
subscriber.receive(subscription: Inner(request: request,
streamHandler: streamHandler,
downstream: subscriber))
}
private final class Inner<Downstream: Subscriber>: Subscription
where Downstream.Input == Output {
typealias Failure = Downstream.Failure
private let downstream: Protected<Downstream?>
private let request: DataStreamRequest
private let streamHandler: Handler
init(request: DataStreamRequest, streamHandler: @escaping Handler, downstream: Downstream) {
self.request = request
self.streamHandler = streamHandler
self.downstream = Protected(downstream)
}
func request(_ demand: Subscribers.Demand) {
assert(demand > 0)
guard let downstream = downstream.read({ $0 }) else { return }
self.downstream.write(nil)
streamHandler { stream in
_ = downstream.receive(stream)
if case .complete = stream.event {
downstream.receive(completion: .finished)
}
}.resume()
}
func cancel() {
request.cancel()
downstream.write(nil)
}
}
}
extension DataStreamRequest {
/// Creates a `DataStreamPublisher` for this instance using the given `DataStreamSerializer` and `DispatchQueue`.
///
/// - Parameters:
/// - serializer: `DataStreamSerializer` used to serialize the streamed `Data`.
/// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default.
/// - Returns: The `DataStreamPublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishStream<Serializer: DataStreamSerializer>(using serializer: Serializer,
on queue: DispatchQueue = .main) -> DataStreamPublisher<Serializer.SerializedObject> {
DataStreamPublisher(self, queue: queue, serializer: serializer)
}
/// Creates a `DataStreamPublisher` for this instance which uses a `PassthroughStreamSerializer` to stream `Data`
/// unserialized.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default.
/// - Returns: The `DataStreamPublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishData(queue: DispatchQueue = .main) -> DataStreamPublisher<Data> {
publishStream(using: PassthroughStreamSerializer(), on: queue)
}
/// Creates a `DataStreamPublisher` for this instance which uses a `StringStreamSerializer` to serialize stream
/// `Data` values into `String` values.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default.
/// - Returns: The `DataStreamPublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishString(queue: DispatchQueue = .main) -> DataStreamPublisher<String> {
publishStream(using: StringStreamSerializer(), on: queue)
}
/// Creates a `DataStreamPublisher` for this instance which uses a `DecodableStreamSerializer` with the provided
/// parameters to serialize stream `Data` values into the provided type.
///
/// - Parameters:
/// - type: `Decodable` type to which to decode stream `Data`. Inferred from the context by default.
/// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default.
/// - decoder: `DataDecoder` instance used to decode stream `Data`. `JSONDecoder()` by default.
/// - preprocessor: `DataPreprocessor` which filters incoming stream `Data` before serialization.
/// `PassthroughPreprocessor()` by default.
/// - Returns: The `DataStreamPublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishDecodable<T: Decodable>(type: T.Type = T.self,
queue: DispatchQueue = .main,
decoder: DataDecoder = JSONDecoder(),
preprocessor: DataPreprocessor = PassthroughPreprocessor()) -> DataStreamPublisher<T> {
publishStream(using: DecodableStreamSerializer(decoder: decoder,
dataPreprocessor: preprocessor),
on: queue)
}
}
/// A Combine `Publisher` that publishes the `DownloadResponse<Value, AFError>` of the provided `DownloadRequest`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public struct DownloadResponsePublisher<Value>: Publisher {
public typealias Output = DownloadResponse<Value, AFError>
public typealias Failure = Never
private typealias Handler = (@escaping (_ response: DownloadResponse<Value, AFError>) -> Void) -> DownloadRequest
private let request: DownloadRequest
private let responseHandler: Handler
/// Creates an instance which will serialize responses using the provided `ResponseSerializer`.
///
/// - Parameters:
/// - request: `DownloadRequest` for which to publish the response.
/// - queue: `DispatchQueue` on which the `DownloadResponse` value will be published. `.main` by default.
/// - serializer: `ResponseSerializer` used to produce the published `DownloadResponse`.
public init<Serializer: ResponseSerializer>(_ request: DownloadRequest, queue: DispatchQueue, serializer: Serializer)
where Value == Serializer.SerializedObject {
self.request = request
responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) }
}
/// Creates an instance which will serialize responses using the provided `DownloadResponseSerializerProtocol` value.
///
/// - Parameters:
/// - request: `DownloadRequest` for which to publish the response.
/// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default.
/// - serializer: `DownloadResponseSerializerProtocol` used to produce the published `DownloadResponse`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public init<Serializer: DownloadResponseSerializerProtocol>(_ request: DownloadRequest,
queue: DispatchQueue,
serializer: Serializer)
where Value == Serializer.SerializedObject {
self.request = request
responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) }
}
/// Publishes only the `Result` of the `DownloadResponse` value.
///
/// - Returns: The `AnyPublisher` publishing the `Result<Value, AFError>` value.
public func result() -> AnyPublisher<Result<Value, AFError>, Never> {
map(\.result).eraseToAnyPublisher()
}
/// Publishes the `Result` of the `DownloadResponse` as a single `Value` or fail with the `AFError` instance.
///
/// - Returns: The `AnyPublisher<Value, AFError>` publishing the stream.
public func value() -> AnyPublisher<Value, AFError> {
setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher()
}
public func receive<S>(subscriber: S) where S: Subscriber, DownloadResponsePublisher.Failure == S.Failure, DownloadResponsePublisher.Output == S.Input {
subscriber.receive(subscription: Inner(request: request,
responseHandler: responseHandler,
downstream: subscriber))
}
private final class Inner<Downstream: Subscriber>: Subscription
where Downstream.Input == Output {
typealias Failure = Downstream.Failure
private let downstream: Protected<Downstream?>
private let request: DownloadRequest
private let responseHandler: Handler
init(request: DownloadRequest, responseHandler: @escaping Handler, downstream: Downstream) {
self.request = request
self.responseHandler = responseHandler
self.downstream = Protected(downstream)
}
func request(_ demand: Subscribers.Demand) {
assert(demand > 0)
guard let downstream = downstream.read({ $0 }) else { return }
self.downstream.write(nil)
responseHandler { response in
_ = downstream.receive(response)
downstream.receive(completion: .finished)
}.resume()
}
func cancel() {
request.cancel()
downstream.write(nil)
}
}
}
extension DownloadRequest {
/// Creates a `DownloadResponsePublisher` for this instance using the given `ResponseSerializer` and `DispatchQueue`.
///
/// - Parameters:
/// - serializer: `ResponseSerializer` used to serialize the response `Data` from disk.
/// - queue: `DispatchQueue` on which the `DownloadResponse` will be published.`.main` by default.
///
/// - Returns: The `DownloadResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishResponse<Serializer: ResponseSerializer, T>(using serializer: Serializer, on queue: DispatchQueue = .main) -> DownloadResponsePublisher<T>
where Serializer.SerializedObject == T {
DownloadResponsePublisher(self, queue: queue, serializer: serializer)
}
/// Creates a `DownloadResponsePublisher` for this instance using the given `DownloadResponseSerializerProtocol` and
/// `DispatchQueue`.
///
/// - Parameters:
/// - serializer: `DownloadResponseSerializer` used to serialize the response `Data` from disk.
/// - queue: `DispatchQueue` on which the `DownloadResponse` will be published.`.main` by default.
///
/// - Returns: The `DownloadResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishResponse<Serializer: DownloadResponseSerializerProtocol, T>(using serializer: Serializer, on queue: DispatchQueue = .main) -> DownloadResponsePublisher<T>
where Serializer.SerializedObject == T {
DownloadResponsePublisher(self, queue: queue, serializer: serializer)
}
/// Creates a `DownloadResponsePublisher` for this instance and uses a `URLResponseSerializer` to serialize the
/// response.
///
/// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default.
///
/// - Returns: The `DownloadResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishURL(queue: DispatchQueue = .main) -> DownloadResponsePublisher<URL> {
publishResponse(using: URLResponseSerializer(), on: queue)
}
/// Creates a `DownloadResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the
/// response.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default.
/// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()`
/// by default.
/// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
/// default.
/// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of
/// status code. `[.head]` by default.
///
/// - Returns: The `DownloadResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishData(queue: DispatchQueue = .main,
preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher<Data> {
publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
on: queue)
}
/// Creates a `DownloadResponsePublisher` for this instance and uses a `StringResponseSerializer` to serialize the
/// response.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default.
/// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()`
/// by default.
/// - encoding: `String.Encoding` to parse the response. `nil` by default, in which case the encoding
/// will be determined by the server response, falling back to the default HTTP character
/// set, `ISO-8859-1`.
/// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
/// default.
/// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of
/// status code. `[.head]` by default.
///
/// - Returns: The `DownloadResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishString(queue: DispatchQueue = .main,
preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil,
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher<String> {
publishResponse(using: StringResponseSerializer(dataPreprocessor: preprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
on: queue)
}
@_disfavoredOverload
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
@available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).")
public func publishDecodable<T: Decodable>(type: T.Type = T.self,
queue: DispatchQueue = .main,
preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyResponseMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DownloadResponsePublisher<T> {
publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyResponseMethods),
on: queue)
}
/// Creates a `DownloadResponsePublisher` for this instance and uses a `DecodableResponseSerializer` to serialize
/// the response.
///
/// - Parameters:
/// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by default.
/// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default.
/// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization.
/// `PassthroughPreprocessor()` by default.
/// - decoder: `DataDecoder` instance used to decode response `Data`. `JSONDecoder()` by default.
/// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by
/// default.
/// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless
/// of status code. `[.head]` by default.
///
/// - Returns: The `DownloadResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishDecodable<T: Decodable>(type: T.Type = T.self,
queue: DispatchQueue = .main,
preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DownloadResponsePublisher<T> {
publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
on: queue)
}
}
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
extension DownloadResponsePublisher where Value == URL? {
/// Creates an instance which publishes a `DownloadResponse<URL?, AFError>` value without serialization.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public init(_ request: DownloadRequest, queue: DispatchQueue) {
self.request = request
responseHandler = { request.response(queue: queue, completionHandler: $0) }
}
}
extension DownloadRequest {
/// Creates a `DownloadResponsePublisher` for this instance which does not serialize the response before publishing.
///
/// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default.
///
/// - Returns: The `DownloadResponsePublisher`.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func publishUnserialized(on queue: DispatchQueue = .main) -> DownloadResponsePublisher<URL?> {
DownloadResponsePublisher(self, queue: queue)
}
}
#endif
+962
View File
@@ -0,0 +1,962 @@
//
// Concurrency.swift
//
// Copyright (c) 2021 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#if canImport(_Concurrency)
import Foundation
// MARK: - Request Event Streams
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension Request {
/// Creates a `StreamOf<Progress>` for the instance's upload progress.
///
/// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `StreamOf<Progress>`.
public func uploadProgress(bufferingPolicy: StreamOf<Progress>.BufferingPolicy = .unbounded) -> StreamOf<Progress> {
stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
uploadProgress(queue: underlyingQueue) { progress in
continuation.yield(progress)
}
}
}
/// Creates a `StreamOf<Progress>` for the instance's download progress.
///
/// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `StreamOf<Progress>`.
public func downloadProgress(bufferingPolicy: StreamOf<Progress>.BufferingPolicy = .unbounded) -> StreamOf<Progress> {
stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
downloadProgress(queue: underlyingQueue) { progress in
continuation.yield(progress)
}
}
}
/// Creates a `StreamOf<URLRequest>` for the `URLRequest`s produced for the instance.
///
/// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `StreamOf<URLRequest>`.
public func urlRequests(bufferingPolicy: StreamOf<URLRequest>.BufferingPolicy = .unbounded) -> StreamOf<URLRequest> {
stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
onURLRequestCreation(on: underlyingQueue) { request in
continuation.yield(request)
}
}
}
/// Creates a `StreamOf<URLSessionTask>` for the `URLSessionTask`s produced for the instance.
///
/// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `StreamOf<URLSessionTask>`.
public func urlSessionTasks(bufferingPolicy: StreamOf<URLSessionTask>.BufferingPolicy = .unbounded) -> StreamOf<URLSessionTask> {
stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
onURLSessionTaskCreation(on: underlyingQueue) { task in
continuation.yield(task)
}
}
}
/// Creates a `StreamOf<String>` for the cURL descriptions produced for the instance.
///
/// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `StreamOf<String>`.
public func cURLDescriptions(bufferingPolicy: StreamOf<String>.BufferingPolicy = .unbounded) -> StreamOf<String> {
stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
cURLDescription(on: underlyingQueue) { description in
continuation.yield(description)
}
}
}
fileprivate func stream<T>(of type: T.Type = T.self,
bufferingPolicy: StreamOf<T>.BufferingPolicy = .unbounded,
yielder: @escaping (StreamOf<T>.Continuation) -> Void) -> StreamOf<T> {
StreamOf<T>(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
yielder(continuation)
// Must come after serializers run in order to catch retry progress.
onFinish {
continuation.finish()
}
}
}
}
// MARK: - DataTask
/// Value used to `await` a `DataResponse` and associated values.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public struct DataTask<Value> {
/// `DataResponse` produced by the `DataRequest` and its response handler.
public var response: DataResponse<Value, AFError> {
get async {
if shouldAutomaticallyCancel {
return await withTaskCancellationHandler {
await task.value
} onCancel: {
cancel()
}
} else {
return await task.value
}
}
}
/// `Result` of any response serialization performed for the `response`.
public var result: Result<Value, AFError> {
get async { await response.result }
}
/// `Value` returned by the `response`.
public var value: Value {
get async throws {
try await result.get()
}
}
private let request: DataRequest
private let task: Task<DataResponse<Value, AFError>, Never>
private let shouldAutomaticallyCancel: Bool
fileprivate init(request: DataRequest, task: Task<DataResponse<Value, AFError>, Never>, shouldAutomaticallyCancel: Bool) {
self.request = request
self.task = task
self.shouldAutomaticallyCancel = shouldAutomaticallyCancel
}
/// Cancel the underlying `DataRequest` and `Task`.
public func cancel() {
task.cancel()
}
/// Resume the underlying `DataRequest`.
public func resume() {
request.resume()
}
/// Suspend the underlying `DataRequest`.
public func suspend() {
request.suspend()
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension DataRequest {
/// Creates a `StreamOf<HTTPURLResponse>` for the instance's responses.
///
/// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `StreamOf<HTTPURLResponse>`.
public func httpResponses(bufferingPolicy: StreamOf<HTTPURLResponse>.BufferingPolicy = .unbounded) -> StreamOf<HTTPURLResponse> {
stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
onHTTPResponse(on: underlyingQueue) { response in
continuation.yield(response)
}
}
}
/// Sets an async closure returning a `Request.ResponseDisposition`, called whenever the `DataRequest` produces an
/// `HTTPURLResponse`.
///
/// - Note: Most requests will only produce a single response for each outgoing attempt (initial + retries).
/// However, some types of response may trigger multiple `HTTPURLResponse`s, such as multipart streams,
/// where responses after the first will contain the part headers.
///
/// - Parameters:
/// - handler: Async closure executed when a new `HTTPURLResponse` is received and returning a
/// `ResponseDisposition` value. This value determines whether to continue the request or cancel it as
/// if `cancel()` had been called on the instance. Note, this closure is called on an arbitrary thread,
/// so any synchronous calls in it will execute in that context.
///
/// - Returns: The instance.
@_disfavoredOverload
@discardableResult
public func onHTTPResponse(
perform handler: @escaping @Sendable (_ response: HTTPURLResponse) async -> ResponseDisposition
) -> Self {
onHTTPResponse(on: underlyingQueue) { response, completionHandler in
Task {
let disposition = await handler(response)
completionHandler(disposition)
}
}
return self
}
/// Sets an async closure called whenever the `DataRequest` produces an `HTTPURLResponse`.
///
/// - Note: Most requests will only produce a single response for each outgoing attempt (initial + retries).
/// However, some types of response may trigger multiple `HTTPURLResponse`s, such as multipart streams,
/// where responses after the first will contain the part headers.
///
/// - Parameters:
/// - handler: Async closure executed when a new `HTTPURLResponse` is received. Note, this closure is called on an
/// arbitrary thread, so any synchronous calls in it will execute in that context.
///
/// - Returns: The instance.
@discardableResult
public func onHTTPResponse(perform handler: @escaping @Sendable (_ response: HTTPURLResponse) async -> Void) -> Self {
onHTTPResponse { response in
await handler(response)
return .allow
}
return self
}
/// Creates a `DataTask` to `await` a `Data` value.
///
/// - Parameters:
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DataTask`'s async
/// properties. `true` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion.
/// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
///
/// - Returns: The `DataTask`.
public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataTask<Data> {
serializingResponse(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
automaticallyCancelling: shouldAutomaticallyCancel)
}
/// Creates a `DataTask` to `await` serialization of a `Decodable` value.
///
/// - Parameters:
/// - type: `Decodable` type to decode from response data.
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DataTask`'s async
/// properties. `true` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer.
/// `PassthroughPreprocessor()` by default.
/// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
///
/// - Returns: The `DataTask`.
public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<Value>.defaultEmptyRequestMethods) -> DataTask<Value> {
serializingResponse(using: DecodableResponseSerializer<Value>(dataPreprocessor: dataPreprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
automaticallyCancelling: shouldAutomaticallyCancel)
}
/// Creates a `DataTask` to `await` serialization of a `String` value.
///
/// - Parameters:
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DataTask`'s async
/// properties. `true` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer.
/// `PassthroughPreprocessor()` by default.
/// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case
/// the encoding will be determined from the server response, falling back to the
/// default HTTP character set, `ISO-8859-1`.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
///
/// - Returns: The `DataTask`.
public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil,
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DataTask<String> {
serializingResponse(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
automaticallyCancelling: shouldAutomaticallyCancel)
}
/// Creates a `DataTask` to `await` serialization using the provided `ResponseSerializer` instance.
///
/// - Parameters:
/// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data.
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DataTask`'s async
/// properties. `true` by default.
///
/// - Returns: The `DataTask`.
public func serializingResponse<Serializer: ResponseSerializer>(using serializer: Serializer,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true)
-> DataTask<Serializer.SerializedObject> {
dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in
response(queue: underlyingQueue,
responseSerializer: serializer,
completionHandler: $0)
}
}
/// Creates a `DataTask` to `await` serialization using the provided `DataResponseSerializerProtocol` instance.
///
/// - Parameters:
/// - serializer: `DataResponseSerializerProtocol` responsible for serializing the request,
/// response, and data.
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DataTask`'s async
/// properties. `true` by default.
///
/// - Returns: The `DataTask`.
public func serializingResponse<Serializer: DataResponseSerializerProtocol>(using serializer: Serializer,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true)
-> DataTask<Serializer.SerializedObject> {
dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in
response(queue: underlyingQueue,
responseSerializer: serializer,
completionHandler: $0)
}
}
private func dataTask<Value>(automaticallyCancelling shouldAutomaticallyCancel: Bool,
forResponse onResponse: @escaping (@escaping (DataResponse<Value, AFError>) -> Void) -> Void)
-> DataTask<Value> {
let task = Task {
await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
onResponse {
continuation.resume(returning: $0)
}
}
} onCancel: {
self.cancel()
}
}
return DataTask<Value>(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel)
}
}
// MARK: - DownloadTask
/// Value used to `await` a `DownloadResponse` and associated values.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public struct DownloadTask<Value> {
/// `DownloadResponse` produced by the `DownloadRequest` and its response handler.
public var response: DownloadResponse<Value, AFError> {
get async {
if shouldAutomaticallyCancel {
return await withTaskCancellationHandler {
await task.value
} onCancel: {
cancel()
}
} else {
return await task.value
}
}
}
/// `Result` of any response serialization performed for the `response`.
public var result: Result<Value, AFError> {
get async { await response.result }
}
/// `Value` returned by the `response`.
public var value: Value {
get async throws {
try await result.get()
}
}
private let task: Task<AFDownloadResponse<Value>, Never>
private let request: DownloadRequest
private let shouldAutomaticallyCancel: Bool
fileprivate init(request: DownloadRequest, task: Task<AFDownloadResponse<Value>, Never>, shouldAutomaticallyCancel: Bool) {
self.request = request
self.task = task
self.shouldAutomaticallyCancel = shouldAutomaticallyCancel
}
/// Cancel the underlying `DownloadRequest` and `Task`.
public func cancel() {
task.cancel()
}
/// Resume the underlying `DownloadRequest`.
public func resume() {
request.resume()
}
/// Suspend the underlying `DownloadRequest`.
public func suspend() {
request.suspend()
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension DownloadRequest {
/// Creates a `DownloadTask` to `await` a `Data` value.
///
/// - Parameters:
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DownloadTask`'s async
/// properties. `true` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion.
/// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
///
/// - Returns: The `DownloadTask`.
public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask<Data> {
serializingDownload(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
automaticallyCancelling: shouldAutomaticallyCancel)
}
/// Creates a `DownloadTask` to `await` serialization of a `Decodable` value.
///
/// - Note: This serializer reads the entire response into memory before parsing.
///
/// - Parameters:
/// - type: `Decodable` type to decode from response data.
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DownloadTask`'s async
/// properties. `true` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer.
/// `PassthroughPreprocessor()` by default.
/// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
///
/// - Returns: The `DownloadTask`.
public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<Value>.defaultEmptyRequestMethods) -> DownloadTask<Value> {
serializingDownload(using: DecodableResponseSerializer<Value>(dataPreprocessor: dataPreprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
automaticallyCancelling: shouldAutomaticallyCancel)
}
/// Creates a `DownloadTask` to `await` serialization of the downloaded file's `URL` on disk.
///
/// - Parameters:
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DownloadTask`'s async
/// properties. `true` by default.
///
/// - Returns: The `DownloadTask`.
public func serializingDownloadedFileURL(automaticallyCancelling shouldAutomaticallyCancel: Bool = true) -> DownloadTask<URL> {
serializingDownload(using: URLResponseSerializer(),
automaticallyCancelling: shouldAutomaticallyCancel)
}
/// Creates a `DownloadTask` to `await` serialization of a `String` value.
///
/// - Parameters:
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DownloadTask`'s async
/// properties. `true` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// serializer. `PassthroughPreprocessor()` by default.
/// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case
/// the encoding will be determined from the server response, falling back to the
/// default HTTP character set, `ISO-8859-1`.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
///
/// - Returns: The `DownloadTask`.
public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil,
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask<String> {
serializingDownload(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
automaticallyCancelling: shouldAutomaticallyCancel)
}
/// Creates a `DownloadTask` to `await` serialization using the provided `ResponseSerializer` instance.
///
/// - Parameters:
/// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data.
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DownloadTask`'s async
/// properties. `true` by default.
///
/// - Returns: The `DownloadTask`.
public func serializingDownload<Serializer: ResponseSerializer>(using serializer: Serializer,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true)
-> DownloadTask<Serializer.SerializedObject> {
downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in
response(queue: underlyingQueue,
responseSerializer: serializer,
completionHandler: $0)
}
}
/// Creates a `DownloadTask` to `await` serialization using the provided `DownloadResponseSerializerProtocol`
/// instance.
///
/// - Parameters:
/// - serializer: `DownloadResponseSerializerProtocol` responsible for serializing the request,
/// response, and data.
/// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
/// enclosing async context is cancelled. Only applies to `DownloadTask`'s async
/// properties. `true` by default.
///
/// - Returns: The `DownloadTask`.
public func serializingDownload<Serializer: DownloadResponseSerializerProtocol>(using serializer: Serializer,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true)
-> DownloadTask<Serializer.SerializedObject> {
downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in
response(queue: underlyingQueue,
responseSerializer: serializer,
completionHandler: $0)
}
}
private func downloadTask<Value>(automaticallyCancelling shouldAutomaticallyCancel: Bool,
forResponse onResponse: @escaping (@escaping (DownloadResponse<Value, AFError>) -> Void) -> Void)
-> DownloadTask<Value> {
let task = Task {
await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
onResponse {
continuation.resume(returning: $0)
}
}
} onCancel: {
self.cancel()
}
}
return DownloadTask<Value>(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel)
}
}
// MARK: - DataStreamTask
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public struct DataStreamTask {
// Type of created streams.
public typealias Stream<Success, Failure: Error> = StreamOf<DataStreamRequest.Stream<Success, Failure>>
private let request: DataStreamRequest
fileprivate init(request: DataStreamRequest) {
self.request = request
}
/// Creates a `Stream` of `Data` values from the underlying `DataStreamRequest`.
///
/// - Parameters:
/// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled
/// which observation of the stream stops. `true` by default.
/// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `Stream`.
public func streamingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream<Data, Never>.BufferingPolicy = .unbounded) -> Stream<Data, Never> {
createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in
request.responseStream(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream)
}
}
/// Creates a `Stream` of `UTF-8` `String`s from the underlying `DataStreamRequest`.
///
/// - Parameters:
/// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled
/// which observation of the stream stops. `true` by default.
/// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
/// - Returns:
public func streamingStrings(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream<String, Never>.BufferingPolicy = .unbounded) -> Stream<String, Never> {
createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in
request.responseStreamString(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream)
}
}
/// Creates a `Stream` of `Decodable` values from the underlying `DataStreamRequest`.
///
/// - Parameters:
/// - type: `Decodable` type to be serialized from stream payloads.
/// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled
/// which observation of the stream stops. `true` by default.
/// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `Stream`.
public func streamingDecodables<T>(_ type: T.Type = T.self,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
bufferingPolicy: Stream<T, AFError>.BufferingPolicy = .unbounded)
-> Stream<T, AFError> where T: Decodable {
streamingResponses(serializedUsing: DecodableStreamSerializer<T>(),
automaticallyCancelling: shouldAutomaticallyCancel,
bufferingPolicy: bufferingPolicy)
}
/// Creates a `Stream` of values using the provided `DataStreamSerializer` from the underlying `DataStreamRequest`.
///
/// - Parameters:
/// - serializer: `DataStreamSerializer` to use to serialize incoming `Data`.
/// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled
/// which observation of the stream stops. `true` by default.
/// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `Stream`.
public func streamingResponses<Serializer: DataStreamSerializer>(serializedUsing serializer: Serializer,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
bufferingPolicy: Stream<Serializer.SerializedObject, AFError>.BufferingPolicy = .unbounded)
-> Stream<Serializer.SerializedObject, AFError> {
createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in
request.responseStream(using: serializer,
on: .streamCompletionQueue(forRequestID: request.id),
stream: onStream)
}
}
private func createStream<Success, Failure: Error>(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
bufferingPolicy: Stream<Success, Failure>.BufferingPolicy = .unbounded,
forResponse onResponse: @escaping (@escaping (DataStreamRequest.Stream<Success, Failure>) -> Void) -> Void)
-> Stream<Success, Failure> {
StreamOf(bufferingPolicy: bufferingPolicy) {
guard shouldAutomaticallyCancel,
request.isInitialized || request.isResumed || request.isSuspended else { return }
cancel()
} builder: { continuation in
onResponse { stream in
continuation.yield(stream)
if case .complete = stream.event {
continuation.finish()
}
}
}
}
/// Cancel the underlying `DataStreamRequest`.
public func cancel() {
request.cancel()
}
/// Resume the underlying `DataStreamRequest`.
public func resume() {
request.resume()
}
/// Suspend the underlying `DataStreamRequest`.
public func suspend() {
request.suspend()
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension DataStreamRequest {
/// Creates a `StreamOf<HTTPURLResponse>` for the instance's responses.
///
/// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default.
///
/// - Returns: The `StreamOf<HTTPURLResponse>`.
public func httpResponses(bufferingPolicy: StreamOf<HTTPURLResponse>.BufferingPolicy = .unbounded) -> StreamOf<HTTPURLResponse> {
stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
onHTTPResponse(on: underlyingQueue) { response in
continuation.yield(response)
}
}
}
/// Sets an async closure returning a `Request.ResponseDisposition`, called whenever the `DataStreamRequest`
/// produces an `HTTPURLResponse`.
///
/// - Note: Most requests will only produce a single response for each outgoing attempt (initial + retries).
/// However, some types of response may trigger multiple `HTTPURLResponse`s, such as multipart streams,
/// where responses after the first will contain the part headers.
///
/// - Parameters:
/// - handler: Async closure executed when a new `HTTPURLResponse` is received and returning a
/// `ResponseDisposition` value. This value determines whether to continue the request or cancel it as
/// if `cancel()` had been called on the instance. Note, this closure is called on an arbitrary thread,
/// so any synchronous calls in it will execute in that context.
///
/// - Returns: The instance.
@_disfavoredOverload
@discardableResult
public func onHTTPResponse(perform handler: @escaping @Sendable (HTTPURLResponse) async -> ResponseDisposition) -> Self {
onHTTPResponse(on: underlyingQueue) { response, completionHandler in
Task {
let disposition = await handler(response)
completionHandler(disposition)
}
}
return self
}
/// Sets an async closure called whenever the `DataStreamRequest` produces an `HTTPURLResponse`.
///
/// - Note: Most requests will only produce a single response for each outgoing attempt (initial + retries).
/// However, some types of response may trigger multiple `HTTPURLResponse`s, such as multipart streams,
/// where responses after the first will contain the part headers.
///
/// - Parameters:
/// - handler: Async closure executed when a new `HTTPURLResponse` is received. Note, this closure is called on an
/// arbitrary thread, so any synchronous calls in it will execute in that context.
///
/// - Returns: The instance.
@discardableResult
public func onHTTPResponse(perform handler: @escaping @Sendable (HTTPURLResponse) async -> Void) -> Self {
onHTTPResponse { response in
await handler(response)
return .allow
}
return self
}
/// Creates a `DataStreamTask` used to `await` streams of serialized values.
///
/// - Returns: The `DataStreamTask`.
public func streamTask() -> DataStreamTask {
DataStreamTask(request: self)
}
}
#if canImport(Darwin) && !canImport(FoundationNetworking) // Only Apple platforms support URLSessionWebSocketTask.
// - MARK: WebSocketTask
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@_spi(WebSocket) public struct WebSocketTask {
private let request: WebSocketRequest
fileprivate init(request: WebSocketRequest) {
self.request = request
}
public typealias EventStreamOf<Success, Failure: Error> = StreamOf<WebSocketRequest.Event<Success, Failure>>
public func streamingMessageEvents(
automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
bufferingPolicy: EventStreamOf<URLSessionWebSocketTask.Message, Never>.BufferingPolicy = .unbounded
) -> EventStreamOf<URLSessionWebSocketTask.Message, Never> {
createStream(automaticallyCancelling: shouldAutomaticallyCancel,
bufferingPolicy: bufferingPolicy,
transform: { $0 }) { onEvent in
request.streamMessageEvents(on: .streamCompletionQueue(forRequestID: request.id), handler: onEvent)
}
}
public func streamingMessages(
automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
bufferingPolicy: StreamOf<URLSessionWebSocketTask.Message>.BufferingPolicy = .unbounded
) -> StreamOf<URLSessionWebSocketTask.Message> {
createStream(automaticallyCancelling: shouldAutomaticallyCancel,
bufferingPolicy: bufferingPolicy,
transform: { $0.message }) { onEvent in
request.streamMessageEvents(on: .streamCompletionQueue(forRequestID: request.id), handler: onEvent)
}
}
public func streamingDecodableEvents<Value: Decodable>(
_ type: Value.Type = Value.self,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
using decoder: DataDecoder = JSONDecoder(),
bufferingPolicy: EventStreamOf<Value, Error>.BufferingPolicy = .unbounded
) -> EventStreamOf<Value, Error> {
createStream(automaticallyCancelling: shouldAutomaticallyCancel,
bufferingPolicy: bufferingPolicy,
transform: { $0 }) { onEvent in
request.streamDecodableEvents(Value.self,
on: .streamCompletionQueue(forRequestID: request.id),
using: decoder,
handler: onEvent)
}
}
public func streamingDecodable<Value: Decodable>(
_ type: Value.Type = Value.self,
automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
using decoder: DataDecoder = JSONDecoder(),
bufferingPolicy: StreamOf<Value>.BufferingPolicy = .unbounded
) -> StreamOf<Value> {
createStream(automaticallyCancelling: shouldAutomaticallyCancel,
bufferingPolicy: bufferingPolicy,
transform: { $0.message }) { onEvent in
request.streamDecodableEvents(Value.self,
on: .streamCompletionQueue(forRequestID: request.id),
using: decoder,
handler: onEvent)
}
}
private func createStream<Success, Value, Failure: Error>(
automaticallyCancelling shouldAutomaticallyCancel: Bool,
bufferingPolicy: StreamOf<Value>.BufferingPolicy,
transform: @escaping (WebSocketRequest.Event<Success, Failure>) -> Value?,
forResponse onResponse: @escaping (@escaping (WebSocketRequest.Event<Success, Failure>) -> Void) -> Void
) -> StreamOf<Value> {
StreamOf(bufferingPolicy: bufferingPolicy) {
guard shouldAutomaticallyCancel,
request.isInitialized || request.isResumed || request.isSuspended else { return }
cancel()
} builder: { continuation in
onResponse { event in
if let value = transform(event) {
continuation.yield(value)
}
if case .completed = event.kind {
continuation.finish()
}
}
}
}
/// Send a `URLSessionWebSocketTask.Message`.
///
/// - Parameter message: The `Message`.
///
public func send(_ message: URLSessionWebSocketTask.Message) async throws {
try await withCheckedThrowingContinuation { continuation in
request.send(message, queue: .streamCompletionQueue(forRequestID: request.id)) { result in
continuation.resume(with: result)
}
}
}
/// Close the underlying `WebSocketRequest`.
public func close(sending closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? = nil) {
request.close(sending: closeCode, reason: reason)
}
/// Cancel the underlying `WebSocketRequest`.
///
/// Cancellation will produce an `AFError.explicitlyCancelled` instance.
public func cancel() {
request.cancel()
}
/// Resume the underlying `WebSocketRequest`.
public func resume() {
request.resume()
}
/// Suspend the underlying `WebSocketRequest`.
public func suspend() {
request.suspend()
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension WebSocketRequest {
public func webSocketTask() -> WebSocketTask {
WebSocketTask(request: self)
}
}
#endif
extension DispatchQueue {
fileprivate static let singleEventQueue = DispatchQueue(label: "org.alamofire.concurrencySingleEventQueue",
attributes: .concurrent)
fileprivate static func streamCompletionQueue(forRequestID id: UUID) -> DispatchQueue {
DispatchQueue(label: "org.alamofire.concurrencyStreamCompletionQueue-\(id)", target: .singleEventQueue)
}
}
/// An asynchronous sequence generated from an underlying `AsyncStream`. Only produced by Alamofire.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public struct StreamOf<Element>: AsyncSequence {
public typealias AsyncIterator = Iterator
public typealias BufferingPolicy = AsyncStream<Element>.Continuation.BufferingPolicy
fileprivate typealias Continuation = AsyncStream<Element>.Continuation
private let bufferingPolicy: BufferingPolicy
private let onTermination: (() -> Void)?
private let builder: (Continuation) -> Void
fileprivate init(bufferingPolicy: BufferingPolicy = .unbounded,
onTermination: (() -> Void)? = nil,
builder: @escaping (Continuation) -> Void) {
self.bufferingPolicy = bufferingPolicy
self.onTermination = onTermination
self.builder = builder
}
public func makeAsyncIterator() -> Iterator {
var continuation: AsyncStream<Element>.Continuation?
let stream = AsyncStream<Element>(bufferingPolicy: bufferingPolicy) { innerContinuation in
continuation = innerContinuation
builder(innerContinuation)
}
return Iterator(iterator: stream.makeAsyncIterator()) {
continuation?.finish()
onTermination?()
}
}
public struct Iterator: AsyncIteratorProtocol {
private final class Token {
private let onDeinit: () -> Void
init(onDeinit: @escaping () -> Void) {
self.onDeinit = onDeinit
}
deinit {
onDeinit()
}
}
private var iterator: AsyncStream<Element>.AsyncIterator
private let token: Token
init(iterator: AsyncStream<Element>.AsyncIterator, onCancellation: @escaping () -> Void) {
self.iterator = iterator
token = Token(onDeinit: onCancellation)
}
public mutating func next() async -> Element? {
await iterator.next()
}
}
}
#endif
+907
View File
@@ -0,0 +1,907 @@
//
// EventMonitor.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Protocol outlining the lifetime events inside Alamofire. It includes both events received from the various
/// `URLSession` delegate protocols as well as various events from the lifetime of `Request` and its subclasses.
public protocol EventMonitor {
/// The `DispatchQueue` onto which Alamofire's root `CompositeEventMonitor` will dispatch events. `.main` by default.
var queue: DispatchQueue { get }
// MARK: - URLSession Events
// MARK: URLSessionDelegate Events
/// Event called during `URLSessionDelegate`'s `urlSession(_:didBecomeInvalidWithError:)` method.
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?)
// MARK: URLSessionTaskDelegate Events
/// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didReceive:completionHandler:)` method.
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge)
/// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` method.
func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64)
/// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:needNewBodyStream:)` method.
func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask)
/// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` method.
func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest)
/// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didFinishCollecting:)` method.
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics)
/// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didCompleteWithError:)` method.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
/// Event called during `URLSessionTaskDelegate`'s `urlSession(_:taskIsWaitingForConnectivity:)` method.
func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask)
// MARK: URLSessionDataDelegate Events
/// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:didReceive:completionHandler:)` method.
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse)
/// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:didReceive:)` method.
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
/// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:willCacheResponse:completionHandler:)` method.
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse)
// MARK: URLSessionDownloadDelegate Events
/// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` method.
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64)
/// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` method.
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
/// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didFinishDownloadingTo:)` method.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
// MARK: - Request Events
/// Event called when a `URLRequest` is first created for a `Request`. If a `RequestAdapter` is active, the
/// `URLRequest` will be adapted before being issued.
func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest)
/// Event called when the attempt to create a `URLRequest` from a `Request`'s original `URLRequestConvertible` value fails.
func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError)
/// Event called when a `RequestAdapter` adapts the `Request`'s initial `URLRequest`.
func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest)
/// Event called when a `RequestAdapter` fails to adapt the `Request`'s initial `URLRequest`.
func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError)
/// Event called when a final `URLRequest` is created for a `Request`.
func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest)
/// Event called when a `URLSessionTask` subclass instance is created for a `Request`.
func request(_ request: Request, didCreateTask task: URLSessionTask)
/// Event called when a `Request` receives a `URLSessionTaskMetrics` value.
func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics)
/// Event called when a `Request` fails due to an error created by Alamofire. e.g. When certificate pinning fails.
func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError)
/// Event called when a `Request`'s task completes, possibly with an error. A `Request` may receive this event
/// multiple times if it is retried.
func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?)
/// Event called when a `Request` is about to be retried.
func requestIsRetrying(_ request: Request)
/// Event called when a `Request` finishes and response serializers are being called.
func requestDidFinish(_ request: Request)
/// Event called when a `Request` receives a `resume` call.
func requestDidResume(_ request: Request)
/// Event called when a `Request`'s associated `URLSessionTask` is resumed.
func request(_ request: Request, didResumeTask task: URLSessionTask)
/// Event called when a `Request` receives a `suspend` call.
func requestDidSuspend(_ request: Request)
/// Event called when a `Request`'s associated `URLSessionTask` is suspended.
func request(_ request: Request, didSuspendTask task: URLSessionTask)
/// Event called when a `Request` receives a `cancel` call.
func requestDidCancel(_ request: Request)
/// Event called when a `Request`'s associated `URLSessionTask` is cancelled.
func request(_ request: Request, didCancelTask task: URLSessionTask)
// MARK: DataRequest Events
/// Event called when a `DataRequest` calls a `Validation`.
func request(_ request: DataRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
data: Data?,
withResult result: Request.ValidationResult)
/// Event called when a `DataRequest` creates a `DataResponse<Data?>` value without calling a `ResponseSerializer`.
func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>)
/// Event called when a `DataRequest` calls a `ResponseSerializer` and creates a generic `DataResponse<Value, AFError>`.
func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>)
// MARK: DataStreamRequest Events
/// Event called when a `DataStreamRequest` calls a `Validation` closure.
///
/// - Parameters:
/// - request: `DataStreamRequest` which is calling the `Validation`.
/// - urlRequest: `URLRequest` of the request being validated.
/// - response: `HTTPURLResponse` of the request being validated.
/// - result: Produced `ValidationResult`.
func request(_ request: DataStreamRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
withResult result: Request.ValidationResult)
/// Event called when a `DataStreamSerializer` produces a value from streamed `Data`.
///
/// - Parameters:
/// - request: `DataStreamRequest` for which the value was serialized.
/// - result: `Result` of the serialization attempt.
func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>)
// MARK: UploadRequest Events
/// Event called when an `UploadRequest` creates its `Uploadable` value, indicating the type of upload it represents.
func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable)
/// Event called when an `UploadRequest` failed to create its `Uploadable` value due to an error.
func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError)
/// Event called when an `UploadRequest` provides the `InputStream` from its `Uploadable` value. This only occurs if
/// the `InputStream` does not wrap a `Data` value or file `URL`.
func request(_ request: UploadRequest, didProvideInputStream stream: InputStream)
// MARK: DownloadRequest Events
/// Event called when a `DownloadRequest`'s `URLSessionDownloadTask` finishes and the temporary file has been moved.
func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result<URL, AFError>)
/// Event called when a `DownloadRequest`'s `Destination` closure is called and creates the destination URL the
/// downloaded file will be moved to.
func request(_ request: DownloadRequest, didCreateDestinationURL url: URL)
/// Event called when a `DownloadRequest` calls a `Validation`.
func request(_ request: DownloadRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
fileURL: URL?,
withResult result: Request.ValidationResult)
/// Event called when a `DownloadRequest` creates a `DownloadResponse<URL?, AFError>` without calling a `ResponseSerializer`.
func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>)
/// Event called when a `DownloadRequest` calls a `DownloadResponseSerializer` and creates a generic `DownloadResponse<Value, AFError>`
func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>)
}
extension EventMonitor {
/// The default queue on which `CompositeEventMonitor`s will call the `EventMonitor` methods. `.main` by default.
public var queue: DispatchQueue { .main }
// MARK: Default Implementations
public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {}
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge) {}
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64) {}
public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) {}
public func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest) {}
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didFinishCollecting metrics: URLSessionTaskMetrics) {}
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {}
public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) {}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {}
public func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse) {}
public func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64) {}
public func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {}
public func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {}
public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {}
public func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) {}
public func request(_ request: Request,
didAdaptInitialRequest initialRequest: URLRequest,
to adaptedRequest: URLRequest) {}
public func request(_ request: Request,
didFailToAdaptURLRequest initialRequest: URLRequest,
withError error: AFError) {}
public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {}
public func request(_ request: Request, didCreateTask task: URLSessionTask) {}
public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) {}
public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) {}
public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) {}
public func requestIsRetrying(_ request: Request) {}
public func requestDidFinish(_ request: Request) {}
public func requestDidResume(_ request: Request) {}
public func request(_ request: Request, didResumeTask task: URLSessionTask) {}
public func requestDidSuspend(_ request: Request) {}
public func request(_ request: Request, didSuspendTask task: URLSessionTask) {}
public func requestDidCancel(_ request: Request) {}
public func request(_ request: Request, didCancelTask task: URLSessionTask) {}
public func request(_ request: DataRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
data: Data?,
withResult result: Request.ValidationResult) {}
public func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {}
public func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {}
public func request(_ request: DataStreamRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
withResult result: Request.ValidationResult) {}
public func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) {}
public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) {}
public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) {}
public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) {}
public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result<URL, AFError>) {}
public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) {}
public func request(_ request: DownloadRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
fileURL: URL?,
withResult result: Request.ValidationResult) {}
public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) {}
public func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) {}
}
/// An `EventMonitor` which can contain multiple `EventMonitor`s and calls their methods on their queues.
public final class CompositeEventMonitor: EventMonitor {
public let queue = DispatchQueue(label: "org.alamofire.compositeEventMonitor")
let monitors: [EventMonitor]
init(monitors: [EventMonitor]) {
self.monitors = monitors
}
func performEvent(_ event: @escaping (EventMonitor) -> Void) {
queue.async {
for monitor in self.monitors {
monitor.queue.async { event(monitor) }
}
}
}
public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
performEvent { $0.urlSession(session, didBecomeInvalidWithError: error) }
}
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge) {
performEvent { $0.urlSession(session, task: task, didReceive: challenge) }
}
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64) {
performEvent {
$0.urlSession(session,
task: task,
didSendBodyData: bytesSent,
totalBytesSent: totalBytesSent,
totalBytesExpectedToSend: totalBytesExpectedToSend)
}
}
public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) {
performEvent {
$0.urlSession(session, taskNeedsNewBodyStream: task)
}
}
public func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest) {
performEvent {
$0.urlSession(session,
task: task,
willPerformHTTPRedirection: response,
newRequest: request)
}
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
performEvent { $0.urlSession(session, task: task, didFinishCollecting: metrics) }
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
performEvent { $0.urlSession(session, task: task, didCompleteWithError: error) }
}
@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
performEvent { $0.urlSession(session, taskIsWaitingForConnectivity: task) }
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) {
performEvent { $0.urlSession(session, dataTask: dataTask, didReceive: response) }
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
performEvent { $0.urlSession(session, dataTask: dataTask, didReceive: data) }
}
public func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse) {
performEvent { $0.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) }
}
public func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64) {
performEvent {
$0.urlSession(session,
downloadTask: downloadTask,
didResumeAtOffset: fileOffset,
expectedTotalBytes: expectedTotalBytes)
}
}
public func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
performEvent {
$0.urlSession(session,
downloadTask: downloadTask,
didWriteData: bytesWritten,
totalBytesWritten: totalBytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
}
public func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
performEvent { $0.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) }
}
public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {
performEvent { $0.request(request, didCreateInitialURLRequest: urlRequest) }
}
public func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) {
performEvent { $0.request(request, didFailToCreateURLRequestWithError: error) }
}
public func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) {
performEvent { $0.request(request, didAdaptInitialRequest: initialRequest, to: adaptedRequest) }
}
public func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) {
performEvent { $0.request(request, didFailToAdaptURLRequest: initialRequest, withError: error) }
}
public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {
performEvent { $0.request(request, didCreateURLRequest: urlRequest) }
}
public func request(_ request: Request, didCreateTask task: URLSessionTask) {
performEvent { $0.request(request, didCreateTask: task) }
}
public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) {
performEvent { $0.request(request, didGatherMetrics: metrics) }
}
public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) {
performEvent { $0.request(request, didFailTask: task, earlyWithError: error) }
}
public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) {
performEvent { $0.request(request, didCompleteTask: task, with: error) }
}
public func requestIsRetrying(_ request: Request) {
performEvent { $0.requestIsRetrying(request) }
}
public func requestDidFinish(_ request: Request) {
performEvent { $0.requestDidFinish(request) }
}
public func requestDidResume(_ request: Request) {
performEvent { $0.requestDidResume(request) }
}
public func request(_ request: Request, didResumeTask task: URLSessionTask) {
performEvent { $0.request(request, didResumeTask: task) }
}
public func requestDidSuspend(_ request: Request) {
performEvent { $0.requestDidSuspend(request) }
}
public func request(_ request: Request, didSuspendTask task: URLSessionTask) {
performEvent { $0.request(request, didSuspendTask: task) }
}
public func requestDidCancel(_ request: Request) {
performEvent { $0.requestDidCancel(request) }
}
public func request(_ request: Request, didCancelTask task: URLSessionTask) {
performEvent { $0.request(request, didCancelTask: task) }
}
public func request(_ request: DataRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
data: Data?,
withResult result: Request.ValidationResult) {
performEvent { $0.request(request,
didValidateRequest: urlRequest,
response: response,
data: data,
withResult: result)
}
}
public func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {
performEvent { $0.request(request, didParseResponse: response) }
}
public func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
performEvent { $0.request(request, didParseResponse: response) }
}
public func request(_ request: DataStreamRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
withResult result: Request.ValidationResult) {
performEvent { $0.request(request,
didValidateRequest: urlRequest,
response: response,
withResult: result)
}
}
public func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) {
performEvent { $0.request(request, didParseStream: result) }
}
public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) {
performEvent { $0.request(request, didCreateUploadable: uploadable) }
}
public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) {
performEvent { $0.request(request, didFailToCreateUploadableWithError: error) }
}
public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) {
performEvent { $0.request(request, didProvideInputStream: stream) }
}
public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result<URL, AFError>) {
performEvent { $0.request(request, didFinishDownloadingUsing: task, with: result) }
}
public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) {
performEvent { $0.request(request, didCreateDestinationURL: url) }
}
public func request(_ request: DownloadRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
fileURL: URL?,
withResult result: Request.ValidationResult) {
performEvent { $0.request(request,
didValidateRequest: urlRequest,
response: response,
fileURL: fileURL,
withResult: result) }
}
public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) {
performEvent { $0.request(request, didParseResponse: response) }
}
public func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) {
performEvent { $0.request(request, didParseResponse: response) }
}
}
/// `EventMonitor` that allows optional closures to be set to receive events.
open class ClosureEventMonitor: EventMonitor {
/// Closure called on the `urlSession(_:didBecomeInvalidWithError:)` event.
open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)?
/// Closure called on the `urlSession(_:task:didReceive:completionHandler:)`.
open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> Void)?
/// Closure that receives `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` event.
open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
/// Closure called on the `urlSession(_:task:needNewBodyStream:)` event.
open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> Void)?
/// Closure called on the `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` event.
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> Void)?
/// Closure called on the `urlSession(_:task:didFinishCollecting:)` event.
open var taskDidFinishCollectingMetrics: ((URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void)?
/// Closure called on the `urlSession(_:task:didCompleteWithError:)` event.
open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)?
/// Closure called on the `urlSession(_:taskIsWaitingForConnectivity:)` event.
open var taskIsWaitingForConnectivity: ((URLSession, URLSessionTask) -> Void)?
/// Closure called on the `urlSession(_:dataTask:didReceive:completionHandler:)` event.
open var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> Void)?
/// Closure that receives the `urlSession(_:dataTask:didReceive:)` event.
open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
/// Closure called on the `urlSession(_:dataTask:willCacheResponse:completionHandler:)` event.
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> Void)?
/// Closure called on the `urlSession(_:downloadTask:didFinishDownloadingTo:)` event.
open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)?
/// Closure called on the `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)`
/// event.
open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
/// Closure called on the `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` event.
open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
// MARK: - Request Events
/// Closure called on the `request(_:didCreateInitialURLRequest:)` event.
open var requestDidCreateInitialURLRequest: ((Request, URLRequest) -> Void)?
/// Closure called on the `request(_:didFailToCreateURLRequestWithError:)` event.
open var requestDidFailToCreateURLRequestWithError: ((Request, AFError) -> Void)?
/// Closure called on the `request(_:didAdaptInitialRequest:to:)` event.
open var requestDidAdaptInitialRequestToAdaptedRequest: ((Request, URLRequest, URLRequest) -> Void)?
/// Closure called on the `request(_:didFailToAdaptURLRequest:withError:)` event.
open var requestDidFailToAdaptURLRequestWithError: ((Request, URLRequest, AFError) -> Void)?
/// Closure called on the `request(_:didCreateURLRequest:)` event.
open var requestDidCreateURLRequest: ((Request, URLRequest) -> Void)?
/// Closure called on the `request(_:didCreateTask:)` event.
open var requestDidCreateTask: ((Request, URLSessionTask) -> Void)?
/// Closure called on the `request(_:didGatherMetrics:)` event.
open var requestDidGatherMetrics: ((Request, URLSessionTaskMetrics) -> Void)?
/// Closure called on the `request(_:didFailTask:earlyWithError:)` event.
open var requestDidFailTaskEarlyWithError: ((Request, URLSessionTask, AFError) -> Void)?
/// Closure called on the `request(_:didCompleteTask:with:)` event.
open var requestDidCompleteTaskWithError: ((Request, URLSessionTask, AFError?) -> Void)?
/// Closure called on the `requestIsRetrying(_:)` event.
open var requestIsRetrying: ((Request) -> Void)?
/// Closure called on the `requestDidFinish(_:)` event.
open var requestDidFinish: ((Request) -> Void)?
/// Closure called on the `requestDidResume(_:)` event.
open var requestDidResume: ((Request) -> Void)?
/// Closure called on the `request(_:didResumeTask:)` event.
open var requestDidResumeTask: ((Request, URLSessionTask) -> Void)?
/// Closure called on the `requestDidSuspend(_:)` event.
open var requestDidSuspend: ((Request) -> Void)?
/// Closure called on the `request(_:didSuspendTask:)` event.
open var requestDidSuspendTask: ((Request, URLSessionTask) -> Void)?
/// Closure called on the `requestDidCancel(_:)` event.
open var requestDidCancel: ((Request) -> Void)?
/// Closure called on the `request(_:didCancelTask:)` event.
open var requestDidCancelTask: ((Request, URLSessionTask) -> Void)?
/// Closure called on the `request(_:didValidateRequest:response:data:withResult:)` event.
open var requestDidValidateRequestResponseDataWithResult: ((DataRequest, URLRequest?, HTTPURLResponse, Data?, Request.ValidationResult) -> Void)?
/// Closure called on the `request(_:didParseResponse:)` event.
open var requestDidParseResponse: ((DataRequest, DataResponse<Data?, AFError>) -> Void)?
/// Closure called on the `request(_:didValidateRequest:response:withResult:)` event.
open var requestDidValidateRequestResponseWithResult: ((DataStreamRequest, URLRequest?, HTTPURLResponse, Request.ValidationResult) -> Void)?
/// Closure called on the `request(_:didCreateUploadable:)` event.
open var requestDidCreateUploadable: ((UploadRequest, UploadRequest.Uploadable) -> Void)?
/// Closure called on the `request(_:didFailToCreateUploadableWithError:)` event.
open var requestDidFailToCreateUploadableWithError: ((UploadRequest, AFError) -> Void)?
/// Closure called on the `request(_:didProvideInputStream:)` event.
open var requestDidProvideInputStream: ((UploadRequest, InputStream) -> Void)?
/// Closure called on the `request(_:didFinishDownloadingUsing:with:)` event.
open var requestDidFinishDownloadingUsingTaskWithResult: ((DownloadRequest, URLSessionTask, Result<URL, AFError>) -> Void)?
/// Closure called on the `request(_:didCreateDestinationURL:)` event.
open var requestDidCreateDestinationURL: ((DownloadRequest, URL) -> Void)?
/// Closure called on the `request(_:didValidateRequest:response:temporaryURL:destinationURL:withResult:)` event.
open var requestDidValidateRequestResponseFileURLWithResult: ((DownloadRequest, URLRequest?, HTTPURLResponse, URL?, Request.ValidationResult) -> Void)?
/// Closure called on the `request(_:didParseResponse:)` event.
open var requestDidParseDownloadResponse: ((DownloadRequest, DownloadResponse<URL?, AFError>) -> Void)?
public let queue: DispatchQueue
/// Creates an instance using the provided queue.
///
/// - Parameter queue: `DispatchQueue` on which events will fired. `.main` by default.
public init(queue: DispatchQueue = .main) {
self.queue = queue
}
open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
sessionDidBecomeInvalidWithError?(session, error)
}
open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) {
taskDidReceiveChallenge?(session, task, challenge)
}
open func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64) {
taskDidSendBodyData?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
}
open func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) {
taskNeedNewBodyStream?(session, task)
}
open func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest) {
taskWillPerformHTTPRedirection?(session, task, response, request)
}
open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
taskDidFinishCollectingMetrics?(session, task, metrics)
}
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
taskDidComplete?(session, task, error)
}
@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
taskIsWaitingForConnectivity?(session, task)
}
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) {
dataTaskDidReceiveResponse?(session, dataTask, response)
}
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
dataTaskDidReceiveData?(session, dataTask, data)
}
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) {
dataTaskWillCacheResponse?(session, dataTask, proposedResponse)
}
open func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64) {
downloadTaskDidResumeAtOffset?(session, downloadTask, fileOffset, expectedTotalBytes)
}
open func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
downloadTaskDidWriteData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
}
open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
downloadTaskDidFinishDownloadingToURL?(session, downloadTask, location)
}
// MARK: Request Events
open func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {
requestDidCreateInitialURLRequest?(request, urlRequest)
}
open func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) {
requestDidFailToCreateURLRequestWithError?(request, error)
}
open func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) {
requestDidAdaptInitialRequestToAdaptedRequest?(request, initialRequest, adaptedRequest)
}
open func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) {
requestDidFailToAdaptURLRequestWithError?(request, initialRequest, error)
}
open func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {
requestDidCreateURLRequest?(request, urlRequest)
}
open func request(_ request: Request, didCreateTask task: URLSessionTask) {
requestDidCreateTask?(request, task)
}
open func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) {
requestDidGatherMetrics?(request, metrics)
}
open func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) {
requestDidFailTaskEarlyWithError?(request, task, error)
}
open func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) {
requestDidCompleteTaskWithError?(request, task, error)
}
open func requestIsRetrying(_ request: Request) {
requestIsRetrying?(request)
}
open func requestDidFinish(_ request: Request) {
requestDidFinish?(request)
}
open func requestDidResume(_ request: Request) {
requestDidResume?(request)
}
public func request(_ request: Request, didResumeTask task: URLSessionTask) {
requestDidResumeTask?(request, task)
}
open func requestDidSuspend(_ request: Request) {
requestDidSuspend?(request)
}
public func request(_ request: Request, didSuspendTask task: URLSessionTask) {
requestDidSuspendTask?(request, task)
}
open func requestDidCancel(_ request: Request) {
requestDidCancel?(request)
}
public func request(_ request: Request, didCancelTask task: URLSessionTask) {
requestDidCancelTask?(request, task)
}
open func request(_ request: DataRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
data: Data?,
withResult result: Request.ValidationResult) {
requestDidValidateRequestResponseDataWithResult?(request, urlRequest, response, data, result)
}
open func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {
requestDidParseResponse?(request, response)
}
public func request(_ request: DataStreamRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, withResult result: Request.ValidationResult) {
requestDidValidateRequestResponseWithResult?(request, urlRequest, response, result)
}
open func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) {
requestDidCreateUploadable?(request, uploadable)
}
open func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) {
requestDidFailToCreateUploadableWithError?(request, error)
}
open func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) {
requestDidProvideInputStream?(request, stream)
}
open func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result<URL, AFError>) {
requestDidFinishDownloadingUsingTaskWithResult?(request, task, result)
}
open func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) {
requestDidCreateDestinationURL?(request, url)
}
open func request(_ request: DownloadRequest,
didValidateRequest urlRequest: URLRequest?,
response: HTTPURLResponse,
fileURL: URL?,
withResult result: Request.ValidationResult) {
requestDidValidateRequestResponseFileURLWithResult?(request,
urlRequest,
response,
fileURL,
result)
}
open func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) {
requestDidParseDownloadResponse?(request, response)
}
}
+601
View File
@@ -0,0 +1,601 @@
//
// MultipartFormData.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
#if canImport(MobileCoreServices)
import MobileCoreServices
#elseif canImport(CoreServices)
import CoreServices
#endif
/// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode
/// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead
/// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the
/// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for
/// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset.
///
/// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well
/// and the w3 form documentation.
///
/// - https://www.ietf.org/rfc/rfc2388.txt
/// - https://www.ietf.org/rfc/rfc2045.txt
/// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13
open class MultipartFormData {
// MARK: - Helper Types
enum EncodingCharacters {
static let crlf = "\r\n"
}
enum BoundaryGenerator {
enum BoundaryType {
case initial, encapsulated, final
}
static func randomBoundary() -> String {
let first = UInt32.random(in: UInt32.min...UInt32.max)
let second = UInt32.random(in: UInt32.min...UInt32.max)
return String(format: "alamofire.boundary.%08x%08x", first, second)
}
static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
let boundaryText: String
switch boundaryType {
case .initial:
boundaryText = "--\(boundary)\(EncodingCharacters.crlf)"
case .encapsulated:
boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
case .final:
boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
}
return Data(boundaryText.utf8)
}
}
class BodyPart {
let headers: HTTPHeaders
let bodyStream: InputStream
let bodyContentLength: UInt64
var hasInitialBoundary = false
var hasFinalBoundary = false
init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) {
self.headers = headers
self.bodyStream = bodyStream
self.bodyContentLength = bodyContentLength
}
}
// MARK: - Properties
/// Default memory threshold used when encoding `MultipartFormData`, in bytes.
public static let encodingMemoryThreshold: UInt64 = 10_000_000
/// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
/// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries.
public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
/// The boundary used to separate the body parts in the encoded form data.
public let boundary: String
let fileManager: FileManager
private var bodyParts: [BodyPart]
private var bodyPartError: AFError?
private let streamBufferSize: Int
// MARK: - Lifecycle
/// Creates an instance.
///
/// - Parameters:
/// - fileManager: `FileManager` to use for file operations, if needed.
/// - boundary: Boundary `String` used to separate body parts.
public init(fileManager: FileManager = .default, boundary: String? = nil) {
self.fileManager = fileManager
self.boundary = boundary ?? BoundaryGenerator.randomBoundary()
bodyParts = []
//
// The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more
// information, please refer to the following article:
// - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html
//
streamBufferSize = 1024
}
// MARK: - Body Parts
/// Creates a body part from the data and appends it to the instance.
///
/// The body part data will be encoded using the following format:
///
/// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
/// - `Content-Type: #{mimeType}` (HTTP Header)
/// - Encoded file data
/// - Multipart form boundary
///
/// - Parameters:
/// - data: `Data` to encoding into the instance.
/// - name: Name to associate with the `Data` in the `Content-Disposition` HTTP header.
/// - fileName: Filename to associate with the `Data` in the `Content-Disposition` HTTP header.
/// - mimeType: MIME type to associate with the data in the `Content-Type` HTTP header.
public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil) {
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
let stream = InputStream(data: data)
let length = UInt64(data.count)
append(stream, withLength: length, headers: headers)
}
/// Creates a body part from the file and appends it to the instance.
///
/// The body part data will be encoded using the following format:
///
/// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header)
/// - `Content-Type: #{generated mimeType}` (HTTP Header)
/// - Encoded file data
/// - Multipart form boundary
///
/// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the
/// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the
/// system associated MIME type.
///
/// - Parameters:
/// - fileURL: `URL` of the file whose content will be encoded into the instance.
/// - name: Name to associate with the file content in the `Content-Disposition` HTTP header.
public func append(_ fileURL: URL, withName name: String) {
let fileName = fileURL.lastPathComponent
let pathExtension = fileURL.pathExtension
if !fileName.isEmpty && !pathExtension.isEmpty {
let mime = mimeType(forPathExtension: pathExtension)
append(fileURL, withName: name, fileName: fileName, mimeType: mime)
} else {
setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL))
}
}
/// Creates a body part from the file and appends it to the instance.
///
/// The body part data will be encoded using the following format:
///
/// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header)
/// - Content-Type: #{mimeType} (HTTP Header)
/// - Encoded file data
/// - Multipart form boundary
///
/// - Parameters:
/// - fileURL: `URL` of the file whose content will be encoded into the instance.
/// - name: Name to associate with the file content in the `Content-Disposition` HTTP header.
/// - fileName: Filename to associate with the file content in the `Content-Disposition` HTTP header.
/// - mimeType: MIME type to associate with the file content in the `Content-Type` HTTP header.
public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) {
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
//============================================================
// Check 1 - is file URL?
//============================================================
guard fileURL.isFileURL else {
setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL))
return
}
//============================================================
// Check 2 - is file URL reachable?
//============================================================
#if !(os(Linux) || os(Windows) || os(Android))
do {
let isReachable = try fileURL.checkPromisedItemIsReachable()
guard isReachable else {
setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL))
return
}
} catch {
setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
return
}
#endif
//============================================================
// Check 3 - is file URL a directory?
//============================================================
var isDirectory: ObjCBool = false
let path = fileURL.path
guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else {
setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL))
return
}
//============================================================
// Check 4 - can the file size be extracted?
//============================================================
let bodyContentLength: UInt64
do {
guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else {
setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL))
return
}
bodyContentLength = fileSize.uint64Value
} catch {
setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error))
return
}
//============================================================
// Check 5 - can a stream be created from file URL?
//============================================================
guard let stream = InputStream(url: fileURL) else {
setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL))
return
}
append(stream, withLength: bodyContentLength, headers: headers)
}
/// Creates a body part from the stream and appends it to the instance.
///
/// The body part data will be encoded using the following format:
///
/// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
/// - `Content-Type: #{mimeType}` (HTTP Header)
/// - Encoded stream data
/// - Multipart form boundary
///
/// - Parameters:
/// - stream: `InputStream` to encode into the instance.
/// - length: Length, in bytes, of the stream.
/// - name: Name to associate with the stream content in the `Content-Disposition` HTTP header.
/// - fileName: Filename to associate with the stream content in the `Content-Disposition` HTTP header.
/// - mimeType: MIME type to associate with the stream content in the `Content-Type` HTTP header.
public func append(_ stream: InputStream,
withLength length: UInt64,
name: String,
fileName: String,
mimeType: String) {
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
append(stream, withLength: length, headers: headers)
}
/// Creates a body part with the stream, length, and headers and appends it to the instance.
///
/// The body part data will be encoded using the following format:
///
/// - HTTP headers
/// - Encoded stream data
/// - Multipart form boundary
///
/// - Parameters:
/// - stream: `InputStream` to encode into the instance.
/// - length: Length, in bytes, of the stream.
/// - headers: `HTTPHeaders` for the body part.
public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) {
let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
bodyParts.append(bodyPart)
}
// MARK: - Data Encoding
/// Encodes all appended body parts into a single `Data` value.
///
/// - Note: This method will load all the appended body parts into memory all at the same time. This method should
/// only be used when the encoded data will have a small memory footprint. For large data cases, please use
/// the `writeEncodedData(to:))` method.
///
/// - Returns: The encoded `Data`, if encoding is successful.
/// - Throws: An `AFError` if encoding encounters an error.
public func encode() throws -> Data {
if let bodyPartError {
throw bodyPartError
}
var encoded = Data()
bodyParts.first?.hasInitialBoundary = true
bodyParts.last?.hasFinalBoundary = true
for bodyPart in bodyParts {
let encodedData = try encode(bodyPart)
encoded.append(encodedData)
}
return encoded
}
/// Writes all appended body parts to the given file `URL`.
///
/// This process is facilitated by reading and writing with input and output streams, respectively. Thus,
/// this approach is very memory efficient and should be used for large body part data.
///
/// - Parameter fileURL: File `URL` to which to write the form data.
/// - Throws: An `AFError` if encoding encounters an error.
public func writeEncodedData(to fileURL: URL) throws {
if let bodyPartError {
throw bodyPartError
}
if fileManager.fileExists(atPath: fileURL.path) {
throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
} else if !fileURL.isFileURL {
throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
}
guard let outputStream = OutputStream(url: fileURL, append: false) else {
throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
}
outputStream.open()
defer { outputStream.close() }
bodyParts.first?.hasInitialBoundary = true
bodyParts.last?.hasFinalBoundary = true
for bodyPart in bodyParts {
try write(bodyPart, to: outputStream)
}
}
// MARK: - Private - Body Part Encoding
private func encode(_ bodyPart: BodyPart) throws -> Data {
var encoded = Data()
let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
encoded.append(initialData)
let headerData = encodeHeaders(for: bodyPart)
encoded.append(headerData)
let bodyStreamData = try encodeBodyStream(for: bodyPart)
encoded.append(bodyStreamData)
if bodyPart.hasFinalBoundary {
encoded.append(finalBoundaryData())
}
return encoded
}
private func encodeHeaders(for bodyPart: BodyPart) -> Data {
let headerText = bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" }
.joined()
+ EncodingCharacters.crlf
return Data(headerText.utf8)
}
private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data {
let inputStream = bodyPart.bodyStream
inputStream.open()
defer { inputStream.close() }
var encoded = Data()
while inputStream.hasBytesAvailable {
var buffer = [UInt8](repeating: 0, count: streamBufferSize)
let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
if let error = inputStream.streamError {
throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error))
}
if bytesRead > 0 {
encoded.append(buffer, count: bytesRead)
} else {
break
}
}
guard UInt64(encoded.count) == bodyPart.bodyContentLength else {
let error = AFError.UnexpectedInputStreamLength(bytesExpected: bodyPart.bodyContentLength,
bytesRead: UInt64(encoded.count))
throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error))
}
return encoded
}
// MARK: - Private - Writing Body Part to Output Stream
private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws {
try writeInitialBoundaryData(for: bodyPart, to: outputStream)
try writeHeaderData(for: bodyPart, to: outputStream)
try writeBodyStream(for: bodyPart, to: outputStream)
try writeFinalBoundaryData(for: bodyPart, to: outputStream)
}
private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
return try write(initialData, to: outputStream)
}
private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
let headerData = encodeHeaders(for: bodyPart)
return try write(headerData, to: outputStream)
}
private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
let inputStream = bodyPart.bodyStream
inputStream.open()
defer { inputStream.close() }
var bytesLeftToRead = bodyPart.bodyContentLength
while inputStream.hasBytesAvailable && bytesLeftToRead > 0 {
let bufferSize = min(streamBufferSize, Int(bytesLeftToRead))
var buffer = [UInt8](repeating: 0, count: bufferSize)
let bytesRead = inputStream.read(&buffer, maxLength: bufferSize)
if let streamError = inputStream.streamError {
throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError))
}
if bytesRead > 0 {
if buffer.count != bytesRead {
buffer = Array(buffer[0..<bytesRead])
}
try write(&buffer, to: outputStream)
bytesLeftToRead -= UInt64(bytesRead)
} else {
break
}
}
}
private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
if bodyPart.hasFinalBoundary {
return try write(finalBoundaryData(), to: outputStream)
}
}
// MARK: - Private - Writing Buffered Data to Output Stream
private func write(_ data: Data, to outputStream: OutputStream) throws {
var buffer = [UInt8](repeating: 0, count: data.count)
data.copyBytes(to: &buffer, count: data.count)
return try write(&buffer, to: outputStream)
}
private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws {
var bytesToWrite = buffer.count
while bytesToWrite > 0, outputStream.hasSpaceAvailable {
let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)
if let error = outputStream.streamError {
throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error))
}
bytesToWrite -= bytesWritten
if bytesToWrite > 0 {
buffer = Array(buffer[bytesWritten..<buffer.count])
}
}
}
// MARK: - Private - Content Headers
private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> HTTPHeaders {
var disposition = "form-data; name=\"\(name)\""
if let fileName { disposition += "; filename=\"\(fileName)\"" }
var headers: HTTPHeaders = [.contentDisposition(disposition)]
if let mimeType { headers.add(.contentType(mimeType)) }
return headers
}
// MARK: - Private - Boundary Encoding
private func initialBoundaryData() -> Data {
BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary)
}
private func encapsulatedBoundaryData() -> Data {
BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary)
}
private func finalBoundaryData() -> Data {
BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary)
}
// MARK: - Private - Errors
private func setBodyPartError(withReason reason: AFError.MultipartEncodingFailureReason) {
guard bodyPartError == nil else { return }
bodyPartError = AFError.multipartEncodingFailed(reason: reason)
}
}
#if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers
extension MultipartFormData {
// MARK: - Private - Mime Type
private func mimeType(forPathExtension pathExtension: String) -> String {
#if swift(>=5.9)
if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) {
return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
} else {
if
let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() {
return contentType as String
}
return "application/octet-stream"
}
#else
if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) {
return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
} else {
if
let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() {
return contentType as String
}
return "application/octet-stream"
}
#endif
}
}
#else
extension MultipartFormData {
// MARK: - Private - Mime Type
private func mimeType(forPathExtension pathExtension: String) -> String {
#if canImport(CoreServices) || canImport(MobileCoreServices)
if
let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() {
return contentType as String
}
#endif
return "application/octet-stream"
}
}
#endif
+89
View File
@@ -0,0 +1,89 @@
//
// MultipartUpload.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Internal type which encapsulates a `MultipartFormData` upload.
final class MultipartUpload {
lazy var result = Result { try build() }
private let multipartFormData: Protected<MultipartFormData>
let encodingMemoryThreshold: UInt64
let request: URLRequestConvertible
let fileManager: FileManager
init(encodingMemoryThreshold: UInt64,
request: URLRequestConvertible,
multipartFormData: MultipartFormData) {
self.encodingMemoryThreshold = encodingMemoryThreshold
self.request = request
fileManager = multipartFormData.fileManager
self.multipartFormData = Protected(multipartFormData)
}
func build() throws -> UploadRequest.Uploadable {
let uploadable: UploadRequest.Uploadable
if multipartFormData.contentLength < encodingMemoryThreshold {
let data = try multipartFormData.read { try $0.encode() }
uploadable = .data(data)
} else {
let tempDirectoryURL = fileManager.temporaryDirectory
let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data")
let fileName = UUID().uuidString
let fileURL = directoryURL.appendingPathComponent(fileName)
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
do {
try multipartFormData.read { try $0.writeEncodedData(to: fileURL) }
} catch {
// Cleanup after attempted write if it fails.
try? fileManager.removeItem(at: fileURL)
throw error
}
uploadable = .file(fileURL, shouldRemove: true)
}
return uploadable
}
}
extension MultipartUpload: UploadConvertible {
func asURLRequest() throws -> URLRequest {
var urlRequest = try request.asURLRequest()
multipartFormData.read { multipartFormData in
urlRequest.headers.add(.contentType(multipartFormData.contentType))
}
return urlRequest
}
func createUploadable() throws -> UploadRequest.Uploadable {
try result.get()
}
}
@@ -0,0 +1,292 @@
//
// NetworkReachabilityManager.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#if canImport(SystemConfiguration)
import Foundation
import SystemConfiguration
/// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both cellular and
/// WiFi network interfaces.
///
/// Reachability can be used to determine background information about why a network operation failed, or to retry
/// network requests when a connection is established. It should not be used to prevent a user from initiating a network
/// request, as it's possible that an initial request may be required to establish reachability.
open class NetworkReachabilityManager {
/// Defines the various states of network reachability.
public enum NetworkReachabilityStatus {
/// It is unknown whether the network is reachable.
case unknown
/// The network is not reachable.
case notReachable
/// The network is reachable on the associated `ConnectionType`.
case reachable(ConnectionType)
init(_ flags: SCNetworkReachabilityFlags) {
guard flags.isActuallyReachable else { self = .notReachable; return }
var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)
if flags.isCellular { networkStatus = .reachable(.cellular) }
self = networkStatus
}
/// Defines the various connection types detected by reachability flags.
public enum ConnectionType {
/// The connection type is either over Ethernet or WiFi.
case ethernetOrWiFi
/// The connection type is a cellular connection.
case cellular
}
}
/// A closure executed when the network reachability status changes. The closure takes a single argument: the
/// network reachability status.
public typealias Listener = (NetworkReachabilityStatus) -> Void
/// Default `NetworkReachabilityManager` for the zero address and a `listenerQueue` of `.main`.
public static let `default` = NetworkReachabilityManager()
// MARK: - Properties
/// Whether the network is currently reachable.
open var isReachable: Bool { isReachableOnCellular || isReachableOnEthernetOrWiFi }
/// Whether the network is currently reachable over the cellular interface.
///
/// - Note: Using this property to decide whether to make a high or low bandwidth request is not recommended.
/// Instead, set the `allowsCellularAccess` on any `URLRequest`s being issued.
///
open var isReachableOnCellular: Bool { status == .reachable(.cellular) }
/// Whether the network is currently reachable over Ethernet or WiFi interface.
open var isReachableOnEthernetOrWiFi: Bool { status == .reachable(.ethernetOrWiFi) }
/// `DispatchQueue` on which reachability will update.
public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue")
/// Flags of the current reachability type, if any.
open var flags: SCNetworkReachabilityFlags? {
var flags = SCNetworkReachabilityFlags()
return SCNetworkReachabilityGetFlags(reachability, &flags) ? flags : nil
}
/// The current network reachability status.
open var status: NetworkReachabilityStatus {
flags.map(NetworkReachabilityStatus.init) ?? .unknown
}
/// Mutable state storage.
struct MutableState {
/// A closure executed when the network reachability status changes.
var listener: Listener?
/// `DispatchQueue` on which listeners will be called.
var listenerQueue: DispatchQueue?
/// Previously calculated status.
var previousStatus: NetworkReachabilityStatus?
}
/// `SCNetworkReachability` instance providing notifications.
private let reachability: SCNetworkReachability
/// Protected storage for mutable state.
private let mutableState = Protected(MutableState())
// MARK: - Initialization
/// Creates an instance with the specified host.
///
/// - Note: The `host` value must *not* contain a scheme, just the hostname.
///
/// - Parameters:
/// - host: Host used to evaluate network reachability. Must *not* include the scheme (e.g. `https`).
public convenience init?(host: String) {
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
self.init(reachability: reachability)
}
/// Creates an instance that monitors the address 0.0.0.0.
///
/// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing
/// status of the device, both IPv4 and IPv6.
public convenience init?() {
var zero = sockaddr()
zero.sa_len = UInt8(MemoryLayout<sockaddr>.size)
zero.sa_family = sa_family_t(AF_INET)
guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil }
self.init(reachability: reachability)
}
private init(reachability: SCNetworkReachability) {
self.reachability = reachability
}
deinit {
stopListening()
}
// MARK: - Listening
/// Starts listening for changes in network reachability status.
///
/// - Note: Stops and removes any existing listener.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which to call the `listener` closure. `.main` by default.
/// - listener: `Listener` closure called when reachability changes.
///
/// - Returns: `true` if listening was started successfully, `false` otherwise.
@discardableResult
open func startListening(onQueue queue: DispatchQueue = .main,
onUpdatePerforming listener: @escaping Listener) -> Bool {
stopListening()
mutableState.write { state in
state.listenerQueue = queue
state.listener = listener
}
let weakManager = WeakManager(manager: self)
var context = SCNetworkReachabilityContext(
version: 0,
info: Unmanaged.passUnretained(weakManager).toOpaque(),
retain: { info in
let unmanaged = Unmanaged<WeakManager>.fromOpaque(info)
_ = unmanaged.retain()
return UnsafeRawPointer(unmanaged.toOpaque())
},
release: { info in
let unmanaged = Unmanaged<WeakManager>.fromOpaque(info)
unmanaged.release()
},
copyDescription: { info in
let unmanaged = Unmanaged<WeakManager>.fromOpaque(info)
let weakManager = unmanaged.takeUnretainedValue()
let description = weakManager.manager?.flags?.readableDescription ?? "nil"
return Unmanaged.passRetained(description as CFString)
}
)
let callback: SCNetworkReachabilityCallBack = { _, flags, info in
guard let info else { return }
let weakManager = Unmanaged<WeakManager>.fromOpaque(info).takeUnretainedValue()
weakManager.manager?.notifyListener(flags)
}
let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue)
let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context)
// Manually call listener to give initial state, since the framework may not.
if let currentFlags = flags {
reachabilityQueue.async {
self.notifyListener(currentFlags)
}
}
return callbackAdded && queueAdded
}
/// Stops listening for changes in network reachability status.
open func stopListening() {
SCNetworkReachabilitySetCallback(reachability, nil, nil)
SCNetworkReachabilitySetDispatchQueue(reachability, nil)
mutableState.write { state in
state.listener = nil
state.listenerQueue = nil
state.previousStatus = nil
}
}
// MARK: - Internal - Listener Notification
/// Calls the `listener` closure of the `listenerQueue` if the computed status hasn't changed.
///
/// - Note: Should only be called from the `reachabilityQueue`.
///
/// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status.
func notifyListener(_ flags: SCNetworkReachabilityFlags) {
let newStatus = NetworkReachabilityStatus(flags)
mutableState.write { state in
guard state.previousStatus != newStatus else { return }
state.previousStatus = newStatus
let listener = state.listener
state.listenerQueue?.async { listener?(newStatus) }
}
}
private final class WeakManager {
weak var manager: NetworkReachabilityManager?
init(manager: NetworkReachabilityManager?) {
self.manager = manager
}
}
}
// MARK: -
extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {}
extension SCNetworkReachabilityFlags {
var isReachable: Bool { contains(.reachable) }
var isConnectionRequired: Bool { contains(.connectionRequired) }
var canConnectAutomatically: Bool { contains(.connectionOnDemand) || contains(.connectionOnTraffic) }
var canConnectWithoutUserInteraction: Bool { canConnectAutomatically && !contains(.interventionRequired) }
var isActuallyReachable: Bool { isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) }
var isCellular: Bool {
#if os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))
return contains(.isWWAN)
#else
return false
#endif
}
/// Human readable `String` for all states, to help with debugging.
var readableDescription: String {
let W = isCellular ? "W" : "-"
let R = isReachable ? "R" : "-"
let c = isConnectionRequired ? "c" : "-"
let t = contains(.transientConnection) ? "t" : "-"
let i = contains(.interventionRequired) ? "i" : "-"
let C = contains(.connectionOnTraffic) ? "C" : "-"
let D = contains(.connectionOnDemand) ? "D" : "-"
let l = contains(.isLocalAddress) ? "l" : "-"
let d = contains(.isDirect) ? "d" : "-"
let a = contains(.connectionAutomatic) ? "a" : "-"
return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)\(a)"
}
}
#endif
+111
View File
@@ -0,0 +1,111 @@
//
// RedirectHandler.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// A type that handles how an HTTP redirect response from a remote server should be redirected to the new request.
public protocol RedirectHandler {
/// Determines how the HTTP redirect response should be redirected to the new request.
///
/// The `completion` closure should be passed one of three possible options:
///
/// 1. The new request specified by the redirect (this is the most common use case).
/// 2. A modified version of the new request (you may want to route it somewhere else).
/// 3. A `nil` value to deny the redirect request and return the body of the redirect response.
///
/// - Parameters:
/// - task: The `URLSessionTask` whose request resulted in a redirect.
/// - request: The `URLRequest` to the new location specified by the redirect response.
/// - response: The `HTTPURLResponse` containing the server's response to the original request.
/// - completion: The closure to execute containing the new `URLRequest`, a modified `URLRequest`, or `nil`.
func task(_ task: URLSessionTask,
willBeRedirectedTo request: URLRequest,
for response: HTTPURLResponse,
completion: @escaping (URLRequest?) -> Void)
}
// MARK: -
/// `Redirector` is a convenience `RedirectHandler` making it easy to follow, not follow, or modify a redirect.
public struct Redirector {
/// Defines the behavior of the `Redirector` type.
public enum Behavior {
/// Follow the redirect as defined in the response.
case follow
/// Do not follow the redirect defined in the response.
case doNotFollow
/// Modify the redirect request defined in the response.
case modify((URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?)
}
/// Returns a `Redirector` with a `.follow` `Behavior`.
public static let follow = Redirector(behavior: .follow)
/// Returns a `Redirector` with a `.doNotFollow` `Behavior`.
public static let doNotFollow = Redirector(behavior: .doNotFollow)
/// The `Behavior` of the `Redirector`.
public let behavior: Behavior
/// Creates a `Redirector` instance from the `Behavior`.
///
/// - Parameter behavior: The `Behavior`.
public init(behavior: Behavior) {
self.behavior = behavior
}
}
// MARK: -
extension Redirector: RedirectHandler {
public func task(_ task: URLSessionTask,
willBeRedirectedTo request: URLRequest,
for response: HTTPURLResponse,
completion: @escaping (URLRequest?) -> Void) {
switch behavior {
case .follow:
completion(request)
case .doNotFollow:
completion(nil)
case let .modify(closure):
let request = closure(task, request, response)
completion(request)
}
}
}
extension RedirectHandler where Self == Redirector {
/// Provides a `Redirector` which follows redirects. Equivalent to `Redirector.follow`.
public static var follow: Redirector { .follow }
/// Provides a `Redirector` which does not follow redirects. Equivalent to `Redirector.doNotFollow`.
public static var doNotFollow: Redirector { .doNotFollow }
/// Creates a `Redirector` which modifies the redirected `URLRequest` using the provided closure.
///
/// - Parameter closure: Closure used to modify the redirect.
/// - Returns: The `Redirector`.
public static func modify(using closure: @escaping (URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) -> Redirector {
Redirector(behavior: .modify(closure))
}
}
+146
View File
@@ -0,0 +1,146 @@
//
// RequestCompression.swift
//
// Copyright (c) 2023 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#if canImport(zlib)
import Foundation
import zlib
/// `RequestAdapter` which compresses outgoing `URLRequest` bodies using the `deflate` `Content-Encoding` and adds the
/// appropriate header.
///
/// - Note: Most requests to most APIs are small and so would only be slowed down by applying this adapter. Measure the
/// size of your request bodies and the performance impact of using this adapter before use. Using this adapter
/// with already compressed data, such as images, will, at best, have no effect. Additionally, body compression
/// is a synchronous operation, so measuring the performance impact may be important to determine whether you
/// want to use a dedicated `requestQueue` in your `Session` instance. Finally, not all servers support request
/// compression, so test with all of your server configurations before deploying.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public struct DeflateRequestCompressor: RequestInterceptor {
/// Type that determines the action taken when the `URLRequest` already has a `Content-Encoding` header.
public enum DuplicateHeaderBehavior {
/// Throws a `DuplicateHeaderError`. The default.
case error
/// Replaces the existing header value with `deflate`.
case replace
/// Silently skips compression when the header exists.
case skip
}
/// `Error` produced when the outgoing `URLRequest` already has a `Content-Encoding` header, when the instance has
/// been configured to produce an error.
public struct DuplicateHeaderError: Error {}
/// Behavior to use when the outgoing `URLRequest` already has a `Content-Encoding` header.
public let duplicateHeaderBehavior: DuplicateHeaderBehavior
/// Closure which determines whether the outgoing body data should be compressed.
public let shouldCompressBodyData: (_ bodyData: Data) -> Bool
/// Creates an instance with the provided parameters.
///
/// - Parameters:
/// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use. `.error` by default.
/// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
public init(duplicateHeaderBehavior: DuplicateHeaderBehavior = .error,
shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }) {
self.duplicateHeaderBehavior = duplicateHeaderBehavior
self.shouldCompressBodyData = shouldCompressBodyData
}
public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
// No need to compress unless we have body data. No support for compressing streams.
guard let bodyData = urlRequest.httpBody else {
completion(.success(urlRequest))
return
}
guard shouldCompressBodyData(bodyData) else {
completion(.success(urlRequest))
return
}
if urlRequest.headers.value(for: "Content-Encoding") != nil {
switch duplicateHeaderBehavior {
case .error:
completion(.failure(DuplicateHeaderError()))
return
case .replace:
// Header will be replaced once the body data is compressed.
break
case .skip:
completion(.success(urlRequest))
return
}
}
var compressedRequest = urlRequest
do {
compressedRequest.httpBody = try deflate(bodyData)
compressedRequest.headers.update(.contentEncoding("deflate"))
completion(.success(compressedRequest))
} catch {
completion(.failure(error))
}
}
func deflate(_ data: Data) throws -> Data {
var output = Data([0x78, 0x5E]) // Header
try output.append((data as NSData).compressed(using: .zlib) as Data)
var checksum = adler32Checksum(of: data).bigEndian
output.append(Data(bytes: &checksum, count: MemoryLayout<UInt32>.size))
return output
}
func adler32Checksum(of data: Data) -> UInt32 {
data.withUnsafeBytes { buffer in
UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count)))
}
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension RequestInterceptor where Self == DeflateRequestCompressor {
/// Create a `DeflateRequestCompressor` with default `duplicateHeaderBehavior` and `shouldCompressBodyData` values.
public static var deflateCompressor: DeflateRequestCompressor {
DeflateRequestCompressor()
}
/// Creates a `DeflateRequestCompressor` with the provided `DuplicateHeaderBehavior` and `shouldCompressBodyData`
/// closure.
///
/// - Parameters:
/// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use.
/// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
///
/// - Returns: The `DeflateRequestCompressor`.
public static func deflateCompressor(
duplicateHeaderBehavior: DeflateRequestCompressor.DuplicateHeaderBehavior = .error,
shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }
) -> DeflateRequestCompressor {
DeflateRequestCompressor(duplicateHeaderBehavior: duplicateHeaderBehavior,
shouldCompressBodyData: shouldCompressBodyData)
}
}
#endif
+351
View File
@@ -0,0 +1,351 @@
//
// RequestInterceptor.swift
//
// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Stores all state associated with a `URLRequest` being adapted.
public struct RequestAdapterState {
/// The `UUID` of the `Request` associated with the `URLRequest` to adapt.
public let requestID: UUID
/// The `Session` associated with the `URLRequest` to adapt.
public let session: Session
}
// MARK: -
/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary.
public protocol RequestAdapter {
/// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result.
///
/// - Parameters:
/// - urlRequest: The `URLRequest` to adapt.
/// - session: The `Session` that will execute the `URLRequest`.
/// - completion: The completion handler that must be called when adaptation is complete.
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void)
/// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result.
///
/// - Parameters:
/// - urlRequest: The `URLRequest` to adapt.
/// - state: The `RequestAdapterState` associated with the `URLRequest`.
/// - completion: The completion handler that must be called when adaptation is complete.
func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void)
}
extension RequestAdapter {
public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) {
adapt(urlRequest, for: state.session, completion: completion)
}
}
// MARK: -
/// Outcome of determination whether retry is necessary.
public enum RetryResult {
/// Retry should be attempted immediately.
case retry
/// Retry should be attempted after the associated `TimeInterval`.
case retryWithDelay(TimeInterval)
/// Do not retry.
case doNotRetry
/// Do not retry due to the associated `Error`.
case doNotRetryWithError(Error)
}
extension RetryResult {
var retryRequired: Bool {
switch self {
case .retry, .retryWithDelay: return true
default: return false
}
}
var delay: TimeInterval? {
switch self {
case let .retryWithDelay(delay): return delay
default: return nil
}
}
var error: Error? {
guard case let .doNotRetryWithError(error) = self else { return nil }
return error
}
}
/// A type that determines whether a request should be retried after being executed by the specified session manager
/// and encountering an error.
public protocol RequestRetrier {
/// Determines whether the `Request` should be retried by calling the `completion` closure.
///
/// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs
/// to be retried. The one requirement is that the completion closure is called to ensure the request is properly
/// cleaned up after.
///
/// - Parameters:
/// - request: `Request` that failed due to the provided `Error`.
/// - session: `Session` that produced the `Request`.
/// - error: `Error` encountered while executing the `Request`.
/// - completion: Completion closure to be executed when a retry decision has been determined.
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void)
}
// MARK: -
/// Type that provides both `RequestAdapter` and `RequestRetrier` functionality.
public protocol RequestInterceptor: RequestAdapter, RequestRetrier {}
extension RequestInterceptor {
public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(_ request: Request,
for session: Session,
dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
completion(.doNotRetry)
}
}
/// `RequestAdapter` closure definition.
public typealias AdaptHandler = (URLRequest, Session, _ completion: @escaping (Result<URLRequest, Error>) -> Void) -> Void
/// `RequestRetrier` closure definition.
public typealias RetryHandler = (Request, Session, Error, _ completion: @escaping (RetryResult) -> Void) -> Void
// MARK: -
/// Closure-based `RequestAdapter`.
open class Adapter: RequestInterceptor {
private let adaptHandler: AdaptHandler
/// Creates an instance using the provided closure.
///
/// - Parameter adaptHandler: `AdaptHandler` closure to be executed when handling request adaptation.
public init(_ adaptHandler: @escaping AdaptHandler) {
self.adaptHandler = adaptHandler
}
open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
adaptHandler(urlRequest, session, completion)
}
open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) {
adaptHandler(urlRequest, state.session, completion)
}
}
extension RequestAdapter where Self == Adapter {
/// Creates an `Adapter` using the provided `AdaptHandler` closure.
///
/// - Parameter closure: `AdaptHandler` to use to adapt the request.
/// - Returns: The `Adapter`.
public static func adapter(using closure: @escaping AdaptHandler) -> Adapter {
Adapter(closure)
}
}
// MARK: -
/// Closure-based `RequestRetrier`.
open class Retrier: RequestInterceptor {
private let retryHandler: RetryHandler
/// Creates an instance using the provided closure.
///
/// - Parameter retryHandler: `RetryHandler` closure to be executed when handling request retry.
public init(_ retryHandler: @escaping RetryHandler) {
self.retryHandler = retryHandler
}
open func retry(_ request: Request,
for session: Session,
dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
retryHandler(request, session, error, completion)
}
}
extension RequestRetrier where Self == Retrier {
/// Creates a `Retrier` using the provided `RetryHandler` closure.
///
/// - Parameter closure: `RetryHandler` to use to retry the request.
/// - Returns: The `Retrier`.
public static func retrier(using closure: @escaping RetryHandler) -> Retrier {
Retrier(closure)
}
}
// MARK: -
/// `RequestInterceptor` which can use multiple `RequestAdapter` and `RequestRetrier` values.
open class Interceptor: RequestInterceptor {
/// All `RequestAdapter`s associated with the instance. These adapters will be run until one fails.
public let adapters: [RequestAdapter]
/// All `RequestRetrier`s associated with the instance. These retriers will be run one at a time until one triggers retry.
public let retriers: [RequestRetrier]
/// Creates an instance from `AdaptHandler` and `RetryHandler` closures.
///
/// - Parameters:
/// - adaptHandler: `AdaptHandler` closure to be used.
/// - retryHandler: `RetryHandler` closure to be used.
public init(adaptHandler: @escaping AdaptHandler, retryHandler: @escaping RetryHandler) {
adapters = [Adapter(adaptHandler)]
retriers = [Retrier(retryHandler)]
}
/// Creates an instance from `RequestAdapter` and `RequestRetrier` values.
///
/// - Parameters:
/// - adapter: `RequestAdapter` value to be used.
/// - retrier: `RequestRetrier` value to be used.
public init(adapter: RequestAdapter, retrier: RequestRetrier) {
adapters = [adapter]
retriers = [retrier]
}
/// Creates an instance from the arrays of `RequestAdapter` and `RequestRetrier` values.
///
/// - Parameters:
/// - adapters: `RequestAdapter` values to be used.
/// - retriers: `RequestRetrier` values to be used.
/// - interceptors: `RequestInterceptor`s to be used.
public init(adapters: [RequestAdapter] = [], retriers: [RequestRetrier] = [], interceptors: [RequestInterceptor] = []) {
self.adapters = adapters + interceptors
self.retriers = retriers + interceptors
}
open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
adapt(urlRequest, for: session, using: adapters, completion: completion)
}
private func adapt(_ urlRequest: URLRequest,
for session: Session,
using adapters: [RequestAdapter],
completion: @escaping (Result<URLRequest, Error>) -> Void) {
var pendingAdapters = adapters
guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return }
let adapter = pendingAdapters.removeFirst()
adapter.adapt(urlRequest, for: session) { result in
switch result {
case let .success(urlRequest):
self.adapt(urlRequest, for: session, using: pendingAdapters, completion: completion)
case .failure:
completion(result)
}
}
}
open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) {
adapt(urlRequest, using: state, adapters: adapters, completion: completion)
}
private func adapt(_ urlRequest: URLRequest,
using state: RequestAdapterState,
adapters: [RequestAdapter],
completion: @escaping (Result<URLRequest, Error>) -> Void) {
var pendingAdapters = adapters
guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return }
let adapter = pendingAdapters.removeFirst()
adapter.adapt(urlRequest, using: state) { result in
switch result {
case let .success(urlRequest):
self.adapt(urlRequest, using: state, adapters: pendingAdapters, completion: completion)
case .failure:
completion(result)
}
}
}
open func retry(_ request: Request,
for session: Session,
dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
retry(request, for: session, dueTo: error, using: retriers, completion: completion)
}
private func retry(_ request: Request,
for session: Session,
dueTo error: Error,
using retriers: [RequestRetrier],
completion: @escaping (RetryResult) -> Void) {
var pendingRetriers = retriers
guard !pendingRetriers.isEmpty else { completion(.doNotRetry); return }
let retrier = pendingRetriers.removeFirst()
retrier.retry(request, for: session, dueTo: error) { result in
switch result {
case .retry, .retryWithDelay, .doNotRetryWithError:
completion(result)
case .doNotRetry:
// Only continue to the next retrier if retry was not triggered and no error was encountered
self.retry(request, for: session, dueTo: error, using: pendingRetriers, completion: completion)
}
}
}
}
extension RequestInterceptor where Self == Interceptor {
/// Creates an `Interceptor` using the provided `AdaptHandler` and `RetryHandler` closures.
///
/// - Parameters:
/// - adapter: `AdapterHandler`to use to adapt the request.
/// - retrier: `RetryHandler` to use to retry the request.
/// - Returns: The `Interceptor`.
public static func interceptor(adapter: @escaping AdaptHandler, retrier: @escaping RetryHandler) -> Interceptor {
Interceptor(adaptHandler: adapter, retryHandler: retrier)
}
/// Creates an `Interceptor` using the provided `RequestAdapter` and `RequestRetrier` instances.
/// - Parameters:
/// - adapter: `RequestAdapter` to use to adapt the request
/// - retrier: `RequestRetrier` to use to retry the request.
/// - Returns: The `Interceptor`.
public static func interceptor(adapter: RequestAdapter, retrier: RequestRetrier) -> Interceptor {
Interceptor(adapter: adapter, retrier: retrier)
}
/// Creates an `Interceptor` using the provided `RequestAdapter`s, `RequestRetrier`s, and `RequestInterceptor`s.
/// - Parameters:
/// - adapters: `RequestAdapter`s to use to adapt the request. These adapters will be run until one fails.
/// - retriers: `RequestRetrier`s to use to retry the request. These retriers will be run one at a time until
/// a retry is triggered.
/// - interceptors: `RequestInterceptor`s to use to intercept the request.
/// - Returns: The `Interceptor`.
public static func interceptor(adapters: [RequestAdapter] = [],
retriers: [RequestRetrier] = [],
interceptors: [RequestInterceptor] = []) -> Interceptor {
Interceptor(adapters: adapters, retriers: retriers, interceptors: interceptors)
}
}
+525
View File
@@ -0,0 +1,525 @@
//
// ResponseSerialization.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// The type to which all data response serializers must conform in order to serialize a response.
public protocol DataResponseSerializerProtocol<SerializedObject> {
/// The type of serialized object to be created.
associatedtype SerializedObject
/// Serialize the response `Data` into the provided type.
///
/// - Parameters:
/// - request: `URLRequest` which was used to perform the request, if any.
/// - response: `HTTPURLResponse` received from the server, if any.
/// - data: `Data` returned from the server, if any.
/// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request.
///
/// - Returns: The `SerializedObject`.
/// - Throws: Any `Error` produced during serialization.
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject
}
/// The type to which all download response serializers must conform in order to serialize a response.
public protocol DownloadResponseSerializerProtocol<SerializedObject> {
/// The type of serialized object to be created.
associatedtype SerializedObject
/// Serialize the downloaded response `Data` from disk into the provided type.
///
/// - Parameters:
/// - request: `URLRequest` which was used to perform the request, if any.
/// - response: `HTTPURLResponse` received from the server, if any.
/// - fileURL: File `URL` to which the response data was downloaded.
/// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request.
///
/// - Returns: The `SerializedObject`.
/// - Throws: Any `Error` produced during serialization.
func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject
}
/// A serializer that can handle both data and download responses.
public protocol ResponseSerializer<SerializedObject>: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol {
/// `DataPreprocessor` used to prepare incoming `Data` for serialization.
var dataPreprocessor: DataPreprocessor { get }
/// `HTTPMethod`s for which empty response bodies are considered appropriate.
var emptyRequestMethods: Set<HTTPMethod> { get }
/// HTTP response codes for which empty response bodies are considered appropriate.
var emptyResponseCodes: Set<Int> { get }
}
/// Type used to preprocess `Data` before it handled by a serializer.
public protocol DataPreprocessor {
/// Process `Data` before it's handled by a serializer.
/// - Parameter data: The raw `Data` to process.
func preprocess(_ data: Data) throws -> Data
}
/// `DataPreprocessor` that returns passed `Data` without any transform.
public struct PassthroughPreprocessor: DataPreprocessor {
/// Creates an instance.
public init() {}
public func preprocess(_ data: Data) throws -> Data { data }
}
/// `DataPreprocessor` that trims Google's typical `)]}',\n` XSSI JSON header.
public struct GoogleXSSIPreprocessor: DataPreprocessor {
/// Creates an instance.
public init() {}
public func preprocess(_ data: Data) throws -> Data {
(data.prefix(6) == Data(")]}',\n".utf8)) ? data.dropFirst(6) : data
}
}
extension DataPreprocessor where Self == PassthroughPreprocessor {
/// Provides a `PassthroughPreprocessor` instance.
public static var passthrough: PassthroughPreprocessor { PassthroughPreprocessor() }
}
extension DataPreprocessor where Self == GoogleXSSIPreprocessor {
/// Provides a `GoogleXSSIPreprocessor` instance.
public static var googleXSSI: GoogleXSSIPreprocessor { GoogleXSSIPreprocessor() }
}
extension ResponseSerializer {
/// Default `DataPreprocessor`. `PassthroughPreprocessor` by default.
public static var defaultDataPreprocessor: DataPreprocessor { PassthroughPreprocessor() }
/// Default `HTTPMethod`s for which empty response bodies are always considered appropriate. `[.head]` by default.
public static var defaultEmptyRequestMethods: Set<HTTPMethod> { [.head] }
/// HTTP response codes for which empty response bodies are always considered appropriate. `[204, 205]` by default.
public static var defaultEmptyResponseCodes: Set<Int> { [204, 205] }
public var dataPreprocessor: DataPreprocessor { Self.defaultDataPreprocessor }
public var emptyRequestMethods: Set<HTTPMethod> { Self.defaultEmptyRequestMethods }
public var emptyResponseCodes: Set<Int> { Self.defaultEmptyResponseCodes }
/// Determines whether the `request` allows empty response bodies, if `request` exists.
///
/// - Parameter request: `URLRequest` to evaluate.
///
/// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `request` was `nil`.
public func requestAllowsEmptyResponseData(_ request: URLRequest?) -> Bool? {
request.flatMap(\.httpMethod)
.flatMap(HTTPMethod.init)
.map { emptyRequestMethods.contains($0) }
}
/// Determines whether the `response` allows empty response bodies, if `response` exists.
///
/// - Parameter response: `HTTPURLResponse` to evaluate.
///
/// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `response` was `nil`.
public func responseAllowsEmptyResponseData(_ response: HTTPURLResponse?) -> Bool? {
response.map(\.statusCode)
.map { emptyResponseCodes.contains($0) }
}
/// Determines whether `request` and `response` allow empty response bodies.
///
/// - Parameters:
/// - request: `URLRequest` to evaluate.
/// - response: `HTTPURLResponse` to evaluate.
///
/// - Returns: `true` if `request` or `response` allow empty bodies, `false` otherwise.
public func emptyResponseAllowed(forRequest request: URLRequest?, response: HTTPURLResponse?) -> Bool {
(requestAllowsEmptyResponseData(request) == true) || (responseAllowsEmptyResponseData(response) == true)
}
}
/// By default, any serializer declared to conform to both types will get file serialization for free, as it just feeds
/// the data read from disk into the data response serializer.
extension DownloadResponseSerializerProtocol where Self: DataResponseSerializerProtocol {
public func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> Self.SerializedObject {
guard error == nil else { throw error! }
guard let fileURL else {
throw AFError.responseSerializationFailed(reason: .inputFileNil)
}
let data: Data
do {
data = try Data(contentsOf: fileURL)
} catch {
throw AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL))
}
do {
return try serialize(request: request, response: response, data: data, error: error)
} catch {
throw error
}
}
}
// MARK: - URL
/// A `DownloadResponseSerializerProtocol` that performs only `Error` checking and ensures that a downloaded `fileURL`
/// is present.
public struct URLResponseSerializer: DownloadResponseSerializerProtocol {
/// Creates an instance.
public init() {}
public func serializeDownload(request: URLRequest?,
response: HTTPURLResponse?,
fileURL: URL?,
error: Error?) throws -> URL {
guard error == nil else { throw error! }
guard let url = fileURL else {
throw AFError.responseSerializationFailed(reason: .inputFileNil)
}
return url
}
}
extension DownloadResponseSerializerProtocol where Self == URLResponseSerializer {
/// Provides a `URLResponseSerializer` instance.
public static var url: URLResponseSerializer { URLResponseSerializer() }
}
// MARK: - Data
/// A `ResponseSerializer` that performs minimal response checking and returns any response `Data` as-is. By default, a
/// request returning `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the
/// response has an HTTP status code valid for empty responses, then an empty `Data` value is returned.
public final class DataResponseSerializer: ResponseSerializer {
public let dataPreprocessor: DataPreprocessor
public let emptyResponseCodes: Set<Int>
public let emptyRequestMethods: Set<HTTPMethod>
/// Creates a `DataResponseSerializer` using the provided parameters.
///
/// - Parameters:
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization.
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default.
public init(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) {
self.dataPreprocessor = dataPreprocessor
self.emptyResponseCodes = emptyResponseCodes
self.emptyRequestMethods = emptyRequestMethods
}
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data {
guard error == nil else { throw error! }
guard var data, !data.isEmpty else {
guard emptyResponseAllowed(forRequest: request, response: response) else {
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)
}
return Data()
}
data = try dataPreprocessor.preprocess(data)
return data
}
}
extension ResponseSerializer where Self == DataResponseSerializer {
/// Provides a default `DataResponseSerializer` instance.
public static var data: DataResponseSerializer { DataResponseSerializer() }
/// Creates a `DataResponseSerializer` using the provided parameters.
///
/// - Parameters:
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization.
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default.
///
/// - Returns: The `DataResponseSerializer`.
public static func data(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponseSerializer {
DataResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods)
}
}
// MARK: - String
/// A `ResponseSerializer` that decodes the response data as a `String`. By default, a request returning `nil` or no
/// data is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code
/// valid for empty responses, then an empty `String` is returned.
public final class StringResponseSerializer: ResponseSerializer {
public let dataPreprocessor: DataPreprocessor
/// Optional string encoding used to validate the response.
public let encoding: String.Encoding?
public let emptyResponseCodes: Set<Int>
public let emptyRequestMethods: Set<HTTPMethod>
/// Creates an instance with the provided values.
///
/// - Parameters:
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization.
/// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined
/// from the server response, falling back to the default HTTP character set, `ISO-8859-1`.
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default.
public init(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil,
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) {
self.dataPreprocessor = dataPreprocessor
self.encoding = encoding
self.emptyResponseCodes = emptyResponseCodes
self.emptyRequestMethods = emptyRequestMethods
}
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String {
guard error == nil else { throw error! }
guard var data, !data.isEmpty else {
guard emptyResponseAllowed(forRequest: request, response: response) else {
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)
}
return ""
}
data = try dataPreprocessor.preprocess(data)
var convertedEncoding = encoding
if let encodingName = response?.textEncodingName, convertedEncoding == nil {
convertedEncoding = String.Encoding(ianaCharsetName: encodingName)
}
let actualEncoding = convertedEncoding ?? .isoLatin1
guard let string = String(data: data, encoding: actualEncoding) else {
throw AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding))
}
return string
}
}
extension ResponseSerializer where Self == StringResponseSerializer {
/// Provides a default `StringResponseSerializer` instance.
public static var string: StringResponseSerializer { StringResponseSerializer() }
/// Creates a `StringResponseSerializer` with the provided values.
///
/// - Parameters:
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization.
/// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined
/// from the server response, falling back to the default HTTP character set, `ISO-8859-1`.
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default.
///
/// - Returns: The `StringResponseSerializer`.
public static func string(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil,
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> StringResponseSerializer {
StringResponseSerializer(dataPreprocessor: dataPreprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods)
}
}
// MARK: - JSON
/// A `ResponseSerializer` that decodes the response data using `JSONSerialization`. By default, a request returning
/// `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the response has an
/// HTTP status code valid for empty responses, then an `NSNull` value is returned.
///
/// - Note: This serializer is deprecated and should not be used. Instead, create concrete types conforming to
/// `Decodable` and use a `DecodableResponseSerializer`.
@available(*, deprecated, message: "JSONResponseSerializer deprecated and will be removed in Alamofire 6. Use DecodableResponseSerializer instead.")
public final class JSONResponseSerializer: ResponseSerializer {
public let dataPreprocessor: DataPreprocessor
public let emptyResponseCodes: Set<Int>
public let emptyRequestMethods: Set<HTTPMethod>
/// `JSONSerialization.ReadingOptions` used when serializing a response.
public let options: JSONSerialization.ReadingOptions
/// Creates an instance with the provided values.
///
/// - Parameters:
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization.
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default.
/// - options: The options to use. `.allowFragments` by default.
public init(dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods,
options: JSONSerialization.ReadingOptions = .allowFragments) {
self.dataPreprocessor = dataPreprocessor
self.emptyResponseCodes = emptyResponseCodes
self.emptyRequestMethods = emptyRequestMethods
self.options = options
}
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Any {
guard error == nil else { throw error! }
guard var data, !data.isEmpty else {
guard emptyResponseAllowed(forRequest: request, response: response) else {
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)
}
return NSNull()
}
data = try dataPreprocessor.preprocess(data)
do {
return try JSONSerialization.jsonObject(with: data, options: options)
} catch {
throw AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error))
}
}
}
// MARK: - Empty
/// Protocol representing an empty response. Use `T.emptyValue()` to get an instance.
public protocol EmptyResponse {
/// Empty value for the conforming type.
///
/// - Returns: Value of `Self` to use for empty values.
static func emptyValue() -> Self
}
/// Type representing an empty value. Use `Empty.value` to get the static instance.
public struct Empty: Codable, Sendable {
/// Static `Empty` instance used for all `Empty` responses.
public static let value = Empty()
}
extension Empty: EmptyResponse {
public static func emptyValue() -> Empty {
value
}
}
// MARK: - DataDecoder Protocol
/// Any type which can decode `Data` into a `Decodable` type.
public protocol DataDecoder {
/// Decode `Data` into the provided type.
///
/// - Parameters:
/// - type: The `Type` to be decoded.
/// - data: The `Data` to be decoded.
///
/// - Returns: The decoded value of type `D`.
/// - Throws: Any error that occurs during decode.
func decode<D: Decodable>(_ type: D.Type, from data: Data) throws -> D
}
/// `JSONDecoder` automatically conforms to `DataDecoder`.
extension JSONDecoder: DataDecoder {}
/// `PropertyListDecoder` automatically conforms to `DataDecoder`.
extension PropertyListDecoder: DataDecoder {}
// MARK: - Decodable
/// A `ResponseSerializer` that decodes the response data as a generic value using any type that conforms to
/// `DataDecoder`. By default, this is an instance of `JSONDecoder`. Additionally, a request returning `nil` or no data
/// is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code valid
/// for empty responses then an empty value will be returned. If the decoded type conforms to `EmptyResponse`, the
/// type's `emptyValue()` will be returned. If the decoded type is `Empty`, the `.value` instance is returned. If the
/// decoded type *does not* conform to `EmptyResponse` and isn't `Empty`, an error will be produced.
public final class DecodableResponseSerializer<T: Decodable>: ResponseSerializer {
public let dataPreprocessor: DataPreprocessor
/// The `DataDecoder` instance used to decode responses.
public let decoder: DataDecoder
public let emptyResponseCodes: Set<Int>
public let emptyRequestMethods: Set<HTTPMethod>
/// Creates an instance using the values provided.
///
/// - Parameters:
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization.
/// - decoder: The `DataDecoder`. `JSONDecoder()` by default.
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default.
public init(dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer.defaultEmptyRequestMethods) {
self.dataPreprocessor = dataPreprocessor
self.decoder = decoder
self.emptyResponseCodes = emptyResponseCodes
self.emptyRequestMethods = emptyRequestMethods
}
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T {
guard error == nil else { throw error! }
guard var data, !data.isEmpty else {
guard emptyResponseAllowed(forRequest: request, response: response) else {
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)
}
guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else {
throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)"))
}
return emptyValue
}
data = try dataPreprocessor.preprocess(data)
do {
return try decoder.decode(T.self, from: data)
} catch {
throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error))
}
}
}
extension ResponseSerializer {
/// Creates a `DecodableResponseSerializer` using the values provided.
///
/// - Parameters:
/// - type: `Decodable` type to decode from response data.
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization.
/// - decoder: The `DataDecoder`. `JSONDecoder()` by default.
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default.
///
/// - Returns: The `DecodableResponseSerializer`.
public static func decodable<T: Decodable>(of type: T.Type,
dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DecodableResponseSerializer<T> where Self == DecodableResponseSerializer<T> {
DecodableResponseSerializer<T>(dataPreprocessor: dataPreprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods)
}
}
+430
View File
@@ -0,0 +1,430 @@
//
// RetryPolicy.swift
//
// Copyright (c) 2019-2020 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// A retry policy that retries requests using an exponential backoff for allowed HTTP methods and HTTP status codes
/// as well as certain types of networking errors.
open class RetryPolicy: RequestInterceptor {
/// The default retry limit for retry policies.
public static let defaultRetryLimit: UInt = 2
/// The default exponential backoff base for retry policies (must be a minimum of 2).
public static let defaultExponentialBackoffBase: UInt = 2
/// The default exponential backoff scale for retry policies.
public static let defaultExponentialBackoffScale: Double = 0.5
/// The default HTTP methods to retry.
/// See [RFC 2616 - Section 9.1.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) for more information.
public static let defaultRetryableHTTPMethods: Set<HTTPMethod> = [.delete, // [Delete](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7) - not always idempotent
.get, // [GET](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3) - generally idempotent
.head, // [HEAD](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4) - generally idempotent
.options, // [OPTIONS](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2) - inherently idempotent
.put, // [PUT](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6) - not always idempotent
.trace // [TRACE](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8) - inherently idempotent
]
/// The default HTTP status codes to retry.
/// See [RFC 2616 - Section 10](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10) for more information.
public static let defaultRetryableHTTPStatusCodes: Set<Int> = [408, // [Request Timeout](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.9)
500, // [Internal Server Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1)
502, // [Bad Gateway](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.3)
503, // [Service Unavailable](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4)
504 // [Gateway Timeout](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.5)
]
/// The default URL error codes to retry.
public static let defaultRetryableURLErrorCodes: Set<URLError.Code> = [ // [Security] App Transport Security disallowed a connection because there is no secure network connection.
// - [Disabled] ATS settings do not change at runtime.
// .appTransportSecurityRequiresSecureConnection,
// [System] An app or app extension attempted to connect to a background session that is already connected to a
// process.
// - [Enabled] The other process could release the background session.
.backgroundSessionInUseByAnotherProcess,
// [System] The shared container identifier of the URL session configuration is needed but has not been set.
// - [Disabled] Cannot change at runtime.
// .backgroundSessionRequiresSharedContainer,
// [System] The app is suspended or exits while a background data task is processing.
// - [Enabled] App can be foregrounded or launched to recover.
.backgroundSessionWasDisconnected,
// [Network] The URL Loading system received bad data from the server.
// - [Enabled] Server could return valid data when retrying.
.badServerResponse,
// [Resource] A malformed URL prevented a URL request from being initiated.
// - [Disabled] URL was most likely constructed incorrectly.
// .badURL,
// [System] A connection was attempted while a phone call is active on a network that does not support
// simultaneous phone and data communication (EDGE or GPRS).
// - [Enabled] Phone call could be ended to allow request to recover.
.callIsActive,
// [Client] An asynchronous load has been canceled.
// - [Disabled] Request was cancelled by the client.
// .cancelled,
// [File System] A download task couldnt close the downloaded file on disk.
// - [Disabled] File system error is unlikely to recover with retry.
// .cannotCloseFile,
// [Network] An attempt to connect to a host failed.
// - [Enabled] Server or DNS lookup could recover during retry.
.cannotConnectToHost,
// [File System] A download task couldnt create the downloaded file on disk because of an I/O failure.
// - [Disabled] File system error is unlikely to recover with retry.
// .cannotCreateFile,
// [Data] Content data received during a connection request had an unknown content encoding.
// - [Disabled] Server is unlikely to modify the content encoding during a retry.
// .cannotDecodeContentData,
// [Data] Content data received during a connection request could not be decoded for a known content encoding.
// - [Disabled] Server is unlikely to modify the content encoding during a retry.
// .cannotDecodeRawData,
// [Network] The host name for a URL could not be resolved.
// - [Enabled] Server or DNS lookup could recover during retry.
.cannotFindHost,
// [Network] A request to load an item only from the cache could not be satisfied.
// - [Enabled] Cache could be populated during a retry.
.cannotLoadFromNetwork,
// [File System] A download task was unable to move a downloaded file on disk.
// - [Disabled] File system error is unlikely to recover with retry.
// .cannotMoveFile,
// [File System] A download task was unable to open the downloaded file on disk.
// - [Disabled] File system error is unlikely to recover with retry.
// .cannotOpenFile,
// [Data] A task could not parse a response.
// - [Disabled] Invalid response is unlikely to recover with retry.
// .cannotParseResponse,
// [File System] A download task was unable to remove a downloaded file from disk.
// - [Disabled] File system error is unlikely to recover with retry.
// .cannotRemoveFile,
// [File System] A download task was unable to write to the downloaded file on disk.
// - [Disabled] File system error is unlikely to recover with retry.
// .cannotWriteToFile,
// [Security] A client certificate was rejected.
// - [Disabled] Client certificate is unlikely to change with retry.
// .clientCertificateRejected,
// [Security] A client certificate was required to authenticate an SSL connection during a request.
// - [Disabled] Client certificate is unlikely to be provided with retry.
// .clientCertificateRequired,
// [Data] The length of the resource data exceeds the maximum allowed.
// - [Disabled] Resource will likely still exceed the length maximum on retry.
// .dataLengthExceedsMaximum,
// [System] The cellular network disallowed a connection.
// - [Enabled] WiFi connection could be established during retry.
.dataNotAllowed,
// [Network] The host address could not be found via DNS lookup.
// - [Enabled] DNS lookup could succeed during retry.
.dnsLookupFailed,
// [Data] A download task failed to decode an encoded file during the download.
// - [Enabled] Server could correct the decoding issue with retry.
.downloadDecodingFailedMidStream,
// [Data] A download task failed to decode an encoded file after downloading.
// - [Enabled] Server could correct the decoding issue with retry.
.downloadDecodingFailedToComplete,
// [File System] A file does not exist.
// - [Disabled] File system error is unlikely to recover with retry.
// .fileDoesNotExist,
// [File System] A request for an FTP file resulted in the server responding that the file is not a plain file,
// but a directory.
// - [Disabled] FTP directory is not likely to change to a file during a retry.
// .fileIsDirectory,
// [Network] A redirect loop has been detected or the threshold for number of allowable redirects has been
// exceeded (currently 16).
// - [Disabled] The redirect loop is unlikely to be resolved within the retry window.
// .httpTooManyRedirects,
// [System] The attempted connection required activating a data context while roaming, but international roaming
// is disabled.
// - [Enabled] WiFi connection could be established during retry.
.internationalRoamingOff,
// [Connectivity] A client or server connection was severed in the middle of an in-progress load.
// - [Enabled] A network connection could be established during retry.
.networkConnectionLost,
// [File System] A resource couldnt be read because of insufficient permissions.
// - [Disabled] Permissions are unlikely to be granted during retry.
// .noPermissionsToReadFile,
// [Connectivity] A network resource was requested, but an internet connection has not been established and
// cannot be established automatically.
// - [Enabled] A network connection could be established during retry.
.notConnectedToInternet,
// [Resource] A redirect was specified by way of server response code, but the server did not accompany this
// code with a redirect URL.
// - [Disabled] The redirect URL is unlikely to be supplied during a retry.
// .redirectToNonExistentLocation,
// [Client] A body stream is needed but the client did not provide one.
// - [Disabled] The client will be unlikely to supply a body stream during retry.
// .requestBodyStreamExhausted,
// [Resource] A requested resource couldnt be retrieved.
// - [Disabled] The resource is unlikely to become available during the retry window.
// .resourceUnavailable,
// [Security] An attempt to establish a secure connection failed for reasons that cant be expressed more
// specifically.
// - [Enabled] The secure connection could be established during a retry given the lack of specificity
// provided by the error.
.secureConnectionFailed,
// [Security] A server certificate had a date which indicates it has expired, or is not yet valid.
// - [Enabled] The server certificate could become valid within the retry window.
.serverCertificateHasBadDate,
// [Security] A server certificate was not signed by any root server.
// - [Disabled] The server certificate is unlikely to change during the retry window.
// .serverCertificateHasUnknownRoot,
// [Security] A server certificate is not yet valid.
// - [Enabled] The server certificate could become valid within the retry window.
.serverCertificateNotYetValid,
// [Security] A server certificate was signed by a root server that isnt trusted.
// - [Disabled] The server certificate is unlikely to become trusted within the retry window.
// .serverCertificateUntrusted,
// [Network] An asynchronous operation timed out.
// - [Enabled] The request timed out for an unknown reason and should be retried.
.timedOut
// [System] The URL Loading System encountered an error that it cant interpret.
// - [Disabled] The error could not be interpreted and is unlikely to be recovered from during a retry.
// .unknown,
// [Resource] A properly formed URL couldnt be handled by the framework.
// - [Disabled] The URL is unlikely to change during a retry.
// .unsupportedURL,
// [Client] Authentication is required to access a resource.
// - [Disabled] The user authentication is unlikely to be provided by retrying.
// .userAuthenticationRequired,
// [Client] An asynchronous request for authentication has been canceled by the user.
// - [Disabled] The user cancelled authentication and explicitly took action to not retry.
// .userCancelledAuthentication,
// [Resource] A server reported that a URL has a non-zero content length, but terminated the network connection
// gracefully without sending any data.
// - [Disabled] The server is unlikely to provide data during the retry window.
// .zeroByteResource,
]
/// The total number of times the request is allowed to be retried.
public let retryLimit: UInt
/// The base of the exponential backoff policy (should always be greater than or equal to 2).
public let exponentialBackoffBase: UInt
/// The scale of the exponential backoff.
public let exponentialBackoffScale: Double
/// The HTTP methods that are allowed to be retried.
public let retryableHTTPMethods: Set<HTTPMethod>
/// The HTTP status codes that are automatically retried by the policy.
public let retryableHTTPStatusCodes: Set<Int>
/// The URL error codes that are automatically retried by the policy.
public let retryableURLErrorCodes: Set<URLError.Code>
/// Creates a `RetryPolicy` from the specified parameters.
///
/// - Parameters:
/// - retryLimit: The total number of times the request is allowed to be retried. `2` by default.
/// - exponentialBackoffBase: The base of the exponential backoff policy. `2` by default.
/// - exponentialBackoffScale: The scale of the exponential backoff. `0.5` by default.
/// - retryableHTTPMethods: The HTTP methods that are allowed to be retried.
/// `RetryPolicy.defaultRetryableHTTPMethods` by default.
/// - retryableHTTPStatusCodes: The HTTP status codes that are automatically retried by the policy.
/// `RetryPolicy.defaultRetryableHTTPStatusCodes` by default.
/// - retryableURLErrorCodes: The URL error codes that are automatically retried by the policy.
/// `RetryPolicy.defaultRetryableURLErrorCodes` by default.
public init(retryLimit: UInt = RetryPolicy.defaultRetryLimit,
exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase,
exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale,
retryableHTTPMethods: Set<HTTPMethod> = RetryPolicy.defaultRetryableHTTPMethods,
retryableHTTPStatusCodes: Set<Int> = RetryPolicy.defaultRetryableHTTPStatusCodes,
retryableURLErrorCodes: Set<URLError.Code> = RetryPolicy.defaultRetryableURLErrorCodes) {
precondition(exponentialBackoffBase >= 2, "The `exponentialBackoffBase` must be a minimum of 2.")
self.retryLimit = retryLimit
self.exponentialBackoffBase = exponentialBackoffBase
self.exponentialBackoffScale = exponentialBackoffScale
self.retryableHTTPMethods = retryableHTTPMethods
self.retryableHTTPStatusCodes = retryableHTTPStatusCodes
self.retryableURLErrorCodes = retryableURLErrorCodes
}
open func retry(_ request: Request,
for session: Session,
dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
if request.retryCount < retryLimit, shouldRetry(request: request, dueTo: error) {
completion(.retryWithDelay(pow(Double(exponentialBackoffBase), Double(request.retryCount)) * exponentialBackoffScale))
} else {
completion(.doNotRetry)
}
}
/// Determines whether or not to retry the provided `Request`.
///
/// - Parameters:
/// - request: `Request` that failed due to the provided `Error`.
/// - error: `Error` encountered while executing the `Request`.
///
/// - Returns: `Bool` determining whether or not to retry the `Request`.
open func shouldRetry(request: Request, dueTo error: Error) -> Bool {
guard let httpMethod = request.request?.method, retryableHTTPMethods.contains(httpMethod) else { return false }
if let statusCode = request.response?.statusCode, retryableHTTPStatusCodes.contains(statusCode) {
return true
} else {
let errorCode = (error as? URLError)?.code
let afErrorCode = (error.asAFError?.underlyingError as? URLError)?.code
guard let code = errorCode ?? afErrorCode else { return false }
return retryableURLErrorCodes.contains(code)
}
}
}
extension RequestInterceptor where Self == RetryPolicy {
/// Provides a default `RetryPolicy` instance.
public static var retryPolicy: RetryPolicy { RetryPolicy() }
/// Creates an `RetryPolicy` from the specified parameters.
///
/// - Parameters:
/// - retryLimit: The total number of times the request is allowed to be retried. `2` by default.
/// - exponentialBackoffBase: The base of the exponential backoff policy. `2` by default.
/// - exponentialBackoffScale: The scale of the exponential backoff. `0.5` by default.
/// - retryableHTTPMethods: The HTTP methods that are allowed to be retried.
/// `RetryPolicy.defaultRetryableHTTPMethods` by default.
/// - retryableHTTPStatusCodes: The HTTP status codes that are automatically retried by the policy.
/// `RetryPolicy.defaultRetryableHTTPStatusCodes` by default.
/// - retryableURLErrorCodes: The URL error codes that are automatically retried by the policy.
/// `RetryPolicy.defaultRetryableURLErrorCodes` by default.
///
/// - Returns: The `RetryPolicy`
public static func retryPolicy(retryLimit: UInt = RetryPolicy.defaultRetryLimit,
exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase,
exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale,
retryableHTTPMethods: Set<HTTPMethod> = RetryPolicy.defaultRetryableHTTPMethods,
retryableHTTPStatusCodes: Set<Int> = RetryPolicy.defaultRetryableHTTPStatusCodes,
retryableURLErrorCodes: Set<URLError.Code> = RetryPolicy.defaultRetryableURLErrorCodes) -> RetryPolicy {
RetryPolicy(retryLimit: retryLimit,
exponentialBackoffBase: exponentialBackoffBase,
exponentialBackoffScale: exponentialBackoffScale,
retryableHTTPMethods: retryableHTTPMethods,
retryableHTTPStatusCodes: retryableHTTPStatusCodes,
retryableURLErrorCodes: retryableURLErrorCodes)
}
}
// MARK: -
/// A retry policy that automatically retries idempotent requests for network connection lost errors. For more
/// information about retrying network connection lost errors, please refer to Apple's
/// [technical document](https://developer.apple.com/library/content/qa/qa1941/_index.html).
open class ConnectionLostRetryPolicy: RetryPolicy {
/// Creates a `ConnectionLostRetryPolicy` instance from the specified parameters.
///
/// - Parameters:
/// - retryLimit: The total number of times the request is allowed to be retried.
/// `RetryPolicy.defaultRetryLimit` by default.
/// - exponentialBackoffBase: The base of the exponential backoff policy.
/// `RetryPolicy.defaultExponentialBackoffBase` by default.
/// - exponentialBackoffScale: The scale of the exponential backoff.
/// `RetryPolicy.defaultExponentialBackoffScale` by default.
/// - retryableHTTPMethods: The idempotent http methods to retry.
/// `RetryPolicy.defaultRetryableHTTPMethods` by default.
public init(retryLimit: UInt = RetryPolicy.defaultRetryLimit,
exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase,
exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale,
retryableHTTPMethods: Set<HTTPMethod> = RetryPolicy.defaultRetryableHTTPMethods) {
super.init(retryLimit: retryLimit,
exponentialBackoffBase: exponentialBackoffBase,
exponentialBackoffScale: exponentialBackoffScale,
retryableHTTPMethods: retryableHTTPMethods,
retryableHTTPStatusCodes: [],
retryableURLErrorCodes: [.networkConnectionLost])
}
}
extension RequestInterceptor where Self == ConnectionLostRetryPolicy {
/// Provides a default `ConnectionLostRetryPolicy` instance.
public static var connectionLostRetryPolicy: ConnectionLostRetryPolicy { ConnectionLostRetryPolicy() }
/// Creates a `ConnectionLostRetryPolicy` instance from the specified parameters.
///
/// - Parameters:
/// - retryLimit: The total number of times the request is allowed to be retried.
/// `RetryPolicy.defaultRetryLimit` by default.
/// - exponentialBackoffBase: The base of the exponential backoff policy.
/// `RetryPolicy.defaultExponentialBackoffBase` by default.
/// - exponentialBackoffScale: The scale of the exponential backoff.
/// `RetryPolicy.defaultExponentialBackoffScale` by default.
/// - retryableHTTPMethods: The idempotent http methods to retry.
///
/// - Returns: The `ConnectionLostRetryPolicy`.
public static func connectionLostRetryPolicy(retryLimit: UInt = RetryPolicy.defaultRetryLimit,
exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase,
exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale,
retryableHTTPMethods: Set<HTTPMethod> = RetryPolicy.defaultRetryableHTTPMethods) -> ConnectionLostRetryPolicy {
ConnectionLostRetryPolicy(retryLimit: retryLimit,
exponentialBackoffBase: exponentialBackoffBase,
exponentialBackoffScale: exponentialBackoffScale,
retryableHTTPMethods: retryableHTTPMethods)
}
}
+768
View File
@@ -0,0 +1,768 @@
//
// ServerTrustEvaluation.swift
//
// Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Responsible for managing the mapping of `ServerTrustEvaluating` values to given hosts.
open class ServerTrustManager {
/// Determines whether all hosts for this `ServerTrustManager` must be evaluated. `true` by default.
public let allHostsMustBeEvaluated: Bool
/// The dictionary of policies mapped to a particular host.
public let evaluators: [String: ServerTrustEvaluating]
/// Initializes the `ServerTrustManager` instance with the given evaluators.
///
/// Since different servers and web services can have different leaf certificates, intermediate and even root
/// certificates, it is important to have the flexibility to specify evaluation policies on a per host basis. This
/// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key
/// pinning for host3 and disabling evaluation for host4.
///
/// - Parameters:
/// - allHostsMustBeEvaluated: The value determining whether all hosts for this instance must be evaluated. `true`
/// by default.
/// - evaluators: A dictionary of evaluators mapped to hosts.
public init(allHostsMustBeEvaluated: Bool = true, evaluators: [String: ServerTrustEvaluating]) {
self.allHostsMustBeEvaluated = allHostsMustBeEvaluated
self.evaluators = evaluators
}
#if canImport(Security)
/// Returns the `ServerTrustEvaluating` value for the given host, if one is set.
///
/// By default, this method will return the policy that perfectly matches the given host. Subclasses could override
/// this method and implement more complex mapping implementations such as wildcards.
///
/// - Parameter host: The host to use when searching for a matching policy.
///
/// - Returns: The `ServerTrustEvaluating` value for the given host if found, `nil` otherwise.
/// - Throws: `AFError.serverTrustEvaluationFailed` if `allHostsMustBeEvaluated` is `true` and no matching
/// evaluators are found.
open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
guard let evaluator = evaluators[host] else {
if allHostsMustBeEvaluated {
throw AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: host))
}
return nil
}
return evaluator
}
#endif
}
/// A protocol describing the API used to evaluate server trusts.
public protocol ServerTrustEvaluating {
#if !canImport(Security)
// Implement this once other platforms have API for evaluating server trusts.
#else
/// Evaluates the given `SecTrust` value for the given `host`.
///
/// - Parameters:
/// - trust: The `SecTrust` value to evaluate.
/// - host: The host for which to evaluate the `SecTrust` value.
///
/// - Returns: A `Bool` indicating whether the evaluator considers the `SecTrust` value valid for `host`.
func evaluate(_ trust: SecTrust, forHost host: String) throws
#endif
}
// MARK: - Server Trust Evaluators
#if canImport(Security)
/// An evaluator which uses the default server trust evaluation while allowing you to control whether to validate the
/// host provided by the challenge. Applications are encouraged to always validate the host in production environments
/// to guarantee the validity of the server's certificate chain.
public final class DefaultTrustEvaluator: ServerTrustEvaluating {
private let validateHost: Bool
/// Creates a `DefaultTrustEvaluator`.
///
/// - Parameter validateHost: Determines whether or not the evaluator should validate the host. `true` by default.
public init(validateHost: Bool = true) {
self.validateHost = validateHost
}
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
if validateHost {
try trust.af.performValidation(forHost: host)
}
try trust.af.performDefaultValidation(forHost: host)
}
}
/// An evaluator which Uses the default and revoked server trust evaluations allowing you to control whether to validate
/// the host provided by the challenge as well as specify the revocation flags for testing for revoked certificates.
/// Apple platforms did not start testing for revoked certificates automatically until iOS 10.1, macOS 10.12 and tvOS
/// 10.1 which is demonstrated in our TLS tests. Applications are encouraged to always validate the host in production
/// environments to guarantee the validity of the server's certificate chain.
public final class RevocationTrustEvaluator: ServerTrustEvaluating {
/// Represents the options to be use when evaluating the status of a certificate.
/// Only Revocation Policy Constants are valid, and can be found in [Apple's documentation](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/policies/1563600-revocation_policy_constants).
public struct Options: OptionSet {
/// Perform revocation checking using the CRL (Certification Revocation List) method.
public static let crl = Options(rawValue: kSecRevocationCRLMethod)
/// Consult only locally cached replies; do not use network access.
public static let networkAccessDisabled = Options(rawValue: kSecRevocationNetworkAccessDisabled)
/// Perform revocation checking using OCSP (Online Certificate Status Protocol).
public static let ocsp = Options(rawValue: kSecRevocationOCSPMethod)
/// Prefer CRL revocation checking over OCSP; by default, OCSP is preferred.
public static let preferCRL = Options(rawValue: kSecRevocationPreferCRL)
/// Require a positive response to pass the policy. If the flag is not set, revocation checking is done on a
/// "best attempt" basis, where failure to reach the server is not considered fatal.
public static let requirePositiveResponse = Options(rawValue: kSecRevocationRequirePositiveResponse)
/// Perform either OCSP or CRL checking. The checking is performed according to the method(s) specified in the
/// certificate and the value of `preferCRL`.
public static let any = Options(rawValue: kSecRevocationUseAnyAvailableMethod)
/// The raw value of the option.
public let rawValue: CFOptionFlags
/// Creates an `Options` value with the given `CFOptionFlags`.
///
/// - Parameter rawValue: The `CFOptionFlags` value to initialize with.
public init(rawValue: CFOptionFlags) {
self.rawValue = rawValue
}
}
private let performDefaultValidation: Bool
private let validateHost: Bool
private let options: Options
/// Creates a `RevocationTrustEvaluator` using the provided parameters.
///
/// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use
/// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates.
///
/// - Parameters:
/// - performDefaultValidation: Determines whether default validation should be performed in addition to
/// evaluating the pinned certificates. `true` by default.
/// - validateHost: Determines whether or not the evaluator should validate the host, in addition to
/// performing the default evaluation, even if `performDefaultValidation` is `false`.
/// `true` by default.
/// - options: The `Options` to use to check the revocation status of the certificate. `.any` by
/// default.
public init(performDefaultValidation: Bool = true, validateHost: Bool = true, options: Options = .any) {
self.performDefaultValidation = performDefaultValidation
self.validateHost = validateHost
self.options = options
}
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
if performDefaultValidation {
try trust.af.performDefaultValidation(forHost: host)
}
if validateHost {
try trust.af.performValidation(forHost: host)
}
#if swift(>=5.9)
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, visionOS 1, *) {
try trust.af.evaluate(afterApplying: SecPolicy.af.revocation(options: options))
} else {
try trust.af.validate(policy: SecPolicy.af.revocation(options: options)) { status, result in
AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options))
}
}
#else
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
try trust.af.evaluate(afterApplying: SecPolicy.af.revocation(options: options))
} else {
try trust.af.validate(policy: SecPolicy.af.revocation(options: options)) { status, result in
AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options))
}
}
#endif
}
}
extension ServerTrustEvaluating where Self == RevocationTrustEvaluator {
/// Provides a default `RevocationTrustEvaluator` instance.
public static var revocationChecking: RevocationTrustEvaluator { RevocationTrustEvaluator() }
/// Creates a `RevocationTrustEvaluator` using the provided parameters.
///
/// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use
/// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates.
///
/// - Parameters:
/// - performDefaultValidation: Determines whether default validation should be performed in addition to
/// evaluating the pinned certificates. `true` by default.
/// - validateHost: Determines whether or not the evaluator should validate the host, in addition
/// to performing the default evaluation, even if `performDefaultValidation` is
/// `false`. `true` by default.
/// - options: The `Options` to use to check the revocation status of the certificate. `.any`
/// by default.
/// - Returns: The `RevocationTrustEvaluator`.
public static func revocationChecking(performDefaultValidation: Bool = true,
validateHost: Bool = true,
options: RevocationTrustEvaluator.Options = .any) -> RevocationTrustEvaluator {
RevocationTrustEvaluator(performDefaultValidation: performDefaultValidation,
validateHost: validateHost,
options: options)
}
}
/// Uses the pinned certificates to validate the server trust. The server trust is considered valid if one of the pinned
/// certificates match one of the server certificates. By validating both the certificate chain and host, certificate
/// pinning provides a very secure form of server trust validation mitigating most, if not all, MITM attacks.
/// Applications are encouraged to always validate the host and require a valid certificate chain in production
/// environments.
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating {
private let certificates: [SecCertificate]
private let acceptSelfSignedCertificates: Bool
private let performDefaultValidation: Bool
private let validateHost: Bool
/// Creates a `PinnedCertificatesTrustEvaluator` from the provided parameters.
///
/// - Parameters:
/// - certificates: The certificates to use to evaluate the trust. All `cer`, `crt`, and `der`
/// certificates in `Bundle.main` by default.
/// - acceptSelfSignedCertificates: Adds the provided certificates as anchors for the trust evaluation, allowing
/// self-signed certificates to pass. `false` by default. THIS SETTING SHOULD BE
/// FALSE IN PRODUCTION!
/// - performDefaultValidation: Determines whether default validation should be performed in addition to
/// evaluating the pinned certificates. `true` by default.
/// - validateHost: Determines whether or not the evaluator should validate the host, in addition
/// to performing the default evaluation, even if `performDefaultValidation` is
/// `false`. `true` by default.
public init(certificates: [SecCertificate] = Bundle.main.af.certificates,
acceptSelfSignedCertificates: Bool = false,
performDefaultValidation: Bool = true,
validateHost: Bool = true) {
self.certificates = certificates
self.acceptSelfSignedCertificates = acceptSelfSignedCertificates
self.performDefaultValidation = performDefaultValidation
self.validateHost = validateHost
}
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
guard !certificates.isEmpty else {
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
if acceptSelfSignedCertificates {
try trust.af.setAnchorCertificates(certificates)
}
if performDefaultValidation {
try trust.af.performDefaultValidation(forHost: host)
}
if validateHost {
try trust.af.performValidation(forHost: host)
}
let serverCertificatesData = Set(trust.af.certificateData)
let pinnedCertificatesData = Set(certificates.af.data)
let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
if !pinnedCertificatesInServerData {
throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host,
trust: trust,
pinnedCertificates: certificates,
serverCertificates: trust.af.certificates))
}
}
}
extension ServerTrustEvaluating where Self == PinnedCertificatesTrustEvaluator {
/// Provides a default `PinnedCertificatesTrustEvaluator` instance.
public static var pinnedCertificates: PinnedCertificatesTrustEvaluator { PinnedCertificatesTrustEvaluator() }
/// Creates a `PinnedCertificatesTrustEvaluator` using the provided parameters.
///
/// - Parameters:
/// - certificates: The certificates to use to evaluate the trust. All `cer`, `crt`, and `der`
/// certificates in `Bundle.main` by default.
/// - acceptSelfSignedCertificates: Adds the provided certificates as anchors for the trust evaluation, allowing
/// self-signed certificates to pass. `false` by default. THIS SETTING SHOULD BE
/// FALSE IN PRODUCTION!
/// - performDefaultValidation: Determines whether default validation should be performed in addition to
/// evaluating the pinned certificates. `true` by default.
/// - validateHost: Determines whether or not the evaluator should validate the host, in addition
/// to performing the default evaluation, even if `performDefaultValidation` is
/// `false`. `true` by default.
public static func pinnedCertificates(certificates: [SecCertificate] = Bundle.main.af.certificates,
acceptSelfSignedCertificates: Bool = false,
performDefaultValidation: Bool = true,
validateHost: Bool = true) -> PinnedCertificatesTrustEvaluator {
PinnedCertificatesTrustEvaluator(certificates: certificates,
acceptSelfSignedCertificates: acceptSelfSignedCertificates,
performDefaultValidation: performDefaultValidation,
validateHost: validateHost)
}
}
/// Uses the pinned public keys to validate the server trust. The server trust is considered valid if one of the pinned
/// public keys match one of the server certificate public keys. By validating both the certificate chain and host,
/// public key pinning provides a very secure form of server trust validation mitigating most, if not all, MITM attacks.
/// Applications are encouraged to always validate the host and require a valid certificate chain in production
/// environments.
public final class PublicKeysTrustEvaluator: ServerTrustEvaluating {
private let keys: [SecKey]
private let performDefaultValidation: Bool
private let validateHost: Bool
/// Creates a `PublicKeysTrustEvaluator` from the provided parameters.
///
/// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use
/// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates.
///
/// - Parameters:
/// - keys: The `SecKey`s to use to validate public keys. Defaults to the public keys of all
/// certificates included in the main bundle.
/// - performDefaultValidation: Determines whether default validation should be performed in addition to
/// evaluating the pinned certificates. `true` by default.
/// - validateHost: Determines whether or not the evaluator should validate the host, in addition to
/// performing the default evaluation, even if `performDefaultValidation` is `false`.
/// `true` by default.
public init(keys: [SecKey] = Bundle.main.af.publicKeys,
performDefaultValidation: Bool = true,
validateHost: Bool = true) {
self.keys = keys
self.performDefaultValidation = performDefaultValidation
self.validateHost = validateHost
}
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
guard !keys.isEmpty else {
throw AFError.serverTrustEvaluationFailed(reason: .noPublicKeysFound)
}
if performDefaultValidation {
try trust.af.performDefaultValidation(forHost: host)
}
if validateHost {
try trust.af.performValidation(forHost: host)
}
let pinnedKeysInServerKeys: Bool = {
for serverPublicKey in trust.af.publicKeys {
if keys.contains(serverPublicKey) {
return true
}
}
return false
}()
if !pinnedKeysInServerKeys {
throw AFError.serverTrustEvaluationFailed(reason: .publicKeyPinningFailed(host: host,
trust: trust,
pinnedKeys: keys,
serverKeys: trust.af.publicKeys))
}
}
}
extension ServerTrustEvaluating where Self == PublicKeysTrustEvaluator {
/// Provides a default `PublicKeysTrustEvaluator` instance.
public static var publicKeys: PublicKeysTrustEvaluator { PublicKeysTrustEvaluator() }
/// Creates a `PublicKeysTrustEvaluator` from the provided parameters.
///
/// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use
/// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates.
///
/// - Parameters:
/// - keys: The `SecKey`s to use to validate public keys. Defaults to the public keys of all
/// certificates included in the main bundle.
/// - performDefaultValidation: Determines whether default validation should be performed in addition to
/// evaluating the pinned certificates. `true` by default.
/// - validateHost: Determines whether or not the evaluator should validate the host, in addition to
/// performing the default evaluation, even if `performDefaultValidation` is `false`.
/// `true` by default.
public static func publicKeys(keys: [SecKey] = Bundle.main.af.publicKeys,
performDefaultValidation: Bool = true,
validateHost: Bool = true) -> PublicKeysTrustEvaluator {
PublicKeysTrustEvaluator(keys: keys, performDefaultValidation: performDefaultValidation, validateHost: validateHost)
}
}
/// Uses the provided evaluators to validate the server trust. The trust is only considered valid if all of the
/// evaluators consider it valid.
public final class CompositeTrustEvaluator: ServerTrustEvaluating {
private let evaluators: [ServerTrustEvaluating]
/// Creates a `CompositeTrustEvaluator` from the provided evaluators.
///
/// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust.
public init(evaluators: [ServerTrustEvaluating]) {
self.evaluators = evaluators
}
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
try evaluators.evaluate(trust, forHost: host)
}
}
extension ServerTrustEvaluating where Self == CompositeTrustEvaluator {
/// Creates a `CompositeTrustEvaluator` from the provided evaluators.
///
/// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust.
public static func composite(evaluators: [ServerTrustEvaluating]) -> CompositeTrustEvaluator {
CompositeTrustEvaluator(evaluators: evaluators)
}
}
/// Disables all evaluation which in turn will always consider any server trust as valid.
///
/// - Note: Instead of disabling server trust evaluation, it's a better idea to configure systems to properly trust test
/// certificates, as outlined in [this Apple tech note](https://developer.apple.com/library/archive/qa/qa1948/_index.html).
///
/// **THIS EVALUATOR SHOULD NEVER BE USED IN PRODUCTION!**
@available(*, deprecated, renamed: "DisabledTrustEvaluator", message: "DisabledEvaluator has been renamed DisabledTrustEvaluator.")
public typealias DisabledEvaluator = DisabledTrustEvaluator
/// Disables all evaluation which in turn will always consider any server trust as valid.
///
///
/// - Note: Instead of disabling server trust evaluation, it's a better idea to configure systems to properly trust test
/// certificates, as outlined in [this Apple tech note](https://developer.apple.com/library/archive/qa/qa1948/_index.html).
///
/// **THIS EVALUATOR SHOULD NEVER BE USED IN PRODUCTION!**
public final class DisabledTrustEvaluator: ServerTrustEvaluating {
/// Creates an instance.
public init() {}
public func evaluate(_ trust: SecTrust, forHost host: String) throws {}
}
// MARK: - Extensions
extension [ServerTrustEvaluating] {
#if os(Linux) || os(Windows) || os(Android)
// Add this same convenience method for Linux/Windows.
#else
/// Evaluates the given `SecTrust` value for the given `host`.
///
/// - Parameters:
/// - trust: The `SecTrust` value to evaluate.
/// - host: The host for which to evaluate the `SecTrust` value.
///
/// - Returns: Whether or not the evaluator considers the `SecTrust` value valid for `host`.
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
for evaluator in self {
try evaluator.evaluate(trust, forHost: host)
}
}
#endif
}
extension Bundle: AlamofireExtended {}
extension AlamofireExtension where ExtendedType: Bundle {
/// Returns all valid `cer`, `crt`, and `der` certificates in the bundle.
public var certificates: [SecCertificate] {
paths(forResourcesOfTypes: [".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]).compactMap { path in
guard
let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData,
let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil }
return certificate
}
}
/// Returns all public keys for the valid certificates in the bundle.
public var publicKeys: [SecKey] {
certificates.af.publicKeys
}
/// Returns all pathnames for the resources identified by the provided file extensions.
///
/// - Parameter types: The filename extensions locate.
///
/// - Returns: All pathnames for the given filename extensions.
public func paths(forResourcesOfTypes types: [String]) -> [String] {
Array(Set(types.flatMap { type.paths(forResourcesOfType: $0, inDirectory: nil) }))
}
}
extension SecTrust: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == SecTrust {
/// Evaluates `self` after applying the `SecPolicy` value provided.
///
/// - Parameter policy: The `SecPolicy` to apply to `self` before evaluation.
///
/// - Throws: Any `Error` from applying the `SecPolicy` or from evaluation.
@available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *)
public func evaluate(afterApplying policy: SecPolicy) throws {
try apply(policy: policy).af.evaluate()
}
/// Attempts to validate `self` using the `SecPolicy` provided and transforming any error produced using the closure passed.
///
/// - Parameters:
/// - policy: The `SecPolicy` used to evaluate `self`.
/// - errorProducer: The closure used transform the failed `OSStatus` and `SecTrustResultType`.
/// - Throws: Any `Error` from applying the `policy`, or the result of `errorProducer` if validation fails.
@available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)")
@available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate(afterApplying:)")
@available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)")
@available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate(afterApplying:)")
public func validate(policy: SecPolicy, errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws {
try apply(policy: policy).af.validate(errorProducer: errorProducer)
}
/// Applies a `SecPolicy` to `self`, throwing if it fails.
///
/// - Parameter policy: The `SecPolicy`.
///
/// - Returns: `self`, with the policy applied.
/// - Throws: An `AFError.serverTrustEvaluationFailed` instance with a `.policyApplicationFailed` reason.
public func apply(policy: SecPolicy) throws -> SecTrust {
let status = SecTrustSetPolicies(type, policy)
guard status.af.isSuccess else {
throw AFError.serverTrustEvaluationFailed(reason: .policyApplicationFailed(trust: type,
policy: policy,
status: status))
}
return type
}
/// Evaluate `self`, throwing an `Error` if evaluation fails.
///
/// - Throws: `AFError.serverTrustEvaluationFailed` with reason `.trustValidationFailed` and associated error from
/// the underlying evaluation.
@available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *)
public func evaluate() throws {
var error: CFError?
let evaluationSucceeded = SecTrustEvaluateWithError(type, &error)
if !evaluationSucceeded {
throw AFError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: error))
}
}
/// Validate `self`, passing any failure values through `errorProducer`.
///
/// - Parameter errorProducer: The closure used to transform the failed `OSStatus` and `SecTrustResultType` into an
/// `Error`.
/// - Throws: The `Error` produced by the `errorProducer` closure.
@available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate()")
@available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate()")
@available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate()")
@available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate()")
public func validate(errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws {
var result = SecTrustResultType.invalid
let status = SecTrustEvaluate(type, &result)
guard status.af.isSuccess && result.af.isSuccess else {
throw errorProducer(status, result)
}
}
/// Sets a custom certificate chain on `self`, allowing full validation of a self-signed certificate and its chain.
///
/// - Parameter certificates: The `SecCertificate`s to add to the chain.
/// - Throws: Any error produced when applying the new certificate chain.
public func setAnchorCertificates(_ certificates: [SecCertificate]) throws {
// Add additional anchor certificates.
let status = SecTrustSetAnchorCertificates(type, certificates as CFArray)
guard status.af.isSuccess else {
throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: status,
certificates: certificates))
}
// Trust only the set anchor certs.
let onlyStatus = SecTrustSetAnchorCertificatesOnly(type, true)
guard onlyStatus.af.isSuccess else {
throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: onlyStatus,
certificates: certificates))
}
}
/// The public keys contained in `self`.
public var publicKeys: [SecKey] {
certificates.af.publicKeys
}
/// The `SecCertificate`s contained in `self`.
public var certificates: [SecCertificate] {
#if swift(>=5.9)
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1, *) {
return (SecTrustCopyCertificateChain(type) as? [SecCertificate]) ?? []
} else {
return (0..<SecTrustGetCertificateCount(type)).compactMap { index in
SecTrustGetCertificateAtIndex(type, index)
}
}
#else
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
return (SecTrustCopyCertificateChain(type) as? [SecCertificate]) ?? []
} else {
return (0..<SecTrustGetCertificateCount(type)).compactMap { index in
SecTrustGetCertificateAtIndex(type, index)
}
}
#endif
}
/// The `Data` values for all certificates contained in `self`.
public var certificateData: [Data] {
certificates.af.data
}
/// Validates `self` after applying `SecPolicy.af.default`. This evaluation does not validate the hostname.
///
/// - Parameter host: The hostname, used only in the error output if validation fails.
/// - Throws: An `AFError.serverTrustEvaluationFailed` instance with a `.defaultEvaluationFailed` reason.
public func performDefaultValidation(forHost host: String) throws {
#if swift(>=5.9)
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, visionOS 1, *) {
try evaluate(afterApplying: SecPolicy.af.default)
} else {
try validate(policy: SecPolicy.af.default) { status, result in
AFError.serverTrustEvaluationFailed(reason: .defaultEvaluationFailed(output: .init(host, type, status, result)))
}
}
#else
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
try evaluate(afterApplying: SecPolicy.af.default)
} else {
try validate(policy: SecPolicy.af.default) { status, result in
AFError.serverTrustEvaluationFailed(reason: .defaultEvaluationFailed(output: .init(host, type, status, result)))
}
}
#endif
}
/// Validates `self` after applying `SecPolicy.af.hostname(host)`, which performs the default validation as well as
/// hostname validation.
///
/// - Parameter host: The hostname to use in the validation.
/// - Throws: An `AFError.serverTrustEvaluationFailed` instance with a `.defaultEvaluationFailed` reason.
public func performValidation(forHost host: String) throws {
#if swift(>=5.9)
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, visionOS 1, *) {
try evaluate(afterApplying: SecPolicy.af.hostname(host))
} else {
try validate(policy: SecPolicy.af.hostname(host)) { status, result in
AFError.serverTrustEvaluationFailed(reason: .hostValidationFailed(output: .init(host, type, status, result)))
}
}
#else
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
try evaluate(afterApplying: SecPolicy.af.hostname(host))
} else {
try validate(policy: SecPolicy.af.hostname(host)) { status, result in
AFError.serverTrustEvaluationFailed(reason: .hostValidationFailed(output: .init(host, type, status, result)))
}
}
#endif
}
}
extension SecPolicy: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == SecPolicy {
/// Creates a `SecPolicy` instance which will validate server certificates but not require a host name match.
public static let `default` = SecPolicyCreateSSL(true, nil)
/// Creates a `SecPolicy` instance which will validate server certificates and much match the provided hostname.
///
/// - Parameter hostname: The hostname to validate against.
///
/// - Returns: The `SecPolicy`.
public static func hostname(_ hostname: String) -> SecPolicy {
SecPolicyCreateSSL(true, hostname as CFString)
}
/// Creates a `SecPolicy` which checks the revocation of certificates.
///
/// - Parameter options: The `RevocationTrustEvaluator.Options` for evaluation.
///
/// - Returns: The `SecPolicy`.
/// - Throws: An `AFError.serverTrustEvaluationFailed` error with reason `.revocationPolicyCreationFailed`
/// if the policy cannot be created.
public static func revocation(options: RevocationTrustEvaluator.Options) throws -> SecPolicy {
guard let policy = SecPolicyCreateRevocation(options.rawValue) else {
throw AFError.serverTrustEvaluationFailed(reason: .revocationPolicyCreationFailed)
}
return policy
}
}
extension Array: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == [SecCertificate] {
/// All `Data` values for the contained `SecCertificate`s.
public var data: [Data] {
type.map { SecCertificateCopyData($0) as Data }
}
/// All public `SecKey` values for the contained `SecCertificate`s.
public var publicKeys: [SecKey] {
type.compactMap(\.af.publicKey)
}
}
extension SecCertificate: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == SecCertificate {
/// The public key for `self`, if it can be extracted.
///
/// - Note: On 2020 OSes and newer, only RSA and ECDSA keys are supported.
///
public var publicKey: SecKey? {
let policy = SecPolicyCreateBasicX509()
var trust: SecTrust?
let trustCreationStatus = SecTrustCreateWithCertificates(type, policy, &trust)
guard let createdTrust = trust, trustCreationStatus == errSecSuccess else { return nil }
#if swift(>=5.9)
if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) {
return SecTrustCopyKey(createdTrust)
} else {
return SecTrustCopyPublicKey(createdTrust)
}
#else
if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) {
return SecTrustCopyKey(createdTrust)
} else {
return SecTrustCopyPublicKey(createdTrust)
}
#endif
}
}
extension OSStatus: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == OSStatus {
/// Returns whether `self` is `errSecSuccess`.
public var isSuccess: Bool { type == errSecSuccess }
}
extension SecTrustResultType: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == SecTrustResultType {
/// Returns whether `self` is `.unspecified` or `.proceed`.
public var isSuccess: Bool {
type == .unspecified || type == .proceed
}
}
#endif
File diff suppressed because it is too large Load Diff
+302
View File
@@ -0,0 +1,302 @@
//
// Validation.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
extension Request {
// MARK: Helper Types
fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason
/// Used to represent whether a validation succeeded or failed.
public typealias ValidationResult = Result<Void, Error>
fileprivate struct MIMEType {
let type: String
let subtype: String
var isWildcard: Bool { type == "*" && subtype == "*" }
init?(_ string: String) {
let components: [String] = {
let stripped = string.trimmingCharacters(in: .whitespacesAndNewlines)
let split = stripped[..<(stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)]
return split.components(separatedBy: "/")
}()
if let type = components.first, let subtype = components.last {
self.type = type
self.subtype = subtype
} else {
return nil
}
}
func matches(_ mime: MIMEType) -> Bool {
switch (type, subtype) {
case (mime.type, mime.subtype), (mime.type, "*"), ("*", mime.subtype), ("*", "*"):
return true
default:
return false
}
}
}
// MARK: Properties
fileprivate var acceptableStatusCodes: Range<Int> { 200..<300 }
fileprivate var acceptableContentTypes: [String] {
if let accept = request?.value(forHTTPHeaderField: "Accept") {
return accept.components(separatedBy: ",")
}
return ["*/*"]
}
// MARK: Status Code
fileprivate func validate<S: Sequence>(statusCode acceptableStatusCodes: S,
response: HTTPURLResponse)
-> ValidationResult
where S.Iterator.Element == Int {
if acceptableStatusCodes.contains(response.statusCode) {
return .success(())
} else {
let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode)
return .failure(AFError.responseValidationFailed(reason: reason))
}
}
// MARK: Content Type
fileprivate func validate<S: Sequence>(contentType acceptableContentTypes: S,
response: HTTPURLResponse,
data: Data?)
-> ValidationResult
where S.Iterator.Element == String {
guard let data, !data.isEmpty else { return .success(()) }
return validate(contentType: acceptableContentTypes, response: response)
}
fileprivate func validate<S: Sequence>(contentType acceptableContentTypes: S,
response: HTTPURLResponse)
-> ValidationResult
where S.Iterator.Element == String {
guard
let responseContentType = response.mimeType,
let responseMIMEType = MIMEType(responseContentType)
else {
for contentType in acceptableContentTypes {
if let mimeType = MIMEType(contentType), mimeType.isWildcard {
return .success(())
}
}
let error: AFError = {
let reason: ErrorReason = .missingContentType(acceptableContentTypes: acceptableContentTypes.sorted())
return AFError.responseValidationFailed(reason: reason)
}()
return .failure(error)
}
for contentType in acceptableContentTypes {
if let acceptableMIMEType = MIMEType(contentType), acceptableMIMEType.matches(responseMIMEType) {
return .success(())
}
}
let error: AFError = {
let reason: ErrorReason = .unacceptableContentType(acceptableContentTypes: acceptableContentTypes.sorted(),
responseContentType: responseContentType)
return AFError.responseValidationFailed(reason: reason)
}()
return .failure(error)
}
}
// MARK: -
extension DataRequest {
/// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the
/// request was valid.
public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult
/// Validates that the response has a status code in the specified sequence.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes.
///
/// - Returns: The instance.
@discardableResult
public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
validate { [unowned self] _, response, _ in
self.validate(statusCode: acceptableStatusCodes, response: response)
}
}
/// Validates that the response has a content type in the specified sequence.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
///
/// - returns: The request.
@discardableResult
public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String {
validate { [unowned self] _, response, data in
self.validate(contentType: acceptableContentTypes(), response: response, data: data)
}
}
/// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
/// type matches any specified in the Accept HTTP header field.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - returns: The request.
@discardableResult
public func validate() -> Self {
let contentTypes: () -> [String] = { [unowned self] in
acceptableContentTypes
}
return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())
}
}
extension DataStreamRequest {
/// A closure used to validate a request that takes a `URLRequest` and `HTTPURLResponse` and returns whether the
/// request was valid.
public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse) -> ValidationResult
/// Validates that the response has a status code in the specified sequence.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes.
///
/// - Returns: The instance.
@discardableResult
public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
validate { [unowned self] _, response in
self.validate(statusCode: acceptableStatusCodes, response: response)
}
}
/// Validates that the response has a content type in the specified sequence.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
///
/// - returns: The request.
@discardableResult
public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String {
validate { [unowned self] _, response in
self.validate(contentType: acceptableContentTypes(), response: response)
}
}
/// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
/// type matches any specified in the Accept HTTP header field.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - Returns: The instance.
@discardableResult
public func validate() -> Self {
let contentTypes: () -> [String] = { [unowned self] in
acceptableContentTypes
}
return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())
}
}
// MARK: -
extension DownloadRequest {
/// A closure used to validate a request that takes a URL request, a URL response, a temporary URL and a
/// destination URL, and returns whether the request was valid.
public typealias Validation = (_ request: URLRequest?,
_ response: HTTPURLResponse,
_ fileURL: URL?)
-> ValidationResult
/// Validates that the response has a status code in the specified sequence.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes.
///
/// - Returns: The instance.
@discardableResult
public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
validate { [unowned self] _, response, _ in
self.validate(statusCode: acceptableStatusCodes, response: response)
}
}
/// Validates that the response has a content type in the specified sequence.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
///
/// - returns: The request.
@discardableResult
public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String {
validate { [unowned self] _, response, fileURL in
guard let validFileURL = fileURL else {
return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
}
do {
let data = try Data(contentsOf: validFileURL)
return self.validate(contentType: acceptableContentTypes(), response: response, data: data)
} catch {
return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: validFileURL)))
}
}
}
/// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
/// type matches any specified in the Accept HTTP header field.
///
/// If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - returns: The request.
@discardableResult
public func validate() -> Self {
let contentTypes = { [unowned self] in
acceptableContentTypes
}
return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())
}
}