diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 2fc0f1c14..b9de2e8b9 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -4157,7 +4157,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -4171,7 +4171,7 @@ INFOPLIST_FILE = SignalShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4219,7 +4219,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -4238,7 +4238,7 @@ INFOPLIST_FILE = SignalShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4273,7 +4273,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = SUQ8J2PCT7; @@ -4292,7 +4292,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -4343,7 +4343,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = SUQ8J2PCT7; @@ -4367,7 +4367,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -4405,7 +4405,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -4417,7 +4417,7 @@ INFOPLIST_FILE = LokiPushNotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service"; @@ -4468,7 +4468,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -4485,7 +4485,7 @@ INFOPLIST_FILE = LokiPushNotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service"; @@ -4669,7 +4669,7 @@ CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -4704,7 +4704,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -4736,7 +4736,7 @@ CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -4771,7 +4771,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift b/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift index 02548f496..3ae6393eb 100644 --- a/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift +++ b/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift @@ -22,6 +22,14 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega return result }() + private lazy var addMembersButton: Button = { + let result = Button(style: .prominentOutline, size: .large) + result.setTitle("Add Members", for: UIControl.State.normal) + result.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside) + result.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing) + return result + }() + @objc private lazy var tableView: UITableView = { let result = UITableView() result.dataSource = self @@ -56,13 +64,13 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) backButton.tintColor = Colors.text navigationItem.backBarButtonItem = backButton - setUpViewHierarchy() - updateNavigationBarButtons() - name = thread.groupModel.groupName! func getDisplayName(for publicKey: String) -> String { return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey } members = GroupUtilities.getClosedGroupMembers(thread).sorted { getDisplayName(for: $0) < getDisplayName(for: $1) } + setUpViewHierarchy() + updateNavigationBarButtons() + name = thread.groupModel.groupName! } private func setUpViewHierarchy() { @@ -88,11 +96,8 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega membersLabel.font = .systemFont(ofSize: Values.mediumFontSize) membersLabel.text = "Members" // Add members button - let addMembersButton = Button(style: .prominentOutline, size: .large) - addMembersButton.setTitle("Add Members", for: UIControl.State.normal) - addMembersButton.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside) - addMembersButton.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing) - if (Set(ContactUtilities.getAllContacts()).subtracting(members).isEmpty) { + let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.members).isEmpty + if (!hasContactsToAdd) { addMembersButton.isUserInteractionEnabled = false let disabledColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) addMembersButton.layer.borderColor = disabledColor.cgColor @@ -222,6 +227,11 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey } self.members = members.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) } + let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.members).isEmpty + self.addMembersButton.isUserInteractionEnabled = hasContactsToAdd + let color = hasContactsToAdd ? Colors.accent : Colors.text.withAlphaComponent(Values.unimportantElementOpacity) + self.addMembersButton.layer.borderColor = color.cgColor + self.addMembersButton.setTitleColor(color, for: UIControl.State.normal) } navigationController!.pushViewController(userSelectionVC, animated: true, completion: nil) } @@ -241,13 +251,17 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega guard members != Set(thread.groupModel.groupMemberIds) || name != thread.groupModel.groupName else { return popToConversationVC(self) } - try! Storage.writeSync { [weak self] transaction in - ClosedGroupsProtocol.update(groupPublicKey, with: members, name: name, transaction: transaction).done(on: DispatchQueue.main) { - guard let self = self else { return } - popToConversationVC(self) - }.catch(on: DispatchQueue.main) { error in - guard let self = self else { return } - self.showError(title: "Couldn't Update Group", message: "Please check your internet connection and try again.") + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in + try! Storage.writeSync { [weak self] transaction in + ClosedGroupsProtocol.update(groupPublicKey, with: members, name: name, transaction: transaction).done(on: DispatchQueue.main) { + guard let self = self else { return } + self.dismiss(animated: true, completion: nil) // Dismiss the loader + popToConversationVC(self) + }.catch(on: DispatchQueue.main) { error in + guard let self = self else { return } + self.dismiss(animated: true, completion: nil) // Dismiss the loader + self.showError(title: "Couldn't Update Group", message: "Please check your internet connection and try again.") + } } } } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift index 304ede4d0..5fa530c01 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift @@ -170,9 +170,17 @@ public class ConversationMediaView: UIView { } backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05) - let progressView = MediaDownloadView(attachmentId: attachmentId, radius: maxMessageWidth * 0.1) - self.addSubview(progressView) - progressView.autoPinEdgesToSuperviewEdges() + let view: UIView + if isOnionRouted { // Loki: Due to the way onion routing works we can't get upload progress for those attachments + let activityIndicatorView = UIActivityIndicatorView(style: .white) + activityIndicatorView.isHidden = false + activityIndicatorView.startAnimating() + view = activityIndicatorView + } else { + view = MediaDownloadView(attachmentId: attachmentId, radius: maxMessageWidth * 0.1) + } + addSubview(view) + view.autoPinEdgesToSuperviewEdges() } private func addUploadProgressIfNecessary(_ subview: UIView) -> Bool { diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 7e17155f9..f67d75c4e 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -5398,13 +5398,13 @@ typedef enum : NSUInteger { } dispatch_async(dispatch_get_main_queue(), ^{ __block TSInteraction *targetInteraction; - [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *t) { if (interaction.timestampForUI == timestamp.unsignedLongLongValue) { targetInteraction = interaction; } }]; - } error:nil]; + }]; if (targetInteraction == nil || targetInteraction.interactionType != OWSInteractionType_OutgoingMessage) { return; } NSString *hexEncodedPublicKey = targetInteraction.thread.contactIdentifier; if (hexEncodedPublicKey == nil) { return; } diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index 58017d509..006126dea 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -1157,43 +1157,13 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSLogVerbose(@"downloading profile avatar: %@", userProfile.uniqueId); - NSString *tempDirectory = OWSTemporaryDirectory(); - NSString *tempFilePath = [tempDirectory stringByAppendingPathComponent:fileName]; - NSString *profilePictureURL = userProfile.avatarUrlPath; - NSError *serializationError; - NSMutableURLRequest *request = - [self.avatarHTTPManager.requestSerializer requestWithMethod:@"GET" - URLString:profilePictureURL - parameters:nil - error:&serializationError]; - if (serializationError) { - OWSFailDebug(@"serializationError: %@", serializationError); - return; - } - - NSURLSession* session = [NSURLSession sharedSession]; - NSURLSessionTask* downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { - + [[LKFileServerAPI downloadAttachmentFrom:profilePictureURL].then(^(NSData *data) { @synchronized(self.currentAvatarDownloads) { [self.currentAvatarDownloads removeObject:userProfile.recipientId]; } - - if (error) { - OWSLogError(@"Dowload failed: %@", error); - return; - } - - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *tempFileUrl = [NSURL fileURLWithPath:tempFilePath]; - NSError *moveError; - if (![fileManager moveItemAtURL:location toURL:tempFileUrl error:&moveError]) { - OWSLogError(@"MoveItemAtURL for avatar failed: %@", moveError); - return; - } - - NSData *_Nullable encryptedData = (error ? nil : [NSData dataWithContentsOfFile:tempFilePath]); + NSData *_Nullable encryptedData = data; NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKeyAtStart]; UIImage *_Nullable image = nil; if (decryptedData) { @@ -1213,19 +1183,12 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); if (latestUserProfile.avatarUrlPath.length > 0) { [self downloadAvatarForUserProfile:latestUserProfile]; } - } else if (error) { - if ([response isKindOfClass:NSHTTPURLResponse.class] - && ((NSHTTPURLResponse *)response).statusCode == 403) { - OWSLogInfo(@"no avatar for: %@", userProfile.recipientId); - } else { - OWSLogError(@"avatar download for %@ failed with error: %@", userProfile.recipientId, error); - } } else if (!encryptedData) { OWSLogError(@"avatar encrypted data for %@ could not be read.", userProfile.recipientId); } else if (!decryptedData) { OWSLogError(@"avatar data for %@ could not be decrypted.", userProfile.recipientId); } else if (!image) { - OWSLogError(@"avatar image for %@ could not be loaded with error: %@", userProfile.recipientId, error); + OWSLogError(@"avatar image for %@ could not be loaded.", userProfile.recipientId); } else { [self updateProfileAvatarCache:image filename:fileName]; @@ -1248,9 +1211,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSAssertDebug(backgroundTask); backgroundTask = nil; - }]; - - [downloadTask resume]; + }) retainUntilComplete]; }); } diff --git a/SignalServiceKit/src/Loki/API/DotNetAPI.swift b/SignalServiceKit/src/Loki/API/DotNetAPI.swift index 643338995..6f574985c 100644 --- a/SignalServiceKit/src/Loki/API/DotNetAPI.swift +++ b/SignalServiceKit/src/Loki/API/DotNetAPI.swift @@ -101,6 +101,39 @@ public class DotNetAPI : NSObject { } // MARK: Public API + @objc(downloadAttachmentFrom:) + public static func objc_downloadAttachment(from url: String) -> AnyPromise { + return AnyPromise.from(downloadAttachment(from: url)) + } + + public static func downloadAttachment(from url: String) -> Promise { + var error: NSError? + var host = "https://\(URL(string: url)!.host!)" + let sanitizedURL: String + if FileServerAPI.fileStorageBucketURL.contains(host) { + sanitizedURL = url.replacingOccurrences(of: FileServerAPI.fileStorageBucketURL, with: "\(FileServerAPI.server)/loki/v1") + host = FileServerAPI.server + } else { + sanitizedURL = url.replacingOccurrences(of: host, with: "\(host)/loki/v1") + } + let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: sanitizedURL, parameters: nil, error: &error) + if let error = error { + print("[Loki] Couldn't download attachment due to error: \(error).") + return Promise(error: error) + } + let serverPublicKeyPromise = FileServerAPI.server.contains(host) ? Promise.value(FileServerAPI.fileServerPublicKey) + : PublicChatAPI.getOpenGroupServerPublicKey(for: host) + return serverPublicKeyPromise.then2 { serverPublicKey in + return OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map2 { json in + guard let body = json["body"] as? JSON, let data = body["data"] as? [UInt8] else { + print("[Loki] Couldn't parse attachment from: \(json).") + throw DotNetAPIError.parsingFailed + } + return Data(data) + } + } + } + @objc(uploadAttachment:withID:toServer:) public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> AnyPromise { return AnyPromise.from(uploadAttachment(attachment, with: attachmentID, to: server)) diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift index fc2f69535..c9eb1d222 100644 --- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift @@ -18,6 +18,7 @@ public final class FileServerAPI : DotNetAPI { public static let fileSizeORMultiplier: Double = 6 @objc public static let server = "https://file.getsession.org" + @objc public static let fileStorageBucketURL = "https://file-static.lokinet.org" // MARK: Storage override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift index e18cd245a..24c553ef7 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift @@ -388,27 +388,11 @@ public final class PublicChatAPI : DotNetAPI { if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil { storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction) if let profilePictureURL = info.profilePictureURL { - let configuration = URLSessionConfiguration.default - let manager = AFURLSessionManager.init(sessionConfiguration: configuration) - 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 - let tempFilePath = URL(fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth()).appendingPathComponent(UUID().uuidString) - return tempFilePath - }, - completionHandler: { (response: URLResponse, filePath: URL?, error: Error?) in - if let error = error { - print("[Loki] Couldn't download profile picture for public chat channel with ID: \(channel) on server: \(server).") - return - } - if let filePath = filePath, let avatarData = try? Data.init(contentsOf: filePath) { - let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(avatarData.count), sourceFilename: nil, caption: nil, albumMessageId: nil) - try! attachmentStream.write(avatarData) - groupThread.updateAvatar(with: attachmentStream) - } - }) - task.resume() + FileServerAPI.downloadAttachment(from: "\(server)\(profilePictureURL)").map2 { data in + let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil) + try attachmentStream.write(data) + groupThread.updateAvatar(with: attachmentStream) + } } } } diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift index 706a5ab14..4fb7e8bbf 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift @@ -10,6 +10,7 @@ public final class ClosedGroupUtilities : NSObject { @objc public static let invalidGroupPublicKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Invalid group public key." ]) @objc public static let noData = SSKDecryptionError(domain: "SSKErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Received an empty envelope." ]) @objc public static let noGroupPrivateKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Missing group private key." ]) + @objc public static let selfSend = SSKDecryptionError(domain: "SSKErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Message addressed at self." ]) } @objc(encryptData:usingGroupPublicKey:transaction:error:) @@ -59,6 +60,7 @@ public final class ClosedGroupUtilities : NSObject { // 4. ) Parse the closed group ciphertext message let closedGroupCiphertextMessage = ClosedGroupCiphertextMessage(_throws_with: closedGroupCiphertextMessageAsData) let senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString() + guard senderPublicKey != getUserHexEncodedPublicKey() else { throw SSKDecryptionError.selfSend } // 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content let plaintext = try SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, forGroupWithPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: UInt(closedGroupCiphertextMessage.keyIndex), protocolContext: transaction) diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift index b959d17c1..ef9d5dd3e 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift @@ -118,13 +118,25 @@ public final class ClosedGroupsProtocol : NSObject { print("[Loki] Can't remove self and others simultaneously.") return Promise(error: Error.invalidUpdate) } - // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) - let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], - members: membersAsData, admins: adminsAsData) - let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) - SSKEnvironment.shared.messageSender.send(closedGroupUpdateMessage, success: { seal.fulfill(()) }, failure: { seal.reject($0) }) + // Establish sessions if needed + establishSessionsIfNeeded(with: [String](members), using: transaction) + // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually) + let promises: [Promise] = oldMembers.map { member in + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], + members: membersAsData, admins: adminsAsData) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + return SSKEnvironment.shared.messageSender.sendPromise(message: closedGroupUpdateMessage) + } + when(resolved: promises).done2 { _ in seal.fulfill(()) }.catch2 { seal.reject($0) } promise.done { try! Storage.writeSync { transaction in + let allOldRatchets = Storage.getAllClosedGroupRatchets(for: groupPublicKey) + for (senderPublicKey, oldRatchet) in allOldRatchets { + let collection = Storage.ClosedGroupRatchetCollectionType.old + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction) + } // Delete all ratchets (it's important that this happens * after * sending out the update) Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and @@ -134,8 +146,6 @@ public final class ClosedGroupsProtocol : NSObject { // Notify the PN server LokiPushNotificationManager.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) } else { - // Establish sessions if needed - establishSessionsIfNeeded(with: [String](members), using: transaction) // Send closed group update messages to any new members using established channels for member in newMembers { let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) @@ -203,13 +213,13 @@ public final class ClosedGroupsProtocol : NSObject { return promise } - /// The returned promise is fulfilled when the message has been sent **to the group**. It doesn't wait for the user's new ratchet to be distributed. + /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed. @objc(leaveGroupWithPublicKey:transaction:) public static func objc_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { return AnyPromise.from(leave(groupPublicKey, using: transaction)) } - /// The returned promise is fulfilled when the message has been sent **to the group**. It doesn't wait for the user's new ratchet to be distributed. + /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed. public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) @@ -358,6 +368,11 @@ public final class ClosedGroupsProtocol : NSObject { let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() let wasUserRemoved = !members.contains(userPublicKey) if Set(members).intersection(oldMembers) != Set(oldMembers) { + let allOldRatchets = Storage.getAllClosedGroupRatchets(for: groupPublicKey) + for (senderPublicKey, oldRatchet) in allOldRatchets { + let collection = Storage.ClosedGroupRatchetCollectionType.old + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction) + } Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) if wasUserRemoved { Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift index b53b4725e..21724bf07 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift @@ -40,11 +40,13 @@ public final class SharedSenderKeysImplementation : NSObject { public enum RatchetingError : LocalizedError { case loadingFailed(groupPublicKey: String, senderPublicKey: String) case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String) + case generic public var errorDescription: String? { switch self { case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)." case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)." + case .generic: return "An error occurred" } } } @@ -66,7 +68,8 @@ public final class SharedSenderKeysImplementation : NSObject { let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ]) let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ]) let nextKeyIndex = ratchet.keyIndex + 1 - return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: [ nextMessageKey.toHexString() ]) + let messageKeys = ratchet.messageKeys + [ nextMessageKey.toHexString() ] + return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: messageKeys) } /// - Note: Sync. Don't call from the main thread. @@ -90,11 +93,12 @@ public final class SharedSenderKeysImplementation : NSObject { } /// - Note: Sync. Don't call from the main thread. - private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction) throws -> ClosedGroupRatchet { + private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> ClosedGroupRatchet { #if DEBUG assert(!Thread.isMainThread) #endif - guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey) else { + let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current + guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: collection) else { let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) print("[Loki] \(error.errorDescription!)") throw error @@ -109,20 +113,18 @@ public final class SharedSenderKeysImplementation : NSObject { return ratchet } else { var currentKeyIndex = ratchet.keyIndex - var current = ratchet - var messageKeys: [String] = [] + var result = ratchet while currentKeyIndex < targetKeyIndex { do { - current = try step(current) - messageKeys += current.messageKeys - currentKeyIndex = current.keyIndex + result = try step(result) + currentKeyIndex = result.keyIndex } catch { print("[Loki] Couldn't step ratchet due to error: \(error).") throw error } } - let result = ClosedGroupRatchet(chainKey: current.chainKey, keyIndex: current.keyIndex, messageKeys: messageKeys) // Includes any skipped message keys - Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction) + let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: collection, using: transaction) return result } } @@ -161,30 +163,49 @@ public final class SharedSenderKeysImplementation : NSObject { return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction) } - public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction) throws -> Data { + public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> Data { let ratchet: ClosedGroupRatchet do { - ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction) + ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction, isRetry: isRetry) } catch { - // FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more - // convenient because there's an easy way to get the sender public key from here. - if case RatchetingError.loadingFailed(_, _) = error { - ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) + if !isRetry { + return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true) + } else { + // FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more + // convenient because there's an easy way to get the sender public key from here. + if case RatchetingError.loadingFailed(_, _) = error { + ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) + } + throw error } - throw error } let iv = ivAndCiphertext[0.. 16 { // Pick an arbitrary number of message keys to try; this helps resolve issues caused by messages arriving out of order + lastNMessageKeys = [String](messageKeys[messageKeys.index(messageKeys.endIndex, offsetBy: -16).. String { - return "LokiClosedGroupRatchetCollection.\(groupPublicKey)" + internal static func getClosedGroupRatchetCollection(_ collection: ClosedGroupRatchetCollectionType, for groupPublicKey: String) -> String { + switch collection { + case .old: return "LokiOldClosedGroupRatchetCollection.\(groupPublicKey)" + case .current: return "LokiClosedGroupRatchetCollection.\(groupPublicKey)" + } } - internal static func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String) -> ClosedGroupRatchet? { - let collection = getClosedGroupRatchetCollection(for: groupPublicKey) + internal static func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> ClosedGroupRatchet? { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) var result: ClosedGroupRatchet? read { transaction in result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet @@ -15,26 +22,31 @@ public extension Storage { return result } - internal static func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, using transaction: YapDatabaseReadWriteTransaction) { - let collection = getClosedGroupRatchetCollection(for: groupPublicKey) + internal static func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, in collection: ClosedGroupRatchetCollectionType = .current, using transaction: YapDatabaseReadWriteTransaction) { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) transaction.setObject(ratchet, forKey: senderPublicKey, inCollection: collection) } - internal static func getAllClosedGroupSenderKeys(for groupPublicKey: String) -> Set { - let collection = getClosedGroupRatchetCollection(for: groupPublicKey) - var result: Set = [] + internal static func getAllClosedGroupRatchets(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> [(senderPublicKey: String, ratchet: ClosedGroupRatchet)] { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) + var result: [(senderPublicKey: String, ratchet: ClosedGroupRatchet)] = [] read { transaction in transaction.enumerateRows(inCollection: collection) { key, object, _, _ in - guard let publicKey = key as? String, let ratchet = object as? ClosedGroupRatchet else { return } - let senderKey = ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) - result.insert(senderKey) + guard let senderPublicKey = key as? String, let ratchet = object as? ClosedGroupRatchet else { return } + result.append((senderPublicKey: senderPublicKey, ratchet: ratchet)) } } return result } - internal static func removeAllClosedGroupRatchets(for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { - let collection = getClosedGroupRatchetCollection(for: groupPublicKey) + internal static func getAllClosedGroupSenderKeys(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> Set { + return Set(getAllClosedGroupRatchets(for: groupPublicKey, from: collection).map { senderPublicKey, ratchet in + ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: senderPublicKey)) + }) + } + + internal static func removeAllClosedGroupRatchets(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current, using transaction: YapDatabaseReadWriteTransaction) { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) transaction.removeAllObjects(inCollection: collection) } } diff --git a/SignalServiceKit/src/Loki/Push Notifications/LokiPushNotificationManager.swift b/SignalServiceKit/src/Loki/Push Notifications/LokiPushNotificationManager.swift index faa163b52..253e63be7 100644 --- a/SignalServiceKit/src/Loki/Push Notifications/LokiPushNotificationManager.swift +++ b/SignalServiceKit/src/Loki/Push Notifications/LokiPushNotificationManager.swift @@ -10,6 +10,7 @@ public final class LokiPushNotificationManager : NSObject { private static let server = "https://live.apns.getsession.org" #endif internal static let pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" + private static let maxRetryCount: UInt = 4 private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60 public enum ClosedGroupOperation: String { @@ -28,12 +29,14 @@ public final class LokiPushNotificationManager : NSObject { let url = URL(string: "\(server)/unregister")! let request = TSRequest(url: url, method: "POST", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] - let promise: Promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { - return print("[Loki] Couldn't unregister from push notifications.") - } - guard json["code"] as? Int != 0 else { - return print("[Loki] Couldn't unregister from push notifications due to error: \(json["message"] as? String ?? "nil").") + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't unregister from push notifications.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't unregister from push notifications due to error: \(json["message"] as? String ?? "nil").") + } } } promise.catch2 { error in @@ -68,16 +71,18 @@ public final class LokiPushNotificationManager : NSObject { let url = URL(string: "\(server)/register")! let request = TSRequest(url: url, method: "POST", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] - let promise: Promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { - return print("[Loki] Couldn't register device token.") - } - guard json["code"] as? Int != 0 else { - return print("[Loki] Couldn't register device token due to error: \(json["message"] as? String ?? "nil").") + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't register device token.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't register device token due to error: \(json["message"] as? String ?? "nil").") + } + userDefaults[.deviceToken] = hexEncodedToken + userDefaults[.lastDeviceTokenUpload] = now + userDefaults[.isUsingFullAPNs] = true } - userDefaults[.deviceToken] = hexEncodedToken - userDefaults[.lastDeviceTokenUpload] = now - userDefaults[.isUsingFullAPNs] = true } promise.catch2 { error in print("[Loki] Couldn't register device token.") @@ -103,14 +108,15 @@ public final class LokiPushNotificationManager : NSObject { let url = URL(string: "\(server)/\(operation.rawValue)")! let request = TSRequest(url: url, method: "POST", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] - let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { - return print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(json["message"] as? String ?? "nil").") + } } - guard json["code"] as? Int != 0 else { - return print("[Loki] Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(json["message"] as? String ?? "nil").") - } - return } promise.catch2 { error in print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") @@ -124,14 +130,15 @@ public final class LokiPushNotificationManager : NSObject { let url = URL(string: "\(server)/notify")! let request = TSRequest(url: url, method: "POST", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] - let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { - return print("[Loki] Couldn't notify PN server.") - } - guard json["code"] as? Int != 0 else { - return print("[Loki] Couldn't notify PN server due to error: \(json["message"] as? String ?? "nil").") + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't notify PN server.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't notify PN server due to error: \(json["message"] as? String ?? "nil").") + } } - return } promise.catch2 { error in print("[Loki] Couldn't notify PN server.") diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m index fb850789b..f55716b41 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m @@ -500,12 +500,6 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); OWSAssertDebug(job); TSAttachmentPointer *attachmentPointer = job.attachmentPointer; - AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; - manager.requestSerializer = [AFHTTPRequestSerializer serializer]; - [manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; - manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - // We want to avoid large downloads from a compromised or buggy service. const long kMaxDownloadSize = 10 * 1024 * 1024; __block BOOL hasCheckedContentLength = NO; @@ -513,7 +507,6 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); NSString *tempFilePath = [OWSTemporaryDirectoryAccessibleAfterFirstAuth() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; NSURL *tempFileURL = [NSURL fileURLWithPath:tempFilePath]; - __block NSURLSessionDownloadTask *task; void (^failureHandler)(NSError *) = ^(NSError *error) { OWSLogError(@"Failed to download attachment with error: %@", error.description); @@ -524,125 +517,27 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); failureHandlerParam(task, error); }; + + [[LKFileServerAPI downloadAttachmentFrom:location].then(^(NSData *data) { + BOOL success = [data writeToFile:tempFilePath atomically:YES]; + if (success) { + successHandler(tempFilePath); + } - NSString *method = @"GET"; - NSError *serializationError = nil; - NSMutableURLRequest *request = [manager.requestSerializer requestWithMethod:method - URLString:location - parameters:nil - error:&serializationError]; - if (serializationError) { - return failureHandler(serializationError); - } - - task = [manager downloadTaskWithRequest:request - progress:^(NSProgress *progress) { - OWSAssertDebug(progress != nil); - - // Don't do anything until we've received at least one byte of data. - if (progress.completedUnitCount < 1) { - return; - } - - void (^abortDownload)(void) = ^{ - OWSFailDebug(@"Download aborted."); - [task cancel]; - }; - - if (progress.totalUnitCount > kMaxDownloadSize || progress.completedUnitCount > kMaxDownloadSize) { - // A malicious service might send a misleading content length header, - // so.... - // - // If the current downloaded bytes or the expected total byes - // exceed the max download size, abort the download. - OWSLogError(@"Attachment download exceed expected content length: %lld, %lld.", - (long long)progress.totalUnitCount, - (long long)progress.completedUnitCount); - abortDownload(); - return; - } - - job.progress = progress.fractionCompleted; - - [self fireProgressNotification:MAX(kAttachmentDownloadProgressTheta, progress.fractionCompleted) - attachmentId:attachmentPointer.uniqueId]; - - // We only need to check the content length header once. - if (hasCheckedContentLength) { - return; - } - - // Once we've received some bytes of the download, check the content length - // header for the download. - // - // If the task doesn't exist, or doesn't have a response, or is missing - // the expected headers, or has an invalid or oversize content length, etc., - // abort the download. - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; - if (![httpResponse isKindOfClass:[NSHTTPURLResponse class]]) { - OWSLogError(@"Attachment download has missing or invalid response."); - abortDownload(); - return; - } - - NSDictionary *headers = [httpResponse allHeaderFields]; - if (![headers isKindOfClass:[NSDictionary class]]) { - OWSLogError(@"Attachment download invalid headers."); - abortDownload(); - return; - } - - - NSString *contentLength = headers[@"Content-Length"]; - if (![contentLength isKindOfClass:[NSString class]]) { - OWSLogError(@"Attachment download missing or invalid content length."); - abortDownload(); - return; - } - - - if (contentLength.longLongValue > kMaxDownloadSize) { - OWSLogError(@"Attachment download content length exceeds max download size."); - abortDownload(); - return; - } - - // This response has a valid content length that is less - // than our max download size. Proceed with the download. - hasCheckedContentLength = YES; + NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:tempFilePath]; + if (!fileSize) { + OWSLogError(@"Could not determine attachment file size."); + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + return failureHandler(error); } - destination:^(NSURL *targetPath, NSURLResponse *response) { - return tempFileURL; + if (fileSize.unsignedIntegerValue > kMaxDownloadSize) { + OWSLogError(@"Attachment download length exceeds max size."); + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + return failureHandler(error); } - completionHandler:^(NSURLResponse *response, NSURL *_Nullable filePath, NSError *_Nullable error) { - if (error) { - failureHandler(error); - return; - } - if (![tempFileURL isEqual:filePath]) { - OWSLogError(@"Unexpected temp file path."); - NSError *error = OWSErrorWithCodeDescription( - OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); - return failureHandler(error); - } - - NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:tempFilePath]; - if (!fileSize) { - OWSLogError(@"Could not determine attachment file size."); - NSError *error = OWSErrorWithCodeDescription( - OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); - return failureHandler(error); - } - if (fileSize.unsignedIntegerValue > kMaxDownloadSize) { - OWSLogError(@"Attachment download length exceeds max size."); - NSError *error = OWSErrorWithCodeDescription( - OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); - return failureHandler(error); - } - successHandler(tempFilePath); - }]; - - [task resume]; + }) retainUntilComplete]; } - (void)fireProgressNotification:(CGFloat)progress attachmentId:(NSString *)attachmentId