From 651aa7039d343afb9a7917e045a558d3830b9e49 Mon Sep 17 00:00:00 2001 From: gmbnt Date: Thu, 2 Apr 2020 14:54:52 +1100 Subject: [PATCH] Move HTTP utilities into their own file --- SignalServiceKit/src/Loki/API/HTTP/HTTP.swift | 87 +++++++++++++++++++ .../src/Loki/API/LokiAPI+SwarmAPI.swift | 2 +- .../OnionRequestAPI+Encryption.swift | 6 +- .../API/Onion Requests/OnionRequestAPI.swift | 78 +---------------- 4 files changed, 94 insertions(+), 79 deletions(-) create mode 100644 SignalServiceKit/src/Loki/API/HTTP/HTTP.swift diff --git a/SignalServiceKit/src/Loki/API/HTTP/HTTP.swift b/SignalServiceKit/src/Loki/API/HTTP/HTTP.swift new file mode 100644 index 000000000..d9a5c4e0e --- /dev/null +++ b/SignalServiceKit/src/Loki/API/HTTP/HTTP.swift @@ -0,0 +1,87 @@ +import PromiseKit + +internal enum HTTP { + private static let urlSession = URLSession(configuration: .ephemeral, delegate: urlSessionDelegate, delegateQueue: nil) + private static let urlSessionDelegate = URLSessionDelegateImplementation() + + // MARK: Settings + private static let timeout: TimeInterval = 20 + + // MARK: URL Session Delegate Implementation + private final class URLSessionDelegateImplementation : NSObject, URLSessionDelegate { + + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + // Snode to snode communication uses self-signed certificates but clients can safely ignore this + completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) + } + } + + // MARK: Verb + internal enum Verb : String { + case get = "GET" + case put = "PUT" + case post = "POST" + case delete = "DELETE" + } + + // MARK: Error + internal enum Error : LocalizedError { + case generic + case httpRequestFailed(statusCode: UInt, json: JSON?) + case invalidJSON + + 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." + } + } + } + + internal static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise { + return Promise { seal in + let url = URL(string: url)! + var request = URLRequest(url: url) + request.httpMethod = verb.rawValue + if let parameters = parameters { + do { + guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(Error.invalidJSON) } + request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) + } catch (let error) { + return seal.reject(error) + } + } + request.timeoutInterval = timeout + let task = urlSession.dataTask(with: request) { data, response, error in + guard let data = data, let response = response as? HTTPURLResponse else { + print("[Loki] \(verb.rawValue) request to \(url) failed.") + return seal.reject(Error.generic) + } + if let error = error { + print("[Loki] \(verb.rawValue) request to \(url) failed due to error: \(error).") + return seal.reject(error) + } + let statusCode = UInt(response.statusCode) + var json: JSON? = nil + if let j = try? JSONSerialization.jsonObject(with: data, options: []) 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" + print("[Loki] \(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 { + print("[Loki] Couldn't parse JSON returned by \(verb.rawValue) request to \(url).") + return seal.reject(Error.invalidJSON) + } + } + task.resume() + } + } +} diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift index fe9abeff5..b551e105f 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift @@ -114,7 +114,7 @@ public extension LokiAPI { print("[Loki] Rejecting file server proxy with version number \(version).") return getFileServerProxy() } - }.recover(on: DispatchQueue.global()) { error in + }.recover(on: DispatchQueue.global()) { _ in return getFileServerProxy() } }.done(on: DispatchQueue.global()) { snode in diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift index 7e0a9ef83..76c5c3e35 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift @@ -48,11 +48,11 @@ extension OnionRequestAPI { let (promise, seal) = Promise.pending() getQueue().async { do { - guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(Error.invalidJSON) } + guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) } let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: []) let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ] - guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(Error.invalidJSON) } + guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) } let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: []) let result = try encrypt(plaintext, forSnode: snode) seal.fulfill(result) @@ -73,7 +73,7 @@ extension OnionRequestAPI { "destination" : rhs.publicKeySet!.ed25519Key ] do { - guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(Error.invalidJSON) } + guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) } let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: []) let result = try encrypt(plaintext, forSnode: lhs) seal.fulfill(result) diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index f59677e83..49d4e9418 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -3,9 +3,6 @@ import PromiseKit /// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. internal enum OnionRequestAPI { - private static let urlSession = URLSession(configuration: .ephemeral, delegate: urlSessionDelegate, delegateQueue: nil) - private static let urlSessionDelegate = URLSessionDelegateImplementation() - /// - Note: Exposed for testing purposes. internal static let workQueue = DispatchQueue(label: "OnionRequestAPI.workQueue", qos: .userInitiated) /// - Note: Must only be modified from `workQueue`. @@ -26,29 +23,9 @@ internal enum OnionRequestAPI { private static var guardSnodeCount: UInt { return pathCount } // One per path - // MARK: HTTP Verb - private enum HTTPVerb : String { - case get = "GET" - case put = "PUT" - case post = "POST" - case delete = "DELETE" - } - - // MARK: URL Session Delegate Implementation - private final class URLSessionDelegateImplementation : NSObject, URLSessionDelegate { - - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - // Snode to snode communication uses self-signed certificates but clients can safely ignore this - completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) - } - } - // MARK: Error internal enum Error : LocalizedError { - case generic - case httpRequestFailed(statusCode: UInt, json: JSON?) case insufficientSnodes - case invalidJSON case missingSnodeVersion case randomDataGenerationFailed case snodePublicKeySetMissing @@ -56,10 +33,7 @@ internal enum OnionRequestAPI { var errorDescription: String? { switch self { - case .generic: return "An error occurred." - case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)." case .insufficientSnodes: return "Couldn't find enough snodes to build a path." - case .invalidJSON: return "Invalid JSON." case .missingSnodeVersion: return "Missing snode version." case .randomDataGenerationFailed: return "Couldn't generate random data." case .snodePublicKeySetMissing: return "Missing snode public key set." @@ -75,52 +49,6 @@ internal enum OnionRequestAPI { private typealias OnionBuildingResult = (guardSnode: LokiAPITarget, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data) // MARK: Private API - private static func execute(_ verb: HTTPVerb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = OnionRequestAPI.timeout) -> Promise { - return Promise { seal in - let url = URL(string: url)! - var request = URLRequest(url: url) - request.httpMethod = verb.rawValue - if let parameters = parameters { - do { - guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(Error.invalidJSON) } - request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) - } catch (let error) { - return seal.reject(error) - } - } - request.timeoutInterval = timeout - let task = urlSession.dataTask(with: request) { data, response, error in - guard let data = data, let response = response as? HTTPURLResponse else { - print("[Loki] [Onion Request API] \(verb.rawValue) request to \(url) failed.") - return seal.reject(Error.generic) - } - if let error = error { - print("[Loki] [Onion Request API] \(verb.rawValue) request to \(url) failed due to error: \(error).") - return seal.reject(error) - } - let statusCode = UInt(response.statusCode) - var json: JSON? = nil - if let j = try? JSONSerialization.jsonObject(with: data, options: []) 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" - print("[Loki] [Onion Request API] \(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 { - print("[Loki] [Onion Request API] Couldn't parse JSON returned by \(verb.rawValue) request to \(url).") - return seal.reject(Error.invalidJSON) - } - } - task.resume() - } - } - /// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. private static func testSnode(_ snode: LokiAPITarget) -> Promise { let (promise, seal) = Promise.pending() @@ -128,7 +56,7 @@ internal enum OnionRequestAPI { queue.async { let url = "\(snode.address):\(snode.port)/get_stats/v1" let timeout: TimeInterval = 6 // Use a shorter timeout for testing - execute(.get, url, timeout: timeout).done(on: queue) { rawResponse in + HTTP.execute(.get, url, timeout: timeout).done(on: queue) { rawResponse in guard let json = rawResponse as? JSON, let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) } if version >= "2.0.0" { seal.fulfill(()) @@ -261,9 +189,9 @@ internal enum OnionRequestAPI { "ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString() ] let targetSnodeSymmetricKey = intermediate.targetSnodeSymmetricKey - execute(.post, url, parameters: parameters).done(on: workQueue) { rawResponse in + HTTP.execute(.post, url, parameters: parameters).done(on: workQueue) { rawResponse in guard let json = rawResponse as? JSON, let base64EncodedIVAndCiphertext = json["result"] as? String, - let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext) else { return seal.reject(Error.invalidJSON) } + let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext) else { return seal.reject(HTTP.Error.invalidJSON) } let iv = ivAndCiphertext[0..