Added LokiSnodeProxy

This commit is contained in:
Mikunj 2020-01-20 13:12:26 +11:00
parent 920520077d
commit 54c2e793ed
8 changed files with 245 additions and 21 deletions

2
Pods

@ -1 +1 @@
Subproject commit 80b9ef94d107a4c2794164537da30eb4482af49a
Subproject commit 69d7254eda0f6422c98ce7a3d70a14e7a49ea1c5

View File

@ -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<Any> {
return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map { $0.responseObject }.recover { error -> Promise<Any> 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
}
}
}

View File

@ -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<Any> {
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<Any> 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<Any> 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<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)
}
}
task?.resume()
return promise
}
}

View File

@ -47,7 +47,7 @@ public extension LokiAPI {
}
// MARK: Internal API
private static func getRandomSnode() -> Promise<LokiAPITarget> {
internal static func getRandomSnode() -> Promise<LokiAPITarget> {
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<LokiAPITarget> 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<T> {
return recover(on: LokiAPI.errorHandlingQueue) { error -> Promise<T> 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 {

View File

@ -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<T> in
switch error {
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
case LokiHttpClient.HttpError.networkError(_, _, let underlyingError): throw underlyingError ?? error
default: throw error
}
}

View File

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

View File

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

View File

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