mirror of https://github.com/oxen-io/session-ios
				
				
				
			Clean
							parent
							
								
									f28d77ed4e
								
							
						
					
					
						commit
						ec457a4a26
					
				| @ -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 | ||||
|     } | ||||
| } | ||||
|  | ||||
					Loading…
					
					
				
		Reference in New Issue