// // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation public import LibSignalClient public protocol NetworkManagerProtocol { func asyncRequestImpl( _ request: TSRequest, retryPolicy: NetworkManager.RetryPolicy, ) async throws -> HTTPResponse } extension NetworkManagerProtocol { public func asyncRequest( _ request: TSRequest, retryPolicy: NetworkManager.RetryPolicy = .dont, ) async throws -> HTTPResponse { return try await asyncRequestImpl(request, retryPolicy: retryPolicy) } } // A class used for making HTTP requests against the main service. public class NetworkManager: NetworkManagerProtocol { private let appReadiness: AppReadiness private let reachabilityDidChangeObserver: Task? private var chatConnectionManager: ChatConnectionManager { // TODO: Fix circular dependencies. DependenciesBridge.shared.chatConnectionManager } public let libsignalNet: Net? public init(appReadiness: AppReadiness, libsignalNet: Net?) { self.appReadiness = appReadiness self.libsignalNet = libsignalNet if let libsignalNet { self.reachabilityDidChangeObserver = Task { for await _ in NotificationCenter.default.notifications(named: SSKReachability.owsReachabilityDidChange) { do { Self.resetLibsignalNetProxySettings(libsignalNet, appReadiness: appReadiness) try libsignalNet.networkDidChange() } catch { owsFailDebug("error notify libsignal of network change: \(error)") } } } self.resetLibsignalNetProxySettings() Logger.info("Initialized libsignal Net and reset proxy settings (signalProxyEnabled: \(SignalProxy.isEnabled)).") appReadiness.runNowOrWhenAppDidBecomeReadyAsync { // We did this once already, but doing it properly depends on RemoteConfig. self.resetLibsignalNetProxySettings() } } else { self.reachabilityDidChangeObserver = nil } SwiftSingletons.register(self) } deinit { if let reachabilityDidChangeObserver { reachabilityDidChangeObserver.cancel() } } // MARK: - func resetLibsignalNetProxySettings() { guard let libsignalNet else { // In tests without a libsignal Net instance, no action is needed. return } Self.resetLibsignalNetProxySettings(libsignalNet, appReadiness: appReadiness) } private static func resetLibsignalNetProxySettings(_ libsignalNet: Net, appReadiness: AppReadiness) { guard !SignalProxy.isEnabled else { // Don't stomp on in-app proxy settings, which are managed by SignalProxy. return } if let systemProxy = ProxyConfig.fromCFNetwork() { Logger.info("System '\(systemProxy.scheme)' proxy detected") do { try libsignalNet.setProxy(scheme: systemProxy.scheme, host: systemProxy.host, port: systemProxy.port, username: systemProxy.username, password: systemProxy.password) return } catch { Logger.error("invalid proxy: \(error)") // When setProxy(...) fails, it refuses to connect in case your proxy was load-bearing. // That makes sense for in-app settings, but less so for system-level proxies, given that we are already ignoring system-level proxies we don't understand. // Fall through to the reset call. } } // This may be clearing a system proxy, or a previously set in-app proxy that is no longer in use. libsignalNet.clearProxy() } // MARK: - public struct RetryPolicy { public struct RetryOn: OptionSet { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } static let fiveXXResponse: RetryOn = .init(rawValue: 1 << 0) static let networkFailureOrTimeout: RetryOn = .init(rawValue: 1 << 1) } public let retryOn: [RetryOn] public let maxAttempts: Int public init( retryOn: [RetryOn], maxAttempts: Int, ) { self.retryOn = retryOn self.maxAttempts = maxAttempts } public static let dont: RetryPolicy = RetryPolicy( retryOn: [], maxAttempts: 1, ) public static let hopefullyRecoverable: RetryPolicy = RetryPolicy( retryOn: [.fiveXXResponse, .networkFailureOrTimeout], maxAttempts: 3, ) } public func asyncRequestImpl( _ request: TSRequest, retryPolicy: RetryPolicy, ) async throws -> HTTPResponse { return try await Retry.performWithBackoff( maxAttempts: retryPolicy.maxAttempts, isRetryable: { error -> Bool in if error.isNetworkFailureOrTimeout, retryPolicy.retryOn.contains(.networkFailureOrTimeout) { return true } else if error.is5xxServiceResponse, retryPolicy.retryOn.contains(.fiveXXResponse) { return true } return false }, block: { try await _asyncRequest(request) }, ) } private func _asyncRequest(_ request: TSRequest) async throws -> HTTPResponse { do { return try await chatConnectionManager.makeRequest(request) } catch { if case OWSHTTPError.wrappedFailure(URLError.cancelled) = error { try Task.checkCancellation() } throw error } } } // MARK: - private struct ProxyConfig { var scheme: String var host: String var port: UInt16? var username: String? var password: String? static func fromCFNetwork() -> Self? { let chatURL = URL(string: TSConstants.mainServiceURL)! guard let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() else { return nil } let proxies = CFNetworkCopyProxiesForURL(chatURL as CFURL, settings).takeRetainedValue() as! [NSDictionary] for proxyConfig in proxies { switch proxyConfig[kCFProxyTypeKey] as! NSObject? { case kCFProxyTypeNone: // CFNetworkCopyProxiesForURL returns a list of proxies to try in order, // and that can include "try a direct connection". // But libsignal only supports one global proxy setting, // so if we get told to try a direct connection, that's what we'll do. return nil case kCFProxyTypeHTTP: return ProxyConfig( scheme: "http", host: proxyConfig[kCFProxyHostNameKey] as! String, port: proxyConfig[kCFProxyPortNumberKey] as! UInt16?, username: proxyConfig[kCFProxyUsernameKey] as! String?, password: proxyConfig[kCFProxyPasswordKey] as! String?, ) case kCFProxyTypeHTTPS: // This seems to mean "HTTP proxy for HTTPS connections" rather than "proxy that itself uses TLS". // Leave room for the latter interpretation if the port number is traditionally HTTPS. let port = proxyConfig[kCFProxyPortNumberKey] as! UInt16? return ProxyConfig( scheme: (port == 443 || port == 8443) ? "https" : "http", host: proxyConfig[kCFProxyHostNameKey] as! String, port: port, username: proxyConfig[kCFProxyUsernameKey] as! String?, password: proxyConfig[kCFProxyPasswordKey] as! String?, ) case kCFProxyTypeSOCKS: // iOS doesn't distinguish between SOCKS4 and SOCKS5. Defer to libsignal's default. return ProxyConfig( scheme: "socks", host: proxyConfig[kCFProxyHostNameKey] as! String, port: proxyConfig[kCFProxyPortNumberKey] as! UInt16?, username: proxyConfig[kCFProxyUsernameKey] as! String?, password: proxyConfig[kCFProxyPasswordKey] as! String?, ) case kCFProxyTypeAutoConfigurationJavaScript, kCFProxyTypeAutoConfigurationURL: // CFNetwork provides ways to execute these, but they're not something that can be done synchronously. // PAC files are rare, though; we can come back to this if it turns out to be used in practice. Logger.warn("Skipping PAC-based proxy configuration") continue case kCFProxyTypeFTP: // Not relevant for an HTTPS request (honestly, it should never be returned in the first place) continue case let unknownProxyType?: Logger.warn("Skipping unknown proxy type '\(unknownProxyType)'") continue case nil: Logger.warn("Skipping proxy with nil kCFProxyType; this is probably an Apple bug!") continue } } return nil } } // MARK: - #if TESTABLE_BUILD public class OWSFakeNetworkManager: NetworkManager { override public func asyncRequestImpl( _ request: TSRequest, retryPolicy: RetryPolicy, ) async throws -> HTTPResponse { Logger.info("Ignoring request: \(request)") // Never resolve. return try await withUnsafeThrowingContinuation { (_ continuation: UnsafeContinuation) -> Void in } } } class MockNetworkManager: NetworkManagerProtocol { var asyncRequestHandlers = [(TSRequest, NetworkManager.RetryPolicy) async throws -> HTTPResponse]() func asyncRequestImpl( _ request: TSRequest, retryPolicy: NetworkManager.RetryPolicy, ) async throws -> HTTPResponse { return try await asyncRequestHandlers.removeFirst()(request, retryPolicy) } } #endif