diff --git a/Pods b/Pods index e5b45b28d..44417f949 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit e5b45b28d5e8e409c1acf15c32d3734bb3e8acd7 +Subproject commit 44417f9493b8c89b45c1acb72e538d7cd773ebd7 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c5a7426fb..9a8604f0a 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1355,7 +1355,7 @@ D17BB5C25D615AB49813100C /* Pods_Signal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Signal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; - D221A089169C9E5E00537ABF /* Session.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Session.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D221A089169C9E5E00537ABF /* Signal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Signal.app; sourceTree = BUILT_PRODUCTS_DIR; }; D221A08D169C9E5E00537ABF /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; D221A08F169C9E5E00537ABF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; D221A091169C9E5E00537ABF /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -2625,7 +2625,7 @@ D221A08A169C9E5E00537ABF /* Products */ = { isa = PBXGroup; children = ( - D221A089169C9E5E00537ABF /* Session.app */, + D221A089169C9E5E00537ABF /* Signal.app */, D221A0AA169C9E5F00537ABF /* SignalTests.xctest */, 453518681FC635DD00210559 /* SignalShareExtension.appex */, 453518921FC63DBF00210559 /* SignalMessaging.framework */, @@ -2861,7 +2861,7 @@ ); name = Signal; productName = RedPhone; - productReference = D221A089169C9E5E00537ABF /* Session.app */; + productReference = D221A089169C9E5E00537ABF /* Signal.app */; productType = "com.apple.product-type.application"; }; D221A0A9169C9E5F00537ABF /* SignalTests */ = { @@ -4353,12 +4353,13 @@ LLVM_LTO = NO; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.niels-andriesse.loki-network.Loki-Messenger"; - PRODUCT_NAME = Session; + PRODUCT_NAME = Signal; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; SWIFT_OBJC_BRIDGING_HEADER = "Signal/src/Signal-Bridging-Header.h"; + SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_AFTER_BUILD = YES; @@ -4418,11 +4419,12 @@ LLVM_LTO = NO; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.niels-andriesse.loki-network.Loki-Messenger"; - PRODUCT_NAME = Session; + PRODUCT_NAME = Signal; PROVISIONING_PROFILE = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; SWIFT_OBJC_BRIDGING_HEADER = "Signal/src/Signal-Bridging-Header.h"; + SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; SWIFT_VERSION = 5.0; TEST_AFTER_BUILD = YES; VALID_ARCHS = "arm64 armv7 armv7s"; diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme index c5583e4d9..572fd60ab 100644 --- a/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme +++ b/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme @@ -15,7 +15,7 @@ @@ -33,7 +33,7 @@ @@ -56,7 +56,7 @@ @@ -87,7 +87,7 @@ diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme index 8c3c2011a..e523f2c19 100644 --- a/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme +++ b/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme @@ -15,7 +15,7 @@ @@ -117,7 +117,7 @@ @@ -152,7 +152,7 @@ @@ -193,7 +193,7 @@ diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme index 77ad0a66b..cc64d8a0e 100644 --- a/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme +++ b/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme @@ -30,7 +30,7 @@ @@ -72,7 +72,7 @@ @@ -92,7 +92,7 @@ diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 7396df8e5..258a90a89 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -7,7 +7,7 @@ CarthageVersion 0.33.0 OSXVersion - 10.14.4 + 10.14.5 WebRTCCommit 1445d719bf05280270e9f77576f80f973fd847f8 M73 diff --git a/Signal/src/Jobs/MessageFetcherJob.swift b/Signal/src/Jobs/MessageFetcherJob.swift index bc86d5638..3fac71a50 100644 --- a/Signal/src/Jobs/MessageFetcherJob.swift +++ b/Signal/src/Jobs/MessageFetcherJob.swift @@ -47,8 +47,30 @@ public class MessageFetcherJob: NSObject { // } // ======== - Logger.info("fetching messages via REST.") - + Logger.info("Fetching messages via REST.") + let promise = fetchUndeliveredMessages().then { promises -> Promise in + let promises = promises.map { promise -> Promise in + return promise.then { envelopes -> Promise in + for envelope in envelopes { + Logger.info("Envelope received.") + do { + let envelopeData = try envelope.serializedData() + self.messageReceiver.handleReceivedEnvelopeData(envelopeData) + } catch { + owsFailDebug("Failed to serialize envelope.") + } + self.acknowledgeDelivery(envelope: envelope) + } + return Promise.value(()) + } + } + return when(resolved: promises).asVoid() + } + promise.retainUntilComplete() + return promise + + /* Loki: Original code + * ======== let promise = self.fetchUndeliveredMessages().then { (envelopes: [SSKProtoEnvelope], more: Bool) -> Promise in for envelope in envelopes { Logger.info("received envelope.") @@ -73,6 +95,8 @@ public class MessageFetcherJob: NSObject { promise.retainUntilComplete() return promise + * ======== + */ } @objc @@ -174,36 +198,30 @@ public class MessageFetcherJob: NSObject { } } - private func fetchUndeliveredMessages() -> Promise<(envelopes: [SSKProtoEnvelope], more: Bool)> { - return Promise { resolver in - LokiAPI.getMessages().done { envelopes in - resolver.fulfill((envelopes: envelopes, more: false)) - }.catch { error in - resolver.reject(error) - } - // Loki: Original code - // ======== -// let request = OWSRequestFactory.getMessagesRequest() -// self.networkManager.makeRequest( -// request, -// success: { (_: URLSessionDataTask?, responseObject: Any?) -> Void in -// guard let (envelopes, more) = self.parseMessagesResponse(responseObject: responseObject) else { -// Logger.error("response object had unexpected content") -// return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError()) -// } + private func fetchUndeliveredMessages() -> Promise>> { + return LokiAPI.getMessages() + // Loki: Original code + // ======== +// let request = OWSRequestFactory.getMessagesRequest() +// self.networkManager.makeRequest( +// request, +// success: { (_: URLSessionDataTask?, responseObject: Any?) -> Void in +// guard let (envelopes, more) = self.parseMessagesResponse(responseObject: responseObject) else { +// Logger.error("response object had unexpected content") +// return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError()) +// } // -// resolver.fulfill((envelopes: envelopes, more: more)) -// }, -// failure: { (_: URLSessionDataTask?, error: Error?) in -// guard let error = error else { -// Logger.error("error was surpringly nil. sheesh rough day.") -// return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError()) -// } +// resolver.fulfill((envelopes: envelopes, more: more)) +// }, +// failure: { (_: URLSessionDataTask?, error: Error?) in +// guard let error = error else { +// Logger.error("error was surpringly nil. sheesh rough day.") +// return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError()) +// } // -// resolver.reject(error) -// }) - // ======== - } +// resolver.reject(error) +// }) + // ======== } private func acknowledgeDelivery(envelope: SSKProtoEnvelope) { diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 1637ae1e2..6f8a794a9 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -619,7 +619,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) { switch (self.homeViewMode) { case HomeViewMode_Inbox: // TODO: Should our app name be translated? Probably not. - self.title = NSLocalizedString(@"Session", @"Title for the home view's default mode."); + self.title = NSLocalizedString(@"Signal", @"Title for the home view's default mode."); break; case HomeViewMode_Archive: self.title = NSLocalizedString(@"HOME_VIEW_TITLE_ARCHIVE", @"Title for the home view's 'archive' mode."); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index dab2c54e4..5df4197b7 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2578,7 +2578,7 @@ "New Message" = "New Message"; "Secure session reset in progress" = "Secure session reset in progress"; "Secure session reset done" = "Secure session reset done"; -"Session" = "Session"; +"Signal" = "Signal"; "You've sent %@ a friend request" = "You've sent %@ a friend request"; "You've declined %@'s friend request" = "You've declined %@'s friend request"; "You've accepted %@'s friend request" = "You've accepted %@'s friend request"; diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift b/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift index 2a47b47e2..255a5652e 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift @@ -35,22 +35,6 @@ extension LokiAPI { } } - /// Unwrap data sent by the storage server. - /// - /// - Parameter data: The data from the storage server (not base 64 encoded). - /// - Returns: An `SSKProtoEnvelope` object. - /// - Throws: A `WrappingError` if something went wrong. - static func unwrap(data: Data) throws -> SSKProtoEnvelope { - do { - let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data) - let envelope = webSocketMessage.request!.body! - return try SSKProtoEnvelope.parseData(envelope) - } catch let error { - owsFailDebug("[Loki] Failed to unwrap data: \(error).") - throw WrappingError.failedToUnwrapData - } - } - /// Wrap an `SSKProtoEnvelope` in a `WebSocketProtoWebSocketMessage`. private static func createWebSocketMessage(around envelope: SSKProtoEnvelope) throws -> WebSocketProtoWebSocketMessage { do { @@ -91,4 +75,20 @@ extension LokiAPI { throw WrappingError.failedToWrapMessageInEnvelope } } + + /// Unwrap data sent by the storage server. + /// + /// - Parameter data: The data from the storage server (not base 64 encoded). + /// - Returns: An `SSKProtoEnvelope` object. + /// - Throws: A `WrappingError` if something went wrong. + static func unwrap(data: Data) throws -> SSKProtoEnvelope { + do { + let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data) + let envelope = webSocketMessage.request!.body! + return try SSKProtoEnvelope.parseData(envelope) + } catch let error { + owsFailDebug("[Loki] Failed to unwrap data: \(error).") + throw WrappingError.failedToUnwrapData + } + } } diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index 748fcf45d..f5e191993 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -1,20 +1,27 @@ import PromiseKit @objc public final class LokiAPI : NSObject { + private static let storage = OWSPrimaryStorage.shared() + // MARK: Settings private static let version = "v1" + private static let defaultSnodePort: UInt16 = 8080 + private static let targetSnodeCount = 2 public static let defaultMessageTTL: UInt64 = 4 * 24 * 60 * 60 - // MARK: Types - private enum Method : String { - case getMessages = "retrieve" - case sendMessage = "store" - case getSwarm = "get_snodes_for_pubkey" - } + // MARK: Caching + private static var swarmCache: [String:[Target]] = [:] - public struct Target : Hashable { + // MARK: Types + private struct Target : Hashable { let address: String let port: UInt16 + + enum Method : String { + case getSwarm = "get_snodes_for_pubkey" + case getMessages = "retrieve" + case sendMessage = "store" + } } public typealias RawResponse = Any @@ -32,69 +39,159 @@ import PromiseKit // MARK: Lifecycle override private init() { } - // MARK: API - private static func invoke(_ method: Method, on target: Target, with parameters: [String:Any] = [:]) -> Promise { + // MARK: Internal API + private static func invoke(_ method: Target.Method, on target: Target, with parameters: [String:Any] = [:]) -> Promise { let url = URL(string: "\(target.address):\(target.port)/\(version)/storage_rpc")! let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ]) return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject } } - public static func getRandomSnode() -> Promise { + private static func getRandomSnode() -> Promise { return Promise { seal in - seal.fulfill(Target(address: "http://13.238.53.205", port: 8080)) // TODO: Temporary + seal.fulfill(Target(address: "http://13.238.53.205", port: 8080)) // TODO: For debugging purposes } } - public static func getMessages() -> Promise<[SSKProtoEnvelope]> { - let parameters = [ - "pubKey" : OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey, - "lastHash" : "" // TODO: Implement - ] - return getRandomSnode().then { invoke(.getMessages, on: $0, with: parameters) }.map { rawResponse in // TODO: Use getSwarm() - guard let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON] else { return [] } - return messages.compactMap { message in - guard let base64EncodedData = message["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { - Logger.warn("[Loki] Failed to decode data for message: \(message).") - return nil - } - guard let envelope = try? unwrap(data: data) else { - Logger.warn("[Loki] Failed to unwrap data for message: \(message).") - return nil - } - return envelope - } + private static func getSwarm(for hexEncodedPublicKey: String) -> Promise<[Target]> { + if let cachedSwarm = swarmCache[hexEncodedPublicKey], cachedSwarm.count >= targetSnodeCount { + return Promise<[Target]> { $0.fulfill(cachedSwarm) } + } else { + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ] + return getRandomSnode().then { invoke(.getSwarm, on: $0, with: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 } } } - public static func sendMessage(_ lokiMessage: Message) -> Promise { - return getRandomSnode().then { invoke(.sendMessage, on: $0, with: lokiMessage.toJSON()) } // TODO: Use getSwarm() + private static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise<[Target]> { + // shuffled() uses the system's default random generator, which is cryptographically secure + return getSwarm(for: hexEncodedPublicKey).map { Array($0.shuffled().prefix(targetSnodeCount)) } } - public static func ping(_ hexEncodedPublicKey: String) -> Promise { - return getRandomSnode().then { invoke(.sendMessage, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) } // TODO: Use getSwarm() and figure out correct parameters + // MARK: Public API + public static func getMessages() -> Promise>> { + let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in + let lastHash = getLastMessageHashValue(for: targetSnode) ?? "" + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : lastHash ] + return invoke(.getMessages, on: targetSnode, with: parameters).map { rawResponse in + guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] } + updateLastMessageHashValueIfPossible(for: targetSnode, from: rawMessages) + let newRawMessages = removeDuplicates(from: rawMessages) + return parseProtoEnvelopes(from: newRawMessages) + } + }.map { Set($0) } } - public static func getSwarm(for hexEncodedPublicKey: String) -> Promise> { - return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) }.map { rawResponse in return [] } // TODO: Parse targets from raw response + public static func sendMessage(_ lokiMessage: Message) -> Promise>> { + let parameters = lokiMessage.toJSON() + return getTargetSnodes(for: lokiMessage.destination).mapValues { invoke(.sendMessage, on: $0, with: parameters).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }.map { Set($0) } } - // MARK: Obj-C API + public static func ping(_ hexEncodedPublicKey: String) -> Promise>> { + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ] // TODO: Figure out correct parameters + return getTargetSnodes(for: hexEncodedPublicKey).mapValues { invoke(.sendMessage, on: $0, with: parameters).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }.map { Set($0) } + } + + // MARK: Public API (Obj-C) @objc public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, timestamp: UInt64, requiringPoW isPoWRequired: Bool) -> AnyPromise { - let promise = Message.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired) - .then(sendMessage) - .recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) + let promise = Message.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired).then(sendMessage).mapValues { promise -> AnyPromise in + let anyPromise = AnyPromise(promise) + anyPromise.retainUntilComplete() + return anyPromise + }.map { Set($0) } let anyPromise = AnyPromise(promise) anyPromise.retainUntilComplete() return anyPromise } + + // MARK: Parsing + + // The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions. + + private static func parseTargets(from rawResponse: Any) -> [Target] { + // TODO: For debugging purposes + // ======== + let target = Target(address: "http://13.238.53.205", port: 8080) + return Array(repeating: target, count: 3) + // ======== +// guard let json = rawResponse as? JSON, let addresses = json["snodes"] as? [String] else { +// Logger.warn("[Loki] Failed to parse targets from: \(rawResponse).") +// return [] +// } +// return addresses.map { Target(address: $0, port: defaultSnodePort) } + } + + private static func updateLastMessageHashValueIfPossible(for target: Target, from rawMessages: [JSON]) { + guard let lastMessage = rawMessages.last, let hashValue = lastMessage["hash"] as? String, let expiresAt = lastMessage["expiration"] as? Int else { + Logger.warn("[Loki] Failed to update last message hash value from: \(rawMessages).") + return + } + setLastMessageHashValue(for: target, hashValue: hashValue, expiresAt: UInt64(expiresAt)) + } + + private static func removeDuplicates(from rawMessages: [JSON]) -> [JSON] { + var receivedMessageHashValues = getReceivedMessageHashValues() ?? [] + return rawMessages.filter { rawMessage in + guard let hashValue = rawMessage["hash"] as? String else { + Logger.warn("[Loki] Missing hash value for message: \(rawMessage).") + return false + } + let isDuplicate = receivedMessageHashValues.contains(hashValue) + receivedMessageHashValues.insert(hashValue) + setReceivedMessageHashValues(to: receivedMessageHashValues) + return !isDuplicate + } + } + + private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] { + return rawMessages.compactMap { rawMessage in + guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { + Logger.warn("[Loki] Failed to decode data for message: \(rawMessage).") + return nil + } + guard let envelope = try? unwrap(data: data) else { + Logger.warn("[Loki] Failed to unwrap data for message: \(rawMessage).") + return nil + } + return envelope + } + } + + // MARK: Convenience + private static func getLastMessageHashValue(for target: Target) -> String? { + var result: String? = nil + // Uses a read/write connection because getting the last message hash value also removes expired messages as needed + storage.dbReadWriteConnection.readWrite { transaction in + result = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction) + } + return result + } + + private static func setLastMessageHashValue(for target: Target, hashValue: String, expiresAt: UInt64) { + storage.dbReadWriteConnection.readWrite { transaction in + storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expiresAt: expiresAt, transaction: transaction) + } + } + + private static func getReceivedMessageHashValues() -> Set? { + var result: Set? = nil + storage.dbReadConnection.read { transaction in + result = storage.getReceivedMessageHashes(with: transaction) + } + return result + } + + private static func setReceivedMessageHashValues(to receivedMessageHashValues: Set) { + storage.dbReadWriteConnection.readWrite { transaction in + storage.setReceivedMessageHashes(receivedMessageHashValues, with: transaction) + } + } } -// MARK: - Convenience - +// MARK: Error Handling private extension Promise { func recoverNetworkErrorIfNeeded(on queue: DispatchQueue) -> Promise { - return self.recover(on: queue) { error -> Promise in + return recover(on: queue) { error -> Promise in switch error { case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError default: throw error diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h index 9a9a694b4..beb789e9e 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h @@ -1,6 +1,6 @@ #import "OWSPrimaryStorage.h" -#import "PreKeyRecord.h" -#import "PreKeyBundle.h" +#import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -72,6 +72,32 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction; +# pragma mark - Last Hash Handling + +/** + Get the last message hash for the given service node. + This function will check the stored last hash and remove it if the `expiresAt` has already passed. + + @param serviceNode The service node ID. + @param transaction A read write transaction. + @return The last hash or `nil` if it doesn't exist. + */ +- (NSString *_Nullable)getLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction; + +/** + Set the last message hash for the given service node. + This will override any previous hashes stored for the given service node. + + @param serviceNode The service node ID. + @param hash The last message hash. + @param expiresAt The time the message expires on the server. + @param transaction A read write transaction. + */ +- (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(setLastMessageHash(forServiceNode:hash:expiresAt:transaction:)); + +- (NSSet *_Nullable)getReceivedMessageHashesWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (void)setReceivedMessageHashes:(NSSet *)receivedMessageHashes withTransaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m index 125679a53..26744fbec 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m @@ -4,15 +4,20 @@ #import "OWSPrimaryStorage+keyFromIntLong.h" #import "OWSDevice.h" #import "OWSIdentityManager.h" +#import "NSDate+OWS.h" #import "TSAccountManager.h" #import "TSPreKeyManager.h" #import "YapDatabaseConnection+OWS.h" #import "YapDatabaseTransaction+OWS.h" #import +#import "NSObject+Casting.h" #define OWSPrimaryStoragePreKeyStoreCollection @"TSStorageManagerPreKeyStoreCollection" -#define LokiPreKeyContactCollection @"LokiPreKeyContactCollection" -#define LokiPreKeyBundleCollection @"LokiPreKeyBundleCollection" +#define LKPreKeyContactCollection @"LKPreKeyContactCollection" +#define LKPreKeyBundleCollection @"LKPreKeyBundleCollection" +#define LKLastMessageHashCollection @"LKLastMessageHashCollection" +#define LKReceivedMessageHashesKey @"LKReceivedMessageHashesKey" +#define LKReceivedMessageHashesCollection @"LKReceivedMessageHashesCollection" @implementation OWSPrimaryStorage (Loki) @@ -29,13 +34,13 @@ # pragma mark - Prekey for Contact - (BOOL)hasPreKeyForContact:(NSString *)pubKey { - int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LokiPreKeyContactCollection]; + int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LKPreKeyContactCollection]; return preKeyId > 0; } - (PreKeyRecord *_Nullable)getPreKeyForContact:(NSString *)pubKey transaction:(YapDatabaseReadTransaction *)transaction { OWSAssertDebug(pubKey.length > 0); - int preKeyId = [transaction intForKey:pubKey inCollection:LokiPreKeyContactCollection]; + int preKeyId = [transaction intForKey:pubKey inCollection:LKPreKeyContactCollection]; // If we don't have an id then return nil if (preKeyId <= 0) { return nil; } @@ -46,7 +51,7 @@ - (PreKeyRecord *)getOrCreatePreKeyForContact:(NSString *)pubKey { OWSAssertDebug(pubKey.length > 0); - int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LokiPreKeyContactCollection]; + int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LKPreKeyContactCollection]; // If we don't have an id then generate and store a new one if (preKeyId <= 0) { @@ -71,7 +76,7 @@ OWSAssertDebug(records.count > 0); PreKeyRecord *record = records.firstObject; - [self.dbReadWriteConnection setInt:record.Id forKey:pubKey inCollection:LokiPreKeyContactCollection]; + [self.dbReadWriteConnection setInt:record.Id forKey:pubKey inCollection:LKPreKeyContactCollection]; return record; } @@ -105,17 +110,55 @@ } - (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)pubKey { - return [self.dbReadConnection preKeyBundleForKey:pubKey inCollection:LokiPreKeyBundleCollection]; + return [self.dbReadConnection preKeyBundleForKey:pubKey inCollection:LKPreKeyBundleCollection]; } - (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction { [transaction setObject:bundle forKey:pubKey - inCollection:LokiPreKeyBundleCollection]; + inCollection:LKPreKeyBundleCollection]; } - (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction { - [transaction removeObjectForKey:pubKey inCollection:LokiPreKeyBundleCollection]; + [transaction removeObjectForKey:pubKey inCollection:LKPreKeyBundleCollection]; +} + +# pragma mark - Last Hash + +- (NSString *_Nullable)getLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { + NSDictionary *_Nullable dict = [transaction objectForKey:serviceNode inCollection:LKLastMessageHashCollection]; + if (!dict) { return nil; } + + NSString *_Nullable hash = dict[@"hash"]; + if (!hash) { return nil; } + + // Check if the hash isn't expired + uint64_t now = NSDate.ows_millisecondTimeStamp; + NSNumber *_Nullable expiresAt = dict[@"expiresAt"]; + if (expiresAt && expiresAt.unsignedLongLongValue <= now) { + // The last message has expired from the storage server + [self removeLastMessageHashForServiceNode:serviceNode transaction:transaction]; + return nil; + } + + return hash; +} + +- (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction { + NSDictionary *dict = @{ @"hash" : hash, @"expiresAt": @(expiresAt) }; + [transaction setObject:dict forKey:serviceNode inCollection:LKLastMessageHashCollection]; +} + +- (void)removeLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { + [transaction removeObjectForKey:serviceNode inCollection:LKLastMessageHashCollection]; +} + +- (NSSet *_Nullable)getReceivedMessageHashesWithTransaction:(YapDatabaseReadTransaction *)transaction { + return (NSSet *)[[transaction objectForKey:LKReceivedMessageHashesKey inCollection:LKReceivedMessageHashesCollection] as:NSSet.class]; +} + +- (void)setReceivedMessageHashes:(NSSet *)receivedMessageHashes withTransaction:(YapDatabaseReadWriteTransaction *)transaction { + [transaction setObject:receivedMessageHashes forKey:LKReceivedMessageHashesKey inCollection:LKReceivedMessageHashesCollection]; } @end diff --git a/SignalServiceKit/src/Loki/Utilities/Promise+Hashable.swift b/SignalServiceKit/src/Loki/Utilities/Promise+Hashable.swift new file mode 100644 index 000000000..0d2ec3bd7 --- /dev/null +++ b/SignalServiceKit/src/Loki/Utilities/Promise+Hashable.swift @@ -0,0 +1,13 @@ +import PromiseKit + +extension Promise : Hashable { + + public func hash(into hasher: inout Hasher) { + let reference = ObjectIdentifier(self).hashValue + hasher.combine(reference) + } + + public static func == (lhs: Promise, rhs: Promise) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 419102baa..736a89798 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -1103,11 +1103,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; return messageSend.failure(error); } - // Update the state to show that proof of work is being calculated + // Update the state to show that the proof of work is being calculated [self setIsCalculatingProofOfWorkForMessage:messageSend]; // Convert the message to a Loki message and send it using the Loki messaging API NSDictionary *signalMessage = deviceMessages.firstObject; - // Update the thread's friend request status if needed + // Update the message and thread if needed NSInteger *messageType = ((NSNumber *)signalMessage[@"type"]).integerValue; if (messageType == TSFriendRequestMessageType) { [message.thread saveFriendRequestStatus:TSThreadFriendRequestStatusRequestSending withTransaction:nil]; @@ -1116,51 +1116,53 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; BOOL isPoWRequired = YES; // TODO: Base on message type [[LokiAPI objc_sendSignalMessage:signalMessage to:recipient.recipientId timestamp:message.timestamp requiringPoW:isPoWRequired] .thenOn(OWSDispatch.sendingQueue, ^(id result) { - // Loki - // ======== - if (messageType == TSFriendRequestMessageType) { - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [message.thread saveFriendRequestStatus:TSThreadFriendRequestStatusRequestSent withTransaction:transaction]; - [message.thread removeOutgoingFriendRequestMessagesWithTransaction:transaction]; - // Set the expiration date - NSTimeInterval expirationInterval = 72 * kHourInterval; - NSDate *expireDate = [[NSDate new] dateByAddingTimeInterval:expirationInterval]; - [message saveFriendRequestExpiresAt:[NSDate ows_millisecondsSince1970ForDate:expireDate] withTransaction:transaction]; - }]; - } - // ======== - // Invoke the completion handler - [self messageSendDidSucceed:messageSend - deviceMessages:deviceMessages - wasSentByUD:false - wasSentByWebsocket:false]; - }) - .catchOn(OWSDispatch.sendingQueue, ^(NSError *error) { - // Loki - // ======== - if (messageType == TSFriendRequestMessageType) { - [message.thread saveFriendRequestStatus:TSThreadFriendRequestStatusNone withTransaction:nil]; - } - // ======== - // Handle the error - NSUInteger statusCode = 0; - NSData *_Nullable responseData = nil; - if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { - statusCode = error.code; - NSError *_Nullable underlyingError = error.userInfo[NSUnderlyingErrorKey]; - if (underlyingError) { - responseData = underlyingError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; - } else { - OWSFailDebug(@"Missing underlying error: %@", error); - } - } else { - OWSFailDebug(@"Unexpected error: %@", error); + NSSet *promises = (NSSet *)result; + __block BOOL isSuccess = NO; + NSUInteger promiseCount = promises.count; + __block NSUInteger errorCount = 0; + for (AnyPromise *promise in promises) { + [promise + .thenOn(OWSDispatch.sendingQueue, ^(id result) { + if (isSuccess) { return; } // Succeed as soon as the first promise succeeds + isSuccess = YES; + // Update the message and thread if needed + if (messageType == TSFriendRequestMessageType) { + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [message.thread saveFriendRequestStatus:TSThreadFriendRequestStatusRequestSent withTransaction:transaction]; + [message.thread removeOutgoingFriendRequestMessagesWithTransaction:transaction]; + NSTimeInterval expirationInterval = 72 * kHourInterval; + NSDate *expirationDate = [[NSDate new] dateByAddingTimeInterval:expirationInterval]; + [message saveFriendRequestExpiresAt:[NSDate ows_millisecondsSince1970ForDate:expirationDate] withTransaction:transaction]; + }]; + } + // Invoke the completion handler + [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:false wasSentByWebsocket:false]; + }) + .catchOn(OWSDispatch.sendingQueue, ^(NSError *error) { + errorCount += 1; + if (errorCount != promiseCount) { return; } // Only error out if all promises failed + // Update the thread if needed + if (messageType == TSFriendRequestMessageType) { + [message.thread saveFriendRequestStatus:TSThreadFriendRequestStatusNone withTransaction:nil]; + } + // Handle the error + NSUInteger statusCode = 0; + NSData *_Nullable responseData = nil; + if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { + statusCode = error.code; + NSError *_Nullable underlyingError = error.userInfo[NSUnderlyingErrorKey]; + if (underlyingError) { + responseData = underlyingError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; + } else { + OWSFailDebug(@"Missing underlying error: %@.", error); + } + } else { + OWSFailDebug(@"Unexpected error: %@.", error); + } + [self messageSendDidFail:messageSend deviceMessages:deviceMessages statusCode:statusCode error:error responseData:responseData]; + }) retainUntilComplete]; } - [self messageSendDidFail:messageSend - deviceMessages:deviceMessages - statusCode:statusCode - error:error - responseData:responseData]; + }) retainUntilComplete]; // Loki: Original code