// // Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation import GRDB import SignalCoreKit @objc public class MessageProcessor: NSObject { @objc public static let messageProcessorDidFlushQueue = Notification.Name("messageProcessorDidFlushQueue") @objc public var hasPendingEnvelopes: Bool { !pendingEnvelopes.isEmpty } @objc @available(swift, obsoleted: 1.0) public func processingCompletePromise() -> AnyPromise { return AnyPromise(processingCompletePromise()) } public func processingCompletePromise() -> Promise { guard CurrentAppContext().shouldProcessIncomingMessages else { if DebugFlags.isMessageProcessingVerbose { Logger.verbose("!shouldProcessIncomingMessages") } return Promise.value(()) } if self.hasPendingEnvelopes { if DebugFlags.internalLogging { Logger.info("hasPendingEnvelopes, queuedContentCount: \(self.queuedContentCount)") } return NotificationCenter.default.observe( once: Self.messageProcessorDidFlushQueue ).then { _ in self.processingCompletePromise() }.asVoid() } else if databaseStorage.read( block: { Self.groupsV2MessageProcessor.hasPendingJobs(transaction: $0) } ) { if DebugFlags.internalLogging { let pendingJobCount = databaseStorage.read { Self.groupsV2MessageProcessor.pendingJobCount(transaction: $0) } Logger.verbose("groupsV2MessageProcessor.hasPendingJobs, pendingJobCount: \(pendingJobCount)") } return NotificationCenter.default.observe( once: GroupsV2MessageProcessor.didFlushGroupsV2MessageQueue ).then { _ in self.processingCompletePromise() }.asVoid() } else { if DebugFlags.isMessageProcessingVerbose { Logger.verbose("!hasPendingEnvelopes && !hasPendingJobs") } return Promise.value(()) } } @objc @available(swift, obsoleted: 1.0) public func fetchingAndProcessingCompletePromise() -> AnyPromise { return AnyPromise(fetchingAndProcessingCompletePromise()) } public func fetchingAndProcessingCompletePromise() -> Promise { return firstly { () -> Promise in Self.messageFetcherJob.fetchingCompletePromise() }.then { () -> Promise in self.processingCompletePromise() } } public override init() { super.init() SwiftSingletons.register(self) NotificationCenter.default.addObserver( self, selector: #selector(registrationStateDidChange), name: .registrationStateDidChange, object: nil ) AppReadiness.runNowOrWhenAppDidBecomeReadySync { Self.messagePipelineSupervisor.register(pipelineStage: self) SDSDatabaseStorage.shared.read { transaction in // We may have legacy process jobs queued. We want to schedule them for // processing immediately when we launch, so that we can drain the old queue. let legacyProcessingJobRecords = AnyMessageContentJobFinder().allJobs(transaction: transaction) for jobRecord in legacyProcessingJobRecords { self.processDecryptedEnvelopeData( jobRecord.envelopeData, plaintextData: jobRecord.plaintextData, serverDeliveryTimestamp: jobRecord.serverDeliveryTimestamp, wasReceivedByUD: jobRecord.wasReceivedByUD ) { _ in SDSDatabaseStorage.shared.write { jobRecord.anyRemove(transaction: $0) } } } // We may have legacy decrypt jobs queued. We want to schedule them for // processing immediately when we launch, so that we can drain the old queue. let legacyDecryptJobRecords = AnyJobRecordFinder().allRecords( label: "SSKMessageDecrypt", status: .ready, transaction: transaction ) for jobRecord in legacyDecryptJobRecords { guard let envelopeData = jobRecord.envelopeData else { owsFailDebug("Skipping job with no envelope data") continue } self.processEncryptedEnvelopeData(envelopeData, serverDeliveryTimestamp: jobRecord.serverDeliveryTimestamp, envelopeSource: .unknown) { _ in SDSDatabaseStorage.shared.write { jobRecord.anyRemove(transaction: $0) } } } } } } public struct EnvelopeJob { let encryptedEnvelopeData: Data let encryptedEnvelope: SSKProtoEnvelope? let completion: (Error?) -> Void } public func processEncryptedEnvelopes( envelopeJobs: [EnvelopeJob], serverDeliveryTimestamp: UInt64, envelopeSource: EnvelopeSource ) { for envelopeJob in envelopeJobs { processEncryptedEnvelopeData( envelopeJob.encryptedEnvelopeData, encryptedEnvelope: envelopeJob.encryptedEnvelope, serverDeliveryTimestamp: serverDeliveryTimestamp, envelopeSource: envelopeSource, completion: envelopeJob.completion ) } } public func processEncryptedEnvelopeData( _ encryptedEnvelopeData: Data, encryptedEnvelope optionalEncryptedEnvelopeProto: SSKProtoEnvelope? = nil, serverDeliveryTimestamp: UInt64, envelopeSource: EnvelopeSource, completion: @escaping (Error?) -> Void ) { guard !encryptedEnvelopeData.isEmpty else { completion(OWSAssertionError("Empty envelope, envelopeSource: \(envelopeSource).")) return } // Drop any too-large messages on the floor. Well behaving clients should never send them. guard encryptedEnvelopeData.count <= Self.maxEnvelopeByteCount else { completion(OWSAssertionError("Oversize envelope, envelopeSource: \(envelopeSource).")) return } // Take note of any messages larger than we expect, but still process them. // This likely indicates a misbehaving sending client. if encryptedEnvelopeData.count > Self.largeEnvelopeWarningByteCount { Logger.verbose("encryptedEnvelopeData: \(encryptedEnvelopeData.count) > : \(Self.largeEnvelopeWarningByteCount)") owsFailDebug("Unexpectedly large envelope, envelopeSource: \(envelopeSource).") } let encryptedEnvelopeProto: SSKProtoEnvelope if let optionalEncryptedEnvelopeProto = optionalEncryptedEnvelopeProto { encryptedEnvelopeProto = optionalEncryptedEnvelopeProto } else { do { encryptedEnvelopeProto = try SSKProtoEnvelope(serializedData: encryptedEnvelopeData) } catch { owsFailDebug("Failed to parse encrypted envelope \(error), envelopeSource: \(envelopeSource)") completion(error) return } } let encryptedEnvelope = EncryptedEnvelope( encryptedEnvelopeData: encryptedEnvelopeData, encryptedEnvelope: encryptedEnvelopeProto, serverDeliveryTimestamp: serverDeliveryTimestamp, completion: completion ) let result = pendingEnvelopes.enqueue(encryptedEnvelope: encryptedEnvelope) switch result { case .duplicate: Logger.warn("Duplicate envelope \(encryptedEnvelopeProto.timestamp). Server timestamp: \(encryptedEnvelope.serverTimestamp), serverGuid: \(encryptedEnvelope.serverGuidFormatted), EnvelopeSource: \(envelopeSource).") completion(MessageProcessingError.duplicatePendingEnvelope) case .enqueued: drainPendingEnvelopes() } } public func processDecryptedEnvelopeData( _ envelopeData: Data, plaintextData: Data?, serverDeliveryTimestamp: UInt64, wasReceivedByUD: Bool, completion: @escaping (Error?) -> Void ) { let decryptedEnvelope = DecryptedEnvelope( envelopeData: envelopeData, plaintextData: plaintextData, serverDeliveryTimestamp: serverDeliveryTimestamp, wasReceivedByUD: wasReceivedByUD, completion: completion ) pendingEnvelopes.enqueue(decryptedEnvelope: decryptedEnvelope) drainPendingEnvelopes() } public var queuedContentCount: Int { pendingEnvelopes.count } private static let maxEnvelopeByteCount = 250 * 1024 public static let largeEnvelopeWarningByteCount = 25 * 1024 private let serialQueue = DispatchQueue(label: "MessageProcessor.processingQueue", autoreleaseFrequency: .workItem) private var pendingEnvelopes = PendingEnvelopes() private var isDrainingPendingEnvelopes = false { didSet { assertOnQueue(serialQueue) } } private func drainPendingEnvelopes() { guard Self.messagePipelineSupervisor.isMessageProcessingPermitted else { return } guard TSAccountManager.shared.isRegisteredAndReady else { return } guard CurrentAppContext().shouldProcessIncomingMessages else { return } serialQueue.async { guard !self.isDrainingPendingEnvelopes else { return } self.isDrainingPendingEnvelopes = true self.drainNextBatch() self.isDrainingPendingEnvelopes = false if self.pendingEnvelopes.isEmpty { NotificationCenter.default.postNotificationNameAsync(Self.messageProcessorDidFlushQueue, object: nil) } } } private func drainNextBatch() { assertOnQueue(serialQueue) owsAssertDebug(isDrainingPendingEnvelopes) let shouldContinue: Bool = autoreleasepool { // We want a value that is just high enough to yield perf benefits. let kIncomingMessageBatchSize = 16 // If the app is in the background, use batch size of 1. // This reduces the risk of us never being able to drain any // messages from the queue. We should fine tune this number // to yield the best perf we can get. let batchSize = CurrentAppContext().isInBackground() ? 1 : kIncomingMessageBatchSize let batch = pendingEnvelopes.nextBatch(batchSize: batchSize) let batchEnvelopes = batch.batchEnvelopes let pendingEnvelopesCount = batch.pendingEnvelopesCount guard !batchEnvelopes.isEmpty, messagePipelineSupervisor.isMessageProcessingPermitted else { if DebugFlags.internalLogging { Logger.info("Processing complete: \(self.queuedContentCount) (memoryUsage: \(LocalDevice.memoryUsage).") } return false } Logger.info("Processing batch of \(batchEnvelopes.count)/\(pendingEnvelopesCount) received envelope(s). (memoryUsage: \(LocalDevice.memoryUsage)") var processedEnvelopes: [PendingEnvelope] = [] SDSDatabaseStorage.shared.write { transaction in for envelope in batchEnvelopes { if messagePipelineSupervisor.isMessageProcessingPermitted { self.processEnvelope(envelope, transaction: transaction) processedEnvelopes.append(envelope) } else { // If we're skipping one message, we have to skip them all to preserve ordering // Next time around we can process the skipped messages in order break } } } pendingEnvelopes.removeProcessedEnvelopes(processedEnvelopes) return true } if shouldContinue { self.drainNextBatch() } } private func processEnvelope(_ pendingEnvelope: PendingEnvelope, transaction: SDSAnyWriteTransaction) { assertOnQueue(serialQueue) switch pendingEnvelope.decrypt(transaction: transaction) { case .success(let result): let envelope: SSKProtoEnvelope do { // NOTE: We use envelopeData from the decrypt result, not the pending envelope, // since the envelope may be altered by the decryption process in the UD case. envelope = try SSKProtoEnvelope(serializedData: result.envelopeData) } catch { owsFailDebug("Failed to parse decrypted envelope \(error)") transaction.addAsyncCompletionOffMain { pendingEnvelope.completion(error) } return } // Pre-processing happens during the same transaction that performed decryption messageManager.preprocessEnvelope(envelope: envelope, plaintext: result.plaintextData, transaction: transaction) // If the sender is in the block list, we can skip scheduling any additional processing. if let sourceAddress = envelope.sourceAddress, blockingManager.isAddressBlocked(sourceAddress) { Logger.info("Skipping processing for blocked envelope: \(sourceAddress)") let error = MessageProcessingError.blockedSender transaction.addAsyncCompletionOffMain { pendingEnvelope.completion(error) } return } enum ProcessingStep { case discard case enqueueForGroupProcessing case processNow(shouldDiscardVisibleMessages: Bool) } let processingStep = { () -> ProcessingStep in guard let groupContextV2 = GroupsV2MessageProcessor.groupContextV2( forEnvelope: envelope, plaintextData: result.plaintextData ) else { // Non-v2-group messages can be processed immediately. return .processNow(shouldDiscardVisibleMessages: false) } guard GroupsV2MessageProcessor.canContextBeProcessedImmediately( groupContext: groupContextV2, transaction: transaction ) else { // Some v2 group messages required group state to be // updated before they can be processed. return .enqueueForGroupProcessing } let discardMode = GroupsMessageProcessor.discardMode( envelopeData: result.envelopeData, plaintextData: result.plaintextData, groupContext: groupContextV2, wasReceivedByUD: result.wasReceivedByUD, serverDeliveryTimestamp: result.serverDeliveryTimestamp, transaction: transaction ) if discardMode == .discard { // Some v2 group messages should be discarded and not processed. Logger.verbose("Discarding job.") return .discard } // Some v2 group messages should be processed, but // discarding any "visible" messages, e.g. text messages // or calls. return .processNow(shouldDiscardVisibleMessages: discardMode == .discardVisibleMessages) }() switch processingStep { case .discard: // Do nothing. Logger.verbose("Discarding job.") case .enqueueForGroupProcessing: // If we can't process the message immediately, we enqueue it for // for processing in the same transaction within which it was decrypted // to prevent data loss. Self.groupsV2MessageProcessor.enqueue( envelopeData: result.envelopeData, plaintextData: result.plaintextData, envelope: envelope, wasReceivedByUD: result.wasReceivedByUD, serverDeliveryTimestamp: result.serverDeliveryTimestamp, transaction: transaction ) case .processNow(let shouldDiscardVisibleMessages): // Envelopes can be processed immediately if they're: // 1. Not a GV2 message. // 2. A GV2 message that doesn't require updating the group. // // The advantage to processing the message immediately is that // we can full process the message in the same transaction that // we used to decrypt it. This results in a significant perf // benefit verse queueing the message and waiting for that queue // to open new transactions and process messages. The downside is // that if we *fail* to process this message (e.g. the app crashed // or was killed), we'll have to re-decrypt again before we process. // This is safe, since the decrypt operation would also be rolled // back (since the transaction didn't finalize) and should be rare. Self.messageManager.processEnvelope( envelope, plaintextData: result.plaintextData, wasReceivedByUD: result.wasReceivedByUD, serverDeliveryTimestamp: result.serverDeliveryTimestamp, shouldDiscardVisibleMessages: shouldDiscardVisibleMessages, transaction: transaction ) } transaction.addAsyncCompletionOffMain { pendingEnvelope.completion(nil) } case .failure(let error): transaction.addAsyncCompletionOffMain { pendingEnvelope.completion(error) } } } @objc func registrationStateDidChange() { AppReadiness.runNowOrWhenAppDidBecomeReadySync { self.drainPendingEnvelopes() } } public enum MessageAckBehavior { case shouldAck case shouldNotAck(error: Error) } public static func handleMessageProcessingOutcome(error: Error?) -> MessageAckBehavior { guard let error = error else { // Success. return .shouldAck } if case MessageProcessingError.duplicatePendingEnvelope = error { // _DO NOT_ ACK if de-duplicated before decryption. return .shouldNotAck(error: error) } else if case MessageProcessingError.blockedSender = error { return .shouldAck } else if let owsError = error as? OWSError, owsError.errorCode == OWSErrorCode.failedToDecryptDuplicateMessage.rawValue { // _DO_ ACK if de-duplicated during decryption. return .shouldAck } else { Logger.warn("Failed to process message: \(error)") // This should only happen for malformed envelopes. We may eventually // want to show an error in this case. return .shouldAck } } } // MARK: - extension MessageProcessor: MessageProcessingPipelineStage { public func supervisorDidResumeMessageProcessing(_ supervisor: MessagePipelineSupervisor) { drainPendingEnvelopes() } } // MARK: - private protocol PendingEnvelope { var completion: (Error?) -> Void { get } var wasReceivedByUD: Bool { get } func decrypt(transaction: SDSAnyWriteTransaction) -> Swift.Result func isDuplicateOf(_ other: PendingEnvelope) -> Bool } // MARK: - private struct EncryptedEnvelope: PendingEnvelope, Dependencies { let encryptedEnvelopeData: Data let encryptedEnvelope: SSKProtoEnvelope let serverDeliveryTimestamp: UInt64 let completion: (Error?) -> Void public var serverGuid: String? { encryptedEnvelope.serverGuid } public var serverGuidFormatted: String { String(describing: serverGuid) } public var serverTimestamp: UInt64 { encryptedEnvelope.serverTimestamp } var wasReceivedByUD: Bool { let hasSenderSource: Bool if encryptedEnvelope.hasValidSource { hasSenderSource = true } else { hasSenderSource = false } return encryptedEnvelope.type == .unidentifiedSender && !hasSenderSource } func decrypt(transaction: SDSAnyWriteTransaction) -> Swift.Result { let result = Self.messageDecrypter.decryptEnvelope( encryptedEnvelope, envelopeData: encryptedEnvelopeData, transaction: transaction ) switch result { case .success(let result): return .success(DecryptedEnvelope( envelopeData: result.envelopeData, plaintextData: result.plaintextData, serverDeliveryTimestamp: serverDeliveryTimestamp, wasReceivedByUD: wasReceivedByUD, completion: completion )) case .failure(let error): return .failure(error) } } func isDuplicateOf(_ other: PendingEnvelope) -> Bool { guard let other = other as? EncryptedEnvelope else { return false } guard let serverGuid = self.serverGuid else { owsFailDebug("Missing serverGuid.") return false } guard let otherServerGuid = other.serverGuid else { owsFailDebug("Missing other.serverGuid.") return false } return serverGuid == otherServerGuid } } // MARK: - private struct DecryptedEnvelope: PendingEnvelope { let envelopeData: Data let plaintextData: Data? let serverDeliveryTimestamp: UInt64 let wasReceivedByUD: Bool let completion: (Error?) -> Void func decrypt(transaction: SDSAnyWriteTransaction) -> Swift.Result { return .success(self) } func isDuplicateOf(_ other: PendingEnvelope) -> Bool { // This envelope is only used for legacy envelopes. // We don't need to de-duplicate. false } } // MARK: - @objc public enum EnvelopeSource: UInt, CustomStringConvertible { case unknown case websocketIdentified case websocketUnidentified case rest // We re-decrypt incoming messages after accepting a safety number change. case identityChangeError case debugUI case tests // MARK: - CustomStringConvertible public var description: String { switch self { case .unknown: return "unknown" case .websocketIdentified: return "websocketIdentified" case .websocketUnidentified: return "websocketUnidentified" case .rest: return "rest" case .identityChangeError: return "identityChangeError" case .debugUI: return "debugUI" case .tests: return "tests" } } } // MARK: - public class PendingEnvelopes { private let unfairLock = UnfairLock() private var pendingEnvelopes = [PendingEnvelope]() @objc public var isEmpty: Bool { unfairLock.withLock { pendingEnvelopes.isEmpty } } public var count: Int { unfairLock.withLock { pendingEnvelopes.count } } fileprivate struct Batch { let batchEnvelopes: [PendingEnvelope] let pendingEnvelopesCount: Int } fileprivate func nextBatch(batchSize: Int) -> Batch { unfairLock.withLock { Batch(batchEnvelopes: Array(pendingEnvelopes.prefix(batchSize)), pendingEnvelopesCount: pendingEnvelopes.count) } } fileprivate func removeProcessedEnvelopes(_ processedEnvelopes: [PendingEnvelope]) { unfairLock.withLock { guard pendingEnvelopes.count > processedEnvelopes.count else { pendingEnvelopes = [] return } let oldCount = pendingEnvelopes.count pendingEnvelopes = Array(pendingEnvelopes.suffix(from: processedEnvelopes.count)) let newCount = pendingEnvelopes.count if DebugFlags.internalLogging { Logger.info("\(oldCount) -> \(newCount)") } } } fileprivate func enqueue(decryptedEnvelope: DecryptedEnvelope) { unfairLock.withLock { let oldCount = pendingEnvelopes.count pendingEnvelopes.append(decryptedEnvelope) let newCount = pendingEnvelopes.count if DebugFlags.internalLogging { Logger.info("\(oldCount) -> \(newCount)") } } } public enum EnqueueResult { case duplicate case enqueued } fileprivate func enqueue(encryptedEnvelope: EncryptedEnvelope) -> EnqueueResult { unfairLock.withLock { let oldCount = pendingEnvelopes.count for pendingEnvelope in pendingEnvelopes { if pendingEnvelope.isDuplicateOf(encryptedEnvelope) { return .duplicate } } pendingEnvelopes.append(encryptedEnvelope) let newCount = pendingEnvelopes.count if DebugFlags.internalLogging { Logger.info("\(oldCount) -> \(newCount)") } return .enqueued } } } // MARK: - public enum MessageProcessingError: Error { case duplicatePendingEnvelope case blockedSender }