From 6936f35f2ae0516857998d4a1e1dc71e245cc914 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 2 Mar 2022 13:09:45 +1100 Subject: [PATCH] Fixed a few issues uncovered while testing and some cleanup Fixed an incorrect optional in RoomPollInfo Fixed an incorrect parameter name in the ClosedGroupRequestBody Fixed a crash due to a change in the ContactUtilities Cleaned up the duplicate code in the OnionRequestAPI, HTTP and SnodeAPI to all use 'Data' response types Updated the SnodeAPI to casting types to Any (made it hard to catch breaking changes with HTTP and OnionRequestAPI) --- Session/Utilities/BackgroundPoller.swift | 4 +- Session/Utilities/ContactUtilities.swift | 6 +- .../Open Groups/Models/RoomPollInfo.swift | 2 +- .../Open Groups/OpenGroupAPI.swift | 1 - .../Notifications/PushNotificationAPI.swift | 10 +- .../Sending & Receiving/Pollers/Poller.swift | 4 +- SessionSnodeKit/Models/Error.swift | 2 +- SessionSnodeKit/OnionRequestAPI.swift | 70 ++-- SessionSnodeKit/SnodeAPI.swift | 318 ++++++++++-------- SessionUtilitiesKit/Networking/HTTP.swift | 100 ++---- 10 files changed, 258 insertions(+), 259 deletions(-) diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index b0bf06175..903301354 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -50,8 +50,8 @@ public final class BackgroundPoller: NSObject { return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) { return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey) - .then(on: DispatchQueue.main) { rawResponse -> Promise in - let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey) + .then(on: DispatchQueue.main) { responseData -> Promise in + let messages = SnodeAPI.parseRawMessagesResponse(responseData, from: snode, associatedWith: publicKey) let promises = messages .compactMap { json -> Promise? in // Use a best attempt approach here; we don't want to fail diff --git a/Session/Utilities/ContactUtilities.swift b/Session/Utilities/ContactUtilities.swift index f48fa32e2..6c7857374 100644 --- a/Session/Utilities/ContactUtilities.swift +++ b/Session/Utilities/ContactUtilities.swift @@ -32,8 +32,10 @@ enum ContactUtilities { // Sort alphabetically return result - .map { contact -> String in (contact.displayName(for: .regular) ?? contact.sessionID) } - .sorted() + .sorted(by: { lhs, rhs in + (lhs.displayName(for: .regular) ?? lhs.sessionID) < (rhs.displayName(for: .regular) ?? rhs.sessionID) + }) + .map { $0.sessionID } } static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer) -> ()) { diff --git a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift b/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift index 7d30da86b..9523f5d33 100644 --- a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift +++ b/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift @@ -27,7 +27,7 @@ extension OpenGroupAPI { } /// The room token as used in a URL, e.g. "sudoku" - public let token: String? + public let token: String /// Number of recently active users in the room over a recent time period (as given in the active_users_cutoff value) /// diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 25556f9b3..35a90cbcf 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -549,7 +549,6 @@ public final class OpenGroupAPI: NSObject { ) return send(request, using: dependencies) - .decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) } // MARK: - Users diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 91b79943a..743a15539 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -9,7 +9,7 @@ public final class PushNotificationAPI : NSObject { } struct ClosedGroupRequestBody: Codable { - let token: String + let closedGroupPublicKey: String let pubKey: String } @@ -51,7 +51,6 @@ public final class PushNotificationAPI : NSObject { request.httpBody = body let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { - // TODO: Update this to use the V4 union requests once supported OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey) .map2 { _, response in guard let response: UnregisterResponse = try? response?.decoded(as: UnregisterResponse.self) else { @@ -101,7 +100,6 @@ public final class PushNotificationAPI : NSObject { request.httpBody = body let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { - // TODO: Update this to use the V4 union requests once supported OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey) .map2 { _, response in guard let response: RegisterResponse = try? response?.decoded(as: RegisterResponse.self) else { @@ -134,7 +132,10 @@ public final class PushNotificationAPI : NSObject { @discardableResult public static func performOperation(_ operation: ClosedGroupOperation, for closedGroupPublicKey: String, publicKey: String) -> Promise { let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs] - let requestBody: ClosedGroupRequestBody = ClosedGroupRequestBody(token: closedGroupPublicKey, pubKey: publicKey) + let requestBody: ClosedGroupRequestBody = ClosedGroupRequestBody( + closedGroupPublicKey: closedGroupPublicKey, + pubKey: publicKey + ) guard isUsingFullAPNs else { return Promise { $0.fulfill(()) } } guard let body: Data = try? JSONEncoder().encode(requestBody) else { @@ -148,7 +149,6 @@ public final class PushNotificationAPI : NSObject { request.httpBody = body let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { - // TODO: Update this to use the V4 union requests once supported OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey) .map2 { _, response in guard let response: RegisterResponse = try? response?.decoded(as: RegisterResponse.self) else { diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index cfb3463ea..a8c2151ed 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -87,9 +87,9 @@ public final class Poller : NSObject { private func poll(_ snode: Snode, seal longTermSeal: Resolver) -> Promise { guard isPolling else { return Promise { $0.fulfill(()) } } let userPublicKey = getUserHexEncodedPublicKey() - return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: DispatchQueue.main) { [weak self] rawResponse -> Promise in + return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: DispatchQueue.main) { [weak self] responseData -> Promise in guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } } - let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: userPublicKey) + let messages = SnodeAPI.parseRawMessagesResponse(responseData, from: snode, associatedWith: userPublicKey) if !messages.isEmpty { SNLog("Received \(messages.count) new message(s).") } diff --git a/SessionSnodeKit/Models/Error.swift b/SessionSnodeKit/Models/Error.swift index d12635df8..e474ce5c8 100644 --- a/SessionSnodeKit/Models/Error.swift +++ b/SessionSnodeKit/Models/Error.swift @@ -5,7 +5,7 @@ import SessionUtilitiesKit extension OnionRequestAPI { public enum Error: LocalizedError { - case httpRequestFailedAtDestination(statusCode: UInt, json: JSON, destination: Destination) + case httpRequestFailedAtDestination(statusCode: UInt, data: Data, destination: Destination) case insufficientSnodes case invalidURL case missingSnodeVersion diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 2d7daa083..de36cc088 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -9,10 +9,6 @@ public protocol OnionRequestAPIType { } public extension OnionRequestAPIType { - static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version = .v3) -> Promise { - return sendOnionRequest(to: snode, invoking: method, with: parameters, using: version, associatedWith: nil) - } - static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { sendOnionRequest(request, to: server, using: .v4, with: x25519PublicKey) } @@ -50,24 +46,32 @@ public enum OnionRequestAPI: OnionRequestAPIType { // MARK: Onion Building Result private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: AESGCM.EncryptionResult, destinationSymmetricKey: Data) - // MARK: Private API + // MARK: - Private API /// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. private static func testSnode(_ snode: Snode) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { let url = "\(snode.address):\(snode.port)/get_stats/v1" let timeout: TimeInterval = 3 // Use a shorter timeout for testing - HTTP.execute(.get, url, timeout: timeout).done2 { json in - guard let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) } - if version >= "2.0.7" { - seal.fulfill(()) - } else { - SNLog("Unsupported snode version: \(version).") - seal.reject(Error.unsupportedSnodeVersion(version)) + + HTTP.execute(.get, url, timeout: timeout) + .done2 { responseData in + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + throw HTTP.Error.invalidJSON + } + guard let version = responseJson["version"] as? String else { return seal.reject(Error.missingSnodeVersion) } + + if version >= "2.0.7" { + seal.fulfill(()) + } + else { + SNLog("Unsupported snode version: \(version).") + seal.reject(Error.unsupportedSnodeVersion(version)) + } + } + .catch2 { error in + seal.reject(error) } - }.catch2 { error in - seal.reject(error) - } } return promise } @@ -280,7 +284,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { // MARK: - Public API /// Sends an onion request to `snode`. Builds new paths as needed. - public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: Version = .v3, associatedWith publicKey: String? = nil) -> Promise { + public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: Version, associatedWith publicKey: String?) -> Promise { let payloadJson: JSON = [ "method": method.rawValue, "params": parameters ] guard let jsonData: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []), let payload: String = String(data: jsonData, encoding: .utf8) else { @@ -294,11 +298,11 @@ public enum OnionRequestAPI: OnionRequestAPIType { return data } .recover2 { error -> Promise in - guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { + guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let data, _) = error else { throw error } - throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error + throw SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey) ?? error } } @@ -347,7 +351,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { } let destinationSymmetricKey = intermediate.destinationSymmetricKey - HTTP.updatedExecute(.post, url, body: body) + HTTP.execute(.post, url, body: body) .done2 { responseData in handleResponse( responseData: responseData, @@ -366,7 +370,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { } promise.catch2 { error in // Must be invoked on Threading.workQueue - guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error, let guardSnode = guardSnode else { + guard case HTTP.Error.httpRequestFailed(let statusCode, let data) = error, let guardSnode = guardSnode else { return } @@ -381,7 +385,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { if pathFailureCount >= pathFailureThreshold { dropGuardSnode(guardSnode) path.forEach { snode in - SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw + SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode) // Intentionally don't throw } drop(path) @@ -392,6 +396,17 @@ public enum OnionRequestAPI: OnionRequestAPIType { } let prefix = "Next node not found: " + let json: JSON? + + if let data: Data = data, let processedJson = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON { + json = processedJson + } + else if let data: Data = data, let result: String = String(data: data, encoding: .utf8) { + json = [ "result": result ] + } + else { + json = nil + } if let message = json?["result"] as? String, message.hasPrefix(prefix) { let ed25519PublicKey = message[message.index(message.startIndex, offsetBy: prefix.count)..= snodeFailureThreshold { - SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw + SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode) // Intentionally don't throw do { try drop(snode) } @@ -483,7 +498,6 @@ public enum OnionRequestAPI: OnionRequestAPIType { case .v4: // Note: We need to remove the leading forward slash unless we are explicitly hitting a legacy // endpoint (in which case we need it to ensure the request signing works correctly - // TODO: Confirm the 'removingPrefix' isn't going to break the request signing on non-legacy endpoints let endpoint: String = url.path .appending(url.query.map { value in "?\(value)" }) @@ -493,9 +507,9 @@ public enum OnionRequestAPI: OnionRequestAPIType { headers: (request.allHTTPHeaderFields ?? [:]) .setting( "Content-Type", - // TODO: Determine what 'Content-Type' 'httpBodyStream' should have???. (request.httpBody == nil && request.httpBodyStream == nil ? nil : - ((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json") // Default to JSON if not defined + // Default to JSON if not defined + ((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json") ) ) .removingValue(forKey: "User-Agent") @@ -573,14 +587,14 @@ public enum OnionRequestAPI: OnionRequestAPIType { } guard 200...299 ~= statusCode else { - return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body, destination: destination)) + return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), data: bodyAsData, destination: destination)) } return seal.fulfill((OnionRequestAPI.ResponseInfo(code: statusCode, headers: [:]), bodyAsData)) } guard 200...299 ~= statusCode else { - return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json, destination: destination)) + return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), data: data, destination: destination)) } return seal.fulfill((OnionRequestAPI.ResponseInfo(code: statusCode, headers: [:]), data)) @@ -628,7 +642,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { return seal.reject( Error.httpRequestFailedAtDestination( statusCode: UInt(responseInfo.code), - json: [:], // TODO: Remove the 'json' value?? + data: data, destination: destination ) ) diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index becc23930..6244f0e5b 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -60,11 +60,6 @@ public final class SnodeAPI : NSObject { } } - // MARK: Type Aliases - public typealias MessageListPromise = Promise<[JSON]> - public typealias RawResponse = Any - public typealias RawResponsePromise = Promise - // MARK: Snode Pool Interaction private static func loadSnodePoolIfNeeded() { guard !hasLoadedSnodePool else { return } @@ -129,30 +124,26 @@ public final class SnodeAPI : NSObject { } // MARK: Internal API - internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise { + internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> Promise { if Features.useOnionRequests { return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, using: .v3, associatedWith: publicKey) - .map2 { responseData in - guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { - throw Error.generic - } - - // FIXME: Would be nice to change this to not send 'Any' - return responseJson as Any - } - } else { + } + else { let url = "\(snode.address):\(snode.port)/storage_rpc/v1" - return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise in - guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { throw error } - throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error - } + return HTTP.execute(.post, url, parameters: parameters) + .recover2 { error -> Promise in + guard case HTTP.Error.httpRequestFailed(let statusCode, let data) = error else { throw error } + throw SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey) ?? error + } } } private static func getNetworkTime(from snode: Snode) -> Promise { - return invoke(.getInfo, on: snode, parameters: [:]).map2 { rawResponse in - guard let json = rawResponse as? JSON, - let timestamp = json["timestamp"] as? UInt64 else { throw HTTP.Error.invalidJSON } + return invoke(.getInfo, on: snode, parameters: [:]).map2 { responseData in + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + throw HTTP.Error.invalidJSON + } + guard let timestamp = responseJson["timestamp"] as? UInt64 else { throw HTTP.Error.invalidJSON } return timestamp } } @@ -179,21 +170,27 @@ public final class SnodeAPI : NSObject { let (promise, seal) = Promise>.pending() Threading.workQueue.async { attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { - HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true).map2 { json -> Set in - guard 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 { - SNLog("Failed to parse snode from: \(rawSnode).") - return nil + HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true) + .map2 { responseData -> Set in + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + throw HTTP.Error.invalidJSON } - return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) - }) - } - }.done2 { snodePool in + guard let intermediate = responseJson["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 { + SNLog("Failed to parse snode from: \(rawSnode).") + return nil + } + return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) + }) + } + } + .done2 { snodePool in SNLog("Got snode pool from seed node: \(target).") seal.fulfill(snodePool) - }.catch2 { error in + } + .catch2 { error in SNLog("Failed to contact seed node at: \(target).") seal.reject(error) } @@ -223,20 +220,24 @@ public final class SnodeAPI : NSObject { ] ] ] - return invoke(.oxenDaemonRPCCall, 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 { - SNLog("Failed to parse snode from: \(rawSnode).") - return nil + return invoke(.oxenDaemonRPCCall, on: snode, parameters: parameters) + .map2 { responseData in + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + throw HTTP.Error.invalidJSON } - return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) - }) - } + guard let intermediate = responseJson["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 { + SNLog("Failed to parse snode from: \(rawSnode).") + return nil + } + return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) + }) + } } } let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set in @@ -336,8 +337,11 @@ public final class SnodeAPI : NSObject { for result in results { switch result { case .rejected(let error): return seal.reject(error) - case .fulfilled(let rawResponse): - guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, + case .fulfilled(let responseData): + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + throw HTTP.Error.invalidJSON + } + guard let intermediate = responseJson["result"] as? JSON, let hexEncodedCiphertext = intermediate["encrypted_value"] as? String else { return seal.reject(HTTP.Error.invalidJSON) } let ciphertext = [UInt8](Data(hex: hexEncodedCiphertext)) let isArgon2Based = (intermediate["nonce"] == nil) @@ -390,37 +394,23 @@ public final class SnodeAPI : NSObject { attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { invoke(.getSwarm, on: snode, associatedWith: publicKey, parameters: parameters) } - }.map2 { rawSnodes in - let swarm = parseSnodes(from: rawSnodes) + }.map2 { responseData in + let swarm = parseSnodes(from: responseData) setSwarm(to: swarm, for: publicKey) return swarm } } } - public static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise { - let (promise, seal) = RawResponsePromise.pending() + public static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> Promise { + let (promise, seal) = Promise.pending() Threading.workQueue.async { getMessagesInternal(from: snode, associatedWith: publicKey).done2 { seal.fulfill($0) }.catch2 { seal.reject($0) } } return promise } - - public static func getMessages(for publicKey: String) -> Promise> { - let (promise, seal) = Promise>.pending() - Threading.workQueue.async { - attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - getTargetSnodes(for: publicKey).mapValues2 { targetSnode in - return getMessagesInternal(from: targetSnode, associatedWith: publicKey).map2 { rawResponse in - parseRawMessagesResponse(rawResponse, from: targetSnode, associatedWith: publicKey) - } - }.map2 { Set($0) } - }.done2 { seal.fulfill($0) }.catch2 { seal.reject($0) } - } - return promise - } - private static func getMessagesInternal(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise { + private static func getMessagesInternal(from snode: Snode, associatedWith publicKey: String) -> Promise { let storage = SNSnodeKitConfiguration.shared.storage // NOTE: All authentication logic is currently commented out, the reason being that we can't currently support @@ -447,18 +437,21 @@ public final class SnodeAPI : NSObject { return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters) } - public static func sendMessage(_ message: SnodeMessage) -> Promise> { - let (promise, seal) = Promise>.pending() + public static func sendMessage(_ message: SnodeMessage) -> Promise>> { + let (promise, seal) = Promise>>.pending() let publicKey = Features.useTestnet ? message.recipient.removingIdPrefixIfNeeded() : message.recipient Threading.workQueue.async { - getTargetSnodes(for: publicKey).map2 { targetSnodes in - let parameters = message.toJSON() - return Set(targetSnodes.map { targetSnode in - attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters) - } - }) - }.done2 { seal.fulfill($0) }.catch2 { seal.reject($0) } + getTargetSnodes(for: publicKey) + .map2 { targetSnodes in + let parameters = message.toJSON() + return Set(targetSnodes.map { targetSnode in + attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { + invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters) + } + }) + } + .done2 { seal.fulfill($0) } + .catch2 { seal.reject($0) } } return promise } @@ -485,29 +478,34 @@ public final class SnodeAPI : NSObject { "signature": signature.toBase64() ] return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters).map2{ rawResponse -> [String:Bool] in - guard let json = rawResponse as? JSON, let swarm = json["swarm"] as? JSON else { throw HTTP.Error.invalidJSON } - var result: [String:Bool] = [:] - for (snodePublicKey, rawJSON) in swarm { - guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON } - let isFailed = json["failed"] as? Bool ?? false - if !isFailed { - guard let hashes = json["deleted"] as? [String], let signature = json["signature"] as? String else { throw HTTP.Error.invalidJSON } - // The signature format is ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) - let verificationData = (userX25519PublicKey + serverHashes.joined(separator: "") + hashes.joined(separator: "")).data(using: String.Encoding.utf8)! - let isValid = sodium.sign.verify(message: Bytes(verificationData), publicKey: Bytes(Data(hex: snodePublicKey)), signature: Bytes(Data(base64Encoded: signature)!)) - result[snodePublicKey] = isValid - } else { - if let reason = json["reason"] as? String, let statusCode = json["code"] as? String { - SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).") + invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters) + .map2 { responseData -> [String: Bool] in + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + throw HTTP.Error.invalidJSON + } + guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON } + + var result: [String: Bool] = [:] + for (snodePublicKey, rawJSON) in swarm { + guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON } + let isFailed = json["failed"] as? Bool ?? false + if !isFailed { + guard let hashes = json["deleted"] as? [String], let signature = json["signature"] as? String else { throw HTTP.Error.invalidJSON } + // The signature format is ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) + let verificationData = (userX25519PublicKey + serverHashes.joined(separator: "") + hashes.joined(separator: "")).data(using: String.Encoding.utf8)! + let isValid = sodium.sign.verify(message: Bytes(verificationData), publicKey: Bytes(Data(hex: snodePublicKey)), signature: Bytes(Data(base64Encoded: signature)!)) + result[snodePublicKey] = isValid } else { - SNLog("Couldn't delete data from: \(snodePublicKey).") + if let reason = json["reason"] as? String, let statusCode = json["code"] as? String { + SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).") + } else { + SNLog("Couldn't delete data from: \(snodePublicKey).") + } + result[snodePublicKey] = false } - result[snodePublicKey] = false } + return result } - return result - } } } } @@ -532,29 +530,36 @@ public final class SnodeAPI : NSObject { "signature" : signature.toBase64() ] return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.clearAllData, on: snode, parameters: parameters).map2 { rawResponse -> [String:Bool] in - guard let json = rawResponse as? JSON, let swarm = json["swarm"] as? JSON else { throw HTTP.Error.invalidJSON } - var result: [String:Bool] = [:] - for (snodePublicKey, rawJSON) in swarm { - guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON } - let isFailed = json["failed"] as? Bool ?? false - if !isFailed { - guard let hashes = json["deleted"] as? [String], let signature = json["signature"] as? String else { throw HTTP.Error.invalidJSON } - // The signature format is ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) - let verificationData = (userX25519PublicKey + String(timestamp) + hashes.joined(separator: "")).data(using: String.Encoding.utf8)! - let isValid = sodium.sign.verify(message: Bytes(verificationData), publicKey: Bytes(Data(hex: snodePublicKey)), signature: Bytes(Data(base64Encoded: signature)!)) - result[snodePublicKey] = isValid - } else { - if let reason = json["reason"] as? String, let statusCode = json["code"] as? String { - SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).") + invoke(.clearAllData, on: snode, parameters: parameters) + .map2 { responseData -> [String: Bool] in + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + throw HTTP.Error.invalidJSON + } + guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON } + + var result: [String: Bool] = [:] + + for (snodePublicKey, rawJSON) in swarm { + guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON } + let isFailed = json["failed"] as? Bool ?? false + if !isFailed { + guard let hashes = json["deleted"] as? [String], let signature = json["signature"] as? String else { throw HTTP.Error.invalidJSON } + // The signature format is ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) + let verificationData = (userX25519PublicKey + String(timestamp) + hashes.joined(separator: "")).data(using: String.Encoding.utf8)! + let isValid = sodium.sign.verify(message: Bytes(verificationData), publicKey: Bytes(Data(hex: snodePublicKey)), signature: Bytes(Data(base64Encoded: signature)!)) + result[snodePublicKey] = isValid } else { - SNLog("Couldn't delete data from: \(snodePublicKey).") + if let reason = json["reason"] as? String, let statusCode = json["code"] as? String { + SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).") + } else { + SNLog("Couldn't delete data from: \(snodePublicKey).") + } + result[snodePublicKey] = false } - result[snodePublicKey] = false } + + return result } - return result - } } } } @@ -566,9 +571,13 @@ public final class SnodeAPI : NSObject { // The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions. - private static func parseSnodes(from rawResponse: Any) -> Set { - guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else { - SNLog("Failed to parse snodes from: \(rawResponse).") + private static func parseSnodes(from responseData: Data) -> Set { + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + SNLog("Failed to parse snodes from response data.") + return [] + } + guard let rawSnodes = responseJson["snodes"] as? [JSON] else { + SNLog("Failed to parse snodes from: \(responseJson).") return [] } return Set(rawSnodes.compactMap { rawSnode in @@ -581,8 +590,11 @@ public final class SnodeAPI : NSObject { }) } - public static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [JSON] { - guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] } + public static func parseRawMessagesResponse(_ responseData: Data, from snode: Snode, associatedWith publicKey: String) -> [JSON] { + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + return [] + } + guard let rawMessages = responseJson["messages"] as? [JSON] else { return [] } updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages) return removeDuplicates(from: rawMessages, associatedWith: publicKey) } @@ -622,7 +634,7 @@ public final class SnodeAPI : NSObject { // MARK: Error Handling /// - Note: Should only be invoked from `Threading.workQueue` to avoid race conditions. @discardableResult - internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? { + internal static func handleError(withStatusCode statusCode: UInt, data: Data?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif @@ -641,37 +653,47 @@ public final class SnodeAPI : NSObject { SnodeAPI.snodeFailureCount[snode] = 0 } } + switch statusCode { - case 500, 502, 503: - // The snode is unreachable - handleBadSnode() - case 406: - SNLog("The user's clock is out of sync with the service node network.") - return Error.clockOutOfSync - case 421: - // The snode isn't associated with the given public key anymore - if let publicKey = publicKey { - func invalidateSwarm() { - SNLog("Invalidating swarm for: \(publicKey).") - SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey) - } - if let json = json { - let snodes = parseSnodes(from: json) - if !snodes.isEmpty { - setSwarm(to: snodes, for: publicKey) - } else { + case 500, 502, 503: + // The snode is unreachable + handleBadSnode() + + case 406: + SNLog("The user's clock is out of sync with the service node network.") + return Error.clockOutOfSync + + case 421: + // The snode isn't associated with the given public key anymore + if let publicKey = publicKey { + func invalidateSwarm() { + SNLog("Invalidating swarm for: \(publicKey).") + SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey) + } + + if let data: Data = data { + let snodes = parseSnodes(from: data) + + if !snodes.isEmpty { + setSwarm(to: snodes, for: publicKey) + } + else { + invalidateSwarm() + } + } + else { invalidateSwarm() } - } else { - invalidateSwarm() } - } else { - SNLog("Got a 421 without an associated public key.") - } - default: - handleBadSnode() - SNLog("Unhandled response code: \(statusCode).") + else { + SNLog("Got a 421 without an associated public key.") + } + + default: + handleBadSnode() + SNLog("Unhandled response code: \(statusCode).") } + return nil } } diff --git a/SessionUtilitiesKit/Networking/HTTP.swift b/SessionUtilitiesKit/Networking/HTTP.swift index 6d8b7d45b..a13fffbb9 100644 --- a/SessionUtilitiesKit/Networking/HTTP.swift +++ b/SessionUtilitiesKit/Networking/HTTP.swift @@ -67,7 +67,8 @@ public enum HTTP { } } - // MARK: Verb + // MARK: - Verb + public enum Verb: String, Codable { case get = "GET" case put = "PUT" @@ -75,92 +76,47 @@ public enum HTTP { case delete = "DELETE" } - // MARK: Error + // MARK: - Error + public enum Error : LocalizedError { case generic - case httpRequestFailed(statusCode: UInt, json: JSON?) + case httpRequestFailed(statusCode: UInt, data: Data?) case invalidJSON case invalidResponse public var errorDescription: String? { switch self { - case .generic: return "An error occurred." - case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)." - case .invalidJSON: return "Invalid JSON." - case .invalidResponse: return "Invalid Response" + case .generic: return "An error occurred." + case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)." + case .invalidJSON: return "Invalid JSON." + case .invalidResponse: return "Invalid Response" } } } - // MARK: Main - public static func execute(_ verb: Verb, _ url: String, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { + // MARK: - Main + + public static func execute(_ verb: Verb, _ url: String, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession) } - public static func execute(_ verb: Verb, _ url: String, parameters: JSON?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { + public static func execute(_ verb: Verb, _ url: String, parameters: JSON?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { if let parameters = parameters { do { guard JSONSerialization.isValidJSONObject(parameters) else { return Promise(error: Error.invalidJSON) } let body = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ]) return execute(verb, url, body: body, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession) - } catch (let error) { + } + catch (let error) { return Promise(error: error) } - } else { + } + else { return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession) } } - public static func execute(_ verb: Verb, _ url: String, body: Data?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { - var request = URLRequest(url: URL(string: url)!) - request.httpMethod = verb.rawValue - request.httpBody = body - request.timeoutInterval = timeout - request.allHTTPHeaderFields?.removeValue(forKey: "User-Agent") - request.setValue("WhatsApp", forHTTPHeaderField: "User-Agent") // Set a fake value - request.setValue("en-us", forHTTPHeaderField: "Accept-Language") // Set a fake value - let (promise, seal) = Promise.pending() - let urlSession = useSeedNodeURLSession ? seedNodeURLSession : snodeURLSession - let task = urlSession.dataTask(with: request) { data, response, error in - guard let data = data, let response = response as? HTTPURLResponse else { - if let error = error { - SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).") - } else { - SNLog("\(verb.rawValue) request to \(url) failed.") - } - // Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:) - return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil)) - } - if let error = error { - SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).") - // Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:) - return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil)) - } - let statusCode = UInt(response.statusCode) - var json: JSON? = nil - if let j = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON { - json = j - } else if let result = String(data: data, encoding: .utf8) { - json = [ "result" : result ] - } - guard 200...299 ~= statusCode else { - let jsonDescription = json?.prettifiedDescription ?? "no debugging info provided" - SNLog("\(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).") - return seal.reject(Error.httpRequestFailed(statusCode: statusCode, json: json)) - } - if let json = json { - seal.fulfill(json) - } else { - SNLog("Couldn't parse JSON returned by \(verb.rawValue) request to \(url).") - return seal.reject(Error.invalidJSON) - } - } - task.resume() - return promise - } - - // TODO: Consilidate the above and this method - public static func updatedExecute(_ verb: Verb, _ url: String, body: Data?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { + public static func execute(_ verb: Verb, _ url: String, body: Data?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { var request = URLRequest(url: URL(string: url)!) request.httpMethod = verb.rawValue request.httpBody = body @@ -178,21 +134,27 @@ public enum HTTP { SNLog("\(verb.rawValue) request to \(url) failed.") } // Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:) - return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil)) + return seal.reject(Error.httpRequestFailed(statusCode: 0, data: nil)) } if let error = error { SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).") // Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:) - return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil)) + return seal.reject(Error.httpRequestFailed(statusCode: 0, data: data)) } let statusCode = UInt(response.statusCode) guard 200...299 ~= statusCode else { -// let jsonDescription = json?.prettifiedDescription ?? "no debugging info provided" -// SNLog("\(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).") -// return seal.reject(Error.httpRequestFailed(statusCode: statusCode, json: json)) - // TODO: Provide error from backend here - return seal.reject(Error.httpRequestFailed(statusCode: statusCode, json: [:])) + var json: JSON? = nil + if let processedJson: JSON = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON { + json = processedJson + } + else if let result: String = String(data: data, encoding: .utf8) { + json = [ "result": result ] + } + + let jsonDescription: String = (json?.prettifiedDescription ?? "no debugging info provided") + SNLog("\(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).") + return seal.reject(Error.httpRequestFailed(statusCode: statusCode, data: data)) } seal.fulfill(data)