TM-SGNL-iOS/SignalServiceKit/Network/API/RESTNetworkManager.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

184 lines
6.4 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
class RESTNetworkManager {
private let udSessionManagerPool = OWSSessionManagerPool()
private let nonUdSessionManagerPool = OWSSessionManagerPool()
init() {
SwiftSingletons.register(self)
}
private func makeRequest(_ request: TSRequest) async throws -> any HTTPResponse {
let isUdRequest = request.isUDRequest
if (isUdRequest) {
owsPrecondition(!request.shouldHaveAuthorizationHeaders)
}
let sessionManagerPool = isUdRequest ? self.udSessionManagerPool : self.nonUdSessionManagerPool
let sessionManager = sessionManagerPool.get()
defer {
sessionManagerPool.returnToPool(sessionManager)
}
let result = try await sessionManager.performRequest(request)
#if TESTABLE_BUILD
if DebugFlags.logCurlOnSuccess {
HTTPUtils.logCurl(for: request as URLRequest)
}
#endif
OutageDetection.shared.reportConnectionSuccess()
return result
}
func makePromise(request: TSRequest) -> Promise<HTTPResponse> {
return Promise.wrapAsync { return try await self.asyncRequest(request) }
}
func asyncRequest(_ request: TSRequest) async throws -> HTTPResponse {
return try await makeRequest(request)
}
}
// MARK: -
private class RESTSessionManager {
private let urlSession: OWSURLSessionProtocol
let createdDate = MonotonicDate()
init() {
urlSession = SSKEnvironment.shared.signalServiceRef.urlSessionForMainSignalService()
}
public func performRequest(_ request: TSRequest) async throws -> any HTTPResponse {
// We should only use the RESTSessionManager for requests to the Signal main service.
let urlSession = self.urlSession
owsAssertDebug(urlSession.unfrontedBaseUrl == URL(string: TSConstants.mainServiceIdentifiedURL))
do {
return try await urlSession.performRequest(request)
} catch let httpError as OWSHTTPError {
// OWSUrlSession should only throw OWSHTTPError or OWSAssertionError.
HTTPUtils.applyHTTPError(httpError)
if httpError.httpStatusCode == 401, request.shouldCheckDeregisteredOn401 {
try await makeIsDeregisteredRequest()
}
throw httpError
} catch let error as CancellationError {
throw error
} catch {
owsFailDebug("Unexpected error: \(error)")
throw OWSHTTPError.invalidRequest
}
}
private func makeIsDeregisteredRequest() async throws(CancellationError) {
let isDeregisteredRequest = WhoAmIRequestFactory.amIDeregisteredRequest()
let result: WhoAmIRequestFactory.Responses.AmIDeregistered?
do {
let response = try await self.performRequest(isDeregisteredRequest)
result = WhoAmIRequestFactory.Responses.AmIDeregistered(rawValue: response.responseStatusCode)
} catch let error as OWSHTTPError {
result = WhoAmIRequestFactory.Responses.AmIDeregistered(rawValue: error.responseStatusCode)
} catch let error as CancellationError {
throw error
} catch {
result = nil
}
switch result {
case .deregistered:
Logger.warn("AmIDeregistered response says we are deregistered, marking as such.")
await DependenciesBridge.shared.db.awaitableWrite { 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.")
}
}
}
// 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 * 60 * NSEC_PER_SEC
private let pool = AtomicValue<[RESTSessionManager]>([], lock: .init())
// accessed from both networkManagerQueue and the main thread so needs a lock
@Atomic private var lastDiscardDate: MonotonicDate?
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 = MonotonicDate()
}
@objc
private func isSignalProxyReadyDidChange() {
AssertIsOnMainThread()
lastDiscardDate = MonotonicDate()
}
func get() -> RESTSessionManager {
// 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.update(block: { $0.popLast() }) else {
return RESTSessionManager()
}
if shouldDiscardSessionManager(sessionManager) {
continue
}
return sessionManager
}
}
func returnToPool(_ sessionManager: RESTSessionManager) {
if shouldDiscardSessionManager(sessionManager) {
return
}
let maxPoolSize = CurrentAppContext().isNSE ? 5 : 32
pool.update {
if $0.count < maxPoolSize {
$0.append(sessionManager)
}
}
}
private func shouldDiscardSessionManager(_ sessionManager: RESTSessionManager) -> Bool {
if let lastDiscardDate, sessionManager.createdDate < lastDiscardDate {
return true
}
return (MonotonicDate() - sessionManager.createdDate) > maxSessionManagerAge
}
}