From 8719ea676d7ac606a79115728d899317872df140 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 19 Feb 2020 13:55:58 +1100 Subject: [PATCH] Enforce threading convention All encryption and decryption on the global queue; don't bother for trivial operations --- .../src/Loki/API/LokiAPI+SwarmAPI.swift | 5 +- SignalServiceKit/src/Loki/API/LokiAPI.swift | 7 +- .../src/Loki/API/LokiDotNetAPI.swift | 10 +- .../src/Loki/API/LokiFileServerAPI.swift | 9 +- .../src/Loki/API/LokiFileServerProxy.swift | 146 +++++++++--------- .../src/Loki/API/LokiLongPoller.swift | 4 +- .../src/Loki/API/LokiSnodeProxy.swift | 114 +++++++------- .../API/Public Chat/LokiPublicChatAPI.swift | 94 +++++------ .../Public Chat/LokiPublicChatManager.swift | 2 +- .../Public Chat/LokiPublicChatPoller.swift | 4 +- 10 files changed, 211 insertions(+), 184 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift index ac45f7091..ba3b130de 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift @@ -48,7 +48,7 @@ public extension LokiAPI { ] ]) print("[Loki] Invoking get_n_service_nodes on \(target).") - return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { intermediate in + return TSNetworkManager.shared().perform(request).map(on: DispatchQueue.global()) { intermediate in 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 LokiAPIError.randomSnodePoolUpdatingFailed } randomSnodePool = try Set(rawTargets.flatMap { rawTarget in @@ -60,7 +60,7 @@ public extension LokiAPI { }) // randomElement() uses the system's default random generator, which is cryptographically secure return randomSnodePool.randomElement()! - }.recover(on: DispatchQueue.global()) { error -> Promise in + }.recover { error -> Promise in print("[Loki] Failed to contact seed node at: \(target).") throw error }.retryingIfNeeded(maxRetryCount: 16) // The seed nodes have historically been unreliable @@ -77,6 +77,7 @@ public extension LokiAPI { return Promise<[LokiAPITarget]> { $0.fulfill(cachedSwarm) } } else { let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ] + // All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works return getRandomSnode().then(on: DispatchQueue.global()) { invoke(.getSwarm, on: $0, associatedWith: hexEncodedPublicKey, parameters: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 } } } diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index a093c22c5..54aad067a 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -129,6 +129,7 @@ public final class LokiAPI : NSObject { } public static func getDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> Promise<[Destination]> { + // All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works let (promise, seal) = Promise<[Destination]>.pending() func getDestinations(in transaction: YapDatabaseReadTransaction? = nil) { func getDestinationsInternal(in transaction: YapDatabaseReadTransaction) { @@ -185,7 +186,7 @@ public final class LokiAPI : NSObject { } func sendLokiMessageUsingSwarmAPI() -> Promise> { notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp)) - return lokiMessage.calculatePoW().then(on: DispatchQueue.global()) { lokiMessageWithPoW -> Promise> in + return lokiMessage.calculatePoW().then { lokiMessageWithPoW -> Promise> in notificationCenter.post(name: .contactingNetwork, object: NSNumber(value: signalMessage.timestamp)) return getTargetSnodes(for: destination).map { swarm in return Set(swarm.map { target in @@ -209,7 +210,7 @@ public final class LokiAPI : NSObject { return Promise.value([ target ]).mapValues { sendLokiMessage(lokiMessage, to: $0) }.map { Set($0) }.retryingIfNeeded(maxRetryCount: maxRetryCount).get { _ in LokiP2PAPI.markOnline(destination) onP2PSuccess() - }.recover(on: DispatchQueue.global()) { error -> Promise> in + }.recover { error -> Promise> in LokiP2PAPI.markOffline(destination) if lokiMessage.isPing { print("[Loki] Failed to ping \(destination); marking contact as offline.") @@ -417,7 +418,7 @@ public final class LokiAPI : NSObject { private extension Promise { fileprivate func recoveringNetworkErrorsIfNeeded() -> Promise { - return recover(on: DispatchQueue.global()) { error -> Promise in + return recover { error -> Promise in switch error { case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError case LokiHTTPClient.HTTPError.networkError(_, _, let underlyingError): throw underlyingError ?? error diff --git a/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift b/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift index d6b061679..3a021b5c4 100644 --- a/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift @@ -159,11 +159,13 @@ public class LokiDotNetAPI : NSObject { } } if server == LokiFileServerAPI.server { - proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token + DispatchQueue.global().async { + proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token + } } else { getAuthToken(for: server).done(on: DispatchQueue.global()) { token in proceed(with: token) - }.catch(on: DispatchQueue.global()) { error in + }.catch { error in print("[Loki] Couldn't upload attachment due to error: \(error).") seal.reject(error) } @@ -177,7 +179,8 @@ public class LokiDotNetAPI : NSObject { let queryParameters = "pubKey=\(userHexEncodedPublicKey)" let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")! let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in + // All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works + return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map(on: DispatchQueue.global()) { rawResponse in guard let json = rawResponse as? JSON, let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String, let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else { throw LokiDotNetAPIError.parsingFailed @@ -201,6 +204,7 @@ public class LokiDotNetAPI : NSObject { let url = URL(string: "\(server)/loki/v1/submit_challenge")! let parameters = [ "pubKey" : userHexEncodedPublicKey, "token" : token ] let request = TSRequest(url: url, method: "POST", parameters: parameters) + // All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in token } } diff --git a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift index e33c9a43f..bc8b865cc 100644 --- a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift @@ -26,6 +26,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI { /// Gets the device links associated with the given hex encoded public keys from the /// server and stores and returns the valid ones. public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set, in transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise> { + // All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]" print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).") return getAuthToken(for: server, in: transaction).then(on: DispatchQueue.global()) { token -> Promise> in @@ -85,7 +86,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI { public static func setDeviceLinks(_ deviceLinks: Set) -> Promise { print("[Loki] Updating device links.") - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in + return getAuthToken(for: server).then { token -> Promise in let isMaster = deviceLinks.contains { $0.master.hexEncodedPublicKey == userHexEncodedPublicKey } let deviceLinksAsJSON = deviceLinks.map { $0.toJSON() } let value = !deviceLinksAsJSON.isEmpty ? [ "isPrimary" : isMaster ? 1 : 0, "authorisations" : deviceLinksAsJSON ] : nil @@ -94,7 +95,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI { let url = URL(string: "\(server)/users/me")! let request = TSRequest(url: url, method: "PATCH", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.retryingIfNeeded(maxRetryCount: 8).recover(on: DispatchQueue.global()) { error in + return TSNetworkManager.shared().perform(request).map { _ in }.retryingIfNeeded(maxRetryCount: 8).recover { error in print("Couldn't update device links due to error: \(error).") throw error } @@ -138,9 +139,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI { // MARK: Profile Pictures (Public API) public static func setProfilePicture(_ profilePicture: Data) -> Promise { return Promise() { seal in - guard profilePicture.count < maxFileSize else { - return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded) - } + guard profilePicture.count < maxFileSize else { return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded) } getAuthToken(for: server).done { token in let url = "\(server)/users/me/avatar" let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ] diff --git a/SignalServiceKit/src/Loki/API/LokiFileServerProxy.swift b/SignalServiceKit/src/Loki/API/LokiFileServerProxy.swift index 84c19106b..05d7f1fd2 100644 --- a/SignalServiceKit/src/Loki/API/LokiFileServerProxy.swift +++ b/SignalServiceKit/src/Loki/API/LokiFileServerProxy.swift @@ -44,79 +44,85 @@ internal class LokiFileServerProxy : LokiHTTPClient { } internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise { - let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey) - guard let symmetricKey = uncheckedSymmetricKey else { return Promise(error: Error.symmetricKeyGenerationFailed) } var headers = getCanonicalHeaders(for: request) - return LokiAPI.getRandomSnode().then { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] proxy -> Promise in - let url = "\(proxy.address):\(proxy.port)/file_proxy" - guard let urlAsString = request.url?.absoluteString, let serverURLEndIndex = urlAsString.range(of: server)?.upperBound, - serverURLEndIndex < urlAsString.endIndex else { throw Error.endpointParsingFailed } - let endpointStartIndex = urlAsString.index(after: serverURLEndIndex) - let endpoint = String(urlAsString[endpointStartIndex.. { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in + DispatchQueue.global().async { + let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey) + guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) } + LokiAPI.getRandomSnode().then(on: DispatchQueue.global()) { proxy -> Promise in + let url = "\(proxy.address):\(proxy.port)/file_proxy" + guard let urlAsString = request.url?.absoluteString, let serverURLEndIndex = urlAsString.range(of: server)?.upperBound, + serverURLEndIndex < urlAsString.endIndex else { throw Error.endpointParsingFailed } + let endpointStartIndex = urlAsString.index(after: serverURLEndIndex) + let endpoint = String(urlAsString[endpointStartIndex.. Promise in - print("[Loki] File server proxy request failed with error: \(error.localizedDescription).") - throw HTTPError.from(error: error) ?? error } } } diff --git a/SignalServiceKit/src/Loki/API/LokiLongPoller.swift b/SignalServiceKit/src/Loki/API/LokiLongPoller.swift index ef1a0b1e0..f2ee4248e 100644 --- a/SignalServiceKit/src/Loki/API/LokiLongPoller.swift +++ b/SignalServiceKit/src/Loki/API/LokiLongPoller.swift @@ -42,7 +42,7 @@ public final class LokiLongPoller : NSObject { // MARK: Private API private func openConnections() { guard !hasStopped else { return } - LokiAPI.getSwarm(for: userHexEncodedPublicKey).then(on: DispatchQueue.global()) { [weak self] _ -> Guarantee<[Result]> in + LokiAPI.getSwarm(for: userHexEncodedPublicKey).then { [weak self] _ -> Guarantee<[Result]> in guard let strongSelf = self else { return Guarantee.value([Result]()) } strongSelf.usedSnodes.removeAll() let connections: [Promise] = (0.. 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 = getCanonicalHeaders(for: request) - return LokiAPI.getRandomSnode().then { [target = self.target, keyPair = self.keyPair, httpSession = self.httpSession] proxy -> Promise 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: JSON = [ - "method" : request.httpMethod, - "body" : String(bytes: parametersAsData, encoding: .utf8), - "headers" : headers - ] - let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: []) - let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey) - let proxyRequestHeaders = [ - "X-Sender-Public-Key" : keyPair.publicKey.toHexString(), - "X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey - ] - 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.fulfill(result) + return Promise { [target = self.target, keyPair = self.keyPair, httpSession = self.httpSession] seal in + DispatchQueue.global().async { + let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeySet.encryptionKey), privateKey: keyPair.privateKey) + guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) } + LokiAPI.getRandomSnode().then(on: DispatchQueue.global()) { proxy -> Promise 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: JSON = [ + "method" : request.httpMethod, + "body" : String(bytes: parametersAsData, encoding: .utf8), + "headers" : headers + ] + let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: []) + let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey) + let proxyRequestHeaders = [ + "X-Sender-Public-Key" : keyPair.publicKey.toHexString(), + "X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey + ] + 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.fulfill(result) + } + } + task.resume() + return promise + }.map(on: DispatchQueue.global()) { rawResponse in + guard let responseAsData = rawResponse as? Data, let cipherText = Data(base64Encoded: responseAsData) 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 statusCode = json["status"] as? Int else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) } + let isSuccess = (200...299) ~= statusCode + 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? JSON { + body = bodyAsJSON + } + } + guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: statusCode, message: body)) } + return body + }.done { rawResponse in + seal.fulfill(rawResponse) + }.catch { error in + print("[Loki] Proxy request failed with error: \(error.localizedDescription).") + seal.reject(HTTPError.from(error: error) ?? error) } } - task.resume() - return promise - }.map { rawResponse in - guard let responseAsData = rawResponse as? Data, let cipherText = Data(base64Encoded: responseAsData) 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 statusCode = json["status"] as? Int else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) } - let isSuccess = (200...299) ~= statusCode - 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? JSON { - body = bodyAsJSON - } - } - guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: statusCode, message: body)) } - return body - }.recover { error -> Promise in - print("[Loki] Proxy request failed with error: \(error.localizedDescription).") - throw HTTPError.from(error: error) ?? error } } } diff --git a/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatAPI.swift b/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatAPI.swift index 68758a419..77480decc 100644 --- a/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatAPI.swift @@ -82,7 +82,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")! let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in + return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global()) { rawResponse in guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else { print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).") throw LokiDotNetAPIError.parsingFailed @@ -155,33 +155,41 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } public static func sendMessage(_ message: LokiPublicChatMessage, to channel: UInt64, on server: String) -> Promise { - guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: LokiDotNetAPIError.signingFailed) } - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in - print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).") - let url = URL(string: "\(server)/channels/\(channel)/messages")! - let parameters = signedMessage.toJSON() - let request = TSRequest(url: url, method: "POST", parameters: parameters) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - let displayName = userDisplayName - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in - // ISO8601DateFormatter doesn't support milliseconds before iOS 11 - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String, - let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else { - print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).") - throw LokiDotNetAPIError.parsingFailed + return Promise { [privateKey = userKeyPair.privateKey] seal in + DispatchQueue.global().async { + guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(LokiDotNetAPIError.signingFailed) } + getAuthToken(for: server).then { token -> Promise in + print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).") + let url = URL(string: "\(server)/channels/\(channel)/messages")! + let parameters = signedMessage.toJSON() + let request = TSRequest(url: url, method: "POST", parameters: parameters) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + let displayName = userDisplayName + return LokiFileServerProxy(for: server).perform(request).map { rawResponse in + // ISO8601DateFormatter doesn't support milliseconds before iOS 11 + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String, + let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else { + print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).") + throw LokiDotNetAPIError.parsingFailed + } + let timestamp = UInt64(date.timeIntervalSince1970) * 1000 + return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature) + } + }.recover { error -> Promise in + if let error = error as? NetworkManagerError, error.statusCode == 401 { + print("[Loki] Group chat auth token for: \(server) expired; dropping it.") + storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection) + } + throw error + }.retryingIfNeeded(maxRetryCount: maxRetryCount).done { message in + seal.fulfill(message) + }.catch { error in + seal.reject(error) } - let timestamp = UInt64(date.timeIntervalSince1970) * 1000 - return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature) - } - }.recover(on: DispatchQueue.global()) { error -> Promise in - if let error = error as? NetworkManagerError, error.statusCode == 401 { - print("[Loki] Group chat auth token for: \(server) expired; dropping it.") - storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection) } - throw error - }.retryingIfNeeded(maxRetryCount: maxRetryCount) + } } public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> { @@ -194,7 +202,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")! let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in + return LokiFileServerProxy(for: server).perform(request).map { rawResponse in guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else { print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).") throw LokiDotNetAPIError.parsingFailed @@ -212,14 +220,14 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise { - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in + return getAuthToken(for: server).then { token -> Promise in let isModerationRequest = !isSentByUser print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).") let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)" let url = URL(string: urlAsString)! let request = TSRequest(url: url, method: "DELETE", parameters: [:]) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in + return LokiFileServerProxy(for: server).perform(request).done { result -> Void in print("[Loki] Deleted message with ID: \(messageID) on server: \(server).") }.retryingIfNeeded(maxRetryCount: maxRetryCount) } @@ -228,7 +236,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { public static func getModerators(for channel: UInt64, on server: String) -> Promise> { let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")! let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in + return LokiFileServerProxy(for: server).perform(request).map { rawResponse in guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else { print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).") throw LokiDotNetAPIError.parsingFailed @@ -244,34 +252,34 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } public static func join(_ channel: UInt64, on server: String) -> Promise { - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in + return getAuthToken(for: server).then { token -> Promise in let url = URL(string: "\(server)/channels/\(channel)/subscribe")! let request = TSRequest(url: url, method: "POST", parameters: [:]) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in + return LokiFileServerProxy(for: server).perform(request).done { result -> Void in print("[Loki] Joined channel with ID: \(channel) on server: \(server).") }.retryingIfNeeded(maxRetryCount: maxRetryCount) } } public static func leave(_ channel: UInt64, on server: String) -> Promise { - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in + return getAuthToken(for: server).then { token -> Promise in let url = URL(string: "\(server)/channels/\(channel)/subscribe")! let request = TSRequest(url: url, method: "DELETE", parameters: [:]) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in + return LokiFileServerProxy(for: server).perform(request).done { result -> Void in print("[Loki] Left channel with ID: \(channel) on server: \(server).") }.retryingIfNeeded(maxRetryCount: maxRetryCount) } } public static func getUserCount(for channel: UInt64, on server: String) -> Promise { - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in + return getAuthToken(for: server).then { token -> Promise in let queryParameters = "count=200" let url = URL(string: "\(server)/channels/\(channel)/subscribers?\(queryParameters)")! let request = TSRequest(url: url) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in + return LokiFileServerProxy(for: server).perform(request).map { rawResponse in guard let json = rawResponse as? JSON, let users = json["data"] as? [JSON] else { print("[Loki] Couldn't parse user count for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).") throw LokiDotNetAPIError.parsingFailed @@ -291,11 +299,11 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { guard let hexEncodedPublicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) } displayNameUpdatees[publicChatID] = [] print("[Loki] Getting display names for: \(hexEncodedPublicKeys).") - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in + return getAuthToken(for: server).then { token -> Promise in let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1" let url = URL(string: "\(server)/users?\(queryParameters)")! let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in + return LokiFileServerProxy(for: server).perform(request).map { rawResponse in guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else { print("[Loki] Couldn't parse display names for users: \(hexEncodedPublicKeys) from: \(rawResponse).") throw LokiDotNetAPIError.parsingFailed @@ -320,12 +328,12 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise { print("[Loki] Updating display name on server: \(server).") - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in + return getAuthToken(for: server).then { token -> Promise in let parameters: JSON = [ "name" : (newDisplayName ?? "") ] let url = URL(string: "\(server)/users/me")! let request = TSRequest(url: url, method: "PATCH", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.recover(on: DispatchQueue.global()) { error in + return LokiFileServerProxy(for: server).perform(request).map { _ in }.recover { error in print("Couldn't update display name due to error: \(error).") throw error } @@ -334,7 +342,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { public static func setProfilePictureURL(to url: String?, using profileKey: Data, on server: String) -> Promise { print("[Loki] Updating profile picture on server: \(server).") - return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise in + return getAuthToken(for: server).then { token -> Promise in var annotation: JSON = [ "type" : profilePictureType ] if let url = url { annotation["value"] = [ "profileKey" : profileKey.base64EncodedString(), "url" : url ] @@ -343,7 +351,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { let url = URL(string: "\(server)/users/me")! let request = TSRequest(url: url, method: "PATCH", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.recover(on: DispatchQueue.global()) { error in + return LokiFileServerProxy(for: server).perform(request).map { _ in }.recover { error in print("[Loki] Couldn't update profile picture due to error: \(error).") throw error } @@ -353,7 +361,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { public static func getInfo(for channel: UInt64, on server: String) -> Promise { let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")! let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in + return LokiFileServerProxy(for: server).perform(request).map { rawResponse in guard let json = rawResponse as? JSON, let data = json["data"] as? JSON, let annotations = data["annotations"] as? [JSON], diff --git a/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatManager.swift b/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatManager.swift index aec2d96a4..53bed33c7 100644 --- a/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatManager.swift +++ b/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatManager.swift @@ -56,7 +56,7 @@ public final class LokiPublicChatManager : NSObject { return Promise(error: Error.chatCreationFailed) } } - return LokiPublicChatAPI.getAuthToken(for: server).then(on: DispatchQueue.global()) { token in + return LokiPublicChatAPI.getAuthToken(for: server).then { token in return LokiPublicChatAPI.getInfo(for: channel, on: server) }.map { channelInfo -> LokiPublicChat in guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed } diff --git a/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatPoller.swift b/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatPoller.swift index dee48d5a7..fbf50c42d 100644 --- a/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatPoller.swift +++ b/SignalServiceKit/src/Loki/API/Public Chat/LokiPublicChatPoller.swift @@ -206,7 +206,9 @@ public final class LokiPublicChatPoller : NSObject { } } } else { - proceed() + DispatchQueue.global().async { + proceed() + } } } }