diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index 1db5aa8f8..d3ae2d75e 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -3,6 +3,7 @@ import PromiseKit /// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. public enum OnionRequestAPI { + private static var pathFailureCount: [Path:UInt] = [:] public static var guardSnodes: Set = [] public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user @@ -11,11 +12,11 @@ public enum OnionRequestAPI { private static let pathSize: UInt = 3 /// The number of times a path can fail before it's replaced. private static let pathFailureThreshold: UInt = 2 - private static var pathFailureCount: [Path:UInt] = [:] - + /// The number of paths to maintain. public static let targetPathCount: UInt = 2 - private static var guardSnodeCount: UInt { return targetPathCount } // One per path + /// The number of guard snodes required to maintain `targetPathCount` paths. + private static var targetGuardSnodeCount: UInt { return targetPathCount } // One per path // MARK: Destination internal enum Destination { @@ -75,7 +76,7 @@ public enum OnionRequestAPI { /// Finds `guardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes` /// if not enough (reliable) snodes are available. private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> Promise> { - if guardSnodes.count >= guardSnodeCount { + if guardSnodes.count >= targetGuardSnodeCount { return Promise> { $0.fulfill(guardSnodes) } } else { print("[Test] reusableGuardSnodes: \(reusableGuardSnodes)") @@ -83,7 +84,7 @@ public enum OnionRequestAPI { return SnodeAPI.getRandomSnode().then2 { _ -> Promise> in // Just used to populate the snode pool var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) - guard unusedSnodes.count >= (guardSnodeCount - reusableGuardSnodeCount) else { throw Error.insufficientSnodes } + guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { throw Error.insufficientSnodes } func getGuardSnode() -> Promise { // randomElement() uses the system's default random generator, which is cryptographically secure guard let candidate = unusedSnodes.randomElement() else { return Promise { $0.reject(Error.insufficientSnodes) } } @@ -94,7 +95,7 @@ public enum OnionRequestAPI { withDelay(0.1, completionQueue: SnodeAPI.workQueue) { getGuardSnode() } } } - let promises = (0..<(guardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() } + let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() } return when(fulfilled: promises).map2 { guardSnodes in let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes) print("[Test] guardSnodes: \(guardSnodesAsSet)") @@ -118,7 +119,7 @@ public enum OnionRequestAPI { print("[Test] reusablePaths: \(reusablePaths)") var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes) let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) - let pathSnodeCount = (guardSnodeCount - reusableGuardSnodeCount) * pathSize - (guardSnodeCount - reusableGuardSnodeCount) + let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount) guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes } // Don't test path snodes as this would reveal the user's IP to them return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in @@ -272,68 +273,7 @@ public enum OnionRequestAPI { }.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) } } - // MARK: Internal API - /// Sends an onion request to `snode`. Builds new paths as needed. - internal static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise { - let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] - return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise in - guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json) = error else { throw error } - throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error - } - } - - /// Sends an onion request to `server`. Builds new paths as needed. - internal static func sendOnionRequest(_ request: NSURLRequest, to server: String, using x25519PublicKey: String, isJSONRequired: Bool = true) -> Promise { - let rawHeaders = request.allHTTPHeaderFields ?? [:] - var headers: JSON = rawHeaders.mapValues { value in - switch value.lowercased() { - case "true": return true - case "false": return false - default: return value - } - } - guard let url = request.url?.absoluteString, let host = request.url?.host else { return Promise(error: Error.invalidURL) } - var endpoint = "" - if server.count < url.count { - guard let serverEndIndex = url.range(of: server)?.upperBound else { return Promise(error: Error.invalidURL) } - let endpointStartIndex = url.index(after: serverEndIndex) - endpoint = String(url[endpointStartIndex.. Promise { + private static func sendOnionRequest(with payload: JSON, to destination: Destination, isJSONRequired: Bool = true) -> Promise { let (promise, seal) = Promise.pending() var guardSnode: Snode! SnodeAPI.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` @@ -395,7 +335,8 @@ public enum OnionRequestAPI { let path = paths.first { $0.contains(guardSnode) } func handleUnspecificError() { guard let path = path else { return } - let pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?? 0 + var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?? 0 + pathFailureCount += 1 if pathFailureCount >= pathFailureThreshold { dropGuardSnode(guardSnode) path.forEach { snode in @@ -404,7 +345,7 @@ public enum OnionRequestAPI { print("[Test] Unspecific error; dropping: \(path)") drop(path) } else { - OnionRequestAPI.pathFailureCount[path] = pathFailureCount + 1 + OnionRequestAPI.pathFailureCount[path] = pathFailureCount } } let prefix = "Next node not found: " @@ -427,4 +368,65 @@ public enum OnionRequestAPI { } return promise } + + // MARK: Internal API + /// Sends an onion request to `snode`. Builds new paths as needed. + internal static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise { + let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] + return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise in + guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json) = error else { throw error } + throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error + } + } + + /// Sends an onion request to `server`. Builds new paths as needed. + internal static func sendOnionRequest(_ request: NSURLRequest, to server: String, using x25519PublicKey: String, isJSONRequired: Bool = true) -> Promise { + let rawHeaders = request.allHTTPHeaderFields ?? [:] + var headers: JSON = rawHeaders.mapValues { value in + switch value.lowercased() { + case "true": return true + case "false": return false + default: return value + } + } + guard let url = request.url?.absoluteString, let host = request.url?.host else { return Promise(error: Error.invalidURL) } + var endpoint = "" + if server.count < url.count { + guard let serverEndIndex = url.range(of: server)?.upperBound else { return Promise(error: Error.invalidURL) } + let endpointStartIndex = url.index(after: serverEndIndex) + endpoint = String(url[endpointStartIndex..