From 54c2e793ed936bbb9bc4050f2a96d0faf8a9d6d1 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 20 Jan 2020 13:12:26 +1100 Subject: [PATCH] Added LokiSnodeProxy --- Pods | 2 +- .../src/Loki/API/Clients/LokiHttpClient.swift | 51 ++++++ .../src/Loki/API/Clients/LokiSnodeProxy.swift | 154 ++++++++++++++++++ .../src/Loki/API/LokiAPI+SwarmAPI.swift | 19 +-- SignalServiceKit/src/Loki/API/LokiAPI.swift | 8 +- .../src/Loki/API/LokiAPITarget.swift | 22 ++- .../src/Loki/API/LokiP2PAPI.swift | 2 +- .../src/Network/OWSSignalService.m | 8 +- 8 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 SignalServiceKit/src/Loki/API/Clients/LokiHttpClient.swift create mode 100644 SignalServiceKit/src/Loki/API/Clients/LokiSnodeProxy.swift diff --git a/Pods b/Pods index 80b9ef94d..69d7254ed 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 80b9ef94d107a4c2794164537da30eb4482af49a +Subproject commit 69d7254eda0f6422c98ce7a3d70a14e7a49ea1c5 diff --git a/SignalServiceKit/src/Loki/API/Clients/LokiHttpClient.swift b/SignalServiceKit/src/Loki/API/Clients/LokiHttpClient.swift new file mode 100644 index 000000000..368674095 --- /dev/null +++ b/SignalServiceKit/src/Loki/API/Clients/LokiHttpClient.swift @@ -0,0 +1,51 @@ +import PromiseKit + +internal class LokiHttpClient { + enum HttpError: LocalizedError { + /// Wraps TSNetworkManager failure callback params in a single throwable error + 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 ?? "")" + } + } + } + + func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> Promise { + return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map { $0.responseObject }.recover { error -> Promise in + throw LokiHttpClient.HttpError.from(error: error) ?? error + } + } +} + +extension LokiHttpClient.HttpError { + 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] + 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: nil, underlyingError: nil) + } + return nil + } + + var isNetworkError: Bool { + switch self { + case .networkError(_, _, let underlyingError): + return underlyingError != nil && IsNSErrorNetworkFailure(underlyingError) + } + return false + } + + var statusCode: Int { + switch self { + case .networkError(let code, _, _): + return code + } + } +} diff --git a/SignalServiceKit/src/Loki/API/Clients/LokiSnodeProxy.swift b/SignalServiceKit/src/Loki/API/Clients/LokiSnodeProxy.swift new file mode 100644 index 000000000..7a0aa05fd --- /dev/null +++ b/SignalServiceKit/src/Loki/API/Clients/LokiSnodeProxy.swift @@ -0,0 +1,154 @@ +import PromiseKit + +internal class LokiSnodeProxy: LokiHttpClient { + internal let target: LokiAPITarget + + internal enum Error : LocalizedError { + case invalidPublicKeys + case failedToEncryptRequest + case failedToParseProxyResponse + case targetNodeHttpError(code: Int, message: Any?) + + public 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")" + } + } + } + + // 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: - Ephemeral key + private var _kp: ECKeyPair + private var _lastGenerated: TimeInterval + private let keyPairRefreshTime: TimeInterval = 3 * 60 * 1000 // 3 minutes + + // MARK: - Class functions + + init(target: LokiAPITarget) { + self.target = target + _kp = Curve25519.generateKeyPair() + _lastGenerated = Date().timeIntervalSince1970 + super.init() + } + + private func getKeyPair() -> ECKeyPair { + if (Date().timeIntervalSince1970 > _lastGenerated + keyPairRefreshTime) { + _kp = Curve25519.generateKeyPair() + _lastGenerated = Date().timeIntervalSince1970 + } + return _kp + } + + override func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> Promise { + guard let targetHexEncodedPublicKeys = target.publicKeys else { + return Promise(error: Error.invalidPublicKeys) + } + + let keyPair = getKeyPair() + guard let symmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeys.encryption), privateKey: keyPair.privateKey) else { + return Promise(error: Error.failedToEncryptRequest) + } + + return LokiAPI.getRandomSnode().then { snode -> Promise in + let url = "\(snode.address):\(snode.port)/proxy" + print("[Loki][Snode proxy] Proxy request to \(self.target) via \(snode).") + var peepee = request.parameters + let jsonBodyData = try JSONSerialization.data(withJSONObject: peepee, options: []) + let jsonBodyString = String(bytes: jsonBodyData, encoding: .utf8) + let params: [String : Any] = [ "method" : request.httpMethod, "body" : jsonBodyString, "headers" : self.getHeaders(request: request) ] + let jsonParams = try JSONSerialization.data(withJSONObject: params, options: []) + let ivAndCipherText = try DiffieHellman.encrypt(jsonParams, using: symmetricKey) + let headers = [ "X-Sender-Public-Key" : keyPair.publicKey.hexadecimalString, "X-Target-Snode-Key" : targetHexEncodedPublicKeys.identification] + 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 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) + } + + let success = (200..<300).contains(code) + var body: Any? = nil + if let string = json["body"] as? String { + if let jsonBody = try? JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: .allowFragments) as? [String: Any] { + body = jsonBody + } else { + body = string + } + } + + if (!success) { + throw HttpError.networkError(code: code, response: body, underlyingError: Error.targetNodeHttpError(code: code, message: body)) + } + + return body + }.recover { error -> Promise in + print("[Loki][Snode proxy] Failed proxy request. \(error.localizedDescription)") + throw HttpError.from(error: error) ?? error + } + } + + private func getHeaders(request: TSRequest) -> [String: Any] { + guard let headers = request.allHTTPHeaderFields else { + return [:] + } + 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 { + let (promise, resolver) = Promise.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) + } + } + + task?.resume() + return promise + } +} diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift index ef2972e17..099059c99 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift @@ -47,7 +47,7 @@ public extension LokiAPI { } // MARK: Internal API - private static func getRandomSnode() -> Promise { + internal static func getRandomSnode() -> Promise { if randomSnodePool.isEmpty { let target = seedNodePool.randomElement()! let url = URL(string: "\(target)/json_rpc")! @@ -59,7 +59,8 @@ public extension LokiAPI { "fields" : [ "public_ip" : true, "storage_port" : true, - "pubkey_ed25519": true + "pubkey_ed25519": true, + "pubkey_x25519": true ] ] ]) @@ -68,11 +69,11 @@ public extension LokiAPI { let rawResponse = intermediate.responseObject guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw "Failed to update random snode pool from: \(rawResponse)." } randomSnodePool = try Set(rawTargets.flatMap { rawTarget in - guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let publicKey = rawTarget["pubkey_ed25519"] as? String, address != "0.0.0.0" else { + guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let identificationKey = rawTarget["pubkey_ed25519"] as? String, let encryptionKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else { print("Failed to update random snode pool from: \(rawTarget).") return nil } - return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKey: publicKey) + return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKeys: LokiAPITarget.Keys(identification: identificationKey, encryption: encryptionKey)) }) return randomSnodePool.randomElement()! }.recover(on: DispatchQueue.global()) { error -> Promise in @@ -109,11 +110,11 @@ public extension LokiAPI { return [] } return rawSnodes.flatMap { rawSnode in - guard let address = rawSnode["ip"] as? String, let portAsString = rawSnode["port"] as? String, let port = UInt16(portAsString), let publicKey = rawSnode["pubkey_ed25519"] as? String, address != "0.0.0.0" else { + guard let address = rawSnode["ip"] as? String, let portAsString = rawSnode["port"] as? String, let port = UInt16(portAsString), let identificationKey = rawSnode["pubkey_ed25519"] as? String, let encryptionKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else { print("[Loki] Failed to parse target from: \(rawSnode).") return nil } - return LokiAPITarget(address: "https://\(address)", port: port, publicKey: publicKey) + return LokiAPITarget(address: "https://\(address)", port: port, publicKeys: LokiAPITarget.Keys(identification: identificationKey, encryption: encryptionKey)) } } } @@ -123,7 +124,7 @@ internal extension Promise { internal func handlingSwarmSpecificErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise { return recover(on: LokiAPI.errorHandlingQueue) { error -> Promise in - if let error = error as? NetworkManagerError { + if let error = error as? LokiHttpClient.HttpError { switch error.statusCode { case 0, 400, 500, 503: // The snode is unreachable @@ -143,9 +144,7 @@ internal extension Promise { LokiAPI.dropIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey) case 432: // The PoW difficulty is too low - if case NetworkManagerError.taskError(_, let underlyingError) = error, let nsError = underlyingError as? NSError, - let data = nsError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] as? Data, let json = try? JSONSerialization.jsonObject(with: data, options: []) 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 1e8abcc1d..a85041c0b 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -89,8 +89,9 @@ 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 TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { $0.responseObject } - .handlingSwarmSpecificErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey).recoveringNetworkErrorsIfNeeded() + return LokiSnodeProxy(target: target).perform(request, withCompletionQueue: DispatchQueue.global()) + .handlingSwarmSpecificErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey) + .recoveringNetworkErrorsIfNeeded() } internal static func getRawMessages(from target: LokiAPITarget, usingLongPolling useLongPolling: Bool) -> RawResponsePromise { @@ -180,7 +181,7 @@ public final class LokiAPI : NSObject { } } if let peer = LokiP2PAPI.getInfo(for: destination), (lokiMessage.isPing || peer.isOnline) { - let target = LokiAPITarget(address: peer.address, port: peer.port, publicKey: nil) + let target = LokiAPITarget(address: peer.address, port: peer.port, publicKeys: nil) return Promise.value([ target ]).mapValues { sendLokiMessage(lokiMessage, to: $0) }.map { Set($0) }.retryingIfNeeded(maxRetryCount: maxRetryCount).get { _ in LokiP2PAPI.markOnline(destination) onP2PSuccess() @@ -386,6 +387,7 @@ private extension Promise { return recover(on: DispatchQueue.global()) { error -> Promise in switch error { case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError + case LokiHttpClient.HttpError.networkError(_, _, let underlyingError): throw underlyingError ?? error default: throw error } } diff --git a/SignalServiceKit/src/Loki/API/LokiAPITarget.swift b/SignalServiceKit/src/Loki/API/LokiAPITarget.swift index 2f8f9e5d3..5c7d0ad5e 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPITarget.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPITarget.swift @@ -2,7 +2,7 @@ internal final class LokiAPITarget : NSObject, NSCoding { internal let address: String internal let port: UInt16 - internal let publicKey: String? + internal let publicKeys: Keys? // MARK: Types internal enum Method : String { @@ -13,25 +13,37 @@ internal final class LokiAPITarget : NSObject, NSCoding { case sendMessage = "store" } + internal struct Keys { + let identification: String + let encryption: String + } + // MARK: Initialization - internal init(address: String, port: UInt16, publicKey: String?) { + internal init(address: String, port: UInt16, publicKeys: Keys?) { self.address = address self.port = port - self.publicKey = publicKey + self.publicKeys = publicKeys } // MARK: Coding internal init?(coder: NSCoder) { address = coder.decodeObject(forKey: "address") as! String port = coder.decodeObject(forKey: "port") as! UInt16 - publicKey = coder.decodeObject(forKey: "publicKey") as? String + if let identificationKey = coder.decodeObject(forKey: "identificationKey") as? String, let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String { + publicKeys = Keys(identification: identificationKey, encryption: encryptionKey) + } else { + publicKeys = nil + } super.init() } internal func encode(with coder: NSCoder) { coder.encode(address, forKey: "address") coder.encode(port, forKey: "port") - coder.encode(publicKey, forKey: "publicKey") + if let keys = publicKeys { + coder.encode(keys.identification, forKey: "identificationKey") + coder.encode(keys.encryption, forKey: "encryptionKey") + } } // MARK: Equality diff --git a/SignalServiceKit/src/Loki/API/LokiP2PAPI.swift b/SignalServiceKit/src/Loki/API/LokiP2PAPI.swift index a71d3574d..afcc6ed69 100644 --- a/SignalServiceKit/src/Loki/API/LokiP2PAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiP2PAPI.swift @@ -33,7 +33,7 @@ public class LokiP2PAPI : NSObject { /// - Parameter url: The url to our local server @objc public static func setOurP2PAddress(url: URL) { guard let scheme = url.scheme, let host = url.host, let port = url.port else { return } - let target = LokiAPITarget(address: "\(scheme)://\(host)", port: UInt16(port), publicKey: nil) + let target = LokiAPITarget(address: "\(scheme)://\(host)", port: UInt16(port), publicKeys: nil) ourP2PAddress = target } diff --git a/SignalServiceKit/src/Network/OWSSignalService.m b/SignalServiceKit/src/Network/OWSSignalService.m index 208c95f24..71fc0297f 100644 --- a/SignalServiceKit/src/Network/OWSSignalService.m +++ b/SignalServiceKit/src/Network/OWSSignalService.m @@ -193,7 +193,13 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange = sessionManager.securityPolicy = securityPolicy; sessionManager.requestSerializer = [AFJSONRequestSerializer serializer]; sessionManager.requestSerializer.HTTPShouldHandleCookies = NO; - sessionManager.responseSerializer = [AFJSONResponseSerializer serializerWithReadingOptions:NSJSONReadingAllowFragments]; + + // We could get JSON or text responses + NSArray *serializers = @[ + [AFJSONResponseSerializer serializerWithReadingOptions:NSJSONReadingAllowFragments], + [AFHTTPResponseSerializer serializer] + ]; + sessionManager.responseSerializer = [AFCompoundResponseSerializer compoundSerializerWithResponseSerializers:serializers]; return sessionManager; }