Signal-iOS/SignalServiceKit/Network/API/RESTNetworkManager.swift
2024-07-09 10:42:56 -04:00

254 lines
9.1 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
private let networkManagerQueue = DispatchQueue(
label: "org.signal.network-manager",
autoreleaseFrequency: .workItem)
class RESTNetworkManager {
fileprivate typealias Success = (HTTPResponse) -> Void
fileprivate typealias Failure = (OWSHTTPError) -> Void
private let udSessionManagerPool = OWSSessionManagerPool()
private let nonUdSessionManagerPool = OWSSessionManagerPool()
init() {
SwiftSingletons.register(self)
}
private func makeRequest(
_ request: TSRequest,
completionQueue: DispatchQueue,
success: @escaping Success,
failure: @escaping Failure
) {
networkManagerQueue.async {
self.makeRequestSync(request, completionQueue: completionQueue, success: success, failure: failure)
}
}
private func makeRequestSync(
_ request: TSRequest,
completionQueue: DispatchQueue,
success successParam: @escaping Success,
failure failureParam: @escaping Failure
) {
let isUdRequest = request.isUDRequest
let label = isUdRequest ? "UD request" : "Non-UD request"
if (isUdRequest) {
owsPrecondition(!request.shouldHaveAuthorizationHeaders)
}
Logger.info("Making \(label): \(request)")
let sessionManagerPool = isUdRequest ? self.udSessionManagerPool : self.nonUdSessionManagerPool
let sessionManager = sessionManagerPool.get()
let success = { (response: HTTPResponse) in
#if TESTABLE_BUILD
if DebugFlags.logCurlOnSuccess {
HTTPUtils.logCurl(for: request as URLRequest)
}
#endif
networkManagerQueue.async {
sessionManagerPool.returnToPool(sessionManager)
}
completionQueue.async {
Logger.info("\(label) succeeded (\(response.responseStatusCode)) : \(request)")
successParam(response)
OutageDetection.shared.reportConnectionSuccess()
}
}
let failure = { (error: OWSHTTPError) in
networkManagerQueue.async {
sessionManagerPool.returnToPool(sessionManager)
}
completionQueue.async {
failureParam(error)
}
}
sessionManager.performRequest(request, success: success, failure: failure)
}
func makePromise(request: TSRequest) -> Promise<HTTPResponse> {
let (promise, future) = Promise<HTTPResponse>.pending()
makeRequest(request,
completionQueue: .global(),
success: { (response: HTTPResponse) in
future.resolve(response)
},
failure: { (error: OWSHTTPError) in
future.reject(error)
})
return promise
}
}
// MARK: -
private class RESTSessionManager {
private let urlSession: OWSURLSessionProtocol
public let createdDate = Date()
init() {
assertOnQueue(networkManagerQueue)
urlSession = SSKEnvironment.shared.signalService.urlSessionForMainSignalService()
}
public func performRequest(_ request: TSRequest,
success: @escaping RESTNetworkManager.Success,
failure: @escaping RESTNetworkManager.Failure) {
assertOnQueue(networkManagerQueue)
owsAssertDebug(!FeatureFlags.deprecateREST)
// We should only use the RESTSessionManager for requests to the Signal main service.
let urlSession = self.urlSession
owsAssertDebug(urlSession.unfrontedBaseUrl == URL(string: TSConstants.mainServiceIdentifiedURL))
guard let requestUrl = request.url else {
owsFailDebug("Missing requestUrl.")
failure(.missingRequest)
return
}
firstly {
urlSession.promiseForTSRequest(request)
}.done(on: DispatchQueue.global()) { (response: HTTPResponse) in
success(response)
}.catch(on: DispatchQueue.global()) { error in
// OWSUrlSession should only throw OWSHTTPError or OWSAssertionError.
if let httpError = error as? OWSHTTPError {
HTTPUtils.applyHTTPError(httpError)
if httpError.httpStatusCode == 401, request.shouldCheckDeregisteredOn401 {
networkManagerQueue.async {
self.makeIsDeregisteredRequest(
originalRequestFailureHandler: failure,
originalRequestFailure: httpError
)
}
} else {
failure(httpError)
}
} else {
owsFailDebug("Unexpected error: \(error)")
failure(.invalidRequest(requestUrl: requestUrl))
}
}
}
private func makeIsDeregisteredRequest(
originalRequestFailureHandler: @escaping RESTNetworkManager.Failure,
originalRequestFailure: OWSHTTPError
) {
let isDeregisteredRequest = WhoAmIRequestFactory.amIDeregisteredRequest()
let handleDeregisteredResponse: (WhoAmIRequestFactory.Responses.AmIDeregistered?) -> Void = { response in
switch response {
case .deregistered:
Logger.warn("AmIDeregistered response says we are deregistered, marking as such.")
DependenciesBridge.shared.db.write { tx in
DependenciesBridge.shared.registrationStateChangeManager.setIsDeregisteredOrDelinked(true, tx: tx)
}
case .notDeregistered:
Logger.info("AmIDeregistered response says not deregistered; account probably disabled. Doing nothing.")
case .none, .unexpectedError:
Logger.error("Got unexpected AmIDeregistered response. Doing nothing.")
}
}
self.performRequest(
isDeregisteredRequest,
success: { rawResponse in
let response = WhoAmIRequestFactory.Responses.AmIDeregistered(rawValue: rawResponse.responseStatusCode)
handleDeregisteredResponse(response)
originalRequestFailureHandler(originalRequestFailure)
}, failure: { rawFailure in
let response = WhoAmIRequestFactory.Responses.AmIDeregistered(rawValue: rawFailure.responseStatusCode)
handleDeregisteredResponse(response)
originalRequestFailureHandler(originalRequestFailure)
}
)
}
}
// MARK: -
// Session managers are stateful (e.g. the headers in the requestSerializer).
// Concurrent requests can interfere with each other. Therefore we use a pool
// do not re-use a session manager until its request succeeds or fails.
private class OWSSessionManagerPool {
private let maxSessionManagerAge = 5 * kMinuteInterval
// must only be accessed from the networkManagerQueue for thread-safety
private var pool: [RESTSessionManager] = []
// accessed from both networkManagerQueue and the main thread so needs a lock
@Atomic private var lastDiscardDate: Date?
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(OWSSessionManagerPool.isCensorshipCircumventionActiveDidChange),
name: Notification.Name.isCensorshipCircumventionActiveDidChange,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(OWSSessionManagerPool.isSignalProxyReadyDidChange),
name: Notification.Name.isSignalProxyReadyDidChange,
object: nil)
}
@objc
private func isCensorshipCircumventionActiveDidChange() {
AssertIsOnMainThread()
lastDiscardDate = Date()
}
@objc
private func isSignalProxyReadyDidChange() {
AssertIsOnMainThread()
lastDiscardDate = Date()
}
func get() -> RESTSessionManager {
assertOnQueue(networkManagerQueue)
// Iterate over the pool, discarding expired session managers
// until we find an unexpired session manager in the pool or
// drain the pool and create a new session manager.
while true {
guard let sessionManager = pool.popLast() else {
return RESTSessionManager()
}
if shouldDiscardSessionManager(sessionManager) {
continue
}
return sessionManager
}
}
func returnToPool(_ sessionManager: RESTSessionManager) {
assertOnQueue(networkManagerQueue)
let maxPoolSize = CurrentAppContext().isNSE ? 5 : 32
guard pool.count < maxPoolSize && !shouldDiscardSessionManager(sessionManager) else {
return
}
pool.append(sessionManager)
}
private func shouldDiscardSessionManager(_ sessionManager: RESTSessionManager) -> Bool {
if lastDiscardDate?.isAfter(sessionManager.createdDate) ?? false {
return true
}
return fabs(sessionManager.createdDate.timeIntervalSinceNow) > maxSessionManagerAge
}
}