diff --git a/Podfile.lock b/Podfile.lock index 153b82414..af8dda1fe 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -125,7 +125,7 @@ EXTERNAL SOURCES: :branch: signal-master :git: https://github.com/WhisperSystems/JSQMessagesViewController.git SignalServiceKit: - :path: . + :path: "." SocketRocket: :git: https://github.com/facebook/SocketRocket.git diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 5dc3e4d92..42d0adefd 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -134,6 +134,7 @@ 4542F0961EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */; }; 4542F0971EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */; }; 45464DBC1DFA041F001D3FD6 /* DataChannelMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */; }; + 4563ADF11F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */; }; 45666EC61D99483D008FE134 /* OWSAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */; }; 45666EC91D994C0D008FE134 /* OWSGroupAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC81D994C0D008FE134 /* OWSGroupAvatarBuilder.m */; }; 45666F561D9B2827008FE134 /* OWSScrubbingLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666F551D9B2827008FE134 /* OWSScrubbingLogFormatter.m */; }; @@ -566,6 +567,7 @@ 4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+retainUntilComplete.swift"; sourceTree = ""; }; 45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChannelMessage.swift; sourceTree = ""; }; 454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = ""; }; + 4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS106EnsureProfileComplete.swift; sourceTree = ""; }; 45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAvatarBuilder.h; sourceTree = ""; }; 45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAvatarBuilder.m; sourceTree = ""; }; 45666EC71D994C0D008FE134 /* OWSGroupAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGroupAvatarBuilder.h; sourceTree = ""; }; @@ -1130,6 +1132,7 @@ 45666F7A1D9C0533008FE134 /* OWSDatabaseMigration.m */, 45666F7C1D9C0814008FE134 /* OWSDatabaseMigrationRunner.h */, 45666F7D1D9C0814008FE134 /* OWSDatabaseMigrationRunner.m */, + 4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */, ); name = Migrations; sourceTree = ""; @@ -2144,6 +2147,7 @@ 34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */, 76EB058A18170B33006006FC /* Release.m in Sources */, 45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */, + 4563ADF11F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift in Sources */, 450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */, 76EB057A18170B33006006FC /* OWSContactsManager.m in Sources */, 76EB064218170B33006006FC /* StringUtil.m in Sources */, diff --git a/Signal/src/ProfileFetcherJob.swift b/Signal/src/ProfileFetcherJob.swift index c53a39701..aebf6702f 100644 --- a/Signal/src/ProfileFetcherJob.swift +++ b/Signal/src/ProfileFetcherJob.swift @@ -3,6 +3,7 @@ // import Foundation +import PromiseKit @objc class ProfileFetcherJob: NSObject { @@ -12,34 +13,54 @@ class ProfileFetcherJob: NSObject { let networkManager: TSNetworkManager let storageManager: TSStorageManager - let thread: TSThread - // This property is only accessed on the main queue. static var fetchDateMap = [String: Date]() public class func run(thread: TSThread, networkManager: TSNetworkManager) { - ProfileFetcherJob(thread: thread, networkManager: networkManager).run() + ProfileFetcherJob(networkManager: networkManager).run(thread: thread) } - init(thread: TSThread, networkManager: TSNetworkManager) { + init(networkManager: TSNetworkManager) { self.networkManager = networkManager self.storageManager = TSStorageManager.shared() - - self.thread = thread } - public func run() { + public func run(thread: TSThread) { AssertIsOnMainThread() DispatchQueue.main.async { - for recipientId in self.thread.recipientIdentifiers { - self.getProfile(recipientId: recipientId) + for recipientId in thread.recipientIdentifiers { + self.updateProfile(recipientId: recipientId) } } } - public func getProfile(recipientId: String, remainingRetries: Int = 3) { + enum ProfileFetcherJobError: Error { + case throttled(lastTimeInterval: TimeInterval), + unknownNetworkError + } + + public func updateProfile(recipientId: String, remainingRetries: Int = 3) { + self.getProfile(recipientId: recipientId).then { profile in + self.updateProfile(signalServiceProfile: profile) + }.catch { error in + switch error { + case ProfileFetcherJobError.throttled(let lastTimeInterval): + Logger.info("\(self.TAG) skipping updateProfile: \(recipientId), lastTimeInterval: \(lastTimeInterval)") + case let error as SignalServiceProfile.ValidationError: + Logger.warn("\(self.TAG) skipping updateProfile retry. Invalid profile for: \(recipientId) error: \(error)") + default: + if remainingRetries > 0 { + self.updateProfile(recipientId: recipientId, remainingRetries: remainingRetries - 1) + } else { + owsFail("\(self.TAG) in \(#function) failed to get profile with error: \(error)") + } + } + }.retainUntilComplete() + } + public func getProfile(recipientId: String) -> Promise { + AssertIsOnMainThread() if let lastDate = ProfileFetcherJob.fetchDateMap[recipientId] { let lastTimeInterval = fabs(lastDate.timeIntervalSinceNow) // Don't check a profile more often than every N minutes. @@ -48,8 +69,7 @@ class ProfileFetcherJob: NSObject { // facilitate debugging. let kGetProfileMaxFrequencySeconds = _isDebugAssertConfiguration() ? 0 : 60.0 * 5.0 guard lastTimeInterval > kGetProfileMaxFrequencySeconds else { - Logger.info("\(self.TAG) skipping getProfile: \(recipientId), lastTimeInterval: \(lastTimeInterval)") - return + return Promise(error: ProfileFetcherJobError.throttled(lastTimeInterval: lastTimeInterval)) } } ProfileFetcherJob.fetchDateMap[recipientId] = Date() @@ -58,33 +78,31 @@ class ProfileFetcherJob: NSObject { let request = OWSGetProfileRequest(recipientId: recipientId) + let (promise, fulfill, reject) = Promise.pending() + self.networkManager.makeRequest( request, success: { (_: URLSessionDataTask?, responseObject: Any?) -> Void in - guard let profileResponse = SignalServiceProfile(recipientId: recipientId, rawResponse: responseObject) else { - owsFail("\(self.TAG) response object had unexpected content") - return + do { + let profile = try SignalServiceProfile(recipientId: recipientId, rawResponse: responseObject) + fulfill(profile) + } catch { + reject(error) } - - self.processResponse(signalServiceProfile: profileResponse) }, failure: { (_: URLSessionDataTask?, error: Error?) in - guard let error = error else { - owsFail("\(self.TAG) error in \(#function) was surpringly nil. sheesh rough day.") - return - } - - Logger.error("\(self.TAG) failed to fetch profile for recipient: \(recipientId) with error: \(error)") - if remainingRetries > 1 { - DispatchQueue.global().async { - self.getProfile(recipientId: recipientId, remainingRetries:remainingRetries - 1) - } + if let error = error { + reject(error) } + + reject(ProfileFetcherJobError.unknownNetworkError) }) + + return promise } - private func processResponse(signalServiceProfile: SignalServiceProfile) { + private func updateProfile(signalServiceProfile: SignalServiceProfile) { verifyIdentityUpToDateAsync(recipientId: signalServiceProfile.recipientId, latestIdentityKey: signalServiceProfile.identityKey) // Eventually we'll want to do more things with new SignalServiceProfile fields here. @@ -105,31 +123,32 @@ class ProfileFetcherJob: NSObject { struct SignalServiceProfile { let TAG = "[SignalServiceProfile]" + enum ValidationError: Error { + case invalid(description: String) + case invalidIdentityKey(description: String) + } + public let recipientId: String public let identityKey: Data - init?(recipientId: String, rawResponse: Any?) { + init(recipientId: String, rawResponse: Any?) throws { self.recipientId = recipientId guard let responseDict = rawResponse as? [String: Any?] else { - Logger.error("\(TAG) unexpected type: \(String(describing: rawResponse))") - return nil + throw ValidationError.invalid(description: "\(TAG) unexpected type: \(String(describing: rawResponse))") } guard let identityKeyString = responseDict["identityKey"] as? String else { - Logger.error("\(TAG) missing identity key: \(String(describing: rawResponse))") - return nil + throw ValidationError.invalidIdentityKey(description: "\(TAG) missing identity key: \(String(describing: rawResponse))") } guard let identityKeyWithType = Data(base64Encoded: identityKeyString) else { - Logger.error("\(TAG) unable to parse identity key: \(identityKeyString)") - return nil + throw ValidationError.invalidIdentityKey(description: "\(TAG) unable to parse identity key: \(identityKeyString)") } let kIdentityKeyLength = 33 guard identityKeyWithType.count == kIdentityKeyLength else { - Logger.error("\(TAG) malformed key \(identityKeyString) with decoded length: \(identityKeyWithType.count)") - return nil + throw ValidationError.invalidIdentityKey(description: "\(TAG) malformed key \(identityKeyString) with decoded length: \(identityKeyWithType.count)") } // `removeKeyType` is an objc category method only on NSData, so temporarily cast. diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index b59c9cac9..8b8509f74 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -4,7 +4,6 @@ #import -#import "TSMessageAdapter.h" #import "AttachmentSharing.h" #import "Environment.h" #import "FLAnimatedImage.h" @@ -15,6 +14,7 @@ #import "OWSCallNotificationsAdaptee.h" #import "OWSContactAvatarBuilder.h" #import "OWSContactsManager.h" +#import "OWSDatabaseMigration.h" #import "OWSLogger.h" #import "OWSMessageEditing.h" #import "OWSProgressView.h" @@ -24,6 +24,7 @@ #import "PushManager.h" #import "SettingsTableViewController.h" #import "SignalsViewController.h" +#import "TSMessageAdapter.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIUtil.h" @@ -77,6 +78,7 @@ #import #import #import +#import #import #import #import diff --git a/Signal/src/environment/Migrations/OWSDatabaseMigrationRunner.m b/Signal/src/environment/Migrations/OWSDatabaseMigrationRunner.m index f3815bba1..527faee6b 100644 --- a/Signal/src/environment/Migrations/OWSDatabaseMigrationRunner.m +++ b/Signal/src/environment/Migrations/OWSDatabaseMigrationRunner.m @@ -8,6 +8,7 @@ #import "OWS103EnableVideoCalling.h" #import "OWS104CreateRecipientIdentities.h" #import "OWS105AttachmentFilePaths.h" +#import "Signal-Swift.h" NS_ASSUME_NONNULL_BEGIN @@ -32,7 +33,8 @@ NS_ASSUME_NONNULL_BEGIN [[OWS102MoveLoggingPreferenceToUserDefaults alloc] initWithStorageManager:self.storageManager], [[OWS103EnableVideoCalling alloc] initWithStorageManager:self.storageManager], // OWS104CreateRecipientIdentities is run separately. See runSafeBlockingMigrations. - [[OWS105AttachmentFilePaths alloc] initWithStorageManager:self.storageManager] + [[OWS105AttachmentFilePaths alloc] initWithStorageManager:self.storageManager], + [[OWS106EnsureProfileComplete alloc] initWithStorageManager:self.storageManager] ]; } diff --git a/Signal/src/environment/OWS106EnsureProfileComplete.swift b/Signal/src/environment/OWS106EnsureProfileComplete.swift new file mode 100644 index 000000000..8c930c205 --- /dev/null +++ b/Signal/src/environment/OWS106EnsureProfileComplete.swift @@ -0,0 +1,110 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +class OWS106EnsureProfileComplete: OWSDatabaseMigration { + + let TAG = "[OWS106EnsureProfileComplete]" + + // increment a similar constant for each migration. + class func migrationId() -> String { + return "106" + } + + // Overriding runUp since we have some specific completion criteria which + // is more likely to fail since it involves network requests. + override func runUp() { + CompleteRegistrationFixerJob(completionHandler: { + Logger.info("\(self.TAG) Completed. Saving.") + self.save() + }).start() + } + + /** + * A previous client bug made it possible for re-registering users to register their new account + * but never upload new pre-keys. The symptom is that there will be accounts with no uploaded + * identity key. We detect that here and fix the situation + */ + private class CompleteRegistrationFixerJob { + + let TAG = "[CompleteRegistrationFixerJob]" + + // Duration between retries if update fails. + static let kRetryInterval: TimeInterval = 5 * 60 + + var timer: Timer? + let completionHandler: () -> Void + + init (completionHandler: @escaping () -> Void) { + self.completionHandler = completionHandler + } + + func start() { + assert(self.timer == nil) + + let timer = WeakTimer.scheduledTimer(timeInterval: CompleteRegistrationFixerJob.kRetryInterval, target: self, userInfo: nil, repeats: true) { [weak self] timer in + guard let strongSelf = self else { + return + } + + var isCompleted = false + strongSelf.ensureProfileComplete().then { _ -> Void in + guard isCompleted == false else { + Logger.info("Already saved. Skipping redundant call.") + return + } + Logger.info("\(strongSelf.TAG) complete. Canceling timer and saving.") + isCompleted = true + timer.invalidate() + strongSelf.completionHandler() + }.catch { error in + Logger.error("\(strongSelf.TAG) failed with \(error). We'll try again in \(CompleteRegistrationFixerJob.kRetryInterval) seconds.") + }.retainUntilComplete() + } + self.timer = timer + + timer.fire() + } + + func ensureProfileComplete() -> Promise { + guard let localRecipientId = TSAccountManager.localNumber() else { + // local app doesn't think we're registered, so nothing to worry about. + return Promise(value: ()) + } + + let (promise, fulfill, reject) = Promise.pending() + + guard let networkManager = Environment.getCurrent().networkManager else { + owsFail("\(TAG) network manager was unexpectedly not set") + return Promise(error: OWSErrorMakeAssertionError()) + } + + ProfileFetcherJob(networkManager: networkManager).getProfile(recipientId: localRecipientId).then { _ -> Void in + Logger.info("\(self.TAG) verified recipient profile is in good shape: \(localRecipientId)") + + fulfill() + }.catch { error in + switch error { + case SignalServiceProfile.ValidationError.invalidIdentityKey(let description): + Logger.warn("\(self.TAG) detected incomplete profile for \(localRecipientId) error: \(description)") + // This is the error condition we're looking for. Update prekeys to properly set the identity key, completing registration. + TSPreKeyManager.registerPreKeys(with: .signedAndOneTime, + success: { + Logger.info("\(self.TAG) successfully uploaded pre-keys. Profile should be fixed.") + fulfill() + }, + failure: { _ in + reject(OWSErrorWithCodeDescription(.signalServiceFailure, "\(self.TAG) Unknown error in \(#function)")) + }) + default: + reject(error) + } + }.retainUntilComplete() + + return promise + } + } +} diff --git a/Signal/src/util/WeakTimer.swift b/Signal/src/util/WeakTimer.swift index 1eea5e153..094d56d2c 100644 --- a/Signal/src/util/WeakTimer.swift +++ b/Signal/src/util/WeakTimer.swift @@ -1,5 +1,5 @@ // -// Copyright © 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // /** @@ -33,7 +33,7 @@ final class WeakTimer { action: action).timer! } - @objc fileprivate func fire(timer: Timer) { + @objc public func fire(timer: Timer) { if target != nil { action(timer) } else { diff --git a/SignalServiceKit/src/Storage/TSYapDatabaseObject.m b/SignalServiceKit/src/Storage/TSYapDatabaseObject.m index 71b71c0f5..032d55b48 100644 --- a/SignalServiceKit/src/Storage/TSYapDatabaseObject.m +++ b/SignalServiceKit/src/Storage/TSYapDatabaseObject.m @@ -78,6 +78,15 @@ #pragma mark Class Methods ++ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey +{ + if ([propertyKey isEqualToString:@"TAG"]) { + return MTLPropertyStorageNone; + } else { + return [super storageBehaviorForPropertyWithKey:propertyKey]; + } +} + + (YapDatabaseConnection *)dbReadConnection { // We use TSYapDatabaseObject's dbReadWriteConnection (not TSStorageManager's