Signal-iOS/SignalServiceKit/Network/SignalProxy/SignalProxy.swift
Jordan Rose 0ac5558493 Move libsignal-net proxy handling into SignalProxy
Since this already affects more than just the chat connection, having
it in OWSChatConnection isn't really the best choice. This way we can
be sure the libsignal Net instance's proxy settings will be updated
before any notifications go out.
2024-05-17 16:20:13 -07:00

169 lines
5.8 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public extension Notification.Name {
static let isSignalProxyReadyDidChange = Self(SignalProxy.isSignalProxyReadyDidChangeNotificationName)
}
@objc
public class SignalProxy: NSObject {
@objc
public static let isSignalProxyReadyDidChangeNotificationName = "isSignalProxyReadyDidChange"
@objc
public static var isEnabled: Bool { useProxy && host != nil }
@objc
public static var isEnabledAndReady: Bool { isEnabled && relayServer.isReady }
public static var connectionProxyDictionary: [AnyHashable: Any]? { relayServer.connectionProxyDictionary }
@Atomic public private(set) static var host: String?
@Atomic public private(set) static var useProxy = false
private static let relayServer = RelayServer()
private static let keyValueStore = SDSKeyValueStore(collection: "SignalProxy")
private static let proxyHostKey = "proxyHostKey"
private static let proxyUseKey = "proxyUseKey"
public static func setProxyHost(host: String?, useProxy: Bool, transaction: SDSAnyWriteTransaction) {
let hostToStore = host?.nilIfEmpty
let useProxyToStore = hostToStore == nil ? false : useProxy
owsAssertDebug(useProxyToStore == useProxy)
keyValueStore.setString(hostToStore, key: proxyHostKey, transaction: transaction)
keyValueStore.setBool(useProxyToStore, key: proxyUseKey, transaction: transaction)
transaction.addSyncCompletion {
self.host = hostToStore
self.useProxy = useProxyToStore
self.ensureProxyState(restartIfNeeded: true)
}
}
@objc
public class func warmCaches() {
AppReadiness.runNowOrWhenAppWillBecomeReady {
databaseStorage.read { transaction in
host = keyValueStore.getString(proxyHostKey, transaction: transaction)
useProxy = keyValueStore.getBool(proxyUseKey, defaultValue: false, transaction: transaction)
}
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: .OWSApplicationDidBecomeActive, object: nil)
ensureProxyState()
}
}
@objc
public class func isValidProxyLink(_ url: URL) -> Bool {
guard url.user == nil, url.password == nil, url.port == nil else {
return false
}
guard url.host?.caseInsensitiveCompare("signal.tube") == .orderedSame else {
return false
}
guard let scheme = url.scheme else {
return false
}
let isValidScheme = (
scheme.caseInsensitiveCompare("https") == .orderedSame ||
scheme.caseInsensitiveCompare("sgnl") == .orderedSame
)
guard isValidScheme else {
return false
}
guard isValidProxyFragment(url.fragment) else { return false }
return true
}
public class func isValidProxyFragment(_ fragment: String?) -> Bool {
guard
let fragment = fragment?.nilIfEmpty,
// To quote [RFC 1034][0]: "the total number of octets that represent a domain name
// [...] is limited to 255." To be extra careful, we set a maximum of 2048.
// [0]: https://tools.ietf.org/html/rfc1034
fragment.utf8.count <= 2048,
let proxyUrl = URL(string: "fake-protocol://\(fragment)"),
proxyUrl.scheme == "fake-protocol",
proxyUrl.user == nil || (DebugFlags.internalSettings && proxyUrl.user == "UNENCRYPTED_FOR_TESTING"),
proxyUrl.password == nil,
proxyUrl.path.isEmpty,
proxyUrl.query == nil,
proxyUrl.fragment == nil,
proxyUrl.port != 0,
let proxyHost = proxyUrl.host
else {
return false
}
// There must be at least 2 domain labels, and none of them can be empty.
let labels = proxyHost.split(separator: ".", omittingEmptySubsequences: false)
guard labels.count >= 2 else {
return false
}
guard labels.allSatisfy({ !$0.isEmpty }) else {
return false
}
return true
}
@objc
private class func applicationDidBecomeActive() {
ensureProxyState()
}
private class func ensureProxyState(restartIfNeeded: Bool = false) {
// The NSE manages the proxy relay itself
guard !CurrentAppContext().isNSE else { return }
let libsignalNet = NSObject.networkManager.libsignalNet
if isEnabled {
if let (proxyHost, proxyPort) = host.flatMap({ ProxyClient.parseHost($0) }) {
do {
try libsignalNet?.setProxy(host: proxyHost, port: proxyPort)
} catch {
owsFailDebug("failed to set proxy on libsignal-net (need better validation)")
// This will poison the Net instance, failing all new connections,
// until a valid proxy is set or cleared.
}
} else {
// We can't print the invalid host in the logs, because that's private!
owsFailDebug("failed to parse previously-validated proxy host")
}
if restartIfNeeded && relayServer.isStarted {
relayServer.restartIfNeeded(ignoreBackoff: true)
} else {
relayServer.start()
}
} else {
libsignalNet?.clearProxy()
relayServer.stop()
}
}
public class func startRelayServer() {
guard isEnabled else { return }
Logger.info("Starting the proxy relay server...")
relayServer.start()
}
public class func stopRelayServer() {
guard isEnabled else { return }
Logger.info("Stopping the proxy relay server...")
relayServer.stop()
}
}