diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index ebcf8504b..7a56e48de 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -13,7 +13,7 @@ public enum OnionRequestAPI { public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user // MARK: Settings - public static let maxFileSize = 10_000_000 // 10 MB + public static let maxRequestSize = 10_000_000 // 10 MB /// The number of snodes (including the guard snode) in a path. private static let pathSize: UInt = 3 /// The number of times a path can fail before it's replaced. @@ -88,27 +88,25 @@ public enum OnionRequestAPI { return Promise> { $0.fulfill(guardSnodes) } } else { SNLog("Populating guard snode cache.") - 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 >= (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) } } - unusedSnodes.remove(candidate) // All used snodes should be unique - SNLog("Testing guard snode: \(candidate).") - // Loop until a reliable guard snode is found - return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in - withDelay(0.1, completionQueue: Threading.workQueue) { getGuardSnode() } - } - } - let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() } - return when(fulfilled: promises).map2 { guardSnodes in - let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes) - OnionRequestAPI.guardSnodes = guardSnodesAsSet - return guardSnodesAsSet + var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue + let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) + guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { return Promise(error: 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) } } + unusedSnodes.remove(candidate) // All used snodes should be unique + SNLog("Testing guard snode: \(candidate).") + // Loop until a reliable guard snode is found + return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in + withDelay(0.1, completionQueue: Threading.workQueue) { getGuardSnode() } } } + let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() } + return when(fulfilled: promises).map2 { guardSnodes in + let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes) + OnionRequestAPI.guardSnodes = guardSnodesAsSet + return guardSnodesAsSet + } } } @@ -120,35 +118,33 @@ public enum OnionRequestAPI { DispatchQueue.main.async { NotificationCenter.default.post(name: .buildingPaths, object: nil) } - return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool - let reusableGuardSnodes = reusablePaths.map { $0[0] } - return getGuardSnodes(reusing: reusableGuardSnodes).map2 { guardSnodes -> [Path] in - var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes).subtracting(reusablePaths.flatMap { $0 }) - let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) - 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 - let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in - // randomElement() uses the system's default random generator, which is cryptographically secure - let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above - unusedSnodes.remove(pathSnode) // All used snodes should be unique - return pathSnode - } - SNLog("Built new onion request path: \(result.prettifiedDescription).") - return result + let reusableGuardSnodes = reusablePaths.map { $0[0] } + return getGuardSnodes(reusing: reusableGuardSnodes).map2 { guardSnodes -> [Path] in + var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes).subtracting(reusablePaths.flatMap { $0 }) + let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) + 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 + let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in + // randomElement() uses the system's default random generator, which is cryptographically secure + let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above + unusedSnodes.remove(pathSnode) // All used snodes should be unique + return pathSnode } - }.map2 { paths in - OnionRequestAPI.paths = paths + reusablePaths - SNSnodeKitConfiguration.shared.storage.writeSync { transaction in - SNLog("Persisting onion request paths to database.") - SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: paths, using: transaction) - } - DispatchQueue.main.async { - NotificationCenter.default.post(name: .pathsBuilt, object: nil) - } - return paths + SNLog("Built new onion request path: \(result.prettifiedDescription).") + return result } + }.map2 { paths in + OnionRequestAPI.paths = paths + reusablePaths + SNSnodeKitConfiguration.shared.storage.writeSync { transaction in + SNLog("Persisting onion request paths to database.") + SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: paths, using: transaction) + } + DispatchQueue.main.async { + NotificationCenter.default.post(name: .pathsBuilt, object: nil) + } + return paths } } @@ -353,7 +349,7 @@ public enum OnionRequestAPI { let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2" let finalEncryptionResult = intermediate.finalEncryptionResult let onion = finalEncryptionResult.ciphertext - if case Destination.server = destination, Double(onion.count) > 0.75 * Double(maxFileSize) { + if case Destination.server = destination, Double(onion.count) > 0.75 * Double(maxRequestSize) { SNLog("Approaching request size limit: ~\(onion.count) bytes.") } let parameters: JSON = [ diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 3f1643635..cc79b9961 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -148,7 +148,7 @@ public final class SnodeAPI : NSObject { if hasInsufficientSnodes || hasSnodePoolExpired { if let getSnodePoolPromise = getSnodePoolPromise { return getSnodePoolPromise } let promise: Promise> - if snodePool.isEmpty { + if snodePool.count < minSnodePoolCount { promise = getSnodePoolFromSeedNode() } else { promise = getSnodePoolFromSnode().recover2 { _ in @@ -232,11 +232,11 @@ public final class SnodeAPI : NSObject { snodePool.remove(snode) snodes.insert(snode) } - let rawSnodePoolPromises: [Promise>] = snodes.map { snode in + let snodePoolPromises: [Promise>] = snodes.map { snode in return attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { let parameters: JSON = [ "endpoint" : "get_service_nodes", - "oxend_params" : [ + "params" : [ "limit" : 256, "active_only" : true, "fields" : [ @@ -244,11 +244,11 @@ public final class SnodeAPI : NSObject { ] ] ] - return invoke(.getAllSnodes, on: snode, parameters: parameters).map2 { rawResponse in - guard let json = rawResponse as? JSON, let intermediate1 = json["result"] as? String, - let intermediate1AsData = intermediate1.data(using: String.Encoding.utf8), - let intermediate2 = try JSONSerialization.jsonObject(with: intermediate1AsData, options: [ .fragmentsAllowed ]) as? JSON, - let rawSnodes = intermediate2["service_node_states"] as? [JSON] else { throw Error.snodePoolUpdatingFailed } + let promise: Promise> = invoke(.getAllSnodes, on: snode, parameters: parameters).map2 { rawResponse in + guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, + let rawSnodes = intermediate["service_node_states"] as? [JSON] else { + throw Error.snodePoolUpdatingFailed + } return Set(rawSnodes.compactMap { rawSnode in guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int, let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else { @@ -258,9 +258,10 @@ public final class SnodeAPI : NSObject { return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) }) } + return promise } } - let promise = when(fulfilled: rawSnodePoolPromises).map2 { results -> Set in + let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set in var result: Set = results[0] results.forEach { result = result.union($0) } if result.count > 24 { // We want the snodes to agree on at least this many snodes @@ -269,9 +270,6 @@ public final class SnodeAPI : NSObject { throw Error.inconsistentSnodePools } } - promise.catch2 { error in - SNLog("\(error)") - } return promise }