diff --git a/SignalServiceKit/src/Messages/OWSMessageSend.swift b/SignalServiceKit/src/Messages/OWSMessageSend.swift new file mode 100644 index 000000000..e9d907f9a --- /dev/null +++ b/SignalServiceKit/src/Messages/OWSMessageSend.swift @@ -0,0 +1,66 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SignalMetadataKit + +@objc +public class OWSMessageSend: NSObject { + @objc + public let message: TSOutgoingMessage + + // thread may be nil if message is an OWSOutgoingSyncMessage. + @objc + public let thread: TSThread? + + @objc + public let recipient: SignalRecipient + + // TODO: Should this be per-recipient or per-message? + private static let kMaxRetriesPerRecipient: Int = 3 + + @objc + public var remainingAttempts = OWSMessageSend.kMaxRetriesPerRecipient + + // We "fail over" to REST sends after _any_ error sending + // via the web socket. + @objc + public var useWebsocketIfAvailable = true + + // We "fail over" to non-UD sends after certain errors sending + // via UD. + @objc + public var canUseUD = true + + @objc + public let udAccessKey: SMKUDAccessKey? + + @objc + public let localNumber: String + + @objc + public let isLocalNumber: Bool + + @objc + public init(message: TSOutgoingMessage, + thread: TSThread?, + recipient: SignalRecipient, udManager: OWSUDManager, + localNumber: String) { + self.message = message + self.thread = thread + self.recipient = recipient + + var udAccessKey: SMKUDAccessKey? + var isLocalNumber: Bool = false + if let recipientId = recipient.uniqueId { + udAccessKey = udManager.udAccessKeyForRecipient(recipientId) + isLocalNumber = localNumber == recipientId + } else { + owsFailDebug("SignalRecipient missing recipientId") + } + self.udAccessKey = udAccessKey + self.localNumber = localNumber + self.isLocalNumber = isLocalNumber + } +} diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 6b2d42daa..0f6b2ee13 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -39,6 +39,7 @@ #import "TSOutgoingMessage.h" #import "TSPreKeyManager.h" #import "TSQuotedMessage.h" +#import "TSRequest.h" #import "TSSocketManager.h" #import "TSThread.h" #import @@ -47,8 +48,10 @@ #import #import #import +#import #import #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -195,7 +198,8 @@ void AssertIsOnSendingQueue() @end -int const OWSMessageSenderRetryAttempts = 3; +#pragma mark - + NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException"; NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; @@ -207,6 +211,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; @end +#pragma mark - + @implementation OWSMessageSender - (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage @@ -225,6 +231,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; return self; } +#pragma mark -Dependencies + - (id)contactsManager { OWSAssertDebug(SSKEnvironment.shared.contactsManager); @@ -246,6 +254,20 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; return SSKEnvironment.shared.networkManager; } +- (id)udManager +{ + OWSAssertDebug(SSKEnvironment.shared.udManager); + + return SSKEnvironment.shared.udManager; +} + +- (TSAccountManager *)tsAccountManager +{ + return TSAccountManager.sharedInstance; +} + +#pragma mark - + - (NSOperationQueue *)sendingQueueForMessage:(TSOutgoingMessage *)message { OWSAssertDebug(message); @@ -435,23 +457,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; }); } -- (NSArray *)signalRecipientsForRecipientIds:(NSArray *)recipientIds - message:(TSOutgoingMessage *)message -{ - OWSAssertDebug(recipientIds); - OWSAssertDebug(message); - - NSMutableArray *recipients = [NSMutableArray new]; - [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - for (NSString *recipientId in recipientIds) { - SignalRecipient *recipient = - [SignalRecipient getOrBuildUnsavedRecipientForRecipientId:recipientId transaction:transaction]; - [recipients addObject:recipient]; - } - }]; - return recipients; -} - - (void)sendMessageToService:(TSOutgoingMessage *)message success:(void (^)(void))successHandler failure:(RetryableFailureHandler)failureHandler @@ -459,7 +464,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; dispatch_async([OWSDispatch sendingQueue], ^{ TSThread *_Nullable thread = message.thread; - // TODO: It would be nice to combine the "contact" and "group" send logic here. + // In the "self-send" special case, we ony need to send a sync message with a delivery receipt. if ([thread isKindOfClass:[TSContactThread class]] && [((TSContactThread *)thread).contactIdentifier isEqualToString:[TSAccountManager localNumber]]) { // Send to self. @@ -476,8 +481,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; successHandler(); return; - } else if ([thread isKindOfClass:[TSGroupThread class]]) { + } + NSMutableSet *recipientIds = [NSMutableSet new]; + if (thread.isGroupThread) { TSGroupThread *gThread = (TSGroupThread *)thread; // Send to the intersection of: @@ -491,48 +498,15 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // * The recipient is still in the group. // * The recipient is in the "sending" state. - NSMutableSet *sendingRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds]; - [sendingRecipientIds intersectSet:[NSSet setWithArray:gThread.groupModel.groupMemberIds]]; - [sendingRecipientIds minusSet:[NSSet setWithArray:self.blockingManager.blockedPhoneNumbers]]; - - // Mark skipped recipients as such. We skip because: - // - // * Recipient is no longer in the group. - // * Recipient is blocked. - // - // Elsewhere, we skip recipient if their Signal account has been deactivated. - NSMutableSet *obsoleteRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds]; - [obsoleteRecipientIds minusSet:sendingRecipientIds]; - if (obsoleteRecipientIds.count > 0) { - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - for (NSString *recipientId in obsoleteRecipientIds) { - // Mark this recipient as "skipped". - [message updateWithSkippedRecipient:recipientId transaction:transaction]; - } - }]; - } - - if (sendingRecipientIds.count < 1) { - // All recipients are already sent or can be skipped. - successHandler(); - return; - } - - NSArray *recipients = - [self signalRecipientsForRecipientIds:sendingRecipientIds.allObjects message:message]; - OWSAssertDebug(recipients.count == sendingRecipientIds.count); - - [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; - - NSString *recipientContactId - = ([message isKindOfClass:[OWSOutgoingSyncMessage class]] ? [TSAccountManager localNumber] - : contactThread.contactIdentifier); + [recipientIds addObjectsFromArray:message.sendingRecipientIds]; + // Only send to members in the latest known group member list. + [recipientIds intersectSet:[NSSet setWithArray:gThread.groupModel.groupMemberIds]]; + } else if ([message isKindOfClass:[OWSOutgoingSyncMessage class]]) { + [recipientIds addObject:[TSAccountManager localNumber]]; + } else if ([thread isKindOfClass:[TSContactThread class]]) { + NSString *recipientContactId = ((TSContactThread *)thread).contactIdentifier; + // Treat 1:1 sends to blocked contacts as failures. // 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 @@ -547,28 +521,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; return; } - NSArray *recipients = - [self signalRecipientsForRecipientIds:@[recipientContactId] message:message]; - OWSAssertDebug(recipients.count == 1); - SignalRecipient *recipient = recipients.firstObject; - - if (!recipient) { - NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); - OWSLogWarn(@"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 sendMessageToService:message - recipient:recipient - thread:thread - attempts:OWSMessageSenderRetryAttempts - useWebsocketIfAvailable:YES - success:successHandler - failure:failureHandler]; + [recipientIds addObject:recipientContactId]; } else { // Neither a group nor contact thread? This should never happen. OWSFailDebug(@"Unknown message type: %@", [message class]); @@ -577,37 +530,76 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [error setIsRetryable:NO]; failureHandler(error); } + + [recipientIds minusSet:[NSSet setWithArray:self.blockingManager.blockedPhoneNumbers]]; + + if ([recipientIds containsObject:TSAccountManager.localNumber]) { + OWSFailDebug(@"Message send recipients should not include self."); + } + [recipientIds removeObject:TSAccountManager.localNumber]; + + // Mark skipped recipients as such. We skip because: + // + // * Recipient is no longer in the group. + // * Recipient is blocked. + // + // Elsewhere, we skip recipient if their Signal account has been deactivated. + NSMutableSet *obsoleteRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds]; + [obsoleteRecipientIds minusSet:recipientIds]; + if (obsoleteRecipientIds.count > 0) { + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (NSString *recipientId in obsoleteRecipientIds) { + // Mark this recipient as "skipped". + [message updateWithSkippedRecipient:recipientId transaction:transaction]; + } + }]; + } + + if (recipientIds.count < 1) { + // All recipients are already sent or can be skipped. + successHandler(); + return; + } + + NSMutableArray *messageSends = [NSMutableArray new]; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + for (NSString *recipientId in recipientIds) { + SignalRecipient *recipient = + [SignalRecipient getOrBuildUnsavedRecipientForRecipientId:recipientId transaction:transaction]; + OWSMessageSend *messageSend = + [[OWSMessageSend alloc] initWithMessage:message + thread:thread + recipient:recipient + udManager:self.udManager + localNumber:self.tsAccountManager.localNumber]; + [messageSends addObject:messageSend]; + } + }]; + OWSAssertDebug(messageSends.count == recipientIds.count); + OWSAssertDebug(messageSends.count > 0); + + [self sendWithMessageSends:messageSends + isGroupSend:thread.isGroupThread + success:successHandler + failure:failureHandler]; }); } -- (void)groupSend:(NSArray *)recipients - message:(TSOutgoingMessage *)message - thread:(TSThread *)thread - success:(void (^)(void))successHandler - failure:(RetryableFailureHandler)failureHandler +- (void)sendWithMessageSends:(NSArray *)messageSends + isGroupSend:(BOOL)isGroupSend + success:(void (^)(void))successHandler + failure:(RetryableFailureHandler)failureHandler { - [self saveGroupMessage:message inThread:thread]; + OWSAssertDebug(messageSends.count > 0); + AssertIsOnSendingQueue(); NSMutableArray *sendPromises = [NSMutableArray array]; NSMutableArray *sendErrors = [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; - } - - // ...otherwise we send. - + for (OWSMessageSend *messageSend in messageSends) { // For group sends, we're using chained promises to make the code more readable. AnyPromise *sendPromise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { - [self sendMessageToService:message - recipient:recipient - thread:thread - attempts:OWSMessageSenderRetryAttempts - useWebsocketIfAvailable:YES + [self sendMessageToRecipient:messageSend success:^{ // The value doesn't matter, we just need any non-NSError value. resolve(@(1)); @@ -643,7 +635,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // Some errors should be ignored when sending messages // to groups. See discussion on // NSError (OWSMessageSender) category. - if ([error shouldBeIgnoredForGroups]) { + if (isGroupSend && [error shouldBeIgnoredForGroups]) { continue; } @@ -664,7 +656,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; } } - // If any of the group send errors are retryable, we want to retry. + // If any of the send errors are retryable, we want to retry. // Therefore, prefer to propagate a retryable error. if (firstRetryableError) { return failureHandler(firstRetryableError); @@ -674,7 +666,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // 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) { + if (messageSends.lastObject.message.sentRecipientsCount == 0) { NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients, NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS", @"Error indicating that an outgoing message had no valid recipients.")); @@ -713,60 +705,19 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; }]; } -- (void)sendMessageToService:(TSOutgoingMessage *)message - recipient:(SignalRecipient *)recipient - thread:(nullable TSThread *)thread - attempts:(int)remainingAttemptsParam - useWebsocketIfAvailable:(BOOL)useWebsocketIfAvailable - success:(void (^)(void))successHandler - failure:(RetryableFailureHandler)failureHandler +- (nullable NSArray *)deviceMessagesForMessageSendSafe:(OWSMessageSend *)messageSend + error:(NSError **)errorHandle { - OWSAssertDebug(message); - OWSAssertDebug(recipient); - OWSAssertDebug(thread || [message isKindOfClass:[OWSOutgoingSyncMessage class]]); - - OWSLogInfo(@"attempting to send message: %@, timestamp: %llu, recipient: %@", - message.class, - message.timestamp, - recipient.uniqueId); + OWSAssertDebug(messageSend); + OWSAssertDebug(errorHandle); 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 - rotateSignedPreKeyWithSuccess:^{ - OWSLogInfo(@"New prekeys registered with server."); - NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(); - [error setIsRetryable:YES]; - return failureHandler(error); - } - failure:^(NSError *error) { - OWSLogWarn(@"Failed to update prekeys with the server: %@", error); - return failureHandler(error); - }]; - } - - if (remainingAttemptsParam <= 0) { - // We should always fail with a specific error. - OWSProdFail([OWSAnalyticsEvents messageSenderErrorGenericSendFailure]); - - NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); - [error setIsRetryable:YES]; - return failureHandler(error); - } - int remainingAttempts = remainingAttemptsParam - 1; + SignalRecipient *recipient = messageSend.recipient; NSArray *deviceMessages; @try { - deviceMessages = [self deviceMessages:message recipient:recipient]; + deviceMessages = [self deviceMessagesForMessageSendUnsafe:messageSend]; } @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 @@ -788,67 +739,127 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [error setIsRetryable:NO]; // Avoid the "Too many failures with this contact" error rate limiting. [error setIsFatal:YES]; + *errorHandle = error; PreKeyBundle *_Nullable newKeyBundle = exception.userInfo[TSInvalidPreKeyBundleKey]; if (newKeyBundle == nil) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorMissingNewPreKeyBundle]); - failureHandler(error); - return; + return nil; } if (![newKeyBundle isKindOfClass:[PreKeyBundle class]]) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorUnexpectedKeyBundle]); - failureHandler(error); - return; + return nil; } NSData *newIdentityKeyWithVersion = newKeyBundle.identityKey; if (![newIdentityKeyWithVersion isKindOfClass:[NSData class]]) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyType]); - failureHandler(error); - return; + return nil; } // TODO migrate to storing the full 33 byte representation of the identity key. if (newIdentityKeyWithVersion.length != kIdentityKeyLength) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyLength]); - failureHandler(error); - return; + return nil; } NSData *newIdentityKey = [newIdentityKeyWithVersion removeKeyType]; [[OWSIdentityManager sharedManager] saveRemoteIdentity:newIdentityKey recipientId:recipient.recipientId]; - failureHandler(error); - return; + return nil; } 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); + *errorHandle = error; + return nil; } - if (remainingAttempts == 0) { - OWSLogWarn(@"Terminal failure to build any device messages. Giving up with exception:%@", exception); + if (messageSend.remainingAttempts == 0) { + OWSLogWarn(@"Terminal failure to build any device messages. Giving up with exception: %@", 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); + *errorHandle = error; + return nil; } + + OWSLogWarn(@"Could not build device messages: %@", exception); + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + [error setIsRetryable:YES]; + *errorHandle = error; + return nil; } - NSString *localNumber = [TSAccountManager localNumber]; - BOOL isLocalNumber = [localNumber isEqualToString:recipient.uniqueId]; - if (isLocalNumber) { + return deviceMessages; +} + +- (void)sendMessageToRecipient:(OWSMessageSend *)messageSend + success:(void (^)(void))successHandler + failure:(RetryableFailureHandler)failureHandler +{ + OWSAssertDebug(messageSend); + OWSAssertDebug(messageSend.thread || [messageSend.message isKindOfClass:[OWSOutgoingSyncMessage class]]); + + TSOutgoingMessage *message = messageSend.message; + SignalRecipient *recipient = messageSend.recipient; + + OWSLogInfo(@"attempting to send message: %@, timestamp: %llu, recipient: %@", + 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 + rotateSignedPreKeyWithSuccess:^{ + OWSLogInfo(@"New prekeys registered with server."); + NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(); + [error setIsRetryable:YES]; + return failureHandler(error); + } + failure:^(NSError *error) { + OWSLogWarn(@"Failed to update prekeys with the server: %@", error); + return failureHandler(error); + }]; + } + + if (messageSend.remainingAttempts <= 0) { + // We should always fail with a specific error. + OWSProdFail([OWSAnalyticsEvents messageSenderErrorGenericSendFailure]); + + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + [error setIsRetryable:YES]; + return failureHandler(error); + } + // Consume an attempt. + messageSend.remainingAttempts = messageSend.remainingAttempts - 1; + + NSError *deviceMessagesError; + NSArray *_Nullable deviceMessages = + [self deviceMessagesForMessageSendSafe:messageSend error:&deviceMessagesError]; + if (deviceMessagesError || !deviceMessages) { + OWSAssertDebug(deviceMessagesError); + return failureHandler(deviceMessagesError); + } + + if (messageSend.isLocalNumber) { OWSAssertDebug([message isKindOfClass:[OWSOutgoingSyncMessage class]]); // Messages sent to the "local number" should be sync messages. // @@ -878,9 +889,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; OWSAssertDebug([message isKindOfClass:[OWSOutgoingSyncMessage class]]); dispatch_async([OWSDispatch sendingQueue], ^{ - // This emulates the completion logic of an actual successful save (see below). + // This emulates the completion logic of an actual successful send (see below). [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [message updateWithSkippedRecipient:localNumber transaction:transaction]; + [message updateWithSkippedRecipient:messageSend.localNumber transaction:transaction]; }]; successHandler(); }); @@ -918,51 +929,55 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; TSRequest *request = [OWSRequestFactory submitMessageRequestWithRecipient:recipient.uniqueId messages:deviceMessages timeStamp:message.timestamp]; - if (useWebsocketIfAvailable && TSSocketManager.canMakeRequests) { + + BOOL isUDSend = messageSend.canUseUD && messageSend.udAccessKey != nil; + if (isUDSend) { + DDLogVerbose(@"UD send."); + request.shouldHaveAuthorizationHeaders = YES; + [request setValue:[messageSend.udAccessKey.keyData base64EncodedString] forKey:@"Unidentified-Access-Key"]; + } + + // TODO: UD sends over websocket. + if (messageSend.useWebsocketIfAvailable && TSSocketManager.canMakeRequests && !isUDSend) { [TSSocketManager.sharedManager makeRequest:request success:^(id _Nullable responseObject) { - [self messageSendDidSucceed:message - recipient:recipient - isLocalNumber:isLocalNumber - deviceMessages:deviceMessages - success:successHandler]; + [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages success:successHandler]; } failure:^(NSInteger statusCode, NSData *_Nullable responseData, NSError *error) { dispatch_async([OWSDispatch sendingQueue], ^{ - OWSLogDebug(@"falling back to REST since first attempt failed."); + OWSLogDebug(@"Web socket send failed; failing over to REST."); // Websockets can fail in different ways, so we don't decrement remainingAttempts for websocket // failure. Instead we fall back to REST, which will decrement retries. e.g. after linking a new // device, sync messages will fail until the websocket re-opens. - [self sendMessageToService:message - recipient:recipient - thread:thread - attempts:remainingAttemptsParam - useWebsocketIfAvailable:NO - success:successHandler - failure:failureHandler]; + messageSend.useWebsocketIfAvailable = NO; + [self sendMessageToRecipient:messageSend success:successHandler failure:failureHandler]; }); }]; } else { [self.networkManager makeRequest:request success:^(NSURLSessionDataTask *task, id responseObject) { - [self messageSendDidSucceed:message - recipient:recipient - isLocalNumber:isLocalNumber - deviceMessages:deviceMessages - success:successHandler]; + [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages success:successHandler]; } failure:^(NSURLSessionDataTask *task, NSError *error) { NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; NSInteger statusCode = response.statusCode; NSData *_Nullable responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; - [self messageSendDidFail:message - recipient:recipient - thread:thread - isLocalNumber:isLocalNumber + if (isUDSend && statusCode > 0) { + // If a UD send fails due to service response (as opposed to network + // failure), mark recipient as _not_ in UD mode, then retry. + // + // TODO: Do we want to discriminate based on exact error? + OWSLogDebug(@"UD send failed; failing over to non-UD send."); + [self.udManager removeUDRecipientId:recipient.uniqueId]; + messageSend.canUseUD = NO; + [self sendMessageToRecipient:messageSend success:successHandler failure:failureHandler]; + return; + } + + [self messageSendDidFail:messageSend deviceMessages:deviceMessages - remainingAttempts:remainingAttempts statusCode:statusCode error:error responseData:responseData @@ -972,20 +987,19 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; } } -- (void)messageSendDidSucceed:(TSOutgoingMessage *)message - recipient:(SignalRecipient *)recipient - isLocalNumber:(BOOL)isLocalNumber +- (void)messageSendDidSucceed:(OWSMessageSend *)messageSend deviceMessages:(NSArray *)deviceMessages success:(void (^)(void))successHandler { - OWSAssertDebug(message); - OWSAssertDebug(recipient); + OWSAssertDebug(messageSend); OWSAssertDebug(deviceMessages); OWSAssertDebug(successHandler); + SignalRecipient *recipient = messageSend.recipient; + OWSLogInfo(@"Message send succeeded."); - if (isLocalNumber && deviceMessages.count == 0) { + if (messageSend.isLocalNumber && deviceMessages.count == 0) { OWSLogInfo(@"Sent a message with no device messages; clearing 'mayHaveLinkedDevices'."); // In order to avoid skipping necessary sync messages, the default value // for mayHaveLinkedDevices is YES. Once we've successfully sent a @@ -999,42 +1013,40 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; dispatch_async([OWSDispatch sendingQueue], ^{ [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [message updateWithSentRecipient:recipient.uniqueId transaction:transaction]; + [messageSend.message updateWithSentRecipient:messageSend.recipient.uniqueId transaction:transaction]; // If we've just delivered a message to a user, we know they // have a valid Signal account. [SignalRecipient markRecipientAsRegisteredAndGet:recipient.recipientId transaction:transaction]; }]; - [self handleMessageSentLocally:message]; + [self handleMessageSentLocally:messageSend.message]; successHandler(); }); } -- (void)messageSendDidFail:(TSOutgoingMessage *)message - recipient:(SignalRecipient *)recipient - thread:(nullable TSThread *)thread - isLocalNumber:(BOOL)isLocalNumber +- (void)messageSendDidFail:(OWSMessageSend *)messageSend deviceMessages:(NSArray *)deviceMessages - remainingAttempts:(int)remainingAttempts statusCode:(NSInteger)statusCode error:(NSError *)responseError responseData:(nullable NSData *)responseData success:(void (^)(void))successHandler failure:(RetryableFailureHandler)failureHandler { - OWSAssertDebug(message); - OWSAssertDebug(recipient); - OWSAssertDebug(thread || [message isKindOfClass:[OWSOutgoingSyncMessage class]]); + OWSAssertDebug(messageSend); + OWSAssertDebug(messageSend.thread || [messageSend.message isKindOfClass:[OWSOutgoingSyncMessage class]]); OWSAssertDebug(deviceMessages); OWSAssertDebug(responseError); OWSAssertDebug(successHandler); OWSAssertDebug(failureHandler); + TSOutgoingMessage *message = messageSend.message; + SignalRecipient *recipient = messageSend.recipient; + OWSLogInfo(@"sending to recipient: %@, failed with error.", recipient.uniqueId); void (^retrySend)(void) = ^void() { - if (remainingAttempts <= 0) { + if (messageSend.remainingAttempts <= 0) { // Since we've already repeatedly failed to send to the messaging API, // it's unlikely that repeating the whole process will succeed. [responseError setIsRetryable:NO]; @@ -1043,19 +1055,15 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; dispatch_async([OWSDispatch sendingQueue], ^{ OWSLogDebug(@"Retrying: %@", message.debugDescription); - [self sendMessageToService:message - recipient:recipient - thread:thread - attempts:remainingAttempts - useWebsocketIfAvailable:NO - success:successHandler - failure:failureHandler]; + // TODO: Should this use sendMessageToRecipient or sendMessageToService? + [self sendMessageToRecipient:messageSend success:successHandler failure:failureHandler]; }); }; void (^handle404)(void) = ^{ OWSLogWarn(@"Unregistered recipient: %@", recipient.uniqueId); + TSThread *_Nullable thread = messageSend.thread; OWSAssertDebug(thread); dispatch_async([OWSDispatch sendingQueue], ^{ @@ -1207,17 +1215,19 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; OWSOutgoingSentMessageTranscript *sentMessageTranscript = [[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message]; - NSString *recipientId = [TSAccountManager localNumber]; + NSString *recipientId = self.tsAccountManager.localNumber; __block SignalRecipient *recipient; [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { recipient = [SignalRecipient markRecipientAsRegisteredAndGet:recipientId transaction:transaction]; }]; - [self sendMessageToService:sentMessageTranscript - recipient:recipient - thread:message.thread - attempts:OWSMessageSenderRetryAttempts - useWebsocketIfAvailable:YES + OWSMessageSend *messageSend = [[OWSMessageSend alloc] initWithMessage:sentMessageTranscript + thread:message.thread + recipient:recipient + udManager:self.udManager + localNumber:self.tsAccountManager.localNumber]; + + [self sendMessageToRecipient:messageSend success:^{ OWSLogInfo(@"Successfully sent sync transcript."); } @@ -1231,20 +1241,25 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; }]; } -- (NSArray *)deviceMessages:(TSOutgoingMessage *)message recipient:(SignalRecipient *)recipient +// NOTE: This method uses exceptions for control flow. +- (NSArray *)deviceMessagesForMessageSendUnsafe:(OWSMessageSend *)messageSend { - OWSAssertDebug(message); - OWSAssertDebug(recipient); + OWSAssertDebug(messageSend.message); + OWSAssertDebug(messageSend.recipient); + + TSOutgoingMessage *message = messageSend.message; + SignalRecipient *recipient = messageSend.recipient; NSMutableArray *messagesArray = [NSMutableArray arrayWithCapacity:recipient.devices.count]; - NSData *_Nullable plainText = [message buildPlainTextData:recipient]; + NSData *_Nullable plainText = [messageSend.message buildPlainTextData:messageSend.recipient]; if (!plainText) { OWSRaiseException(InvalidMessageException, @"Failed to build message proto"); } - OWSLogDebug(@"built message: %@ plainTextData.length: %lu", [message class], (unsigned long)plainText.length); + OWSLogDebug( + @"built message: %@ plainTextData.length: %lu", [messageSend.message class], (unsigned long)plainText.length); - for (NSNumber *deviceNumber in recipient.devices) { + for (NSNumber *deviceNumber in messageSend.recipient.devices) { @try { __block NSDictionary *messageDict; __block NSException *encryptionException; @@ -1252,7 +1267,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { @try { messageDict = [self encryptedMessageWithPlaintext:plainText - recipient:recipient + recipient:messageSend.recipient deviceId:deviceNumber keyingStorage:self.primaryStorage isSilent:message.isSilent @@ -1412,23 +1427,24 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; } } -- (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread -{ - if (message.groupMetaMessage == TSGroupMetaMessageDeliver) { - // TODO: Why is this necessary? - [message save]; - } else if (message.groupMetaMessage == TSGroupMetaMessageQuit) { - [[[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]; - } -} +// TODO: Huh? +//- (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread +//{ +// if (message.groupMetaMessage == TSGroupMetaMessageDeliver) { +// // TODO: Why is this necessary? +// [message save]; +// } else if (message.groupMetaMessage == TSGroupMetaMessageQuit) { +// [[[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)handleStaleDevicesWithResponseJson:(NSDictionary *)responseJson diff --git a/SignalServiceKit/src/Messages/UD/OWSUDManager.swift b/SignalServiceKit/src/Messages/UD/OWSUDManager.swift index 0c362d8f0..c8f511fd9 100644 --- a/SignalServiceKit/src/Messages/UD/OWSUDManager.swift +++ b/SignalServiceKit/src/Messages/UD/OWSUDManager.swift @@ -26,6 +26,10 @@ public enum OWSUDError: Error { // No-op if this recipient id is already marked as _NOT_ a "UD recipient". @objc func removeUDRecipientId(_ recipientId: String) + // Returns the UD access key for a given recipient if they are + // a UD recipient and we have a valid profile key for them. + @objc func udAccessKeyForRecipient(_ recipientId: String) -> SMKUDAccessKey? + // MARK: - Sender Certificate // We use completion handlers instead of a promise so that message sending @@ -81,6 +85,12 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager { ensureSenderCertificate().retainUntilComplete() } + // MARK: - Dependencies + + private var profileManager: ProfileManagerProtocol { + return SSKEnvironment.shared.profileManager + } + // MARK: - Recipient state @objc @@ -98,6 +108,28 @@ public class OWSUDManagerImpl: NSObject, OWSUDManager { dbConnection.removeObject(forKey: recipientId, inCollection: kUDRecipientModeCollection) } + // Returns the UD access key for a given recipient if they are + // a UD recipient and we have a valid profile key for them. + @objc + public func udAccessKeyForRecipient(_ recipientId: String) -> SMKUDAccessKey? { + guard isUDRecipientId(recipientId) else { + return nil + } + guard let profileKey = profileManager.profileKeyData(forRecipientId: recipientId) else { + // Mark as "not a UD recipient". + removeUDRecipientId(recipientId) + return nil + } + do { + let udAccessKey = try SMKUDAccessKey(profileKey: profileKey) + return udAccessKey + } catch { + Logger.error("Could not determine udAccessKey: \(error)") + removeUDRecipientId(recipientId) + return nil + } + } + // MARK: - Sender Certificate #if DEBUG diff --git a/SignalServiceKit/src/Network/API/Requests/TSRequest.h b/SignalServiceKit/src/Network/API/Requests/TSRequest.h index a56ba703d..4cfa4540e 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSRequest.h +++ b/SignalServiceKit/src/Network/API/Requests/TSRequest.h @@ -8,7 +8,7 @@ @property (atomic, nullable) NSString *authUsername; @property (atomic, nullable) NSString *authPassword; -@property (nonatomic, readonly) NSDictionary *parameters; +@property (nonatomic, readonly) NSDictionary *parameters; - (instancetype)init NS_UNAVAILABLE; @@ -26,4 +26,6 @@ method:(NSString *)method parameters:(nullable NSDictionary *)parameters; +- (void)setParameterWithValue:(id)value forKey:(NSString *)key; + @end diff --git a/SignalServiceKit/src/Network/API/Requests/TSRequest.m b/SignalServiceKit/src/Network/API/Requests/TSRequest.m index 745f8923a..e165eb1f4 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSRequest.m +++ b/SignalServiceKit/src/Network/API/Requests/TSRequest.m @@ -110,4 +110,15 @@ } } +- (void)setParameterWithValue:(id)value forKey:(NSString *)key +{ + OWSAssertDebug(value); + OWSAssertDebug(key.length > 0); + + NSMutableDictionary *parameters + = (self.parameters ? [self.parameters mutableCopy] : [NSMutableDictionary new]); + parameters[key] = value; + _parameters = [parameters copy]; +} + @end diff --git a/SignalServiceKit/src/Tests/OWSFakeUDManager.swift b/SignalServiceKit/src/Tests/OWSFakeUDManager.swift index 1d372b27c..6b9d72364 100644 --- a/SignalServiceKit/src/Tests/OWSFakeUDManager.swift +++ b/SignalServiceKit/src/Tests/OWSFakeUDManager.swift @@ -3,6 +3,7 @@ // import Foundation +import SignalMetadataKit #if DEBUG @@ -30,6 +31,28 @@ public class OWSFakeUDManager: NSObject, OWSUDManager { udRecipientSet.remove(recipientId) } + // Returns the UD access key for a given recipient if they are + // a UD recipient and we have a valid profile key for them. + @objc + public func udAccessKeyForRecipient(_ recipientId: String) -> SMKUDAccessKey? { + guard isUDRecipientId(recipientId) else { + return nil + } + guard let profileKey = Randomness.generateRandomBytes(Int32(kAES256_KeyByteLength)) else { + // Mark as "not a UD recipient". + removeUDRecipientId(recipientId) + return nil + } + do { + let udAccessKey = try SMKUDAccessKey(profileKey: profileKey) + return udAccessKey + } catch { + Logger.error("Could not determine udAccessKey: \(error)") + removeUDRecipientId(recipientId) + return nil + } + } + // MARK: - Server Certificate // Tests can control the behavior of this mock by setting this property.