// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "OWSMessageSender.h" #import "ContactsUpdater.h" #import "NSData+keyVersionByte.h" #import "NSData+messagePadding.h" #import "OWSBlockingManager.h" #import "OWSDevice.h" #import "OWSDisappearingMessagesJob.h" #import "OWSError.h" #import "OWSIdentityManager.h" #import "OWSMessageServiceParams.h" #import "OWSOutgoingSentMessageTranscript.h" #import "OWSOutgoingSyncMessage.h" #import "OWSUploadingService.h" #import "PreKeyBundle+jsonDict.h" #import "SignalRecipient.h" #import "TSAccountManager.h" #import "TSAttachmentStream.h" #import "TSContactThread.h" #import "TSGroupThread.h" #import "TSIncomingMessage.h" #import "TSInfoMessage.h" #import "TSInvalidIdentityKeySendingErrorMessage.h" #import "TSNetworkManager.h" #import "TSOutgoingMessage.h" #import "TSPreKeyManager.h" #import "TSStorageManager+PreKeyStore.h" #import "TSStorageManager+SignedPreKeyStore.h" #import "TSStorageManager+sessionStore.h" #import "TSStorageManager.h" #import "TSThread.h" #import "Threading.h" #import #import #import #import #import #import #import NS_ASSUME_NONNULL_BEGIN void AssertIsOnSendingQueue() { #ifdef DEBUG if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(10, 0)) { dispatch_assert_queue([OWSDispatch sendingQueue]); } // else, skip assert as it's a development convenience. #endif } static void *kNSError_MessageSender_IsRetryable = &kNSError_MessageSender_IsRetryable; static void *kNSError_MessageSender_ShouldBeIgnoredForGroups = &kNSError_MessageSender_ShouldBeIgnoredForGroups; static void *kNSError_MessageSender_IsFatal = &kNSError_MessageSender_IsFatal; // isRetryable and isFatal are opposites but not redundant. // // If a group message send fails, the send will be retried if any of the errors were retryable UNLESS // any of the errors were fatal. Fatal errors trump retryable errors. @implementation NSError (OWSMessageSender) - (BOOL)isRetryable { NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_IsRetryable); // This value should always be set for all errors by the time OWSSendMessageOperation // queries it's value. If not, default to retrying in production. OWSAssert(value); return value ? [value boolValue] : YES; } - (void)setIsRetryable:(BOOL)value { objc_setAssociatedObject(self, kNSError_MessageSender_IsRetryable, @(value), OBJC_ASSOCIATION_COPY); } - (BOOL)shouldBeIgnoredForGroups { NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_ShouldBeIgnoredForGroups); // This value will NOT always be set for all errors by the time we query it's value. // Default to NOT ignoring. return value ? [value boolValue] : NO; } - (void)setShouldBeIgnoredForGroups:(BOOL)value { objc_setAssociatedObject(self, kNSError_MessageSender_ShouldBeIgnoredForGroups, @(value), OBJC_ASSOCIATION_COPY); } - (BOOL)isFatal { NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_IsFatal); // This value will NOT always be set for all errors by the time we query it's value. // Default to NOT fatal. return value ? [value boolValue] : NO; } - (void)setIsFatal:(BOOL)value { objc_setAssociatedObject(self, kNSError_MessageSender_IsFatal, @(value), OBJC_ASSOCIATION_COPY); } @end #pragma mark - /** * OWSSendMessageOperation encapsulates all the work associated with sending a message, e.g. uploading attachments, * getting proper keys, and retrying upon failure. * * Used by `OWSMessageSender` to serialize message sending, ensuring that messages are emitted in the order they * were sent. */ @interface OWSSendMessageOperation : NSOperation - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithMessage:(TSOutgoingMessage *)message messageSender:(OWSMessageSender *)messageSender success:(void (^)())successHandler failure:(void (^)(NSError *_Nonnull error))failureHandler NS_DESIGNATED_INITIALIZER; #pragma mark - background task mgmt - (void)startBackgroundTask; - (void)endBackgroundTask; @end #pragma mark - typedef NS_ENUM(NSInteger, OWSSendMessageOperationState) { OWSSendMessageOperationStateNew, OWSSendMessageOperationStateExecuting, OWSSendMessageOperationStateFinished }; @interface OWSMessageSender (OWSSendMessageOperation) - (void)attemptToSendMessage:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(RetryableFailureHandler)failureHandler; @end #pragma mark - NSString *const OWSSendMessageOperationKeyIsExecuting = @"isExecuting"; NSString *const OWSSendMessageOperationKeyIsFinished = @"isFinished"; NSUInteger const OWSSendMessageOperationMaxRetries = 4; @interface OWSSendMessageOperation () @property (nonatomic, readonly) TSOutgoingMessage *message; @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) void (^successHandler)(); @property (nonatomic, readonly) void (^failureHandler)(NSError *_Nonnull error); @property (nonatomic) OWSSendMessageOperationState operationState; @property (nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier; @end #pragma mark - @implementation OWSSendMessageOperation - (instancetype)initWithMessage:(TSOutgoingMessage *)message messageSender:(OWSMessageSender *)messageSender success:(void (^)())aSuccessHandler failure:(void (^)(NSError *_Nonnull error))aFailureHandler { self = [super init]; if (!self) { return self; } _operationState = OWSSendMessageOperationStateNew; _backgroundTaskIdentifier = UIBackgroundTaskInvalid; _message = message; _messageSender = messageSender; __weak typeof(self) weakSelf = self; _successHandler = ^{ typeof(self) strongSelf = weakSelf; if (!strongSelf) { OWSProdCFail([OWSAnalyticsEvents messageSenderErrorSendOperationDidNotComplete]); return; } [message updateWithMessageState:TSOutgoingMessageStateSentToService]; aSuccessHandler(); [strongSelf markAsComplete]; }; _failureHandler = ^(NSError *_Nonnull error) { typeof(self) strongSelf = weakSelf; if (!strongSelf) { OWSProdCFail([OWSAnalyticsEvents messageSenderErrorSendOperationDidNotComplete]); return; } [strongSelf.message updateWithSendingError:error]; DDLogDebug(@"%@ failed with error: %@", strongSelf.tag, error); aFailureHandler(error); [strongSelf markAsComplete]; }; return self; } #pragma mark - background task mgmt // We want to make sure to finish sending any in-flight messages when the app is backgrounded. // We have to call `startBackgroundTask` *before* the task is enqueued, since we can't guarantee when the operation will // be dequeued. - (void)startBackgroundTask { AssertIsOnMainThread(); OWSAssert(self.backgroundTaskIdentifier == UIBackgroundTaskInvalid); self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ DDLogWarn(@"%@ Timed out while in background trying to send message: %@", self.tag, self.message); [self endBackgroundTask]; }]; } - (void)endBackgroundTask { [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier]; } - (void)setBackgroundTaskIdentifier:(UIBackgroundTaskIdentifier)backgroundTaskIdentifier { AssertIsOnMainThread(); // Should only be sent once per operation OWSAssert(_backgroundTaskIdentifier == UIBackgroundTaskInvalid); OWSAssert(backgroundTaskIdentifier != UIBackgroundTaskInvalid); _backgroundTaskIdentifier = backgroundTaskIdentifier; } #pragma mark - NSOperation overrides - (BOOL)isExecuting { return self.operationState == OWSSendMessageOperationStateExecuting; } - (BOOL)isFinished { return self.operationState == OWSSendMessageOperationStateFinished; } - (void)start { // Should call `startBackgroundTask` before enqueuing the operation // to ensure we don't get suspended before the operation completes. OWSAssert(self.backgroundTaskIdentifier != UIBackgroundTaskInvalid); [self willChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; self.operationState = OWSSendMessageOperationStateExecuting; [self didChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; [self main]; } - (void)main { [self tryWithRemainingRetries:OWSSendMessageOperationMaxRetries]; } #pragma mark - methods - (void)tryWithRemainingRetries:(NSUInteger)remainingRetries { // Use this flag to ensure a given operation only succeeds or fails once. __block BOOL onceFlag = NO; RetryableFailureHandler retryableFailureHandler = ^(NSError *_Nonnull error) { DDLogInfo(@"%@ Sending failed. Remaining retries: %lu", self.tag, (unsigned long)remainingRetries); OWSAssert(!onceFlag); onceFlag = YES; if (![error isRetryable] || [error isFatal]) { DDLogInfo(@"%@ Skipping retry due to terminal error: %@", self.tag, error); self.failureHandler(error); return; } if (remainingRetries > 0) { [self tryWithRemainingRetries:remainingRetries - 1]; } else { DDLogWarn(@"%@ Too many failures. Giving up sending.", self.tag); self.failureHandler(error); } }; [self.messageSender attemptToSendMessage:self.message success:^{ OWSAssert(!onceFlag); onceFlag = YES; self.successHandler(); } failure:retryableFailureHandler]; } - (void)markAsComplete { [self willChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; [self willChangeValueForKey:OWSSendMessageOperationKeyIsFinished]; // Ensure we call the success or failure handler exactly once. @synchronized(self) { OWSAssert(self.operationState != OWSSendMessageOperationStateFinished); self.operationState = OWSSendMessageOperationStateFinished; } [self didChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; [self didChangeValueForKey:OWSSendMessageOperationKeyIsFinished]; [self endBackgroundTask]; } #pragma mark - Logging + (NSString *)tag { return [NSString stringWithFormat:@"[%@]", self.class]; } - (NSString *)tag { return self.class.tag; } @end int const OWSMessageSenderRetryAttempts = 3; NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException"; NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; @interface OWSMessageSender () @property (nonatomic, readonly) TSNetworkManager *networkManager; @property (nonatomic, readonly) TSStorageManager *storageManager; @property (nonatomic, readonly) OWSBlockingManager *blockingManager; @property (nonatomic, readonly) OWSUploadingService *uploadingService; @property (nonatomic, readonly) YapDatabaseConnection *dbConnection; @property (nonatomic, readonly) id contactsManager; @property (nonatomic, readonly) ContactsUpdater *contactsUpdater; @property (atomic, readonly) NSMutableDictionary *sendingQueueMap; @end @implementation OWSMessageSender - (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager storageManager:(TSStorageManager *)storageManager contactsManager:(id)contactsManager contactsUpdater:(ContactsUpdater *)contactsUpdater { self = [super init]; if (!self) { return self; } _networkManager = networkManager; _storageManager = storageManager; _contactsManager = contactsManager; _contactsUpdater = contactsUpdater; _sendingQueueMap = [NSMutableDictionary new]; _uploadingService = [[OWSUploadingService alloc] initWithNetworkManager:networkManager]; _dbConnection = storageManager.newDatabaseConnection; OWSSingletonAssert(); return self; } - (void)setBlockingManager:(OWSBlockingManager *)blockingManager { OWSAssert(blockingManager); OWSAssert(!_blockingManager); _blockingManager = blockingManager; } - (NSOperationQueue *)sendingQueueForMessage:(TSOutgoingMessage *)message { OWSAssert(message); NSString *kDefaultQueueKey = @"kDefaultQueueKey"; NSString *queueKey = message.uniqueThreadId ?: kDefaultQueueKey; OWSAssert(queueKey.length > 0); @synchronized(self) { NSOperationQueue *sendingQueue = self.sendingQueueMap[queueKey]; if (!sendingQueue) { sendingQueue = [NSOperationQueue new]; sendingQueue.qualityOfService = NSOperationQualityOfServiceUserInitiated; sendingQueue.maxConcurrentOperationCount = 1; self.sendingQueueMap[queueKey] = sendingQueue; } return sendingQueue; } } - (void)sendMessage:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(void (^)(NSError *error))failureHandler { OWSAssert(message); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // This method will use a read/write transaction. This transaction // will block until any open read/write transactions are complete. // // That's key - we don't want to send any messages in response // to an incoming message until processing of that batch of messages // is complete. For example, we wouldn't want to auto-reply to a // group info request before that group info request's batch was // finished processing. Otherwise, we might receive a delivery // notice for a group update we hadn't yet saved to the db. // // So we're using YDB behavior to ensure this invariant, which is a bit // unorthodox. [message updateWithMessageState:TSOutgoingMessageStateAttemptingOut]; OWSSendMessageOperation *sendMessageOperation = [[OWSSendMessageOperation alloc] initWithMessage:message messageSender:self success:successHandler failure:failureHandler]; // startBackgroundTask must be called on the main thread. dispatch_async(dispatch_get_main_queue(), ^{ // We call `startBackgroundTask` here to prevent our app from suspending while being backgrounded // until the operation is completed - at which point the OWSSendMessageOperation ends it's background task. [sendMessageOperation startBackgroundTask]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message]; [sendingQueue addOperation:sendMessageOperation]; }); }); }); } - (void)attemptToSendMessage:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(RetryableFailureHandler)failureHandler { [self ensureAnyAttachmentsUploaded:message success:^() { [self deliverMessage:message success:successHandler failure:^(NSError *error) { DDLogDebug(@"%@ Message send attempt failed: %@", self.tag, message.debugDescription); failureHandler(error); }]; } failure:^(NSError *error) { DDLogDebug(@"%@ Attachment upload attempt failed: %@", self.tag, message.debugDescription); failureHandler(error); }]; } - (void)ensureAnyAttachmentsUploaded:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(RetryableFailureHandler)failureHandler { if (!message.hasAttachments) { return successHandler(); } TSAttachmentStream *attachmentStream = [TSAttachmentStream fetchObjectWithUniqueID:message.attachmentIds.firstObject]; if (!attachmentStream) { OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotLoadAttachment]); NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); // Not finding local attachment is a terminal failure. [error setIsRetryable:NO]; return failureHandler(error); } [self.uploadingService uploadAttachmentStream:attachmentStream message:message success:successHandler failure:failureHandler]; } - (void)sendTemporaryAttachmentData:(DataSource *)dataSource contentType:(NSString *)contentType inMessage:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(void (^)(NSError *error))failureHandler { OWSAssert(dataSource); void (^successWithDeleteHandler)() = ^() { successHandler(); DDLogDebug(@"Removing temporary attachment message."); [message remove]; }; void (^failureWithDeleteHandler)(NSError *error) = ^(NSError *error) { failureHandler(error); DDLogDebug(@"Removing temporary attachment message."); [message remove]; }; [self sendAttachmentData:dataSource contentType:contentType sourceFilename:nil inMessage:message success:successWithDeleteHandler failure:failureWithDeleteHandler]; } - (void)sendAttachmentData:(DataSource *)dataSource contentType:(NSString *)contentType sourceFilename:(nullable NSString *)sourceFilename inMessage:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(void (^)(NSError *error))failureHandler { OWSAssert(dataSource); dispatch_async([OWSDispatch attachmentsQueue], ^{ TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:contentType byteCount:(UInt32)dataSource.dataLength sourceFilename:sourceFilename]; if (message.isVoiceMessage) { attachmentStream.attachmentType = TSAttachmentTypeVoiceMessage; } if (![attachmentStream writeDataSource:dataSource]) { OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotWriteAttachment]); NSError *error = OWSErrorMakeWriteAttachmentDataError(); return failureHandler(error); } [attachmentStream save]; [message.attachmentIds addObject:attachmentStream.uniqueId]; if (sourceFilename) { message.attachmentFilenameMap[attachmentStream.uniqueId] = sourceFilename; } [message save]; [self sendMessage:message success:successHandler failure:failureHandler]; }); } - (NSArray *)getRecipients:(NSArray *)identifiers error:(NSError **)error { NSMutableArray *recipients = [NSMutableArray new]; for (NSString *recipientId in identifiers) { SignalRecipient *existingRecipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientId]; if (existingRecipient) { [recipients addObject:existingRecipient]; } else { SignalRecipient *newRecipient = [self.contactsUpdater synchronousLookup:recipientId error:error]; if (newRecipient) { [recipients addObject:newRecipient]; } } } if (recipients.count == 0 && !*error) { // error should be set in contactsUpater, but just in case. OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotFindContacts1]); *error = OWSErrorMakeFailedToSendOutgoingMessageError(); } return [recipients copy]; } - (void)deliverMessage:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(RetryableFailureHandler)failureHandler { dispatch_async([OWSDispatch sendingQueue], ^{ TSThread *thread = message.thread; if ([thread isKindOfClass:[TSGroupThread class]]) { TSGroupThread *gThread = (TSGroupThread *)thread; NSError *error; NSArray *recipients = [self getRecipients:gThread.groupModel.groupMemberIds error:&error]; if (recipients.count == 0) { if (!error) { OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotFindContacts2]); error = OWSErrorMakeFailedToSendOutgoingMessageError(); } // If no recipients were found, there's no reason to retry. It will just fail again. [error setIsRetryable:NO]; failureHandler(error); return; } [self groupSend:recipients message:message thread:gThread success:successHandler failure:failureHandler]; } else if ([thread isKindOfClass:[TSContactThread class]] || [message isKindOfClass:[OWSOutgoingSyncMessage class]]) { TSContactThread *contactThread = (TSContactThread *)thread; if ([contactThread.contactIdentifier isEqualToString:[TSAccountManager localNumber]] && ![message isKindOfClass:[OWSOutgoingSyncMessage class]]) { [self handleSendToMyself:message]; successHandler(); return; } NSString *recipientContactId = [message isKindOfClass:[OWSOutgoingSyncMessage class]] ? [TSAccountManager localNumber] : contactThread.contactIdentifier; // If we block a user, don't send 1:1 messages to them. The UI // should prevent this from occurring, but in some edge cases // you might, for example, have a pending outgoing message when // you block them. OWSAssert(recipientContactId.length > 0); if ([_blockingManager isRecipientIdBlocked:recipientContactId]) { DDLogInfo(@"%@ skipping 1:1 send to blocked contact: %@", self.tag, recipientContactId); NSError *error = OWSErrorMakeMessageSendFailedToBlockListError(); // No need to retry - the user will continue to be blocked. [error setIsRetryable:NO]; failureHandler(error); return; } SignalRecipient *recipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientContactId]; if (!recipient) { NSError *error; // possibly returns nil. recipient = [self.contactsUpdater synchronousLookup:recipientContactId error:&error]; if (error) { if (error.code == OWSErrorCodeNoSuchSignalRecipient) { DDLogWarn(@"%@ recipient contact not found", self.tag); [self unregisteredRecipient:recipient message:message thread:thread]; } OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotFindContacts3]); // No need to repeat trying to find a failure. Apart from repeatedly failing, it would also cause us // to print redundant error messages. [error setIsRetryable:NO]; failureHandler(error); return; } } if (!recipient) { NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); DDLogWarn(@"recipient contact still not found after attempting lookup."); // No need to repeat trying to find a failure. Apart from repeatedly failing, it would also cause us to // print redundant error messages. [error setIsRetryable:NO]; failureHandler(error); return; } [self sendMessage:message recipient:recipient thread:thread attempts:OWSMessageSenderRetryAttempts success:successHandler failure:failureHandler]; } else { // Neither a group nor contact thread? This should never happen. OWSFail(@"%@ Unknown message type: %@", self.tag, NSStringFromClass([message class])); NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); [error setIsRetryable:NO]; failureHandler(error); } }); } // For group sends, we're using chained futures to make the code more readable. - (TOCFuture *)sendMessageFuture:(TSOutgoingMessage *)message recipient:(SignalRecipient *)recipient thread:(TSThread *)thread { TOCFutureSource *futureSource = [[TOCFutureSource alloc] init]; [self sendMessage:message recipient:recipient thread:thread attempts:OWSMessageSenderRetryAttempts success:^{ DDLogInfo(@"%@ Marking group message as sent to recipient: %@", self.tag, recipient.uniqueId); [message updateWithSentRecipient:recipient.uniqueId]; [futureSource trySetResult:@1]; } failure:^(NSError *error) { [futureSource trySetFailure:error]; }]; return futureSource.future; } - (void)groupSend:(NSArray *)recipients message:(TSOutgoingMessage *)message thread:(TSThread *)thread success:(void (^)())successHandler failure:(RetryableFailureHandler)failureHandler { [self saveGroupMessage:message inThread:thread]; NSMutableArray *futures = [NSMutableArray array]; for (SignalRecipient *recipient in recipients) { NSString *recipientId = recipient.recipientId; // We don't need to send the message to ourselves... if ([recipientId isEqualToString:[TSAccountManager localNumber]]) { continue; } // We don't need to sent the message to all group members if // it has a "single group recipient". if (message.singleGroupRecipient && ![message.singleGroupRecipient isEqualToString:recipientId]) { continue; } if ([message wasSentToRecipient:recipientId]) { // Skip recipients we have already sent this message to (on an // earlier retry, perhaps). DDLogInfo(@"%@ Skipping group message recipient; already sent: %@", self.tag, recipient.uniqueId); continue; } // ...otherwise we send. [futures addObject:[self sendMessageFuture:message recipient:recipient thread:thread]]; } TOCFuture *completionFuture = futures.toc_thenAll; [completionFuture thenDo:^(id value) { successHandler(); }]; [completionFuture catchDo:^(id failure) { // failure from toc_thenAll yields an array of failed Futures, rather than the future's failure. NSError *firstRetryableError = nil; NSError *firstNonRetryableError = nil; if ([failure isKindOfClass:[NSArray class]]) { NSArray *groupSendFutures = (NSArray *)failure; for (TOCFuture *groupSendFuture in groupSendFutures) { if (groupSendFuture.hasFailed) { id failureResult = groupSendFuture.forceGetFailure; if ([failureResult isKindOfClass:[NSError class]]) { NSError *error = failureResult; // Some errors should be ignored when sending messages // to groups. See discussion on // NSError (OWSMessageSender) category. if ([error shouldBeIgnoredForGroups]) { continue; } // Some errors should never be retried, in order to avoid // hitting rate limits, for example. Unfortunately, since // group send retry is all-or-nothing, we need to fail // immediately even if some of the other recipients had // retryable errors. if ([error isFatal]) { failureHandler(error); return; } if ([error isRetryable] && !firstRetryableError) { firstRetryableError = error; } else if (![error isRetryable] && !firstNonRetryableError) { firstNonRetryableError = error; } } } } } // If any of the group send errors are retryable, we want to retry. // Therefore, prefer to propagate a retryable error. if (firstRetryableError) { return failureHandler(firstRetryableError); } else if (firstNonRetryableError) { return failureHandler(firstNonRetryableError); } else { // If we only received errors that we should ignore, // consider this send a success, unless the message could // not be sent to any recipient. if (message.sentRecipientsCount == 0) { NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients, NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS", @"Error indicating that an outgoing message had no valid recipients.")); [error setIsRetryable:NO]; failureHandler(error); } else { successHandler(); } } }]; } - (void)unregisteredRecipient:(SignalRecipient *)recipient message:(TSOutgoingMessage *)message thread:(TSThread *)thread { [self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [recipient removeWithTransaction:transaction]; [[TSInfoMessage userNotRegisteredMessageInThread:thread] saveWithTransaction:transaction]; }]; } - (void)sendMessage:(TSOutgoingMessage *)message recipient:(SignalRecipient *)recipient thread:(TSThread *)thread attempts:(int)remainingAttempts success:(void (^)())successHandler failure:(RetryableFailureHandler)failureHandler { DDLogInfo(@"%@ attempting to send message: %@, timestamp: %llu, recipient: %@", self.tag, message.class, message.timestamp, recipient.uniqueId); AssertIsOnSendingQueue(); if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) { OWSProdError([OWSAnalyticsEvents messageSendErrorFailedDueToPrekeyUpdateFailures]); // Retry prekey update every time user tries to send a message while app // is disabled due to prekey update failures. // // Only try to update the signed prekey; updating it is sufficient to // re-enable message sending. [TSPreKeyManager registerPreKeysWithMode:RefreshPreKeysMode_SignedOnly success:^{ DDLogInfo(@"%@ New prekeys registered with server.", self.tag); } failure:^(NSError *error) { DDLogWarn(@"%@ Failed to update prekeys with the server: %@", self.tag, error); }]; NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(); [error setIsRetryable:YES]; return failureHandler(error); } if (remainingAttempts <= 0) { // We should always fail with a specific error. OWSProdFail([OWSAnalyticsEvents messageSenderErrorGenericSendFailure]); NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); [error setIsRetryable:YES]; return failureHandler(error); } remainingAttempts -= 1; NSArray *deviceMessages; @try { deviceMessages = [self deviceMessages:message forRecipient:recipient]; } @catch (NSException *exception) { deviceMessages = @[]; if ([exception.name isEqualToString:UntrustedIdentityKeyException]) { // This *can* happen under normal usage, but it should happen relatively rarely. // We expect it to happen whenever Bob reinstalls, and Alice messages Bob before // she can pull down his latest identity. // If it's happening a lot, we should rethink our profile fetching strategy. OWSProdInfo([OWSAnalyticsEvents messageSendErrorFailedDueToUntrustedKey]); NSString *localizedErrorDescriptionFormat = NSLocalizedString(@"FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_KEY", @"action sheet header when re-sending message which failed because of untrusted identity keys"); NSString *localizedErrorDescription = [NSString stringWithFormat:localizedErrorDescriptionFormat, [self.contactsManager displayNameForPhoneIdentifier:recipient.recipientId]]; NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeUntrustedIdentityKey, localizedErrorDescription); // Key will continue to be unaccepted, so no need to retry. It'll only cause us to hit the Pre-Key request // rate limit [error setIsRetryable:NO]; // Avoid the "Too many failures with this contact" error rate limiting. [error setIsFatal:YES]; PreKeyBundle *newKeyBundle = exception.userInfo[TSInvalidPreKeyBundleKey]; if (![newKeyBundle isKindOfClass:[PreKeyBundle class]]) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorUnexpectedKeyBundle]); failureHandler(error); return; } NSData *newIdentityKeyWithVersion = newKeyBundle.identityKey; if (![newIdentityKeyWithVersion isKindOfClass:[NSData class]]) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyType]); failureHandler(error); return; } // TODO migrate to storing the full 33 byte representation of the identity key. if (newIdentityKeyWithVersion.length != kIdentityKeyLength) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyLength]); failureHandler(error); return; } NSData *newIdentityKey = [newIdentityKeyWithVersion removeKeyType]; [[OWSIdentityManager sharedManager] saveRemoteIdentity:newIdentityKey recipientId:recipient.recipientId]; failureHandler(error); return; } if ([exception.name isEqualToString:OWSMessageSenderRateLimitedException]) { NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceRateLimited, NSLocalizedString(@"FAILED_SENDING_BECAUSE_RATE_LIMIT", @"action sheet header when re-sending message which failed because of too many attempts")); // We're already rate-limited. No need to exacerbate the problem. [error setIsRetryable:NO]; // Avoid exacerbating the rate limiting. [error setIsFatal:YES]; return failureHandler(error); } if (remainingAttempts == 0) { DDLogWarn( @"%@ Terminal failure to build any device messages. Giving up with exception:%@", self.tag, exception); NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); // Since we've already repeatedly failed to build messages, it's unlikely that repeating the whole process // will succeed. [error setIsRetryable:NO]; return failureHandler(error); } } NSString *localNumber = [TSAccountManager localNumber]; if ([localNumber isEqualToString:recipient.uniqueId]) { __block BOOL hasSecondaryDevices; [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { hasSecondaryDevices = [OWSDevice hasSecondaryDevicesWithTransaction:transaction]; }]; BOOL hasDeviceMessages = deviceMessages.count > 0; if (!hasSecondaryDevices && !hasDeviceMessages) { DDLogInfo(@"%@ Ignoring sync message without secondary devices: %@", self.tag, [message class]); OWSAssert([message isKindOfClass:[OWSOutgoingSyncMessage class]]); dispatch_async([OWSDispatch sendingQueue], ^{ // This emulates the completion logic of an actual successful save (see below). [recipient save]; successHandler(); }); return; } else if (hasSecondaryDevices) { // We may have just linked a new secondary device which is not yet reflected in // the SignalRecipient that corresponds to ourself. Proceed. Client should learn // of new secondary devices when this message send fails. DDLogWarn(@"%@ sync message has no device messages but account has secondary devices.", self.tag); } else if (hasDeviceMessages) { OWSFail(@"%@ sync message has device messages for unknown secondary devices.", self.tag); } else { // Account has secondary devices; proceed as usual. } } else { OWSAssert(deviceMessages.count > 0); } TSSubmitMessageRequest *request = [[TSSubmitMessageRequest alloc] initWithRecipient:recipient.uniqueId messages:deviceMessages relay:recipient.relay timeStamp:message.timestamp]; [self.networkManager makeRequest:request success:^(NSURLSessionDataTask *task, id responseObject) { dispatch_async([OWSDispatch sendingQueue], ^{ [recipient save]; [self handleMessageSentLocally:message]; successHandler(); }); } failure:^(NSURLSessionDataTask *task, NSError *error) { DDLogInfo(@"%@ sending to recipient: %@, failed with error: %@", self.tag, recipient.uniqueId, error); [DDLog flushLog]; NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; long statuscode = response.statusCode; NSData *responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; void (^retrySend)() = ^void() { if (remainingAttempts <= 0) { // Since we've already repeatedly failed to send to the messaging API, // it's unlikely that repeating the whole process will succeed. [error setIsRetryable:NO]; return failureHandler(error); } dispatch_async([OWSDispatch sendingQueue], ^{ DDLogDebug(@"%@ Retrying: %@", self.tag, message.debugDescription); [self sendMessage:message recipient:recipient thread:thread attempts:remainingAttempts success:successHandler failure:failureHandler]; }); }; switch (statuscode) { case 401: { DDLogWarn(@"%@ Unable to send due to invalid credentials. Did the user's client get de-authed by registering elsewhere?", self.tag); NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceFailure, NSLocalizedString(@"ERROR_DESCRIPTION_SENDING_UNAUTHORIZED", @"Error message when attempting to send message")); // No need to retry if we've been de-authed. [error setIsRetryable:NO]; return failureHandler(error); } case 404: { DDLogWarn(@"%@ Unregistered recipient: %@", self.tag, recipient.uniqueId); [self unregisteredRecipient:recipient message:message thread:thread]; NSError *error = OWSErrorMakeNoSuchSignalRecipientError(); // No need to retry if the recipient is not registered. [error setIsRetryable:NO]; // If one member of a group deletes their account, // the group should ignore errors when trying to send // messages to this ex-member. [error setShouldBeIgnoredForGroups:YES]; return failureHandler(error); } case 409: { // Mismatched devices DDLogWarn(@"%@ Mismatch Devices for recipient: %@", self.tag, recipient.uniqueId); NSError *error; NSDictionary *serializedResponse = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error]; if (error) { OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotParseMismatchedDevicesJson]); [error setIsRetryable:YES]; return failureHandler(error); } [self handleMismatchedDevices:serializedResponse recipient:recipient completion:retrySend]; break; } case 410: { // Stale devices DDLogWarn(@"%@ Stale devices for recipient: %@", self.tag, recipient.uniqueId); if (!responseData) { DDLogWarn(@"Stale devices but server didn't specify devices in response."); NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); [error setIsRetryable:YES]; return failureHandler(error); } [self handleStaleDevicesWithResponse:responseData recipientId:recipient.uniqueId completion:retrySend]; break; } default: retrySend(); break; } }]; } - (void)handleMismatchedDevices:(NSDictionary *)dictionary recipient:(SignalRecipient *)recipient completion:(void (^)())completionHandler { NSArray *extraDevices = [dictionary objectForKey:@"extraDevices"]; NSArray *missingDevices = [dictionary objectForKey:@"missingDevices"]; dispatch_async([OWSDispatch sessionStoreQueue], ^{ if (extraDevices.count < 1 && missingDevices.count < 1) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorNoMissingOrExtraDevices]); } if (extraDevices && extraDevices.count > 0) { DDLogInfo(@"%@ removing extra devices: %@", self.tag, extraDevices); for (NSNumber *extraDeviceId in extraDevices) { [self.storageManager deleteSessionForContact:recipient.uniqueId deviceId:extraDeviceId.intValue]; } [recipient removeDevices:[NSSet setWithArray:extraDevices]]; } if (missingDevices && missingDevices.count > 0) { DDLogInfo(@"%@ Adding missing devices: %@", self.tag, missingDevices); [recipient addDevices:[NSSet setWithArray:missingDevices]]; } [recipient save]; completionHandler(); }); } - (void)handleMessageSentLocally:(TSOutgoingMessage *)message { if (message.shouldSyncTranscript) { // TODO: I suspect we shouldn't optimistically set hasSyncedTranscript. // We could set this in a success handler for [sendSyncTranscriptForMessage:]. [message updateWithHasSyncedTranscript:YES]; [self sendSyncTranscriptForMessage:message]; } [OWSDisappearingMessagesJob setExpirationForMessage:message]; } - (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSOutgoingMessage *)outgoingMessage { [OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage contactsManager:self.contactsManager]; } - (void)handleSendToMyself:(TSOutgoingMessage *)outgoingMessage { [self handleMessageSentLocally:outgoingMessage]; if (!(outgoingMessage.body || outgoingMessage.hasAttachments)) { DDLogDebug( @"%@ Refusing to make incoming copy of non-standard message sent to self:%@", self.tag, outgoingMessage); return; } // Getting the local number uses a transaction, so we need to do that before we // create a new transaction to avoid deadlock. NSString *contactId = [TSAccountManager localNumber]; [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { TSContactThread *cThread = [TSContactThread getOrCreateThreadWithContactId:contactId transaction:transaction]; [cThread saveWithTransaction:transaction]; // We want the incoming message to appear after the outgoing message. TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:(outgoingMessage.timestamp + 1) inThread:cThread authorId:[cThread contactIdentifier] sourceDeviceId:[OWSDevice currentDeviceId] messageBody:outgoingMessage.body attachmentIds:outgoingMessage.attachmentIds expiresInSeconds:outgoingMessage.expiresInSeconds]; [incomingMessage saveWithTransaction:transaction]; }]; } - (void)sendSyncTranscriptForMessage:(TSOutgoingMessage *)message { OWSOutgoingSentMessageTranscript *sentMessageTranscript = [[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message]; [self sendMessage:sentMessageTranscript recipient:[SignalRecipient selfRecipient] thread:message.thread attempts:OWSMessageSenderRetryAttempts success:^{ DDLogInfo(@"Succesfully sent sync transcript."); } failure:^(NSError *error) { // FIXME: We don't yet honor the isRetryable flag here, since sendSyncTranscriptForMessage // isn't yet wrapped in our retryable SendMessageOperation. Addressing this would require // a refactor to the MessageSender. Note that we *do* however continue to respect the // OWSMessageSenderRetryAttempts, which is an "inner" retry loop, encompassing only the // messaging API. DDLogInfo(@"Failed to send sync transcript: %@ (isRetryable: %d)", error, [error isRetryable]); }]; } - (NSArray *)deviceMessages:(TSOutgoingMessage *)message forRecipient:(SignalRecipient *)recipient { OWSAssert(message); OWSAssert(recipient); NSMutableArray *messagesArray = [NSMutableArray arrayWithCapacity:recipient.devices.count]; NSData *plainText = [message buildPlainTextData:recipient]; DDLogDebug(@"%@ built message: %@ plainTextData.length: %lu", self.tag, [message class], (unsigned long)plainText.length); for (NSNumber *deviceNumber in recipient.devices) { @try { __block NSDictionary *messageDict; __block NSException *encryptionException; // Mutating session state is not thread safe, so we operate on a serial queue, shared with decryption // operations. dispatch_sync([OWSDispatch sessionStoreQueue], ^{ @try { messageDict = [self encryptedMessageWithPlaintext:plainText toRecipient:recipient.uniqueId deviceId:deviceNumber keyingStorage:self.storageManager isSilent:message.isSilent]; } @catch (NSException *exception) { encryptionException = exception; } }); if (encryptionException) { DDLogInfo(@"%@ Exception during encryption: %@", self.tag, encryptionException); @throw encryptionException; } if (messageDict) { [messagesArray addObject:messageDict]; } else { @throw [NSException exceptionWithName:InvalidMessageException reason:@"Failed to encrypt message" userInfo:nil]; } } @catch (NSException *exception) { if ([exception.name isEqualToString:OWSMessageSenderInvalidDeviceException]) { [recipient removeDevices:[NSSet setWithObject:deviceNumber]]; } else { @throw exception; } } } return [messagesArray copy]; } - (NSDictionary *)encryptedMessageWithPlaintext:(NSData *)plainText toRecipient:(NSString *)identifier deviceId:(NSNumber *)deviceNumber keyingStorage:(TSStorageManager *)storage isSilent:(BOOL)isSilent { OWSAssert(plainText); OWSAssert(identifier.length > 0); OWSAssert(deviceNumber); OWSAssert(storage); if (![storage containsSession:identifier deviceId:[deviceNumber intValue]]) { __block dispatch_semaphore_t sema = dispatch_semaphore_create(0); __block PreKeyBundle *_Nullable bundle; __block NSException *_Nullable exception; [self.networkManager makeRequest:[[TSRecipientPrekeyRequest alloc] initWithRecipient:identifier deviceId:[deviceNumber stringValue]] success:^(NSURLSessionDataTask *task, id responseObject) { bundle = [PreKeyBundle preKeyBundleFromDictionary:responseObject forDeviceNumber:deviceNumber]; dispatch_semaphore_signal(sema); } failure:^(NSURLSessionDataTask *task, NSError *error) { if (!IsNSErrorNetworkFailure(error)) { OWSProdError([OWSAnalyticsEvents messageSenderErrorRecipientPrekeyRequestFailed]); } DDLogError(@"Server replied to PreKeyBundle request with error: %@", error); NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; if (response.statusCode == 404) { // Can't throw exception from within callback as it's probabably a different thread. exception = [NSException exceptionWithName:OWSMessageSenderInvalidDeviceException reason:@"Device not registered" userInfo:nil]; } else if (response.statusCode == 413) { // Can't throw exception from within callback as it's probabably a different thread. exception = [NSException exceptionWithName:OWSMessageSenderRateLimitedException reason:@"Too many prekey requests" userInfo:nil]; } dispatch_semaphore_signal(sema); }]; dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); if (exception) { @throw exception; } if (!bundle) { @throw [NSException exceptionWithName:InvalidVersionException reason:@"Can't get a prekey bundle from the server with required information" userInfo:nil]; } else { SessionBuilder *builder = [[SessionBuilder alloc] initWithSessionStore:storage preKeyStore:storage signedPreKeyStore:storage identityKeyStore:[OWSIdentityManager sharedManager] recipientId:identifier deviceId:[deviceNumber intValue]]; @try { // Mutating session state is not thread safe. @synchronized(self) { [builder processPrekeyBundle:bundle]; } } @catch (NSException *exception) { if ([exception.name isEqualToString:UntrustedIdentityKeyException]) { @throw [NSException exceptionWithName:UntrustedIdentityKeyException reason:nil userInfo:@{ TSInvalidPreKeyBundleKey : bundle, TSInvalidRecipientKey : identifier }]; } @throw exception; } } } SessionCipher *cipher = [[SessionCipher alloc] initWithSessionStore:storage preKeyStore:storage signedPreKeyStore:storage identityKeyStore:[OWSIdentityManager sharedManager] recipientId:identifier deviceId:[deviceNumber intValue]]; id encryptedMessage = [cipher encryptMessage:[plainText paddedMessageBody]]; NSData *serializedMessage = encryptedMessage.serialized; TSWhisperMessageType messageType = [self messageTypeForCipherMessage:encryptedMessage]; OWSMessageServiceParams *messageParams = [[OWSMessageServiceParams alloc] initWithType:messageType recipientId:identifier device:[deviceNumber intValue] content:serializedMessage isSilent:isSilent registrationId:cipher.remoteRegistrationId]; NSError *error; NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:messageParams error:&error]; if (error) { OWSProdError([OWSAnalyticsEvents messageSendErrorCouldNotSerializeMessageJson]); return nil; } return jsonDict; } - (TSWhisperMessageType)messageTypeForCipherMessage:(id)cipherMessage { if ([cipherMessage isKindOfClass:[PreKeyWhisperMessage class]]) { return TSPreKeyWhisperMessageType; } else if ([cipherMessage isKindOfClass:[WhisperMessage class]]) { return TSEncryptedWhisperMessageType; } return TSUnknownMessageType; } - (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread { if (message.groupMetaMessage == TSGroupMessageDeliver) { // TODO: Why is this necessary? [message save]; } else if (message.groupMetaMessage == TSGroupMessageQuit) { [[[TSInfoMessage alloc] initWithTimestamp:message.timestamp inThread:thread messageType:TSInfoMessageTypeGroupQuit customMessage:message.customMessage] save]; } else { [[[TSInfoMessage alloc] initWithTimestamp:message.timestamp inThread:thread messageType:TSInfoMessageTypeGroupUpdate customMessage:message.customMessage] save]; } } // Called when the server indicates that the devices no longer exist - e.g. when the remote recipient has reinstalled. - (void)handleStaleDevicesWithResponse:(NSData *)responseData recipientId:(NSString *)identifier completion:(void (^)())completionHandler { dispatch_async([OWSDispatch sendingQueue], ^{ NSDictionary *serialization = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil]; NSArray *devices = serialization[@"staleDevices"]; if (!([devices count] > 0)) { return; } dispatch_async([OWSDispatch sessionStoreQueue], ^{ for (NSUInteger i = 0; i < [devices count]; i++) { int deviceNumber = [devices[i] intValue]; [[TSStorageManager sharedManager] deleteSessionForContact:identifier deviceId:deviceNumber]; } completionHandler(); }); }); } #pragma mark - Logging + (NSString *)tag { return [NSString stringWithFormat:@"[%@]", self.class]; } - (NSString *)tag { return self.class.tag; } @end NS_ASSUME_NONNULL_END