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 } }