UD send via REST.

pull/1/head
Matthew Chen 6 years ago
parent c856859fbd
commit d08479980d

@ -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
}
}

@ -39,6 +39,7 @@
#import "TSOutgoingMessage.h"
#import "TSPreKeyManager.h"
#import "TSQuotedMessage.h"
#import "TSRequest.h"
#import "TSSocketManager.h"
#import "TSThread.h"
#import <AxolotlKit/AxolotlExceptions.h>
@ -47,8 +48,10 @@
#import <AxolotlKit/SessionBuilder.h>
#import <AxolotlKit/SessionCipher.h>
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/Threading.h>
#import <SignalMetadataKit/SignalMetadataKit-Swift.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
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<ContactsManagerProtocol>)contactsManager
{
OWSAssertDebug(SSKEnvironment.shared.contactsManager);
@ -246,6 +254,20 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
return SSKEnvironment.shared.networkManager;
}
- (id<OWSUDManager>)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<SignalRecipient *> *)signalRecipientsForRecipientIds:(NSArray<NSString *> *)recipientIds
message:(TSOutgoingMessage *)message
{
OWSAssertDebug(recipientIds);
OWSAssertDebug(message);
NSMutableArray<SignalRecipient *> *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<NSString *> *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<NSString *> *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<NSString *> *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<SignalRecipient *> *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<SignalRecipient *> *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<NSString *> *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<OWSMessageSend *> *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<SignalRecipient *> *)recipients
message:(TSOutgoingMessage *)message
thread:(TSThread *)thread
success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler
- (void)sendWithMessageSends:(NSArray<OWSMessageSend *> *)messageSends
isGroupSend:(BOOL)isGroupSend
success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler
{
[self saveGroupMessage:message inThread:thread];
OWSAssertDebug(messageSends.count > 0);
AssertIsOnSendingQueue();
NSMutableArray<AnyPromise *> *sendPromises = [NSMutableArray array];
NSMutableArray<NSError *> *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<NSDictionary *> *)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<NSDictionary *> *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<NSDictionary *> *_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<NSDictionary *> *)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<NSDictionary *> *)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<NSDictionary *> *)deviceMessages:(TSOutgoingMessage *)message recipient:(SignalRecipient *)recipient
// NOTE: This method uses exceptions for control flow.
- (NSArray<NSDictionary *> *)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

@ -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

@ -8,7 +8,7 @@
@property (atomic, nullable) NSString *authUsername;
@property (atomic, nullable) NSString *authPassword;
@property (nonatomic, readonly) NSDictionary *parameters;
@property (nonatomic, readonly) NSDictionary<NSString *, id> *parameters;
- (instancetype)init NS_UNAVAILABLE;
@ -26,4 +26,6 @@
method:(NSString *)method
parameters:(nullable NSDictionary<NSString *, id> *)parameters;
- (void)setParameterWithValue:(id)value forKey:(NSString *)key;
@end

@ -110,4 +110,15 @@
}
}
- (void)setParameterWithValue:(id)value forKey:(NSString *)key
{
OWSAssertDebug(value);
OWSAssertDebug(key.length > 0);
NSMutableDictionary<NSString *, id> *parameters
= (self.parameters ? [self.parameters mutableCopy] : [NSMutableDictionary new]);
parameters[key] = value;
_parameters = [parameters copy];
}
@end

@ -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.

Loading…
Cancel
Save