diff --git a/Pods b/Pods index 8f3d6d467..8b8d1d35f 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 8f3d6d46718795227074d6545776206cf18c0244 +Subproject commit 8b8d1d35f47af7071caaaee0aca8c1f5806bbe8e diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 02faca95d..23bae1487 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -519,7 +519,6 @@ B80C6B572384A56D00FDBC8B /* DeviceLinksVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */; }; B80C6B592384C4E700FDBC8B /* DeviceNameModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B582384C4E700FDBC8B /* DeviceNameModal.swift */; }; B80C6B5B2384C7F900FDBC8B /* DeviceNameModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */; }; - B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */; }; B82B40882399EB0E00A248E7 /* LandingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40872399EB0E00A248E7 /* LandingVC.swift */; }; B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40892399EC0600A248E7 /* FakeChatView.swift */; }; B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408B239A068800A248E7 /* RegisterVC.swift */; }; @@ -1340,7 +1339,6 @@ B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinksVC.swift; sourceTree = ""; }; B80C6B582384C4E700FDBC8B /* DeviceNameModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceNameModal.swift; sourceTree = ""; }; B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceNameModalDelegate.swift; sourceTree = ""; }; - B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LokiRSSFeedPoller.swift; sourceTree = ""; }; B82B40872399EB0E00A248E7 /* LandingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingVC.swift; sourceTree = ""; }; B82B40892399EC0600A248E7 /* FakeChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeChatView.swift; sourceTree = ""; }; B82B408B239A068800A248E7 /* RegisterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterVC.swift; sourceTree = ""; }; @@ -2616,7 +2614,6 @@ children = ( B8CCF63B239757C10091D419 /* Components */, C32B405424A961E1001117B5 /* Dependencies */, - B8BFFF392355426100102A27 /* Shelved */, B8CCF63C239757DB0091D419 /* Utilities */, B8CCF63D2397580E0091D419 /* View Controllers */, ); @@ -2664,14 +2661,6 @@ path = Loki; sourceTree = ""; }; - B8BFFF392355426100102A27 /* Shelved */ = { - isa = PBXGroup; - children = ( - B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */, - ); - path = Shelved; - sourceTree = ""; - }; B8C9689223FA1B05005F64E0 /* Redesign */ = { isa = PBXGroup; children = ( @@ -3895,7 +3884,6 @@ 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */, 4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, - B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */, C353F8F7244808E90011121A /* PNModeSheet.swift in Sources */, 45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */, 340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */, diff --git a/Signal/src/Loki/Shelved/LokiRSSFeedPoller.swift b/Signal/src/Loki/Shelved/LokiRSSFeedPoller.swift deleted file mode 100644 index a0f15ea02..000000000 --- a/Signal/src/Loki/Shelved/LokiRSSFeedPoller.swift +++ /dev/null @@ -1,73 +0,0 @@ -import FeedKit - -@objc(LKRSSFeedPoller) -public final class LokiRSSFeedPoller : NSObject { - private let feed: LokiRSSFeed - private var timer: Timer? = nil - private var hasStarted = false - - private let interval: TimeInterval = 8 * 60 - - @objc(initForFeed:) - public init(for feed: LokiRSSFeed) { - self.feed = feed - super.init() - } - - @objc public func startIfNeeded() { - if hasStarted { return } - timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in self?.poll() } - poll() // Perform initial update - hasStarted = true - } - - @objc public func stop() { - timer?.invalidate() - hasStarted = false - } - - private func poll() { - let feed = self.feed - let url = feed.server - let _ = LokiRSSFeedProxy.fetchContent(for: url).done { xml in - guard let data = xml.data(using: String.Encoding.utf8) else { return print("[Loki] Failed to parse RSS feed for: \(feed.server).") } - FeedParser(data: data).parseAsync { wrapper in - guard case .rss(let x) = wrapper, let items = x.items else { return print("[Loki] Failed to parse RSS feed for: \(feed.server).") } - items.reversed().forEach { item in - guard let title = item.title, let description = item.description, let date = item.pubDate else { return } - let timestamp = UInt64(date.timeIntervalSince1970 * 1000) - let urlRegex = try! NSRegularExpression(pattern: "]*?\\s+)?href=\"([^\"]*)\".*?>(.*?)<.*?\\/a>") - var bodyAsHTML = "\(title)

\(description)".replacingOccurrences(of: "

", with: "


") - while true { - guard let match = urlRegex.firstMatch(in: bodyAsHTML, options: [], range: NSRange(location: 0, length: bodyAsHTML.utf16.count)) else { break } - let matchRange = match.range(at: 0) - let urlRange = match.range(at: 1) - let descriptionRange = match.range(at: 2) - let url = (bodyAsHTML as NSString).substring(with: urlRange) - let description = (bodyAsHTML as NSString).substring(with: descriptionRange) - bodyAsHTML = (bodyAsHTML as NSString).replacingCharacters(in: matchRange, with: "\(description) (\(url))") as String - } - guard let bodyAsData = bodyAsHTML.data(using: String.Encoding.unicode) else { return } - let options = [ NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.html ] - guard let body = try? NSAttributedString(data: bodyAsData, options: options, documentAttributes: nil).string else { return } - let id = LKGroupUtilities.getEncodedRSSFeedIDAsData(feed.id) - let groupContext = SSKProtoGroupContext.builder(id: id, type: .deliver) - groupContext.setName(feed.displayName) - let dataMessage = SSKProtoDataMessage.builder() - dataMessage.setTimestamp(timestamp) - dataMessage.setGroup(try! groupContext.build()) - dataMessage.setBody(body) - let content = SSKProtoContent.builder() - content.setDataMessage(try! dataMessage.build()) - let envelope = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: timestamp) - envelope.setSource(NSLocalizedString("Loki", comment: "")) - envelope.setSourceDevice(OWSDevicePrimaryDeviceId) - envelope.setContent(try! content.build().serializedData()) - try! Storage.writeSync { transaction in - SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction, serverID: 0) - } - } - } - } - } -} diff --git a/Signal/src/Loki/View Controllers/JoinPublicChatVC.swift b/Signal/src/Loki/View Controllers/JoinPublicChatVC.swift index 3ffcc74da..d76c4c423 100644 --- a/Signal/src/Loki/View Controllers/JoinPublicChatVC.swift +++ b/Signal/src/Loki/View Controllers/JoinPublicChatVC.swift @@ -154,7 +154,7 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie .catch(on: .main) { [weak self] error in var title = NSLocalizedString("Couldn't Join", comment: "") var message = "" - if case LokiHTTPClient.HTTPError.networkError(let statusCode, _, _) = error, (statusCode == 401 || statusCode == 403) { + if case HTTP.Error.httpRequestFailed(let statusCode, _) = error, statusCode == 401 || statusCode == 403 { title = NSLocalizedString("Unauthorized", comment: "") message = NSLocalizedString("Please ask the open group operator to add you to the group.", comment: "") } diff --git a/SignalMessaging/environment/VersionMigrations.m b/SignalMessaging/environment/VersionMigrations.m index c81e76932..70d136897 100644 --- a/SignalMessaging/environment/VersionMigrations.m +++ b/SignalMessaging/environment/VersionMigrations.m @@ -95,11 +95,6 @@ NS_ASSUME_NONNULL_BEGIN if ([self isVersion:previousVersion atLeast:@"2.0.0" andLessThan:@"2.3.0"] && [self.tsAccountManager isRegistered]) { [self clearBloomFilterCache]; } - - // Loki - if ([self isVersion:previousVersion lessThan:@"1.2.1"] && [self.tsAccountManager isRegistered]) { - [self updatePublicChatMapping]; - } dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:completion]; @@ -168,43 +163,6 @@ NS_ASSUME_NONNULL_BEGIN } } -# pragma mark Loki - Upgrading to Public Chat Manager - -// Versions less than or equal to 1.2.0 didn't store public chat mappings -+ (void)updatePublicChatMapping -{ - [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) { - for (LKPublicChat *chat in LKPublicChatAPI.defaultChats) { - TSGroupThread *thread = [TSGroupThread threadWithGroupId:[LKGroupUtilities getEncodedOpenGroupIDAsData:chat.id] transaction:transaction]; - if (thread != nil) { - [LKDatabaseUtilities setPublicChat:chat threadID:thread.uniqueId transaction:transaction]; - } else { - // Update the group type and group ID for private group chat version. - // If the thread is still using the old group ID, it needs to be updated. - thread = [TSGroupThread threadWithGroupId:chat.idAsData transaction:transaction]; - if (thread != nil) { - thread.groupModel.groupType = openGroup; - [thread.groupModel updateGroupId:[LKGroupUtilities getEncodedOpenGroupIDAsData:chat.id]]; - [thread saveWithTransaction:transaction]; - [LKDatabaseUtilities setPublicChat:chat threadID:thread.uniqueId transaction:transaction]; - } - } - } - // Update RSS feeds here - LKRSSFeed *lokiNewsFeed = [[LKRSSFeed alloc] initWithId:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:NSLocalizedString(@"Loki News", @"") isDeletable:true]; - LKRSSFeed *lokiMessengerUpdatesFeed = [[LKRSSFeed alloc] initWithId:@"loki.network.messenger-updates.feed" server:@"https://loki.network/category/messenger-updates/feed/" displayName:NSLocalizedString(@"Session Updates", @"") isDeletable:false]; - NSArray *feeds = @[ lokiNewsFeed, lokiMessengerUpdatesFeed ]; - for (LKRSSFeed *feed in feeds) { - TSGroupThread *thread = [TSGroupThread threadWithGroupId:[feed.id dataUsingEncoding:NSUTF8StringEncoding] transaction:transaction]; - if (thread != nil) { - thread.groupModel.groupType = rssFeed; - [thread.groupModel updateGroupId:[LKGroupUtilities getEncodedRSSFeedIDAsData:feed.id]]; - [thread saveWithTransaction:transaction]; - } - } - } error:nil]; -} - @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Loki/API/Deprecated/LokiFileServerProxy.swift b/SignalServiceKit/src/Loki/API/Deprecated/LokiFileServerProxy.swift deleted file mode 100644 index 0422ad484..000000000 --- a/SignalServiceKit/src/Loki/API/Deprecated/LokiFileServerProxy.swift +++ /dev/null @@ -1,127 +0,0 @@ -import PromiseKit -import SessionMetadataKit - -internal class LokiFileServerProxy : LokiHTTPClient { - private let server: String - private let keyPair = Curve25519.generateKeyPair() - - private static let fileServerPublicKey: Data = { - let base64EncodedPublicKey = "BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc" - let publicKeyWithPrefix = Data(base64Encoded: base64EncodedPublicKey)! - let hexEncodedPublicKeyWithPrefix = publicKeyWithPrefix.toHexString() - let hexEncodedPublicKey = hexEncodedPublicKeyWithPrefix.removing05PrefixIfNeeded() - return Data(hex: hexEncodedPublicKey) - }() - - // MARK: Error - internal enum Error : LocalizedError { - case symmetricKeyGenerationFailed - case endpointParsingFailed - case proxyResponseParsingFailed - case fileServerHTTPError(code: Int, message: Any?) - - internal var errorDescription: String? { - switch self { - case .symmetricKeyGenerationFailed: return "Couldn't generate symmetric key." - case .endpointParsingFailed: return "Couldn't parse endpoint." - case .proxyResponseParsingFailed: return "Couldn't parse file server proxy response." - case .fileServerHTTPError(let httpStatusCode, let message): return "File server returned \(httpStatusCode) with description: \(message ?? "no description provided.")." - } - } - } - - // MARK: Initialization - internal init(for server: String) { - self.server = server - super.init() - } - - // MARK: Proxying - override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise { - let isLokiFileServer = (server == FileServerAPI.server) - guard isLokiFileServer else { return super.perform(request, withCompletionQueue: queue) } // Don't proxy open group requests for now - return performLokiFileServerNSURLRequest(request, withCompletionQueue: queue) - } - - internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise { - var headers = getCanonicalHeaders(for: request) - return Promise { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in - DispatchQueue.global(qos: .userInitiated).async { - let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey) - guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) } - SnodeAPI.getRandomSnode().then2 { 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.. SnodeAPI.RawResponsePromise { - return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map2 { $0.responseObject }.recover2 { error -> SnodeAPI.RawResponsePromise in - throw HTTPError.from(error: error) ?? error - } - } - - internal func getCanonicalHeaders(for request: NSURLRequest) -> [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 - } - } - } -} - -// MARK: - HTTP Error - -public extension LokiHTTPClient { - - public enum HTTPError : LocalizedError { - case networkError(code: Int, response: Any?, underlyingError: Error?) - - 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] - // Deserialize response if needed - if let data = response as? Data, let json = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) 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: error) - } - return nil - } - - public var errorDescription: String? { - switch self { - case .networkError(let code, let body, let underlyingError): return underlyingError?.localizedDescription ?? "HTTP request failed with status code: \(code), message: \(body ?? "nil")." - } - } - - internal var statusCode: Int { - switch self { - case .networkError(let code, _, _): return code - } - } - - internal var isNetworkError: Bool { - switch self { - case .networkError(_, _, let underlyingError): return underlyingError != nil && IsNSErrorNetworkFailure(underlyingError) - } - } - } -} diff --git a/SignalServiceKit/src/Loki/API/DotNetAPI.swift b/SignalServiceKit/src/Loki/API/DotNetAPI.swift index 9883e69bd..7a3bd0cee 100644 --- a/SignalServiceKit/src/Loki/API/DotNetAPI.swift +++ b/SignalServiceKit/src/Loki/API/DotNetAPI.swift @@ -68,7 +68,10 @@ public class DotNetAPI : NSObject { let queryParameters = "pubKey=\(getUserHexEncodedPublicKey())" let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")! let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { rawResponse in + let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(server) } : PublicChatAPI.getOpenGroupServerPublicKey(for: server) + return serverPublicKeyPromise.then2 { serverPublicKey in + OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey) + }.map2 { 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 DotNetAPIError.parsingFailed @@ -92,7 +95,10 @@ public class DotNetAPI : NSObject { let url = URL(string: "\(server)/loki/v1/submit_challenge")! let parameters = [ "pubKey" : getUserHexEncodedPublicKey(), "token" : token ] let request = TSRequest(url: url, method: "POST", parameters: parameters) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { _ in token } + let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(server) } : PublicChatAPI.getOpenGroupServerPublicKey(for: server) + return serverPublicKeyPromise.then2 { serverPublicKey in + OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey) + }.map2 { _ in token } } // MARK: Public API @@ -126,8 +132,7 @@ public class DotNetAPI : NSObject { data = unencryptedAttachmentData } // Check the file size if needed - let isLokiFileServer = (server == FileServerAPI.server) - if isLokiFileServer && data.count > FileServerAPI.maxFileSize { + if data.count > FileServerAPI.maxFileSize { return seal.reject(DotNetAPIError.maxFileSizeExceeded) } // Create the request @@ -143,10 +148,16 @@ public class DotNetAPI : NSObject { return seal.reject(error) } // Send the request - func parseResponse(_ responseObject: Any) { + let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(FileServerAPI.fileServerPublicKey) } + : PublicChatAPI.getOpenGroupServerPublicKey(for: server) + attachment.isUploaded = false + attachment.save() + let _ = serverPublicKeyPromise.then2 { serverPublicKey in + OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey) + }.done2 { json in // Parse the server ID & download URL - guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else { - print("[Loki] Couldn't parse attachment from: \(responseObject).") + guard let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else { + print("[Loki] Couldn't parse attachment from: \(json).") return seal.reject(DotNetAPIError.parsingFailed) } // Update the attachment @@ -155,38 +166,8 @@ public class DotNetAPI : NSObject { attachment.downloadURL = downloadURL attachment.save() seal.fulfill(()) - } - let isProxyingRequired = (server == FileServerAPI.server) // Don't proxy open group requests for now - if isProxyingRequired { - attachment.isUploaded = false - attachment.save() - let _ = LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).done2 { responseObject in - parseResponse(responseObject) - }.catch2 { error in - seal.reject(error) - } - } else { - let task = AFURLSessionManager(sessionConfiguration: .default).uploadTask(withStreamedRequest: request as URLRequest, progress: { rawProgress in - // Broadcast progress updates - let progress = max(0.1, rawProgress.fractionCompleted) - let userInfo: [String:Any] = [ kAttachmentUploadProgressKey : progress, kAttachmentUploadAttachmentIDKey : attachmentID ] - DispatchQueue.main.async { - NotificationCenter.default.post(name: .attachmentUploadProgress, object: nil, userInfo: userInfo) - } - }, completionHandler: { response, responseObject, error in - if let error = error { - print("[Loki] Couldn't upload attachment due to error: \(error).") - return seal.reject(error) - } - let statusCode = (response as! HTTPURLResponse).statusCode - let isSuccessful = (200...299) ~= statusCode - guard isSuccessful else { - print("[Loki] Couldn't upload attachment.") - return seal.reject(DotNetAPIError.generic) - } - parseResponse(responseObject) - }) - task.resume() + }.catch2 { error in + seal.reject(error) } } if server == FileServerAPI.server { @@ -210,7 +191,7 @@ internal extension Promise { internal func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise { return recover2 { error -> Promise in - if let error = error as? NetworkManagerError, (error.statusCode == 401 || error.statusCode == 403) { + if case HTTP.Error.httpRequestFailed(let statusCode, _) = error, statusCode == 401 || statusCode == 403 { print("[Loki] Auth token for: \(server) expired; dropping it.") DotNetAPI.clearAuthToken(for: server) } diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift index adae9bcfc..e2652f7dd 100644 --- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift @@ -4,21 +4,14 @@ import PromiseKit public final class FileServerAPI : DotNetAPI { // MARK: Settings - private static let deviceLinkType = "network.loki.messenger.devicemapping" private static let attachmentType = "net.app.core.oembed" + private static let deviceLinkType = "network.loki.messenger.devicemapping" + + internal static let fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C" public static let maxFileSize = 10_000_000 // 10 MB @objc public static let server = "https://file.getsession.org" - - internal static var useOnionRequests = true - - private static let fileServerPublicKey: String = { - let base64EncodedPublicKey = "BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc" - let publicKeyWithPrefix = Data(base64Encoded: base64EncodedPublicKey)! - let hexEncodedPublicKeyWithPrefix = publicKeyWithPrefix.toHexString() - return hexEncodedPublicKeyWithPrefix.removing05PrefixIfNeeded() - }() // MARK: Storage override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } @@ -51,69 +44,52 @@ public final class FileServerAPI : DotNetAPI { public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set) -> Promise> { let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]" print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).") - - func handleRawResponseForDeviceLinks(rawResponse: JSON, data: [JSON]) -> Set { - return Set(data.flatMap { data -> [DeviceLink] in - guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] } - guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }), - let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON], - let hexEncodedPublicKey = data["username"] as? String else { - print("[Loki] Couldn't parse device links from: \(rawResponse).") - return [] - } - return rawDeviceLinks.compactMap { rawDeviceLink in - guard let masterPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slavePublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String, - let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else { - print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).") - return nil - } - let masterSignature: Data? - if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String { - masterSignature = Data(base64Encoded: base64EncodedMasterSignature) - } else { - masterSignature = nil - } - let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature) - let master = DeviceLink.Device(publicKey: masterPublicKey, signature: masterSignature) - let slave = DeviceLink.Device(publicKey: slavePublicKey, signature: slaveSignature) - let deviceLink = DeviceLink(between: master, and: slave) - if let masterSignature = masterSignature { - guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else { - print("[Loki] Received a device link with an invalid master signature.") - return nil - } - } - guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else { - print("[Loki] Received a device link with an invalid slave signature.") - return nil - } - return deviceLink - } - }) - } - return getAuthToken(for: server).then2 { 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) - if (useOnionRequests) { - return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: fileServerPublicKey).map2 { rawResponse -> Set in - guard let data = rawResponse["data"] as? [JSON] else { - print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).") - throw DotNetAPIError.parsingFailed - } - return handleRawResponseForDeviceLinks(rawResponse: rawResponse, data: data) - }.map2 { deviceLinks in - storage.setDeviceLinks(deviceLinks) - return deviceLinks - } - } - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { rawResponse -> Set in - guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else { + return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { rawResponse -> Set in + guard let data = rawResponse["data"] as? [JSON] else { print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).") throw DotNetAPIError.parsingFailed } - return handleRawResponseForDeviceLinks(rawResponse: json, data: data) + return Set(data.flatMap { data -> [DeviceLink] in + guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] } + guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }), + let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON], + let hexEncodedPublicKey = data["username"] as? String else { + print("[Loki] Couldn't parse device links from: \(rawResponse).") + return [] + } + return rawDeviceLinks.compactMap { rawDeviceLink in + guard let masterPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slavePublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String, + let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else { + print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).") + return nil + } + let masterSignature: Data? + if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String { + masterSignature = Data(base64Encoded: base64EncodedMasterSignature) + } else { + masterSignature = nil + } + let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature) + let master = DeviceLink.Device(publicKey: masterPublicKey, signature: masterSignature) + let slave = DeviceLink.Device(publicKey: slavePublicKey, signature: slaveSignature) + let deviceLink = DeviceLink(between: master, and: slave) + if let masterSignature = masterSignature { + guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else { + print("[Loki] Received a device link with an invalid master signature.") + return nil + } + } + guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else { + print("[Loki] Received a device link with an invalid slave signature.") + return nil + } + return deviceLink + } + }) }.map2 { deviceLinks in storage.setDeviceLinks(deviceLinks) return deviceLinks @@ -134,10 +110,7 @@ public final class FileServerAPI : DotNetAPI { let request = TSRequest(url: url, method: "PATCH", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return attempt(maxRetryCount: 8, recoveringOn: SnodeAPI.workQueue) { - if (useOnionRequests) { - return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: fileServerPublicKey).map2 { _ in } - } - return LokiFileServerProxy(for: server).perform(request).map2 { _ in } + OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { _ in } }.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in print("Couldn't update device links due to error: \(error).") throw error @@ -193,19 +166,9 @@ public final class FileServerAPI : DotNetAPI { print("[Loki] Couldn't upload profile picture due to error: \(error).") return Promise(error: error) } - if (useOnionRequests) { - return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: fileServerPublicKey).map2 { json in - guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else { - print("[Loki] Couldn't parse profile picture from: \(json).") - throw DotNetAPIError.parsingFailed - } - UserDefaults.standard[.lastProfilePictureUpload] = Date() - return downloadURL - } - } - return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map2 { responseObject in - guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else { - print("[Loki] Couldn't parse profile picture from: \(responseObject).") + return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { json in + guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else { + print("[Loki] Couldn't parse profile picture from: \(json).") throw DotNetAPIError.parsingFailed } UserDefaults.standard[.lastProfilePictureUpload] = Date() @@ -214,21 +177,21 @@ public final class FileServerAPI : DotNetAPI { } // MARK: Open Group Server Public Key - public static func getOpenGroupKey(for openGroupServer: String) -> Promise { - let serverURL = URL(string: openGroupServer)! - let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(serverURL.host!)")! + public static func getPublicKey(for openGroupServer: String) -> Promise { + let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(URL(string: openGroupServer)!.host!)")! let request = TSRequest(url: url) - let token = "loki" // tokenless request, using a dummy token + let token = "loki" // Tokenless request; use a dummy token request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: fileServerPublicKey).map2 { rawResponse in - guard let bodyAsString = rawResponse["data"] as? String, let bodyAsData = bodyAsString.data(using: .utf8), let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { throw HTTP.Error.invalidJSON } + return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { json in + guard let bodyAsString = json["data"] as? String, let bodyAsData = bodyAsString.data(using: .utf8), + let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { throw HTTP.Error.invalidJSON } guard let base64EncodedPublicKey = body["data"] as? String else { print("[Loki] Couldn't parse open group public key from: \(body).") throw DotNetAPIError.parsingFailed } - let publicKeyWithPrefix = Data(base64Encoded: base64EncodedPublicKey)! - let hexEncodedPublicKeyWithPrefix = publicKeyWithPrefix.toHexString() - return hexEncodedPublicKeyWithPrefix.removing05PrefixIfNeeded() + let prefixedPublicKey = Data(base64Encoded: base64EncodedPublicKey)! + let hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString() + return hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded() } } } diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift index 12e1b6aa0..b3bc3cc25 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift @@ -18,9 +18,9 @@ extension OnionRequestAPI { } /// - Note: Sync. Don't call from the main thread. - private static func encrypt(_ plaintext: Data, using x25519Key: String?) throws -> EncryptionResult { + private static func encrypt(_ plaintext: Data, using hexEncodedX25519PublicKey: String?) throws -> EncryptionResult { guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") } - guard let hexEncodedX25519PublicKey = x25519Key else { throw Error.snodePublicKeySetMissing } + guard let hexEncodedX25519PublicKey = hexEncodedX25519PublicKey else { throw Error.snodePublicKeySetMissing } let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey) let ephemeralKeyPair = Curve25519.generateKeyPair() let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: ephemeralKeyPair.privateKey) @@ -30,24 +30,26 @@ extension OnionRequestAPI { return (ciphertext, Data(bytes: symmetricKey), ephemeralKeyPair.publicKey) } - /// Encrypts `payload` for `snode` and returns the result. Use this to build the core of an onion request. - internal static func encrypt(_ payload: JSON, using x25519Key: String?, to destination: JSON) -> Promise { + /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. + internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { do { - // The wrapper is not needed when it is a file server onion request guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) } - if let destination = destination["destination"] { + // Wrapping isn't needed for file server or open group onion requests + switch destination { + case .snode(let snode): + guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) } let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ] guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) } let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ]) - let result = try encrypt(plaintext, using: x25519Key) + let result = try encrypt(plaintext, using: snodeX25519PublicKey) seal.fulfill(result) - } else { + case .server(_, let serverX25519PublicKey): let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) - let result = try encrypt(plaintext, using: x25519Key) + let result = try encrypt(plaintext, using: serverX25519PublicKey) seal.fulfill(result) } } catch (let error) { @@ -58,16 +60,31 @@ extension OnionRequestAPI { } /// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request. - internal static func encryptHop(with x25519Key: String?, to destination: JSON, using previousEncryptionResult: EncryptionResult) -> Promise { + internal static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: EncryptionResult) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { - var parameters = destination + var parameters: JSON + switch rhs { + case .snode(let snode): + guard let snodeED25519PublicKey = snode.publicKeySet?.ed25519Key else { return seal.reject(Error.snodePublicKeySetMissing) } + parameters = [ "destination" : snodeED25519PublicKey ] + case .server(let host, _): + parameters = [ "host" : host, "target" : "/loki/v1/lsrpc", "method" : "POST" ] + } parameters["ciphertext"] = previousEncryptionResult.ciphertext.base64EncodedString() parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() + let x25519PublicKey: String + switch lhs { + case .snode(let snode): + guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) } + x25519PublicKey = snodeX25519PublicKey + case .server(_, let serverX25519PublicKey): + x25519PublicKey = serverX25519PublicKey + } do { guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) } let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ]) - let result = try encrypt(plaintext, using: x25519Key) + let result = try encrypt(plaintext, using: x25519PublicKey) seal.fulfill(result) } catch (let error) { seal.reject(error) diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index 403e7ad47..8f9e57372 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -19,10 +19,17 @@ public enum OnionRequestAPI { private static var guardSnodeCount: UInt { return pathCount } // One per path + // MARK: Destination + internal enum Destination { + case snode(Snode) + case server(host: String, x25519PublicKey: String) + } + // MARK: Error internal enum Error : LocalizedError { case httpRequestFailedAtTargetSnode(statusCode: UInt, json: JSON) case insufficientSnodes + case invalidURL case missingSnodeVersion case randomDataGenerationFailed case snodePublicKeySetMissing @@ -32,6 +39,7 @@ public enum OnionRequestAPI { switch self { case .httpRequestFailedAtTargetSnode(let statusCode): return "HTTP request failed at target snode with status code: \(statusCode)." case .insufficientSnodes: return "Couldn't find enough snodes to build a path." + case .invalidURL: return "Invalid URL" case .missingSnodeVersion: return "Missing snode version." case .randomDataGenerationFailed: return "Couldn't generate random data." case .snodePublicKeySetMissing: return "Missing snode public key set." @@ -138,7 +146,7 @@ public enum OnionRequestAPI { /// Returns a `Path` to be used for building an onion request. Builds new paths as needed. /// /// - Note: Exposed for testing purposes. - internal static func getPath(excluding snode: Snode?) -> Promise { + private static func getPath(excluding snode: Snode?) -> Promise { guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") } if paths.count < pathCount { let storage = OWSPrimaryStorage.shared() @@ -181,34 +189,29 @@ public enum OnionRequestAPI { } /// Builds an onion around `payload` and returns the result. - private static func buildOnion(around payload: JSON, targetedAt snode: Snode?, to destination: JSON = [:], using x25519Key: String? = nil) -> Promise { + private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise { var guardSnode: Snode! - var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode + var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination var encryptionResult: EncryptionResult! - return getPath(excluding: snode).then2 { path -> Promise in + var snodeToExclude: Snode? + if case .snode(let snode) = destination { snodeToExclude = snode } + return getPath(excluding: snodeToExclude).then2 { path -> Promise in guardSnode = path.first! - // Encrypt in reverse order, i.e. the target snode first - var dest = destination - var x25519PublicKey = x25519Key - if let snode = snode { - dest = [ "destination": snode.publicKeySet!.ed25519Key ] - x25519PublicKey = snode.publicKeySet?.x25519Key - } - return encrypt(payload, using: x25519PublicKey, to: dest).then2 { r -> Promise in + // Encrypt in reverse order, i.e. the destination first + return encrypt(payload, for: destination).then2 { r -> Promise in targetSnodeSymmetricKey = r.symmetricKey // Recursively encrypt the layers of the onion (again in reverse order) encryptionResult = r var path = path - var destination = dest + var rhs = destination func addLayer() -> Promise { if path.isEmpty { return Promise { $0.fulfill(encryptionResult) } } else { - let lhs = path.removeLast() - let x25519Key = lhs.publicKeySet?.x25519Key - return OnionRequestAPI.encryptHop(with: x25519Key, to: destination, using: encryptionResult).then2 { r -> Promise in + let lhs = Destination.snode(path.removeLast()) + return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise in encryptionResult = r - destination = [ "destination": lhs.publicKeySet!.ed25519Key ] + rhs = lhs return addLayer() } } @@ -219,47 +222,48 @@ public enum OnionRequestAPI { } // MARK: Internal API - internal static func getCanonicalHeaders(for request: NSURLRequest) -> [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 - } - } - } - /// Sends an onion request to `snode`. Builds new paths as needed. - internal static func sendOnionRequestSnodeDest(invoking method: Snode.Method, on snode: Snode, with parameters: JSON, associatedWith publicKey: String) -> Promise { + internal static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise { let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] - let promise = sendOnionRequest(on: snode, with: payload, to: [:], using: nil, associatedWith: publicKey) - promise.recover2 { error -> Promise in + return sendOnionRequest(with: payload, to: Destination.snode(snode), associatedWith: publicKey).recover2 { error -> Promise in guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error } throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error } - return promise } - /// Sends an onion request to `file server`. Builds new paths as needed. - internal static func sendOnionRequestLsrpcDest(_ request: NSURLRequest, server: String, using x25519Key: String, noJSON: Bool = false) -> Promise { - var headers = getCanonicalHeaders(for: request) - let urlAsString = request.url!.absoluteString - let serverURLEndIndex = urlAsString.range(of: server)!.upperBound + /// Sends an onion request to `server`. Builds new paths as needed. + internal static func sendOnionRequest(_ request: NSURLRequest, to server: String, using x25519Key: String, isJSONRequired: Bool = true) -> Promise { + let rawHeaders = request.allHTTPHeaderFields ?? [:] + var headers: JSON = rawHeaders.mapValues { value in + switch value.lowercased() { + case "true": return true + case "false": return false + default: return value + } + } + guard let url = request.url?.absoluteString, let host = request.url?.host else { return Promise(error: Error.invalidURL) } var endpoint = "" - if server.count < urlAsString.count { - let endpointStartIndex = urlAsString.index(after: serverURLEndIndex) - endpoint = String(urlAsString[endpointStartIndex.. Promise in - // TODO: File Server API handle Error - throw error + let destination = Destination.server(host: host, x25519PublicKey: x25519Key) + let promise = sendOnionRequest(with: payload, to: destination, associatedWith: getUserHexEncodedPublicKey(), isJSONRequired: isJSONRequired) + promise.catch2 { error in + print("[Loki] [Onion Request API] Couldn't reach server: \(server) due to error: \(error).") } return promise } - internal static func sendOnionRequest(on snode: Snode?, with payload: JSON, to destination: JSON, using x25519Key: String?, associatedWith publicKey: String, noJSON: Bool = false) -> Promise { + internal static func sendOnionRequest(with payload: JSON, to destination: Destination, associatedWith publicKey: String, isJSONRequired: Bool = true) -> Promise { let (promise, seal) = Promise.pending() var guardSnode: Snode! DispatchQueue.global(qos: .userInitiated).async { - buildOnion(around: payload, targetedAt: snode, to: destination, using: x25519Key).done2 { intermediate in + buildOnion(around: payload, targetedAt: destination).done2 { intermediate in guardSnode = intermediate.guardSnode let url = "\(guardSnode.address):\(guardSnode.port)/onion_req" let finalEncryptionResult = intermediate.finalEncryptionResult @@ -310,18 +311,16 @@ public enum OnionRequestAPI { print("[Loki] The user's clock is out of sync with the service node network.") seal.reject(SnodeAPI.SnodeAPIError.clockOutOfSync) } else { - - if noJSON { - let body = ["body": bodyAsString] - guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: body)) } - seal.fulfill(body) - + let body: JSON + if !isJSONRequired { + body = [ "result" : bodyAsString ] } else { guard let bodyAsData = bodyAsString.data(using: .utf8), - let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) } - guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: body)) } - seal.fulfill(body) + let b = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) } + body = b } + guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: body)) } + seal.fulfill(body) } } catch (let error) { seal.reject(error) diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift index 07be2fdb1..d5af0ec4e 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift @@ -7,9 +7,7 @@ public final class PublicChatAPI : DotNetAPI { @objc public static let defaultChats: [PublicChat] = [] // Currently unused public static var displayNameUpdatees: [String:Set] = [:] - - public static var useOnionRequests = true - + // MARK: Settings private static let attachmentType = "net.app.core.oembed" private static let channelInfoType = "net.patter-app.settings" @@ -32,28 +30,6 @@ public final class PublicChatAPI : DotNetAPI { @objc public static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection" @objc public static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection" - @objc public static let openGroupPublicKeyCollection = "LokiGroupChatPublicKeyCollection" - - private static func getOpenGroupPublicKey(on server: String) -> String? { - var result: String? = nil - storage.dbReadConnection.read { transaction in - result = transaction.object(forKey: "\(server)", inCollection: openGroupPublicKeyCollection) as! String? - } - return result - } - - private static func setOpneGroupPublicKey(on server: String, value publicKey: String) { - try! Storage.writeSync { transaction in - transaction.setObject(publicKey, forKey: "\(server)", inCollection: openGroupPublicKeyCollection) - } - } - - private static func removeOpenGroupPublicKey(on server: String) { - try! Storage.writeSync { transaction in - transaction.removeObject(forKey: "\(server)", inCollection: openGroupPublicKeyCollection) - } - } - private static func getLastMessageServerID(for group: UInt64, on server: String) -> UInt? { var result: UInt? = nil storage.dbReadConnection.read { transaction in @@ -97,20 +73,24 @@ public final class PublicChatAPI : DotNetAPI { public static func clearCaches(for channel: UInt64, on server: String) { removeLastMessageServerID(for: channel, on: server) removeLastDeletionServerID(for: channel, on: server) - removeOpenGroupPublicKey(on: server) + try! Storage.writeSync { transaction in + Storage.removeOpenGroupPublicKey(for: server, using: transaction) + } } // MARK: Open Group Public Key Validation - public static func getOpenGroupServerPublicKey(on server: String) -> Promise { - if let publicKey = getOpenGroupPublicKey(on: server) { + public static func getOpenGroupServerPublicKey(for server: String) -> Promise { + if let publicKey = Storage.getOpenGroupPublicKey(for: server) { return Promise.value(publicKey) } else { - return FileServerAPI.getOpenGroupKey(for: server).then2 { hexEncodedPublicKey -> Promise in + return FileServerAPI.getPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { publicKey -> Promise in let url = URL(string: server)! let request = TSRequest(url: url) - return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey, noJSON: true).map2 { _ -> String in - setOpneGroupPublicKey(on: server, value: hexEncodedPublicKey) - return hexEncodedPublicKey + return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .default)) { _ -> String in + try! Storage.writeSync { transaction in + Storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction) + } + return publicKey } } } @@ -123,98 +103,88 @@ public final class PublicChatAPI : DotNetAPI { } public static func getMessages(for channel: UInt64, on server: String) -> Promise<[PublicChatMessage]> { - - func handleMessages(rawResponse: Any) throws -> [PublicChatMessage] { - 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 DotNetAPIError.parsingFailed - } - return rawMessages.flatMap { message in - let isDeleted = (message["is_deleted"] as? Int == 1) - guard !isDeleted else { return nil } - guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == publicChatMessageType }), let value = annotation["value"] as? JSON, - let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64, - let body = message["text"] as? String, let user = message["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, - let timestamp = value["timestamp"] as? UInt64 else { - print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).") - return nil - } - var profilePicture: PublicChatMessage.ProfilePicture? = nil - let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "") - if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }), - let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String { - profilePicture = PublicChatMessage.ProfilePicture(profileKey: profileKey, url: url) - } - let lastMessageServerID = getLastMessageServerID(for: channel, on: server) - if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) } - let quote: PublicChatMessage.Quote? - if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteePublicKey = quoteAsJSON["author"] as? String, - let quotedMessageBody = quoteAsJSON["text"] as? String { - let quotedMessageServerID = message["reply_to"] as? UInt64 - quote = PublicChatMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody, - quotedMessageServerID: quotedMessageServerID) - } else { - quote = nil - } - let signature = PublicChatMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion) - let attachmentsAsJSON = annotations.filter { $0["type"] as? String == attachmentType } - let attachments: [PublicChatMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in - guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = PublicChatMessage.Attachment.Kind(rawValue: kindAsString), - let serverID = value["id"] as? UInt64, let contentType = value["contentType"] as? String, let size = value["size"] as? UInt, let url = value["url"] as? String else { return nil } - let fileName = value["fileName"] as? String ?? UUID().description - let width = value["width"] as? UInt ?? 0 - let height = value["height"] as? UInt ?? 0 - let flags = (value["flags"] as? UInt) ?? 0 - let caption = value["caption"] as? String - let linkPreviewURL = value["linkPreviewUrl"] as? String - let linkPreviewTitle = value["linkPreviewTitle"] as? String - if kind == .linkPreview { - guard linkPreviewURL != nil && linkPreviewTitle != nil else { - print("[Loki] Ignoring public chat message with invalid link preview.") - return nil - } - } - return PublicChatMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, - width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle) - } - let result = PublicChatMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture, - body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature) - guard result.hasValidSignature() else { - print("[Loki] Ignoring public chat message with invalid signature.") - return nil - } - var existingMessageID: String? = nil - storage.dbReadConnection.read { transaction in - existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!), in: transaction) - } - guard existingMessageID == nil else { - print("[Loki] Ignoring duplicate public chat message.") - return nil - } - return result - }.sorted { $0.timestamp < $1.timestamp } - } - var queryParameters = "include_annotations=1" if let lastMessageServerID = getLastMessageServerID(for: channel, on: server) { queryParameters += "&since_id=\(lastMessageServerID)" } else { queryParameters += "&count=\(fallbackBatchCount)&include_deleted=0" } - return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[PublicChatMessage]> in - let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")! - let request = TSRequest(url: url) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublickey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublickey).map2 { rawResponse in - return try handleMessages(rawResponse: rawResponse) + return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[PublicChatMessage]> in + let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")! + let request = TSRequest(url: url) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { 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 DotNetAPIError.parsingFailed } + return rawMessages.flatMap { message in + let isDeleted = (message["is_deleted"] as? Int == 1) + guard !isDeleted else { return nil } + guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == publicChatMessageType }), let value = annotation["value"] as? JSON, + let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64, + let body = message["text"] as? String, let user = message["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, + let timestamp = value["timestamp"] as? UInt64 else { + print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).") + return nil + } + var profilePicture: PublicChatMessage.ProfilePicture? = nil + let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "") + if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }), + let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String { + profilePicture = PublicChatMessage.ProfilePicture(profileKey: profileKey, url: url) + } + let lastMessageServerID = getLastMessageServerID(for: channel, on: server) + if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) } + let quote: PublicChatMessage.Quote? + if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteePublicKey = quoteAsJSON["author"] as? String, + let quotedMessageBody = quoteAsJSON["text"] as? String { + let quotedMessageServerID = message["reply_to"] as? UInt64 + quote = PublicChatMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody, + quotedMessageServerID: quotedMessageServerID) + } else { + quote = nil + } + let signature = PublicChatMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion) + let attachmentsAsJSON = annotations.filter { $0["type"] as? String == attachmentType } + let attachments: [PublicChatMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in + guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = PublicChatMessage.Attachment.Kind(rawValue: kindAsString), + let serverID = value["id"] as? UInt64, let contentType = value["contentType"] as? String, let size = value["size"] as? UInt, let url = value["url"] as? String else { return nil } + let fileName = value["fileName"] as? String ?? UUID().description + let width = value["width"] as? UInt ?? 0 + let height = value["height"] as? UInt ?? 0 + let flags = (value["flags"] as? UInt) ?? 0 + let caption = value["caption"] as? String + let linkPreviewURL = value["linkPreviewUrl"] as? String + let linkPreviewTitle = value["linkPreviewTitle"] as? String + if kind == .linkPreview { + guard linkPreviewURL != nil && linkPreviewTitle != nil else { + print("[Loki] Ignoring public chat message with invalid link preview.") + return nil + } + } + return PublicChatMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, + width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle) + } + let result = PublicChatMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture, + body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature) + guard result.hasValidSignature() else { + print("[Loki] Ignoring public chat message with invalid signature.") + return nil + } + var existingMessageID: String? = nil + storage.dbReadConnection.read { transaction in + existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!), in: transaction) + } + guard existingMessageID == nil else { + print("[Loki] Ignoring duplicate public chat message.") + return nil + } + return result + }.sorted { $0.timestamp < $1.timestamp } } } - return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in - return try handleMessages(rawResponse: rawResponse) - } }.handlingInvalidAuthTokenIfNeeded(for: server) } @@ -225,41 +195,31 @@ public final class PublicChatAPI : DotNetAPI { } public static func sendMessage(_ message: PublicChatMessage, to channel: UInt64, on server: String) -> Promise { - - func handleSendMessageResult(rawResponse: Any, with displayName: String, for signedMessage: PublicChatMessage) throws -> PublicChatMessage { - // 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 DotNetAPIError.parsingFailed - } - let timestamp = UInt64(date.timeIntervalSince1970) * 1000 - return PublicChatMessage(serverID: serverID, senderPublicKey: getUserHexEncodedPublicKey(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature) - } - print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).") let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(DotNetAPIError.signingFailed) } attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in - 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 - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in - return try handleSendMessageResult(rawResponse: rawResponse, with: displayName, for: signedMessage) + getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + 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 OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { 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 DotNetAPIError.parsingFailed } + let timestamp = UInt64(date.timeIntervalSince1970) * 1000 + return PublicChatMessage(serverID: serverID, senderPublicKey: getUserHexEncodedPublicKey(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature) } } - return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in - return try handleSendMessageResult(rawResponse: rawResponse, with: displayName, for: signedMessage) - } }.handlingInvalidAuthTokenIfNeeded(for: server) }.done(on: DispatchQueue.global(qos: .default)) { message in seal.fulfill(message) @@ -272,23 +232,6 @@ public final class PublicChatAPI : DotNetAPI { // MARK: Deletion public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> { - - func handleDeletedMessageServerIDs(rawResponse: Any) throws -> [UInt64] { - 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 DotNetAPIError.parsingFailed - } - return deletions.flatMap { deletion in - guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else { - print("[Loki] Couldn't parse deleted message for public chat channel with ID: \(channel) on server: \(server) from: \(deletion).") - return nil - } - let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) - if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: channel, on: server, to: serverID) } - return messageServerID - } - } - print("[Loki] Getting deleted messages for public chat channel with ID: \(channel) on server: \(server).") let queryParameters: String if let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) { @@ -296,20 +239,27 @@ public final class PublicChatAPI : DotNetAPI { } else { queryParameters = "count=\(fallbackBatchCount)" } - return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[UInt64]> in - let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")! - let request = TSRequest(url: url) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in - return try handleDeletedMessageServerIDs(rawResponse: rawResponse) + return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[UInt64]> in + let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")! + let request = TSRequest(url: url) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { 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 DotNetAPIError.parsingFailed + } + return deletions.flatMap { deletion in + guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else { + print("[Loki] Couldn't parse deleted message for public chat channel with ID: \(channel) on server: \(server) from: \(deletion).") + return nil + } + let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) + if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: channel, on: server, to: serverID) } + return messageServerID } } } - return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in - return try handleDeletedMessageServerIDs(rawResponse: rawResponse) - } }.handlingInvalidAuthTokenIfNeeded(for: server) } @@ -323,61 +273,46 @@ public final class PublicChatAPI : DotNetAPI { 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)" return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in - let url = URL(string: urlAsString)! - let request = TSRequest(url: url, method: "DELETE", parameters: [:]) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).done2 { result -> Void in - print("[Loki] Deleted message with ID: \(messageID) on server: \(server).") - } + getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + let url = URL(string: urlAsString)! + let request = TSRequest(url: url, method: "DELETE", parameters: [:]) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { result -> Void in + print("[Loki] Deleted message with ID: \(messageID) on server: \(server).") } } - return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in - print("[Loki] Deleted message with ID: \(messageID) on server: \(server).") - } }.handlingInvalidAuthTokenIfNeeded(for: server) } } // MARK: Display Name & Profile Picture public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise { - - func handleDisplayNames(rawResponse: Any, for hexEncodedPublicKeys: Set) throws { - 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 DotNetAPIError.parsingFailed - } - try! Storage.writeSync { transaction in - data.forEach { data in - guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return } - let endIndex = hexEncodedPublicKey.endIndex - let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8) - let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex.. 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) - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in - try handleDisplayNames(rawResponse: rawResponse, for: hexEncodedPublicKeys) + print("[Loki] Getting display names for: \(publicKeys).") + return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + let queryParameters = "ids=\(publicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1" + let url = URL(string: "\(server)/users?\(queryParameters)")! + let request = TSRequest(url: url) + return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in + guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else { + print("[Loki] Couldn't parse display names for users: \(publicKeys) from: \(rawResponse).") + throw DotNetAPIError.parsingFailed + } + try! Storage.writeSync { transaction in + data.forEach { data in + guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return } + let endIndex = hexEncodedPublicKey.endIndex + let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8) + let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex.. Promise in - 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)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in - print("Couldn't update display name due to error: \(error).") - throw error - } + getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + 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 OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in + print("Couldn't update display name due to error: \(error).") + throw error } } - return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in - print("Couldn't update display name due to error: \(error).") - throw error - } }.handlingInvalidAuthTokenIfNeeded(for: server) } } @@ -423,22 +352,16 @@ public final class PublicChatAPI : DotNetAPI { } let parameters: JSON = [ "annotations" : [ annotation ] ] return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in - 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)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in - print("[Loki] Couldn't update profile picture due to error: \(error).") - throw error - } + getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + 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 OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in + print("[Loki] Couldn't update profile picture due to error: \(error).") + throw error } } - return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in - print("[Loki] Couldn't update profile picture due to error: \(error).") - throw error - } }.handlingInvalidAuthTokenIfNeeded(for: server) } } @@ -461,10 +384,10 @@ public final class PublicChatAPI : DotNetAPI { let oldProfilePictureURL = storage.getProfilePictureURL(forPublicChatWithID: publicChatID, in: transaction) if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil { storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction) - if let avatarURL = info.profilePictureURL { + if let profilePictureURL = info.profilePictureURL { let configuration = URLSessionConfiguration.default let manager = AFURLSessionManager.init(sessionConfiguration: configuration) - let url = URL(string: "\(server)\(avatarURL)")! + let url = URL(string: "\(server)\(profilePictureURL)")! let request = URLRequest(url: url) let task = manager.downloadTask(with: request, progress: nil, destination: { (targetPath: URL, response: URLResponse) -> URL in @@ -495,84 +418,64 @@ public final class PublicChatAPI : DotNetAPI { } public static func getInfo(for channel: UInt64, on server: String) -> Promise { - - func handleInfo(rawResponse: Any) throws -> PublicChatInfo { - guard let json = rawResponse as? JSON, - let data = json["data"] as? JSON, - let annotations = data["annotations"] as? [JSON], - let annotation = annotations.first, - let info = annotation["value"] as? JSON, - let displayName = info["name"] as? String, - let profilePictureURL = info["avatar"] as? String, - let countInfo = data["counts"] as? JSON, - let memberCount = countInfo["subscribers"] as? Int else { - print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).") - throw DotNetAPIError.parsingFailed - } - let storage = OWSPrimaryStorage.shared() - try! Storage.writeSync { transaction in - storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction) - } - let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount) - updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo) - return publicChatInfo - } - return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in - let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")! - let request = TSRequest(url: url) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - return OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in - return try handleInfo(rawResponse: rawResponse) + getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")! + let request = TSRequest(url: url) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in + guard let json = rawResponse as? JSON, + let data = json["data"] as? JSON, + let annotations = data["annotations"] as? [JSON], + let annotation = annotations.first, + let info = annotation["value"] as? JSON, + let displayName = info["name"] as? String, + let profilePictureURL = info["avatar"] as? String, + let countInfo = data["counts"] as? JSON, + let memberCount = countInfo["subscribers"] as? Int else { + print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).") + throw DotNetAPIError.parsingFailed + } + let storage = OWSPrimaryStorage.shared() + try! Storage.writeSync { transaction in + storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction) } + let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount) + updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo) + return publicChatInfo } } - return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in - return try handleInfo(rawResponse: rawResponse) - } }.handlingInvalidAuthTokenIfNeeded(for: server) } } public static func join(_ channel: UInt64, on server: String) -> Promise { return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { 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)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).done2 { result -> Void in - print("[Loki] Joined channel with ID: \(channel) on server: \(server).") - } + getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { 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 OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { result -> Void in + print("[Loki] Joined channel with ID: \(channel) on server: \(server).") } } - return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in - print("[Loki] Joined channel with ID: \(channel) on server: \(server).") - } }.handlingInvalidAuthTokenIfNeeded(for: server) } } public static func leave(_ channel: UInt64, on server: String) -> Promise { return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { 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)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).done2 { result -> Void in - print("[Loki] Left channel with ID: \(channel) on server: \(server).") - } + getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { 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 OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { result -> Void in + print("[Loki] Left channel with ID: \(channel) on server: \(server).") } } - return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in - print("[Loki] Left channel with ID: \(channel) on server: \(server).") - } }.handlingInvalidAuthTokenIfNeeded(for: server) } } @@ -587,45 +490,32 @@ public final class PublicChatAPI : DotNetAPI { let url = URL(string: "\(server)/loki/v1/channels/\(channel)/messages/\(messageID)/report")! let request = TSRequest(url: url, method: "POST", parameters: [:]) // Only used for the Loki Public Chat which doesn't require authentication - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { _ in} - } + return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in} } - return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { _ in } } // MARK: Moderators public static func getModerators(for channel: UInt64, on server: String) -> Promise> { - - func handleModerators(rawResponse: Any) throws -> Set { - 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 DotNetAPIError.parsingFailed - } - let moderatorsAsSet = Set(moderators); - if self.moderators.keys.contains(server) { - self.moderators[server]![channel] = moderatorsAsSet - } else { - self.moderators[server] = [ channel : moderatorsAsSet ] - } - return moderatorsAsSet - } - - return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise> in - let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")! - let request = TSRequest(url: url) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - if (useOnionRequests) { - return getOpenGroupServerPublicKey(on: server).then2 { hexEncodedPublicKey in - OnionRequestAPI.sendOnionRequestLsrpcDest(request, server: server, using: hexEncodedPublicKey).map2 { rawResponse in - return try handleModerators(rawResponse: rawResponse) + return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise> in + let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")! + let request = TSRequest(url: url) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { 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 DotNetAPIError.parsingFailed + } + let moderatorsAsSet = Set(moderators); + if self.moderators.keys.contains(server) { + self.moderators[server]![channel] = moderatorsAsSet + } else { + self.moderators[server] = [ channel : moderatorsAsSet ] } + return moderatorsAsSet } } - return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in - return try handleModerators(rawResponse: rawResponse) - } }.handlingInvalidAuthTokenIfNeeded(for: server) } diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatManager.swift b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatManager.swift index 331b0acc3..1fe3e6fc6 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatManager.swift +++ b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatManager.swift @@ -56,20 +56,7 @@ public final class PublicChatManager : NSObject { return Promise(error: Error.chatCreationFailed) } } - if (PublicChatAPI.useOnionRequests) { - return PublicChatAPI.getOpenGroupServerPublicKey(on: server).then2 { publicKey in - return PublicChatAPI.getAuthToken(for: server).then2 { token in - return PublicChatAPI.getInfo(for: channel, on: server) - }.map2 { channelInfo -> PublicChat in - guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed } - return chat - } - } - } - // TODO: Remove this when we use onion request totally - return PublicChatAPI.getAuthToken(for: server).then2 { token in - return PublicChatAPI.getInfo(for: channel, on: server) - }.map2 { channelInfo -> PublicChat in + return PublicChatAPI.getInfo(for: channel, on: server).map2 { channelInfo -> PublicChat in guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed } return chat } diff --git a/SignalServiceKit/src/Loki/API/Open Groups/Storage+PublicChats.swift b/SignalServiceKit/src/Loki/API/Open Groups/Storage+PublicChats.swift new file mode 100644 index 000000000..204588abf --- /dev/null +++ b/SignalServiceKit/src/Loki/API/Open Groups/Storage+PublicChats.swift @@ -0,0 +1,22 @@ + +public extension Storage { + + // MARK: Open Group Public Keys + internal static let openGroupPublicKeyCollection = "LokiOpenGroupPublicKeyCollection" + + internal static func getOpenGroupPublicKey(for server: String) -> String? { + var result: String? = nil + read { transaction in + result = transaction.object(forKey: server, inCollection: openGroupPublicKeyCollection) as? String + } + return result + } + + internal static func setOpenGroupPublicKey(for server: String, to publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(publicKey, forKey: server, inCollection: openGroupPublicKeyCollection) + } + + internal static func removeOpenGroupPublicKey(for server: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.removeObject(forKey: server, inCollection: openGroupPublicKeyCollection) + } +} diff --git a/SignalServiceKit/src/Loki/API/Shelved/LokiRSSFeed.swift b/SignalServiceKit/src/Loki/API/Shelved/LokiRSSFeed.swift deleted file mode 100644 index 96daef891..000000000 --- a/SignalServiceKit/src/Loki/API/Shelved/LokiRSSFeed.swift +++ /dev/null @@ -1,17 +0,0 @@ - -@objc(LKRSSFeed) -public final class LokiRSSFeed : NSObject { - @objc public let id: String - @objc public let server: String - @objc public let displayName: String - @objc public let isDeletable: Bool - - @objc public init(id: String, server: String, displayName: String, isDeletable: Bool) { - self.id = "rss://\(id)" - self.server = server - self.displayName = displayName - self.isDeletable = isDeletable - } - - override public var description: String { return displayName } -} diff --git a/SignalServiceKit/src/Loki/API/Shelved/LokiRSSFeedProxy.swift b/SignalServiceKit/src/Loki/API/Shelved/LokiRSSFeedProxy.swift deleted file mode 100644 index e58360592..000000000 --- a/SignalServiceKit/src/Loki/API/Shelved/LokiRSSFeedProxy.swift +++ /dev/null @@ -1,26 +0,0 @@ -import PromiseKit - -public enum LokiRSSFeedProxy { - - public enum Error : LocalizedError { - case proxyResponseParsingFailed - - public var errorDescription: String? { - switch self { - case .proxyResponseParsingFailed: return "Couldn't parse RSS feed proxy response." - } - } - } - - public static func fetchContent(for url: String) -> Promise { - let server = FileServerAPI.server - let endpoints = [ "messenger-updates/feed" : "loki/v1/rss/messenger", "loki.network/feed" : "loki/v1/rss/loki" ] - let endpoint = endpoints.first { url.lowercased().contains($0.key) }!.value - let url = URL(string: server + "/" + endpoint)! - let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request).map2 { response -> String in - guard let json = response as? JSON, let xml = json["data"] as? String else { throw Error.proxyResponseParsingFailed } - return xml - } - } -} diff --git a/SignalServiceKit/src/Loki/API/SnodeAPI.swift b/SignalServiceKit/src/Loki/API/SnodeAPI.swift index 9c0dba1a0..1ad7c9f8b 100644 --- a/SignalServiceKit/src/Loki/API/SnodeAPI.swift +++ b/SignalServiceKit/src/Loki/API/SnodeAPI.swift @@ -47,7 +47,7 @@ public final class SnodeAPI : NSObject { // MARK: Core internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise { if useOnionRequests { - return OnionRequestAPI.sendOnionRequestSnodeDest(invoking: method, on: snode, with: parameters, associatedWith: publicKey).map2 { $0 as Any } + return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any } } else { let url = "\(snode.address):\(snode.port)/storage_rpc/v1" return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise in diff --git a/SignalServiceKit/src/Loki/API/Utilities/HTTP.swift b/SignalServiceKit/src/Loki/API/Utilities/HTTP.swift index f866dfe11..52f8a4516 100644 --- a/SignalServiceKit/src/Loki/API/Utilities/HTTP.swift +++ b/SignalServiceKit/src/Loki/API/Utilities/HTTP.swift @@ -1,11 +1,11 @@ import PromiseKit -internal enum HTTP { +public enum HTTP { private static let urlSession = URLSession(configuration: .ephemeral, delegate: urlSessionDelegate, delegateQueue: nil) private static let urlSessionDelegate = URLSessionDelegateImplementation() // MARK: Settings - private static let timeout: TimeInterval = 20 + public static let timeout: TimeInterval = 20 // MARK: URL Session Delegate Implementation private final class URLSessionDelegateImplementation : NSObject, URLSessionDelegate { @@ -17,7 +17,7 @@ internal enum HTTP { } // MARK: Verb - internal enum Verb : String { + public enum Verb : String { case get = "GET" case put = "PUT" case post = "POST" @@ -25,12 +25,12 @@ internal enum HTTP { } // MARK: Error - internal enum Error : LocalizedError { + public enum Error : LocalizedError { case generic case httpRequestFailed(statusCode: UInt, json: JSON?) case invalidJSON - var errorDescription: String? { + public var errorDescription: String? { switch self { case .generic: return "An error occurred." case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)." @@ -40,7 +40,7 @@ internal enum HTTP { } // MARK: Main - internal static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise { + public static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise { var request = URLRequest(url: URL(string: url)!) request.httpMethod = verb.rawValue if let parameters = parameters {