// // 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 { 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 { let uploadTaskBuilder = UploadTaskBuilderFileUrl(fileUrl: fileUrl) return uploadTaskPromise( request: request, uploadTaskBuilder: uploadTaskBuilder, ignoreAppExpiry: ignoreAppExpiry, progress: progressBlock ) } public func dataTaskPromise(request: URLRequest, ignoreAppExpiry: Bool = false) -> Promise { 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 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 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 { 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 { 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 { 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 { 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 { 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 { 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 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 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 { 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 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 { 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 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) } ?? "" 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) } }