pull/82/head
Niels Andriesse 5 years ago
parent f28d77ed4e
commit ec457a4a26

@ -124,7 +124,7 @@ internal extension Promise {
internal func handlingSwarmSpecificErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<T> { internal func handlingSwarmSpecificErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<T> {
return recover(on: LokiAPI.errorHandlingQueue) { error -> Promise<T> in 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 { switch error.statusCode {
case 0, 400, 500, 503: case 0, 400, 500, 503:
// The snode is unreachable // The snode is unreachable
@ -144,7 +144,7 @@ internal extension Promise {
LokiAPI.dropIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey) LokiAPI.dropIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey)
case 432: case 432:
// The PoW difficulty is too low // 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).") print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
LokiAPI.powDifficulty = UInt(powDifficulty) LokiAPI.powDifficulty = UInt(powDifficulty)
} else { } else {

@ -89,7 +89,7 @@ public final class LokiAPI : NSObject {
let headers = request.allHTTPHeaderFields ?? [:] let headers = request.allHTTPHeaderFields ?? [:]
let headersDescription = headers.isEmpty ? "no custom headers specified" : headers.prettifiedDescription let headersDescription = headers.isEmpty ? "no custom headers specified" : headers.prettifiedDescription
print("[Loki] Invoking \(method.rawValue) on \(target) with \(parameters.prettifiedDescription) (\(headersDescription)).") 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) .handlingSwarmSpecificErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey)
.recoveringNetworkErrorsIfNeeded() .recoveringNetworkErrorsIfNeeded()
} }
@ -387,7 +387,7 @@ private extension Promise {
return recover(on: DispatchQueue.global()) { error -> Promise<T> in return recover(on: DispatchQueue.global()) { error -> Promise<T> in
switch error { switch error {
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError 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 default: throw error
} }
} }

@ -1,25 +1,27 @@
import PromiseKit import PromiseKit
internal class LokiHttpClient { internal class LokiHTTPClient {
enum HttpError: LocalizedError {
internal enum HTTPError: LocalizedError {
case networkError(code: Int, response: Any?, underlyingError: Error?) case networkError(code: Int, response: Any?, underlyingError: Error?)
public var errorDescription: String? { public var errorDescription: String? {
switch self { 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> { internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map { $0.responseObject }.recover { error -> Promise<Any> in return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map { $0.responseObject }.recover { error -> LokiAPI.RawResponsePromise in
throw HttpError.from(error: error) ?? error throw HTTPError.from(error: error) ?? error
} }
} }
} }
extension LokiHttpClient.HttpError { internal extension LokiHTTPClient.HTTPError {
static func from(error: Error) -> LokiHttpClient.HttpError? {
internal static func from(error: Error) -> LokiHTTPClient.HTTPError? {
if let error = error as? NetworkManagerError { if let error = error as? NetworkManagerError {
if case NetworkManagerError.taskError(_, let underlyingError) = error, let nsError = underlyingError as? NSError { if case NetworkManagerError.taskError(_, let underlyingError) = error, let nsError = underlyingError as? NSError {
var response = nsError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] 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 { if let data = response as? Data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? JSON {
response = 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 return nil
} }
var isNetworkError: Bool { internal var isNetworkError: Bool {
switch self { switch self {
case .networkError(_, _, let underlyingError): case .networkError(_, _, let underlyingError): return underlyingError != nil && IsNSErrorNetworkFailure(underlyingError)
return underlyingError != nil && IsNSErrorNetworkFailure(underlyingError)
} }
return false
} }
var statusCode: Int { internal var statusCode: Int {
switch self { switch self {
case .networkError(let code, _, _): case .networkError(let code, _, _): return code
return code
} }
} }
} }

@ -1,147 +1,116 @@
import PromiseKit import PromiseKit
internal class LokiSnodeProxy: LokiHttpClient { internal class LokiSnodeProxy : LokiHTTPClient {
internal let target: LokiAPITarget internal let target: LokiAPITarget
private let keyPair: ECKeyPair 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 { internal enum Error : LocalizedError {
case invalidPublicKeys case targetPublicKeySetMissing
case failedToEncryptRequest case symmetricKeyGenerationFailed
case failedToParseProxyResponse case proxyResponseParsingFailed
case targetNodeHttpError(code: Int, message: Any?) case targetSnodeHTTPError(code: Int, message: Any?)
public var errorDescription: String? { internal var errorDescription: String? {
switch self { switch self {
case .invalidPublicKeys: return "Invalid target public key" case .targetPublicKeySetMissing: return "Missing target public key set"
case .failedToEncryptRequest: return "Failed to encrypt request" case .symmetricKeyGenerationFailed: return "Couldn't generate symmetric key"
case .failedToParseProxyResponse: return "Failed to parse proxy response" case .proxyResponseParsingFailed: return "Couldn't parse proxy response"
case .targetNodeHttpError(let code, let message): return "Target node returned error \(code) - \(message ?? "No message provided")" case .targetSnodeHTTPError(let httpStatusCode, let message): return "Target snode returned error \(httpStatusCode) with description: \(message ?? "no description provided")."
} }
} }
} }
// MARK: - Http // MARK: Initialization
private var sessionManager: AFHTTPSessionManager = { internal init(for target: LokiAPITarget) {
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) {
self.target = target self.target = target
keyPair = Curve25519.generateKeyPair() keyPair = Curve25519.generateKeyPair()
super.init() super.init()
} }
override func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> Promise<Any> { // MARK: Proxying
guard let targetHexEncodedPublicKeys = target.publicKeySet else { override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
return Promise(error: Error.invalidPublicKeys) 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) }
guard let symmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeys.encryptionKey), privateKey: keyPair.privateKey) else { let headers = convertHeadersToProxyEndpointFormat(for: request)
return Promise(error: Error.failedToEncryptRequest) 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).")
return LokiAPI.getRandomSnode().then { snode -> Promise<Any> in let parametersAsData = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
let url = "\(snode.address):\(snode.port)/proxy" let proxyRequestParameters: [String : Any] = [
print("[Loki][Snode proxy] Proxy request to \(self.target) via \(snode).")
let requestParams = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
let params: [String : Any] = [
"method" : request.httpMethod, "method" : request.httpMethod,
"body" : String(bytes: requestParams, encoding: .utf8), "body" : String(bytes: parametersAsData, encoding: .utf8),
"headers" : self.getHeaders(request: request) "headers" : headers
] ]
let proxyParams = try JSONSerialization.data(withJSONObject: params, options: []) let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
let ivAndCipherText = try DiffieHellman.encrypt(proxyParams, using: symmetricKey) let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
let headers = [ let proxyRequestHeaders = [
"X-Sender-Public-Key" : self.keyPair.publicKey.hexadecimalString, "X-Sender-Public-Key" : keyPair.publicKey.map { String(format: "%02hhx", $0) }.joined(),
"X-Target-Snode-Key" : targetHexEncodedPublicKeys.idKey "X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey
] ]
return self.post(url: url, body: ivAndCipherText, headers: headers, timeoutInterval: request.timeoutInterval) let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
}.map { response in let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
guard response is Data, let cipherText = Data(base64Encoded: response as! Data) else { proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
print("[Loki][Snode proxy] Received non-string response") proxyRequest.httpBody = ivAndCipherText
return response 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)
}
} }
task.resume()
let decrypted = try DiffieHellman.decrypt(cipherText, using: symmetricKey) return promise
}.map { rawResponse in
// Unwrap and handle errors if needed guard let data = rawResponse as? Data, let cipherText = Data(base64Encoded: data) else {
guard let json = try? JSONSerialization.jsonObject(with: decrypted, options: .allowFragments) as? [String: Any], let code = json["status"] as? Int else { print("[Loki] Received a non-string encoded response.")
throw HttpError.networkError(code: -1, response: nil, underlyingError: Error.failedToParseProxyResponse) return rawResponse
} }
let response = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
let success = (200..<300).contains(code) 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 var body: Any? = nil
if let string = json["body"] as? String { if let bodyAsString = json["body"] as? String {
body = string body = bodyAsString
if let jsonBody = try? JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: .allowFragments) as? [String: Any] { if let bodyAsJSON = try? JSONSerialization.jsonObject(with: bodyAsString.data(using: .utf8)!, options: .allowFragments) as? [String: Any] {
body = jsonBody body = bodyAsJSON
} }
} }
guard isSuccess else { throw HTTPError.networkError(code: httpStatusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: httpStatusCode, message: body)) }
if (!success) {
throw HttpError.networkError(code: code, response: body, underlyingError: Error.targetNodeHttpError(code: code, message: body))
}
return body return body
}.recover { error -> Promise<Any> in }.recover { error -> Promise<Any> in
print("[Loki][Snode proxy] Failed proxy request. \(error.localizedDescription)") print("[Loki] Proxy request failed with error: \(error.localizedDescription).")
throw HttpError.from(error: error) ?? error throw HTTPError.from(error: error) ?? error
}
}
// MARK:- Private functions
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<Any> { // MARK: Convenience
let (promise, resolver) = Promise<Any>.pending() private func convertHeadersToProxyEndpointFormat(for request: TSRequest) -> [String: Any] {
let request = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil) guard let headers = request.allHTTPHeaderFields else { return [:] }
request.allHTTPHeaderFields = headers return headers.mapValues { value in
request.httpBody = body switch value.lowercased() {
request.timeoutInterval = timeoutInterval case "true": return true
case "false": return false
var task: URLSessionDataTask? = nil default: return value
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
} }
} }

Loading…
Cancel
Save