From 4adf209b1382a31e9c4f83b653a557acb5e6f268 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 18 Oct 2019 09:46:44 +1100 Subject: [PATCH] Upload attachments to public chat server as needed --- Signal/Signal-Info.plist | 2 +- .../src/Loki/API/LokiDotNetAPI.swift | 77 +++++++++++++++++++ .../src/Loki/API/LokiStorageAPI.swift | 77 +------------------ .../src/Messages/OWSMessageSender.m | 3 +- .../src/Network/API/OWSUploadOperation.h | 1 + .../src/Network/API/OWSUploadOperation.m | 13 +++- 6 files changed, 92 insertions(+), 81 deletions(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index ac96885fd..7e723d260 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -127,7 +127,7 @@ NSContactsUsageDescription Signal uses your contacts to find users you know. We do not store your contacts on the server. NSFaceIDUsageDescription - Loki Messenger's Screen Lock feature uses Face ID. + Loki Messenger's Screen Lock feature uses Face ID. NSMicrophoneUsageDescription Loki Messenger needs access to your microphone to record videos. NSPhotoLibraryAddUsageDescription diff --git a/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift b/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift index 38e4a0c07..20e0eef82 100644 --- a/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift @@ -7,6 +7,9 @@ public class LokiDotNetAPI : NSObject { internal static let userKeyPair = OWSIdentityManager.shared().identityKeyPair()! internal static let userHexEncodedPublicKey = userKeyPair.hexEncodedPublicKey + // MARK: Settings + private static let attachmentType = "network.loki" + // MARK: Error public enum Error : Swift.Error { case generic, parsingFailed, encryptionFailed, decryptionFailed, signingFailed @@ -33,6 +36,74 @@ public class LokiDotNetAPI : NSObject { // MARK: Lifecycle override private init() { } + // MARK: Attachments (Public API) + public static func uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> Promise { + return Promise() { seal in + getAuthToken(for: server).done { token in + // Encrypt the attachment + guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else { + print("[Loki] Couldn't read attachment data from disk.") + return seal.reject(Error.generic) + } + var encryptionKey = NSData() + var digest = NSData() + guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else { + print("[Loki] Couldn't encrypt attachment.") + return seal.reject(Error.encryptionFailed) + } + attachment.encryptionKey = encryptionKey as Data + attachment.digest = digest as Data + // Create the request + let url = "\(server)/files" + let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ] + var error: NSError? + var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in + formData.appendPart(withFileData: encryptedAttachmentData, name: "content", fileName: UUID().uuidString, mimeType: "application/binary") + }, error: &error) + request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + if let error = error { + print("[Loki] Couldn't upload attachment due to error: \(error).") + throw error + } + // Send the request + 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(Error.generic) + } + // 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).") + return seal.reject(Error.parsingFailed) + } + // Update the attachment + attachment.serverId = serverID + attachment.isUploaded = true + attachment.downloadURL = downloadURL + attachment.save() + return seal.fulfill(()) + }) + task.resume() + }.catch { error in + print("[Loki] Couldn't upload attachment.") + seal.reject(error) + } + } + } + // MARK: Internal API internal static func getAuthToken(for server: String) -> Promise { if let token = getAuthTokenFromDatabase(for: server) { @@ -77,4 +148,10 @@ public class LokiDotNetAPI : NSObject { let request = TSRequest(url: url, method: "POST", parameters: parameters) return TSNetworkManager.shared().makePromise(request: request).map { _ in token } } + + // MARK: Attachments (Public Obj-C API) + @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/LokiStorageAPI.swift b/SignalServiceKit/src/Loki/API/LokiStorageAPI.swift index cd47840fc..002913e69 100644 --- a/SignalServiceKit/src/Loki/API/LokiStorageAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiStorageAPI.swift @@ -7,10 +7,9 @@ public final class LokiStorageAPI : LokiDotNetAPI { // #if DEBUG // private static let server = "http://file-dev.lokinet.org" // #else - private static let server = "https://file.lokinet.org" + @objc public static let server = "https://file.lokinet.org" // #endif private static let deviceLinkType = "network.loki.messenger.devicemapping" - private static let attachmentType = "network.loki" // MARK: Database override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } @@ -124,78 +123,4 @@ public final class LokiStorageAPI : LokiDotNetAPI { public static func objc_getDeviceLinks(associatedWith hexEncodedPublicKey: String) -> AnyPromise { return AnyPromise.from(getDeviceLinks(associatedWith: hexEncodedPublicKey)) } - - // MARK: Attachments (Public API) - public static func uploadAttachment(_ attachment: TSAttachmentStream, attachmentID: String) -> Promise { - return Promise() { seal in - getAuthToken(for: server).done { token in - // Encrypt the attachment - guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else { - print("[Loki] Couldn't read attachment data from disk.") - return seal.reject(Error.generic) - } - var encryptionKey = NSData() - var digest = NSData() - guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else { - print("[Loki] Couldn't encrypt attachment.") - return seal.reject(Error.encryptionFailed) - } - attachment.encryptionKey = encryptionKey as Data - attachment.digest = digest as Data - // Create the request - let url = "\(server)/files" - let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ] - var error: NSError? - var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in - formData.appendPart(withFileData: encryptedAttachmentData, name: "content", fileName: UUID().uuidString, mimeType: "application/binary") - }, error: &error) - request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - if let error = error { - print("[Loki] Couldn't upload attachment due to error: \(error).") - throw error - } - // Send the request - 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(Error.generic) - } - // 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).") - return seal.reject(Error.parsingFailed) - } - // Update the attachment - attachment.serverId = serverID - attachment.isUploaded = true - attachment.downloadURL = downloadURL - attachment.save() - return seal.fulfill(()) - }) - task.resume() - }.catch { error in - print("[Loki] Couldn't upload attachment.") - seal.reject(error) - } - } - } - - // MARK: Attachments (Public Obj-C API) - @objc(uploadAttachment:withID:) - public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, attachmentID: String) -> AnyPromise { - return AnyPromise.from(uploadAttachment(attachment, attachmentID: attachmentID)) - } } diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 2d07a0f24..d327ddeed 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -389,8 +389,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; failure:failureHandler]; for (NSString *attachmentId in allAttachmentIds) { - OWSUploadOperation *uploadAttachmentOperation = - [[OWSUploadOperation alloc] initWithAttachmentId:attachmentId dbConnection:self.dbConnection]; + OWSUploadOperation *uploadAttachmentOperation = [[OWSUploadOperation alloc] initWithAttachmentId:attachmentId threadID:message.thread.uniqueId dbConnection:self.dbConnection]; // TODO: put attachment uploads on a (low priority) concurrent queue [sendMessageOperation addDependency:uploadAttachmentOperation]; [sendingQueue addOperation:uploadAttachmentOperation]; diff --git a/SignalServiceKit/src/Network/API/OWSUploadOperation.h b/SignalServiceKit/src/Network/API/OWSUploadOperation.h index ebbaadd4f..b8c68d4c3 100644 --- a/SignalServiceKit/src/Network/API/OWSUploadOperation.h +++ b/SignalServiceKit/src/Network/API/OWSUploadOperation.h @@ -19,6 +19,7 @@ extern NSString *const kAttachmentUploadAttachmentIDKey; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithAttachmentId:(NSString *)attachmentId + threadID:(NSString *)threadID dbConnection:(YapDatabaseConnection *)dbConnection NS_DESIGNATED_INITIALIZER; @end diff --git a/SignalServiceKit/src/Network/API/OWSUploadOperation.m b/SignalServiceKit/src/Network/API/OWSUploadOperation.m index da8ae83d6..4db81cfe8 100644 --- a/SignalServiceKit/src/Network/API/OWSUploadOperation.m +++ b/SignalServiceKit/src/Network/API/OWSUploadOperation.m @@ -30,6 +30,7 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f; @interface OWSUploadOperation () @property (readonly, nonatomic) NSString *attachmentId; +@property (readonly, nonatomic) NSString *threadID; @property (readonly, nonatomic) YapDatabaseConnection *dbConnection; @end @@ -39,6 +40,7 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f; @implementation OWSUploadOperation - (instancetype)initWithAttachmentId:(NSString *)attachmentId + threadID:(NSString *)threadID dbConnection:(YapDatabaseConnection *)dbConnection { self = [super init]; @@ -49,6 +51,7 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f; self.remainingRetries = 4; _attachmentId = attachmentId; + _threadID = threadID; _dbConnection = dbConnection; return self; @@ -82,8 +85,14 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f; } [self fireNotificationWithProgress:0]; - - [[LKStorageAPI uploadAttachment:attachmentStream withID:self.attachmentId] + + __block LKPublicChat *publicChat; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.threadID transaction:transaction]; + }]; + NSString *server = (publicChat != nil) ? publicChat.server : LKStorageAPI.server; + + [[LKStorageAPI uploadAttachment:attachmentStream withID:self.attachmentId toServer:server] .thenOn(dispatch_get_main_queue(), ^() { [self reportSuccess]; })