Signal-iOS/SignalServiceKit/Network/OWSUrlSession.swift
2024-04-11 22:19:16 -05:00

1185 lines
46 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalCoreKit
public class OWSURLSession: NSObject, OWSURLSessionProtocol {
// MARK: - OWSURLSessionProtocol conformance
public let endpoint: OWSURLSessionEndpoint
public var failOnError: Bool {
get {
_failOnError.get()
}
set {
_failOnError.set(newValue)
}
}
public var require2xxOr3xx: Bool {
get {
_require2xxOr3xx.get()
}
set {
_require2xxOr3xx.set(newValue)
}
}
public var shouldHandleRemoteDeprecation: Bool {
get {
_shouldHandleRemoteDeprecation.get()
}
set {
_shouldHandleRemoteDeprecation.set(newValue)
}
}
public var allowRedirects: Bool {
get {
_allowRedirects.get()
}
set {
owsAssertDebug(customRedirectHandler == nil || newValue)
_allowRedirects.set(newValue)
}
}
public var customRedirectHandler: ((URLRequest) -> URLRequest?)? {
get {
_customRedirectHandler.get()
}
set {
owsAssertDebug(newValue == nil || allowRedirects)
_customRedirectHandler.set(newValue)
}
}
// Note: not all protocol methods can be made visible to objc, but those
// that can be are declared so here. Objc callers must use this implementation
// directly and not touch the protocol.
public static var defaultSecurityPolicy: OWSHTTPSecurityPolicy {
OWSHTTPSecurityPolicy.systemDefault()
}
public static var signalServiceSecurityPolicy: OWSHTTPSecurityPolicy {
OWSHTTPSecurityPolicy.shared()
}
public static var defaultConfigurationWithCaching: URLSessionConfiguration {
.ephemeral
}
public static var defaultConfigurationWithoutCaching: URLSessionConfiguration {
let configuration = URLSessionConfiguration.ephemeral
configuration.urlCache = nil
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
return configuration
}
// MARK: Default Headers
public static var userAgentHeaderKey: String { OWSHttpHeaders.userAgentHeaderKey }
public static var userAgentHeaderValueSignalIos: String { OWSHttpHeaders.userAgentHeaderValueSignalIos }
public static var acceptLanguageHeaderKey: String { OWSHttpHeaders.acceptLanguageHeaderKey }
public static var acceptLanguageHeaderValue: String { OWSHttpHeaders.acceptLanguageHeaderValue }
// MARK: Initializers
required public init(
endpoint: OWSURLSessionEndpoint,
configuration: URLSessionConfiguration,
maxResponseSize: Int?,
canUseSignalProxy: Bool
) {
if canUseSignalProxy {
configuration.connectionProxyDictionary = SignalProxy.connectionProxyDictionary
}
self.endpoint = endpoint
self.configuration = configuration
self.maxResponseSize = maxResponseSize
self.canUseSignalProxy = canUseSignalProxy
super.init()
// Ensure this is set so that we don't try to create it in deinit().
_ = self.delegateBox
}
convenience public init(
securityPolicy: OWSHTTPSecurityPolicy,
configuration: URLSessionConfiguration
) {
self.init(
endpoint: OWSURLSessionEndpoint(
baseUrl: nil,
frontingInfo: nil,
securityPolicy: securityPolicy,
extraHeaders: [:]
),
configuration: configuration,
maxResponseSize: nil,
canUseSignalProxy: false
)
}
convenience public init(
baseUrl: URL? = nil,
securityPolicy: OWSHTTPSecurityPolicy,
configuration: URLSessionConfiguration,
extraHeaders: [String: String] = [:],
maxResponseSize: Int? = nil,
canUseSignalProxy: Bool = false
) {
self.init(
endpoint: OWSURLSessionEndpoint(
baseUrl: baseUrl,
frontingInfo: nil,
securityPolicy: securityPolicy,
extraHeaders: extraHeaders
),
configuration: configuration,
maxResponseSize: maxResponseSize,
canUseSignalProxy: canUseSignalProxy
)
}
// MARK: Tasks
public func uploadTaskPromise(
request: URLRequest,
data requestData: Data,
progress progressBlock: ProgressBlock?
) -> Promise<HTTPResponse> {
let uploadTaskBuilder = UploadTaskBuilderData(requestData: requestData)
return uploadTaskPromise(request: request, uploadTaskBuilder: uploadTaskBuilder, progress: progressBlock)
}
public func uploadTaskPromise(
request: URLRequest,
fileUrl: URL,
ignoreAppExpiry: Bool,
progress progressBlock: ProgressBlock?
) -> Promise<HTTPResponse> {
let uploadTaskBuilder = UploadTaskBuilderFileUrl(fileUrl: fileUrl)
return uploadTaskPromise(
request: request,
uploadTaskBuilder: uploadTaskBuilder,
ignoreAppExpiry: ignoreAppExpiry,
progress: progressBlock
)
}
public func dataTaskPromise(request: URLRequest, ignoreAppExpiry: Bool = false) -> Promise<HTTPResponse> {
if !ignoreAppExpiry && DependenciesBridge.shared.appExpiry.isExpired {
return Promise(error: OWSAssertionError("App is expired."))
}
let request = prepareRequest(request: request)
let taskState = UploadOrDataTaskState(progressBlock: nil)
var requestConfig: RequestConfig?
let task = session.dataTask(with: request) { [weak self] (responseData: Data?, urlResponse: URLResponse?, _: Error?) in
guard let self = self else {
owsFailDebug("Missing session.")
return
}
guard let requestConfig = requestConfig else {
owsFailDebug("Missing requestConfig.")
return
}
if let responseData = responseData,
let maxResponseSize = self.maxResponseSize {
guard responseData.count <= maxResponseSize else {
self.taskDidFail(requestConfig.task, error: OWSGenericError("Oversize download."))
return
}
}
self.uploadOrDataTaskDidSucceed(requestConfig.task, httpUrlResponse: urlResponse as? HTTPURLResponse, responseData: responseData)
}
addTask(task, taskState: taskState)
guard let requestUrl = request.url else {
owsFail("Request missing url.")
}
requestConfig = self.requestConfig(forTask: task, requestUrl: requestUrl)
task.resume()
return firstly { () -> Promise<(URLSessionTask, Data?)> in
taskState.promise
}.then(on: DispatchQueue.global()) { (_, responseData: Data?) -> Promise<HTTPResponse> in
guard let requestConfig = requestConfig else {
throw OWSAssertionError("Missing requestConfig.")
}
return Self.uploadOrDataTaskCompletionPromise(requestConfig: requestConfig, responseData: responseData)
}
}
@available(swift, obsoleted: 1.0)
func dataTask(_ urlString: String,
method: HTTPMethod,
headers: [String: String]?,
body: Data? = nil,
success: @escaping (HTTPResponse) -> Void,
failure: @escaping (Error) -> Void) {
firstly(on: DispatchQueue.global()) { () -> Promise<HTTPResponse> in
self.dataTaskPromise(urlString, method: method, headers: headers, body: body)
}.done(on: DispatchQueue.global()) { response in
success(response)
}.catch(on: DispatchQueue.global()) { error in
failure(error)
}
}
public func downloadTaskPromise(
request: URLRequest,
progress progressBlock: ProgressBlock?
) -> Promise<OWSUrlDownloadResponse> {
let request = prepareRequest(request: request)
guard let requestUrl = request.url else {
return Promise(error: OWSAssertionError("Request missing url."))
}
return downloadTaskPromise(requestUrl: requestUrl, progress: progressBlock) {
// Don't use a completion block or the delegate will be ignored for download tasks.
session.downloadTask(with: request)
}
}
public func downloadTaskPromise(
requestUrl: URL,
resumeData: Data,
progress progressBlock: ProgressBlock?
) -> Promise<OWSUrlDownloadResponse> {
downloadTaskPromise(requestUrl: requestUrl, progress: progressBlock) {
// Don't use a completion block or the delegate will be ignored for download tasks.
session.downloadTask(withResumeData: resumeData)
}
}
// MARK: - Internal Implementation
private static let operationQueue: OperationQueue = {
let queue = OperationQueue()
queue.underlyingQueue = .global()
return queue
}()
// MARK: Backing Vars
private let _failOnError = AtomicBool(true, lock: .sharedGlobal)
private let _require2xxOr3xx = AtomicBool(true, lock: .sharedGlobal)
private let _shouldHandleRemoteDeprecation = AtomicBool(false, lock: .sharedGlobal)
private let _allowRedirects = AtomicBool(true, lock: .sharedGlobal)
private let _customRedirectHandler = AtomicOptional<(URLRequest) -> URLRequest?>(nil, lock: .sharedGlobal)
// MARK: Internal vars
private let configuration: URLSessionConfiguration
private lazy var session: URLSession = {
URLSession(configuration: configuration, delegate: delegateBox, delegateQueue: Self.operationQueue)
}()
private let maxResponseSize: Int?
private let canUseSignalProxy: Bool
// MARK: Deinit
deinit {
// From NSURLSession.h
// If you do not invalidate the session by calling the invalidateAndCancel() or
// finishTasksAndInvalidate() method, your app leaks memory until it exits
//
// Even though there will be no reference cycle, underlying NSURLSession metadata
// is malloced and kept around as a root leak.
session.invalidateAndCancel()
}
// MARK: Configuration
private struct RequestConfig {
let task: URLSessionTask
let requestUrl: URL
let require2xxOr3xx: Bool
let failOnError: Bool
let shouldHandleRemoteDeprecation: Bool
}
private func requestConfig(forTask task: URLSessionTask, requestUrl: URL) -> RequestConfig {
// Snapshot session state at time request is made.
RequestConfig(task: task,
requestUrl: requestUrl,
require2xxOr3xx: require2xxOr3xx,
failOnError: failOnError,
shouldHandleRemoteDeprecation: shouldHandleRemoteDeprecation)
}
private class func uploadOrDataTaskCompletionPromise(requestConfig: RequestConfig,
responseData: Data?,
monitorId: UInt64? = nil) -> Promise<HTTPResponse> {
firstly {
baseCompletionPromise(requestConfig: requestConfig, responseData: responseData, monitorId: monitorId)
}.map(on: DispatchQueue.global()) { (httpUrlResponse: HTTPURLResponse) -> HTTPResponse in
HTTPResponseImpl.build(requestUrl: requestConfig.requestUrl,
httpUrlResponse: httpUrlResponse,
bodyData: responseData)
}
}
private class func downloadTaskCompletionPromise(requestConfig: RequestConfig,
downloadUrl: URL,
monitorId: UInt64? = nil) -> Promise<OWSUrlDownloadResponse> {
firstly {
baseCompletionPromise(requestConfig: requestConfig, responseData: nil, monitorId: monitorId)
}.map(on: DispatchQueue.global()) { (httpUrlResponse: HTTPURLResponse) -> OWSUrlDownloadResponse in
return OWSUrlDownloadResponse(task: requestConfig.task,
httpUrlResponse: httpUrlResponse,
downloadUrl: downloadUrl)
}
}
private class func baseCompletionPromise(requestConfig: RequestConfig,
responseData: Data?,
monitorId: UInt64? = nil) -> Promise<HTTPURLResponse> {
firstly(on: DispatchQueue.global()) { () -> HTTPURLResponse in
let task = requestConfig.task
if requestConfig.shouldHandleRemoteDeprecation {
checkForRemoteDeprecation(task: task, response: task.response)
}
if let error = task.error {
let requestUrl = requestConfig.requestUrl
if error.isNetworkFailureOrTimeout {
Logger.warn("Request failed: \(error)")
throw OWSHTTPError.networkFailure(requestUrl: requestUrl)
} else {
#if TESTABLE_BUILD
HTTPUtils.logCurl(for: task)
if let responseData = responseData,
let httpUrlResponse = task.response as? HTTPURLResponse,
let contentType = httpUrlResponse.allHeaderFields["Content-Type"] as? String,
contentType == MimeType.applicationJson.rawValue,
let jsonString = String(data: responseData, encoding: .utf8) {
Logger.verbose("Response JSON: \(jsonString)")
}
#endif
if requestConfig.failOnError,
!error.isUnknownDomainError {
owsFailDebugUnlessNetworkFailure(error)
} else {
Logger.error("Request failed: \(error)")
}
}
guard let httpUrlResponse = task.response as? HTTPURLResponse else {
throw OWSHTTPError.invalidResponse(requestUrl: requestUrl)
}
let statusCode = httpUrlResponse.statusCode
let responseHeaders = OWSHttpHeaders(response: httpUrlResponse)
throw OWSHTTPError.forServiceResponse(requestUrl: requestUrl,
responseStatus: statusCode,
responseHeaders: responseHeaders,
responseError: error,
responseData: responseData)
}
guard let httpUrlResponse = task.response as? HTTPURLResponse else {
throw OWSAssertionError("Invalid response: \(type(of: task.response)).")
}
if requestConfig.require2xxOr3xx {
let statusCode = httpUrlResponse.statusCode
guard statusCode >= 200, statusCode < 400 else {
#if TESTABLE_BUILD
HTTPUtils.logCurl(for: task)
Logger.verbose("Status code: \(statusCode)")
#endif
let requestUrl = requestConfig.requestUrl
if statusCode > 0 {
let responseHeaders = OWSHttpHeaders(response: httpUrlResponse)
let error = OWSHTTPError.forServiceResponse(requestUrl: requestUrl,
responseStatus: statusCode,
responseHeaders: responseHeaders,
responseError: nil,
responseData: responseData)
Logger.warn("Request failed: \(error)")
throw error
} else {
owsFailDebug("Missing status code.")
let error = OWSHTTPError.networkFailure(requestUrl: requestUrl)
Logger.warn("Request failed: \(error)")
throw error
}
}
}
#if TESTABLE_BUILD
if DebugFlags.logCurlOnSuccess {
HTTPUtils.logCurl(for: task)
}
#endif
return httpUrlResponse
}
}
private class func checkForRemoteDeprecation(task: URLSessionTask,
response: URLResponse?) {
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == AppExpiryImpl.appExpiredStatusCode else {
return
}
let appExpiry = DependenciesBridge.shared.appExpiry
let db = DependenciesBridge.shared.db
appExpiry.setHasAppExpiredAtCurrentVersion(db: db)
}
// MARK: Request building
// Ensure certain invariants for all requests.
private func prepareRequest(request: URLRequest) -> URLRequest {
var request = request
request.httpShouldHandleCookies = false
request = OWSHttpHeaders.fillInMissingDefaultHeaders(request: request)
// Only requests to Signal services require CC.
// If frontingHost is nil, this instance of OWSURLSession does not perform CC.
if let frontingInfo = endpoint.frontingInfo, let urlString = request.url?.absoluteString.nilIfEmpty {
owsAssertDebug(frontingInfo.isFrontedUrl(urlString), "Unfronted URL: \(urlString)")
}
return request
}
// MARK: - Issuing Requests
public func promiseForTSRequest(_ rawRequest: TSRequest) -> Promise<HTTPResponse> {
guard let rawRequestUrl = rawRequest.url else {
owsFailDebug("Missing requestUrl.")
return Promise(error: OWSHTTPError.missingRequest)
}
let appExpiry = DependenciesBridge.shared.appExpiry
guard !appExpiry.isExpired else {
owsFailDebug("App is expired.")
return Promise(error: OWSHTTPError.invalidAppState(requestUrl: rawRequestUrl))
}
let httpHeaders = OWSHttpHeaders()
// Set User-Agent and Accept-Language headers.
httpHeaders.addDefaultHeaders()
// Then apply any custom headers for the request
httpHeaders.addHeaderMap(rawRequest.allHTTPHeaderFields, overwriteOnConflict: true)
if !rawRequest.isUDRequest,
rawRequest.shouldHaveAuthorizationHeaders {
owsAssertDebug(nil != rawRequest.authUsername?.nilIfEmpty)
owsAssertDebug(nil != rawRequest.authPassword?.nilIfEmpty)
do {
try httpHeaders.addAuthHeader(username: rawRequest.authUsername ?? "",
password: rawRequest.authPassword ?? "")
} catch {
owsFailDebug("Could not add auth header: \(error).")
return Promise(error: OWSHTTPError.invalidAppState(requestUrl: rawRequestUrl))
}
}
let method: HTTPMethod
do {
method = try HTTPMethod.method(for: rawRequest.httpMethod)
} catch {
owsFailDebug("Invalid HTTP method: \(rawRequest.httpMethod)")
return Promise(error: OWSHTTPError.invalidRequest(requestUrl: rawRequestUrl))
}
var requestBody = Data()
if let httpBody = rawRequest.httpBody {
owsAssertDebug(rawRequest.parameters.isEmpty)
requestBody = httpBody
} else if !rawRequest.parameters.isEmpty {
let jsonData: Data?
do {
jsonData = try JSONSerialization.data(withJSONObject: rawRequest.parameters, options: [])
} catch {
owsFailDebug("Could not serialize JSON parameters: \(error).")
return Promise(error: OWSHTTPError.invalidRequest(requestUrl: rawRequestUrl))
}
if let jsonData = jsonData {
requestBody = jsonData
// If we're going to use the json serialized parameters as our body, we should overwrite
// the Content-Type on the request.
httpHeaders.addHeader("Content-Type",
value: "application/json",
overwriteOnConflict: true)
}
}
let urlSession = self
let request: URLRequest
do {
request = try urlSession.endpoint.buildRequest(
rawRequestUrl.absoluteString,
method: method,
headers: httpHeaders.headers,
body: requestBody
)
} catch {
owsFailDebug("Missing or invalid request: \(rawRequestUrl).")
return Promise(error: OWSHTTPError.invalidRequest(requestUrl: rawRequestUrl))
}
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)")
return firstly(on: DispatchQueue.global()) { () throws -> Promise<HTTPResponse> in
urlSession.uploadTaskPromise(request: request, data: requestBody, progress: nil)
}.map(on: DispatchQueue.global()) { (response: HTTPResponse) -> HTTPResponse in
Logger.info("Success: \(rawRequest.description)")
return response
}.ensure(on: DispatchQueue.global()) {
owsAssertDebug(backgroundTask != nil)
backgroundTask = nil
}.recover(on: DispatchQueue.global()) { error -> Promise<HTTPResponse> in
Logger.warn("Failure: \(rawRequest.description), error: \(error)")
throw error
}
}
private func uploadTaskPromise(
request: URLRequest,
uploadTaskBuilder: UploadTaskBuilder,
ignoreAppExpiry: Bool = false,
progress progressBlock: ProgressBlock? = nil
) -> Promise<HTTPResponse> {
if !ignoreAppExpiry && DependenciesBridge.shared.appExpiry.isExpired {
return Promise(error: OWSAssertionError("App is expired."))
}
let request = prepareRequest(request: request)
let taskState = UploadOrDataTaskState(progressBlock: progressBlock)
var requestConfig: RequestConfig?
let task = uploadTaskBuilder.build(session: session, request: request) { [weak self] (responseData: Data?, urlResponse: URLResponse?, _: Error?) in
guard let requestConfig = requestConfig else {
owsFailDebug("Missing requestConfig.")
return
}
self?.uploadOrDataTaskDidSucceed(requestConfig.task, httpUrlResponse: urlResponse as? HTTPURLResponse, responseData: responseData)
}
addTask(task, taskState: taskState)
guard let requestUrl = request.url else {
owsFail("Request missing url.")
}
requestConfig = self.requestConfig(forTask: task, requestUrl: requestUrl)
task.resume()
return firstly { () -> Promise<(URLSessionTask, Data?)> in
taskState.promise
}.then(on: DispatchQueue.global()) { (_, responseData: Data?) -> Promise<HTTPResponse> in
guard let requestConfig = requestConfig else {
throw OWSAssertionError("Missing requestConfig.")
}
return Self.uploadOrDataTaskCompletionPromise(requestConfig: requestConfig, responseData: responseData)
}
}
private func downloadTaskPromise(
requestUrl: URL,
progress progressBlock: ProgressBlock? = nil,
taskBlock: () -> URLSessionDownloadTask
) -> Promise<OWSUrlDownloadResponse> {
let appExpiry = DependenciesBridge.shared.appExpiry
if appExpiry.isExpired {
return Promise(error: OWSAssertionError("App is expired."))
}
let taskState = DownloadTaskState(progressBlock: progressBlock)
var requestConfig: RequestConfig?
let task = taskBlock()
addTask(task, taskState: taskState)
requestConfig = self.requestConfig(forTask: task, requestUrl: requestUrl)
task.resume()
return firstly { () -> Promise<(URLSessionTask, URL)> in
taskState.promise
}.then(on: DispatchQueue.global()) { (_: URLSessionTask, downloadUrl: URL) -> Promise<OWSUrlDownloadResponse> in
guard let requestConfig = requestConfig else {
throw OWSAssertionError("Missing requestConfig.")
}
return Self.downloadTaskCompletionPromise(requestConfig: requestConfig, downloadUrl: downloadUrl)
}
}
// MARK: - TaskState
private let lock = UnfairLock()
lazy private var delegateBox = URLSessionDelegateBox(delegate: self)
typealias TaskIdentifier = Int
private typealias TaskStateMap = [TaskIdentifier: TaskState]
private var taskStateMap = TaskStateMap() {
didSet {
delegateBox.isRetaining = (taskStateMap.count > 0)
}
}
private func addTask(_ task: URLSessionTask, taskState: TaskState) {
lock.withLock {
owsAssertDebug(self.taskStateMap[task.taskIdentifier] == nil)
self.taskStateMap[task.taskIdentifier] = taskState
}
}
private func progressBlock(forTask task: URLSessionTask) -> ProgressBlock? {
lock.withLock {
self.taskStateMap[task.taskIdentifier]?.progressBlock
}
}
private func webSocketState(forTask task: URLSessionTask) -> WebSocketTaskState? {
lock.withLock {
self.taskStateMap[task.taskIdentifier] as? WebSocketTaskState
}
}
private func removeCompletedTaskState(_ task: URLSessionTask) -> TaskState? {
lock.withLock { () -> TaskState? in
guard let taskState = self.taskStateMap[task.taskIdentifier] else {
// This isn't necessarily an error or bug.
// A task might "succeed" after it "fails" in certain edge cases,
// although we make a best effort to avoid them.
Logger.warn("Missing TaskState.")
return nil
}
self.taskStateMap[task.taskIdentifier] = nil
return taskState
}
}
private func downloadTaskDidSucceed(_ task: URLSessionTask, downloadUrl: URL) {
guard let taskState = removeCompletedTaskState(task) as? DownloadTaskState else {
owsFailDebug("Missing TaskState.")
return
}
taskState.future.resolve((task, downloadUrl))
}
private func uploadOrDataTaskDidSucceed(_ task: URLSessionTask, httpUrlResponse: HTTPURLResponse?, responseData: Data?, monitorId: UInt64? = nil) {
guard let taskState = removeCompletedTaskState(task) as? UploadOrDataTaskState else {
owsFailDebug("Missing TaskState.")
return
}
taskState.future.resolve((task, responseData))
}
private func taskDidFail(_ task: URLSessionTask, error: Error) {
guard let taskState = removeCompletedTaskState(task) else {
Logger.warn("Missing TaskState.")
return
}
taskState.reject(error: error, task: task)
task.cancel()
}
// MARK: -
public typealias URLAuthenticationChallengeCompletion = (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
fileprivate func urlSession(didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping URLAuthenticationChallengeCompletion) {
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
var credential: URLCredential?
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust {
if endpoint.securityPolicy.evaluateServerTrust(serverTrust, forDomain: challenge.protectionSpace.host) {
credential = URLCredential(trust: serverTrust)
disposition = .useCredential
} else {
disposition = .cancelAuthenticationChallenge
}
} else {
disposition = .performDefaultHandling
}
completionHandler(disposition, credential)
}
}
// MARK: - URLSessionDelegate
extension OWSURLSession: URLSessionDelegate {
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
Logger.info("Error: \(error)")
taskDidFail(task, error: error)
}
}
public func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping URLAuthenticationChallengeCompletion) {
urlSession(didReceive: challenge, completionHandler: completionHandler)
}
public func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void) {
guard allowRedirects else { return completionHandler(nil) }
if let customRedirectHandler = customRedirectHandler {
completionHandler(customRedirectHandler(newRequest))
} else {
completionHandler(newRequest)
}
}
}
// MARK: - URLSessionTaskDelegate
extension OWSURLSession: URLSessionTaskDelegate {
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping URLAuthenticationChallengeCompletion) {
urlSession(didReceive: challenge, completionHandler: completionHandler)
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
guard let progressBlock = self.progressBlock(forTask: task) else {
return
}
let progress = Progress(parent: nil, userInfo: nil)
// TODO: We could check for NSURLSessionTransferSizeUnknown here.
progress.totalUnitCount = totalBytesExpectedToSend
progress.completedUnitCount = totalBytesSent
progressBlock(task, progress)
}
}
// MARK: - URLSessionDownloadDelegate
extension OWSURLSession: URLSessionDownloadDelegate {
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let maxResponseSize = maxResponseSize {
guard let fileSize = OWSFileSystem.fileSize(of: location) else {
taskDidFail(downloadTask, error: OWSAssertionError("Unknown download size."))
return
}
guard fileSize.intValue <= maxResponseSize else {
taskDidFail(downloadTask, error: OWSAssertionError("Oversize download."))
return
}
}
do {
// Download locations are cleaned up quickly, so we
// need to move the file synchronously.
let temporaryUrl = OWSFileSystem.temporaryFileUrl(fileExtension: nil,
isAvailableWhileDeviceLocked: true)
try OWSFileSystem.moveFile(from: location, to: temporaryUrl)
downloadTaskDidSucceed(downloadTask, downloadUrl: temporaryUrl)
} catch {
owsFailDebugUnlessNetworkFailure(error)
taskDidFail(downloadTask, error: error)
}
}
public func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
if let maxResponseSize = maxResponseSize {
guard totalBytesWritten <= maxResponseSize,
totalBytesExpectedToWrite <= maxResponseSize else {
downloadTask.cancel()
return
}
}
guard let progressBlock = self.progressBlock(forTask: downloadTask) else {
return
}
let progress = Progress(parent: nil, userInfo: nil)
progress.totalUnitCount = totalBytesExpectedToWrite
progress.completedUnitCount = totalBytesWritten
progressBlock(downloadTask, progress)
}
public func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64) {
if let maxResponseSize = maxResponseSize {
guard fileOffset <= maxResponseSize,
expectedTotalBytes <= maxResponseSize else {
downloadTask.cancel()
return
}
}
guard let progressBlock = self.progressBlock(forTask: downloadTask) else {
return
}
let progress = Progress(parent: nil, userInfo: nil)
progress.totalUnitCount = expectedTotalBytes
progress.completedUnitCount = fileOffset
progressBlock(downloadTask, progress)
}
public func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let maxResponseSize = maxResponseSize else {
completionHandler(.allow)
return
}
if response.expectedContentLength == NSURLSessionTransferSizeUnknown {
completionHandler(.allow)
return
}
guard response.expectedContentLength <= maxResponseSize else {
owsFailDebug("Oversize response: \(response.expectedContentLength) > \(maxResponseSize)")
completionHandler(.cancel)
return
}
if response.expectedContentLength > 800_000 {
let formattedContentLength = OWSFormat.formatFileSize(UInt(response.expectedContentLength))
let urlString = response.url.map { String(describing: $0) } ?? "<unknown URL>"
Logger.warn("Large response (\(formattedContentLength)) for \(urlString)")
}
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let maxResponseSize = maxResponseSize else {
return
}
guard dataTask.countOfBytesReceived <= maxResponseSize else {
owsFailDebug("Oversize response: \(dataTask.countOfBytesReceived) > \(maxResponseSize)")
dataTask.cancel()
return
}
}
}
// MARK: - WebSocket
extension OWSURLSession: URLSessionWebSocketDelegate {
public func webSocketTask(requestUrl: URL, didOpenBlock: @escaping (String?) -> Void, didCloseBlock: @escaping (Error) -> Void) -> URLSessionWebSocketTask {
// We can't pass a URLRequest here since it prevents the proxy from
// operating correctly. See `SSKWebSocketNative.init(...)` for more details
// and an example of passing URLRequest options via this web socket.
let task = session.webSocketTask(with: requestUrl)
addTask(task, taskState: WebSocketTaskState(openBlock: didOpenBlock, closeBlock: didCloseBlock))
return task
}
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol: String?) {
webSocketState(forTask: webSocketTask)?.openBlock(didOpenWithProtocol)
}
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
guard let webSocketState = removeCompletedTaskState(webSocketTask) as? WebSocketTaskState else { return }
webSocketState.closeBlock(WebSocketError.closeError(statusCode: closeCode.rawValue, closeReason: reason))
}
}
// MARK: - TaskState
private protocol TaskState {
typealias ProgressBlock = (URLSessionTask, Progress) -> Void
var progressBlock: ProgressBlock? { get }
func reject(error: Error, task: URLSessionTask)
}
// MARK: - DownloadTaskState
private class DownloadTaskState: TaskState {
let progressBlock: ProgressBlock?
let promise: Promise<(URLSessionTask, URL)>
let future: Future<(URLSessionTask, URL)>
init(progressBlock: ProgressBlock?) {
self.progressBlock = progressBlock
let (promise, future) = Promise<(URLSessionTask, URL)>.pending()
self.promise = promise
self.future = future
}
func reject(error: Error, task: URLSessionTask) {
future.reject(error)
}
}
// MARK: - UploadOrDataTaskState
private class UploadOrDataTaskState: TaskState {
let progressBlock: ProgressBlock?
let promise: Promise<(URLSessionTask, Data?)>
let future: Future<(URLSessionTask, Data?)>
init(progressBlock: ProgressBlock?) {
self.progressBlock = progressBlock
let (promise, future) = Promise<(URLSessionTask, Data?)>.pending()
self.promise = promise
self.future = future
}
func reject(error: Error, task: URLSessionTask) {
future.reject(error)
}
}
// MARK: - WebSocketTaskState
private class WebSocketTaskState: TaskState {
typealias OpenBlock = (String?) -> Void
typealias CloseBlock = (Error) -> Void
var progressBlock: ProgressBlock? { nil }
let openBlock: OpenBlock
let closeBlock: CloseBlock
init(openBlock: @escaping OpenBlock, closeBlock: @escaping CloseBlock) {
self.openBlock = openBlock
self.closeBlock = closeBlock
}
func reject(error: Error, task: URLSessionTask) {
// We only want to return HTTP errors during the initial web socket
// upgrade. Once we've switched protocols, the HTTP response is no longer
// relevant but the property remains defined on the task. We use
// `badServerResponse` to distinguish errors during the initial handshake
// from other unexpected errors that occur later (eg losing internet).
if case URLError.badServerResponse = error, let httpResponse = task.response as? HTTPURLResponse {
let retryAfter = OWSHttpHeaders(response: httpResponse).retryAfterDate
closeBlock(WebSocketError.httpError(statusCode: httpResponse.statusCode, retryAfter: retryAfter))
return
}
closeBlock(error)
}
}
// NSURLSession maintains a strong reference to its delegate until explicitly invalidated
// OWSURLSession acts as its own delegate, and may be retained by any number of owners
// We don't really know when to invalidate our session, because a caller may decide to reuse a session
// at any time.
//
// So here's the plan:
// - While we have any outstanding tasks, a strong reference cycle is maintained. Promise holders
// don't need to hold on to the session while waiting for a promise to resolve.
// i.e. OWSURLSession --(session)--> URLSession --(delegate)--> URLSessionDelegateBox
// ^-----------------------(strongReference)-------------------|
//
// - Once all outstanding tasks have been resolved, the box breaks its reference. If there are no
// external references to the OWSURLSession, then everything cleans itself up.
// i.e. OWSURLSession --(session)--> URLSession --(delegate)--> URLSessionDelegateBox
// x-----(weakDelegate)-----|
//
private class URLSessionDelegateBox: NSObject {
private weak var weakDelegate: OWSURLSession?
private var strongReference: OWSURLSession?
init(delegate: OWSURLSession) {
self.weakDelegate = delegate
}
var isRetaining: Bool {
get {
strongReference != nil
}
set {
strongReference = newValue ? weakDelegate : nil
}
}
}
// MARK: -
extension URLSessionDelegateBox: URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
// Any of the optional methods will be forwarded using objc selector forwarding
// If all goes according to plan, weakDelegate will only go nil once everything is being dealloced
// But just in case, let's make sure we provide a fallback implementation to the only non-optional method we've conformed to
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
weakDelegate?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
weakDelegate?.urlSession(session,
downloadTask: downloadTask,
didWriteData: bytesWritten,
totalBytesWritten: totalBytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64) {
weakDelegate?.urlSession(session,
downloadTask: downloadTask,
didResumeAtOffset: fileOffset,
expectedTotalBytes: expectedTotalBytes)
}
public typealias URLAuthenticationChallengeCompletion = OWSURLSession.URLAuthenticationChallengeCompletion
func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping URLAuthenticationChallengeCompletion) {
weakDelegate?.urlSession(session,
task: task,
didReceive: challenge,
completionHandler: completionHandler)
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64) {
weakDelegate?.urlSession(session,
task: task,
didSendBodyData: bytesSent,
totalBytesSent: totalBytesSent,
totalBytesExpectedToSend: totalBytesExpectedToSend)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
weakDelegate?.urlSession(session,
task: task,
didCompleteWithError: error)
}
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping URLAuthenticationChallengeCompletion) {
weakDelegate?.urlSession(session,
didReceive: challenge,
completionHandler: completionHandler)
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void) {
weakDelegate?.urlSession(session,
task: task,
willPerformHTTPRedirection: response,
newRequest: newRequest,
completionHandler: completionHandler)
}
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let delegate = weakDelegate else {
completionHandler(.cancel)
return
}
delegate.urlSession(session,
dataTask: dataTask,
didReceive: response,
completionHandler: completionHandler)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
weakDelegate?.urlSession(session,
dataTask: dataTask,
didReceive: data)
}
}
extension URLSessionDelegateBox: URLSessionWebSocketDelegate {
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol: String?) {
weakDelegate?.urlSession(session, webSocketTask: webSocketTask, didOpenWithProtocol: didOpenWithProtocol)
}
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
weakDelegate?.urlSession(session, webSocketTask: webSocketTask, didCloseWith: closeCode, reason: reason)
}
}
// MARK: -
extension Error {
var isUnknownDomainError: Bool {
let nsError = self as NSError
return (nsError.domain == NSURLErrorDomain &&
nsError.code == -1003)
}
}
// MARK: -
private protocol UploadTaskBuilder {
typealias CompletionBlock = (Data?, URLResponse?, Error?) -> Void
func build(session: URLSession, request: URLRequest, completionBlock: @escaping CompletionBlock) -> URLSessionUploadTask
}
// MARK: -
private struct UploadTaskBuilderData: UploadTaskBuilder {
let requestData: Data
func build(session: URLSession, request: URLRequest, completionBlock: @escaping CompletionBlock) -> URLSessionUploadTask {
session.uploadTask(with: request, from: requestData, completionHandler: completionBlock)
}
}
// MARK: -
private struct UploadTaskBuilderFileUrl: UploadTaskBuilder {
let fileUrl: URL
func build(session: URLSession, request: URLRequest, completionBlock: @escaping CompletionBlock) -> URLSessionUploadTask {
session.uploadTask(with: request, fromFile: fileUrl, completionHandler: completionBlock)
}
}