From ec457a4a2643a2a0582c449589448d82ecb00cc7 Mon Sep 17 00:00:00 2001
From: Niels Andriesse <andriesseniels@gmail.com>
Date: Fri, 24 Jan 2020 15:55:07 +1100
Subject: [PATCH] Clean

---
 .../src/Loki/API/LokiAPI+SwarmAPI.swift       |   4 +-
 SignalServiceKit/src/Loki/API/LokiAPI.swift   |   4 +-
 .../src/Loki/API/LokiHttpClient.swift         |  33 ++-
 .../src/Loki/API/LokiSnodeProxy.swift         | 195 ++++++++----------
 4 files changed, 102 insertions(+), 134 deletions(-)

diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift
index 84634f3a8..a423c356c 100644
--- a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift
+++ b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift
@@ -124,7 +124,7 @@ internal extension Promise {
     
     internal func handlingSwarmSpecificErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<T> {
         return recover(on: LokiAPI.errorHandlingQueue) { error -> Promise<T> in
-            if let error = error as? LokiHttpClient.HttpError {
+            if let error = error as? LokiHTTPClient.HTTPError {
                 switch error.statusCode {
                 case 0, 400, 500, 503:
                     // The snode is unreachable
@@ -144,7 +144,7 @@ internal extension Promise {
                     LokiAPI.dropIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey)
                 case 432:
                     // The PoW difficulty is too low
-                    if case LokiHttpClient.HttpError.networkError(_, let result, _) = error, let json = result as? JSON, let powDifficulty = json["difficulty"] as? Int {
+                    if case LokiHTTPClient.HTTPError.networkError(_, let result, _) = error, let json = result as? JSON, let powDifficulty = json["difficulty"] as? Int {
                         print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
                         LokiAPI.powDifficulty = UInt(powDifficulty)
                     } else {
diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift
index 61bf9fcd9..6a13778a5 100644
--- a/SignalServiceKit/src/Loki/API/LokiAPI.swift
+++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift
@@ -89,7 +89,7 @@ public final class LokiAPI : NSObject {
         let headers = request.allHTTPHeaderFields ?? [:]
         let headersDescription = headers.isEmpty ? "no custom headers specified" : headers.prettifiedDescription
         print("[Loki] Invoking \(method.rawValue) on \(target) with \(parameters.prettifiedDescription) (\(headersDescription)).")
-        return LokiSnodeProxy(target: target).perform(request, withCompletionQueue: DispatchQueue.global())
+        return LokiSnodeProxy(for: target).perform(request, withCompletionQueue: DispatchQueue.global())
             .handlingSwarmSpecificErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey)
             .recoveringNetworkErrorsIfNeeded()
     }
@@ -387,7 +387,7 @@ private extension Promise {
         return recover(on: DispatchQueue.global()) { error -> Promise<T> in
             switch error {
             case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
-            case LokiHttpClient.HttpError.networkError(_, _, let underlyingError): throw underlyingError ?? error
+            case LokiHTTPClient.HTTPError.networkError(_, _, let underlyingError): throw underlyingError ?? error
             default: throw error
             }
         }
diff --git a/SignalServiceKit/src/Loki/API/LokiHttpClient.swift b/SignalServiceKit/src/Loki/API/LokiHttpClient.swift
index aebebd310..790408569 100644
--- a/SignalServiceKit/src/Loki/API/LokiHttpClient.swift
+++ b/SignalServiceKit/src/Loki/API/LokiHttpClient.swift
@@ -1,25 +1,27 @@
 import PromiseKit
 
-internal class LokiHttpClient {
-    enum HttpError: LocalizedError {
+internal class LokiHTTPClient {
+    
+    internal enum HTTPError: LocalizedError {
         case networkError(code: Int, response: Any?, underlyingError: Error?)
         
         public var errorDescription: String? {
            switch self {
-           case .networkError(let code, let body, let underlingError): return underlingError?.localizedDescription ?? "Failed network request with code: \(code) \(body ?? "")"
+           case .networkError(let code, let body, let underlingError): return underlingError?.localizedDescription ?? "Failed HTTP request with status code: \(code), message: \(body ?? "")."
            }
         }
     }
     
-    func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> Promise<Any> {
-        return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map { $0.responseObject }.recover { error -> Promise<Any> in
-            throw HttpError.from(error: error) ?? error
+    internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
+        return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map { $0.responseObject }.recover { error -> LokiAPI.RawResponsePromise in
+            throw HTTPError.from(error: error) ?? error
         }
     }
 }
 
-extension LokiHttpClient.HttpError {
-    static func from(error: Error) -> LokiHttpClient.HttpError? {
+internal extension LokiHTTPClient.HTTPError {
+    
+    internal static func from(error: Error) -> LokiHTTPClient.HTTPError? {
         if let error = error as? NetworkManagerError {
             if case NetworkManagerError.taskError(_, let underlyingError) = error, let nsError = underlyingError as? NSError {
                 var response = nsError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]
@@ -27,25 +29,22 @@ extension LokiHttpClient.HttpError {
                 if let data = response as? Data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? JSON {
                     response = json
                 }
-                return LokiHttpClient.HttpError.networkError(code: error.statusCode, response: response, underlyingError: underlyingError)
+                return LokiHTTPClient.HTTPError.networkError(code: error.statusCode, response: response, underlyingError: underlyingError)
             }
-            return LokiHttpClient.HttpError.networkError(code: error.statusCode, response: nil, underlyingError: error)
+            return LokiHTTPClient.HTTPError.networkError(code: error.statusCode, response: nil, underlyingError: error)
         }
         return nil
     }
     
-    var isNetworkError: Bool {
+    internal var isNetworkError: Bool {
         switch self {
-        case .networkError(_, _, let underlyingError):
-            return underlyingError != nil && IsNSErrorNetworkFailure(underlyingError)
+        case .networkError(_, _, let underlyingError): return underlyingError != nil && IsNSErrorNetworkFailure(underlyingError)
         }
-        return false
     }
 
-    var statusCode: Int {
+    internal var statusCode: Int {
         switch self {
-        case .networkError(let code, _, _):
-            return code
+        case .networkError(let code, _, _): return code
         }
     }
 }
diff --git a/SignalServiceKit/src/Loki/API/LokiSnodeProxy.swift b/SignalServiceKit/src/Loki/API/LokiSnodeProxy.swift
index 470805f8d..7839bf660 100644
--- a/SignalServiceKit/src/Loki/API/LokiSnodeProxy.swift
+++ b/SignalServiceKit/src/Loki/API/LokiSnodeProxy.swift
@@ -1,147 +1,116 @@
 import PromiseKit
 
-internal class LokiSnodeProxy: LokiHttpClient {
+internal class LokiSnodeProxy : LokiHTTPClient {
     internal let target: LokiAPITarget
     private let keyPair: ECKeyPair
     
+    private lazy var httpSession: AFHTTPSessionManager = {
+        let result = AFHTTPSessionManager(sessionConfiguration: .ephemeral)
+        let securityPolicy = AFSecurityPolicy.default()
+        securityPolicy.allowInvalidCertificates = true
+        securityPolicy.validatesDomainName = false
+        result.securityPolicy = securityPolicy
+        result.responseSerializer = AFHTTPResponseSerializer()
+        return result
+    }()
+    
+    // MARK: Error
     internal enum Error : LocalizedError {
-        case invalidPublicKeys
-        case failedToEncryptRequest
-        case failedToParseProxyResponse
-        case targetNodeHttpError(code: Int, message: Any?)
+        case targetPublicKeySetMissing
+        case symmetricKeyGenerationFailed
+        case proxyResponseParsingFailed
+        case targetSnodeHTTPError(code: Int, message: Any?)
            
-        public var errorDescription: String? {
+        internal var errorDescription: String? {
            switch self {
-            case .invalidPublicKeys: return "Invalid target public key"
-            case .failedToEncryptRequest: return "Failed to encrypt request"
-            case .failedToParseProxyResponse: return "Failed to parse proxy response"
-           case .targetNodeHttpError(let code, let message): return "Target node returned error \(code) - \(message ?? "No message provided")"
+           case .targetPublicKeySetMissing: return "Missing target public key set"
+           case .symmetricKeyGenerationFailed: return "Couldn't generate symmetric key"
+           case .proxyResponseParsingFailed: return "Couldn't parse proxy response"
+           case .targetSnodeHTTPError(let httpStatusCode, let message): return "Target snode returned error \(httpStatusCode) with description: \(message ?? "no description provided")."
            }
         }
     }
     
-    // MARK: - Http
-    private var sessionManager: AFHTTPSessionManager = {
-        let manager = AFHTTPSessionManager(sessionConfiguration: URLSessionConfiguration.ephemeral)
-        let securityPolicy = AFSecurityPolicy.default()
-        securityPolicy.allowInvalidCertificates = true
-        securityPolicy.validatesDomainName = false
-        manager.securityPolicy = securityPolicy
-        manager.responseSerializer = AFHTTPResponseSerializer()
-        return manager
-    }()
-    
-    
-    // MARK: - Class functions
-    
-    init(target: LokiAPITarget) {
+    // MARK: Initialization
+    internal init(for target: LokiAPITarget) {
         self.target = target
         keyPair = Curve25519.generateKeyPair()
         super.init()
     }
     
-    override func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> Promise<Any> {
-        guard let targetHexEncodedPublicKeys = target.publicKeySet else {
-            return Promise(error: Error.invalidPublicKeys)
-        }
-        
-        guard let symmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeys.encryptionKey), privateKey: keyPair.privateKey) else {
-            return Promise(error: Error.failedToEncryptRequest)
-        }
-                
-        return LokiAPI.getRandomSnode().then { snode -> Promise<Any> in
-            let url = "\(snode.address):\(snode.port)/proxy"
-            print("[Loki][Snode proxy] Proxy request to \(self.target) via \(snode).")
-            let requestParams = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
-            let params: [String : Any] = [
+    // MARK: Proxying
+    override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
+        guard let targetHexEncodedPublicKeySet = target.publicKeySet else { return Promise(error: Error.targetPublicKeySetMissing) }
+        let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeySet.encryptionKey), privateKey: keyPair.privateKey)
+        guard let symmetricKey = uncheckedSymmetricKey else { return Promise(error: Error.symmetricKeyGenerationFailed) }
+        let headers = convertHeadersToProxyEndpointFormat(for: request)
+        return LokiAPI.getRandomSnode().then { [target = self.target, keyPair = self.keyPair, httpSession = self.httpSession] proxy -> Promise<Any> in
+            let url = "\(proxy.address):\(proxy.port)/proxy"
+            print("[Loki] Proxying request to \(target) through \(proxy).")
+            let parametersAsData = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
+            let proxyRequestParameters: [String : Any] = [
                 "method" : request.httpMethod,
-                "body" : String(bytes: requestParams, encoding: .utf8),
-                "headers" : self.getHeaders(request: request)
+                "body" : String(bytes: parametersAsData, encoding: .utf8),
+                "headers" : headers
             ]
-            let proxyParams = try JSONSerialization.data(withJSONObject: params, options: [])
-            let ivAndCipherText = try DiffieHellman.encrypt(proxyParams, using: symmetricKey)
-            let headers = [
-                "X-Sender-Public-Key" : self.keyPair.publicKey.hexadecimalString,
-                "X-Target-Snode-Key" : targetHexEncodedPublicKeys.idKey
+            let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
+            let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
+            let proxyRequestHeaders = [
+                "X-Sender-Public-Key" : keyPair.publicKey.map { String(format: "%02hhx", $0) }.joined(),
+                "X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey
             ]
-            return self.post(url: url, body: ivAndCipherText, headers: headers, timeoutInterval: request.timeoutInterval)
-        }.map { response in
-            guard response is Data, let cipherText = Data(base64Encoded: response as! Data) else {
-                print("[Loki][Snode proxy] Received non-string response")
-                return response
+            let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
+            let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
+            proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
+            proxyRequest.httpBody = ivAndCipherText
+            proxyRequest.timeoutInterval = request.timeoutInterval
+            var task: URLSessionDataTask!
+            task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
+                if let error = error {
+                    let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
+                    let nsError: NSError = nmError as NSError
+                    nsError.isRetryable = false
+                    resolver.reject(nsError)
+                } else {
+                    OutageDetection.shared.reportConnectionSuccess()
+                    resolver.fulfill(result)
+                }
             }
-            
-            let decrypted = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
-            
-            // Unwrap and handle errors if needed
-            guard let json = try? JSONSerialization.jsonObject(with: decrypted, options: .allowFragments) as? [String: Any], let code = json["status"] as? Int else {
-                throw HttpError.networkError(code: -1, response: nil, underlyingError: Error.failedToParseProxyResponse)
+            task.resume()
+            return promise
+        }.map { rawResponse in
+            guard let data = rawResponse as? Data, let cipherText = Data(base64Encoded: data) else {
+                print("[Loki] Received a non-string encoded response.")
+                return rawResponse
             }
-            
-            let success = (200..<300).contains(code)
+            let response = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
+            let uncheckedJSON = try? JSONSerialization.jsonObject(with: response, options: .allowFragments) as? JSON
+            guard let json = uncheckedJSON, let httpStatusCode = json["status"] as? Int else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
+            let isSuccess = (200..<300).contains(httpStatusCode)
             var body: Any? = nil
-            if let string = json["body"] as? String {
-                body = string
-                if let jsonBody = try? JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: .allowFragments) as? [String: Any] {
-                    body = jsonBody
+            if let bodyAsString = json["body"] as? String {
+                body = bodyAsString
+                if let bodyAsJSON = try? JSONSerialization.jsonObject(with: bodyAsString.data(using: .utf8)!, options: .allowFragments) as? [String: Any] {
+                    body = bodyAsJSON
                 }
             }
-            
-            if (!success) {
-                throw HttpError.networkError(code: code, response: body, underlyingError: Error.targetNodeHttpError(code: code, message: body))
-            }
-            
+            guard isSuccess else { throw HTTPError.networkError(code: httpStatusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: httpStatusCode, message: body)) }
             return body
         }.recover { error -> Promise<Any> in
-            print("[Loki][Snode proxy] Failed proxy request. \(error.localizedDescription)")
-            throw HttpError.from(error: error) ?? error
-        }
-    }
-    
-    // MARK:- Private functions
-    
-    private func getHeaders(request: TSRequest) -> [String: Any] {
-        guard let headers = request.allHTTPHeaderFields else {
-            return [:]
+            print("[Loki] Proxy request failed with error: \(error.localizedDescription).")
+            throw HTTPError.from(error: error) ?? error
         }
-        var newHeaders: [String: Any] = [:]
-        for header in headers {
-            var value: Any = header.value
-            // We need to convert any string boolean values to actual boolean values
-            if (header.value.lowercased() == "true" || header.value.lowercased() == "false") {
-                value = NSString(string: header.value).boolValue
-            }
-            newHeaders[header.key] = value
-        }
-        return newHeaders
     }
     
-    private func post(url: String, body: Data?, headers: [String: String]?, timeoutInterval: TimeInterval) -> Promise<Any> {
-        let (promise, resolver) = Promise<Any>.pending()
-        let request = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
-        request.allHTTPHeaderFields = headers
-        request.httpBody = body
-        request.timeoutInterval = timeoutInterval
-        
-        var task: URLSessionDataTask? = nil
-        
-        task = sessionManager.dataTask(with: request as URLRequest) { (response, result, error) in
-            if let error = error {
-                if let task = task {
-                    let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
-                    let nsError: NSError = nmError as NSError
-                    nsError.isRetryable = false
-                    resolver.reject(nsError)
-                } else {
-                    resolver.reject(error)
-                }
-            } else {
-                OutageDetection.shared.reportConnectionSuccess()
-                resolver.fulfill(result)
+    // MARK: Convenience
+    private func convertHeadersToProxyEndpointFormat(for request: TSRequest) -> [String: Any] {
+        guard let headers = request.allHTTPHeaderFields else { return [:] }
+        return headers.mapValues { value in
+            switch value.lowercased() {
+            case "true": return true
+            case "false": return false
+            default: return value
             }
         }
-        
-        task?.resume()
-        return promise
     }
 }