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.
169 lines
5.8 KiB
Swift
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()
|
|
}
|
|
}
|