Clean
This commit is contained in:
parent
f28d77ed4e
commit
ec457a4a26
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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 {
|
||||
body = string
|
||||
if let jsonBody = try? JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: .allowFragments) as? [String: Any] {
|
||||
body = jsonBody
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
throw HttpError.networkError(code: code, response: body, underlyingError: Error.targetNodeHttpError(code: code, 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 [:]
|
||||
}
|
||||
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 (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 {
|
||||
resolver.reject(error)
|
||||
OutageDetection.shared.reportConnectionSuccess()
|
||||
resolver.fulfill(result)
|
||||
}
|
||||
} else {
|
||||
OutageDetection.shared.reportConnectionSuccess()
|
||||
resolver.fulfill(result)
|
||||
}
|
||||
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 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 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
|
||||
}
|
||||
}
|
||||
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] Proxy request failed with error: \(error.localizedDescription).")
|
||||
throw HTTPError.from(error: error) ?? error
|
||||
}
|
||||
}
|
||||
|
||||
// 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