2016-10-08 01:17:38 +02:00
|
|
|
// Created by Michael Kirk on 10/7/16.
|
|
|
|
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
|
|
|
|
|
|
|
#import "OWSMessageSender.h"
|
2016-10-14 23:00:29 +02:00
|
|
|
#import "ContactsUpdater.h"
|
|
|
|
#import "NSData+messagePadding.h"
|
|
|
|
#import "OWSDisappearingMessagesJob.h"
|
|
|
|
#import "OWSDispatch.h"
|
2016-10-08 01:17:38 +02:00
|
|
|
#import "OWSError.h"
|
2016-10-14 23:00:29 +02:00
|
|
|
#import "OWSLegacyMessageServiceParams.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"
|
2016-10-08 01:17:38 +02:00
|
|
|
#import "TSNetworkManager.h"
|
|
|
|
#import "TSOutgoingMessage.h"
|
2016-10-14 23:00:29 +02:00
|
|
|
#import "TSStorageManager+IdentityKeyStore.h"
|
|
|
|
#import "TSStorageManager+PreKeyStore.h"
|
|
|
|
#import "TSStorageManager+SignedPreKeyStore.h"
|
|
|
|
#import "TSStorageManager+keyingMaterial.h"
|
|
|
|
#import "TSStorageManager+sessionStore.h"
|
|
|
|
#import "TSStorageManager.h"
|
2016-10-08 01:17:38 +02:00
|
|
|
#import "TSThread.h"
|
2016-10-14 23:00:29 +02:00
|
|
|
#import <AxolotlKit/AxolotlExceptions.h>
|
|
|
|
#import <AxolotlKit/CipherMessage.h>
|
|
|
|
#import <AxolotlKit/PreKeyBundle.h>
|
|
|
|
#import <AxolotlKit/SessionBuilder.h>
|
|
|
|
#import <AxolotlKit/SessionCipher.h>
|
|
|
|
#import <TwistedOakCollapsingFutures/CollapsingFutures.h>
|
2016-10-08 01:17:38 +02:00
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
2016-10-14 23:00:29 +02:00
|
|
|
int const OWSMessageSenderRetryAttempts = 3;
|
|
|
|
NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException";
|
2016-11-10 15:59:07 +01:00
|
|
|
NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
2016-10-14 23:00:29 +02:00
|
|
|
|
2016-10-08 01:17:38 +02:00
|
|
|
@interface OWSMessageSender ()
|
|
|
|
|
|
|
|
@property (nonatomic, readonly) TSNetworkManager *networkManager;
|
2016-10-14 23:00:29 +02:00
|
|
|
@property (nonatomic, readonly) TSStorageManager *storageManager;
|
|
|
|
@property (nonatomic, readonly) OWSUploadingService *uploadingService;
|
|
|
|
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
|
|
|
@property (nonatomic, readonly) id<ContactsManagerProtocol> contactsManager;
|
|
|
|
@property (nonatomic, readonly) ContactsUpdater *contactsUpdater;
|
|
|
|
@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob;
|
2016-10-08 01:17:38 +02:00
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation OWSMessageSender
|
|
|
|
|
2016-10-14 23:00:29 +02:00
|
|
|
- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager
|
|
|
|
storageManager:(TSStorageManager *)storageManager
|
|
|
|
contactsManager:(id<ContactsManagerProtocol>)contactsManager
|
|
|
|
contactsUpdater:(ContactsUpdater *)contactsUpdater
|
2016-10-08 01:17:38 +02:00
|
|
|
{
|
|
|
|
self = [super init];
|
|
|
|
if (!self) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
_networkManager = networkManager;
|
2016-10-14 23:00:29 +02:00
|
|
|
_storageManager = storageManager;
|
|
|
|
_contactsManager = contactsManager;
|
|
|
|
_contactsUpdater = contactsUpdater;
|
|
|
|
|
|
|
|
_uploadingService = [[OWSUploadingService alloc] initWithNetworkManager:networkManager];
|
|
|
|
_dbConnection = storageManager.newDatabaseConnection;
|
|
|
|
_disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager];
|
|
|
|
|
2016-10-08 01:17:38 +02:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2016-10-14 23:00:29 +02:00
|
|
|
- (void)sendMessage:(TSOutgoingMessage *)message
|
|
|
|
success:(void (^)())successHandler
|
|
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
|
|
{
|
|
|
|
void (^markAndFailureHandler)(NSError *error) = ^(NSError *error) {
|
|
|
|
[self saveMessage:message withError:error];
|
|
|
|
failureHandler(error);
|
|
|
|
};
|
|
|
|
|
|
|
|
[self ensureAnyAttachmentsUploaded:message
|
|
|
|
success:^() {
|
|
|
|
[self deliverMessage:message success:successHandler failure:markAndFailureHandler];
|
|
|
|
}
|
|
|
|
failure:markAndFailureHandler];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)ensureAnyAttachmentsUploaded:(TSOutgoingMessage *)message
|
|
|
|
success:(void (^)())successHandler
|
|
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
|
|
{
|
|
|
|
if (!message.hasAttachments) {
|
|
|
|
DDLogDebug(@"%@ No attachments for message: %@", self.tag, message);
|
|
|
|
return successHandler();
|
|
|
|
}
|
|
|
|
|
|
|
|
TSAttachmentStream *attachmentStream =
|
|
|
|
[TSAttachmentStream fetchObjectWithUniqueID:message.attachmentIds.firstObject];
|
|
|
|
|
|
|
|
if (!attachmentStream) {
|
|
|
|
DDLogError(@"%@ Unable to find local saved attachment to upload.", self.tag);
|
|
|
|
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
|
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
[self.uploadingService uploadAttachmentStream:attachmentStream
|
|
|
|
message:message
|
|
|
|
success:successHandler
|
|
|
|
failure:failureHandler];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)sendTemporaryAttachmentData:(NSData *)attachmentData
|
|
|
|
contentType:(NSString *)contentType
|
|
|
|
inMessage:(TSOutgoingMessage *)message
|
|
|
|
success:(void (^)())successHandler
|
|
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
|
|
{
|
|
|
|
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:attachmentData
|
|
|
|
contentType:contentType
|
|
|
|
inMessage:message
|
|
|
|
success:successWithDeleteHandler
|
|
|
|
failure:failureWithDeleteHandler];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)sendAttachmentData:(NSData *)data
|
|
|
|
contentType:(NSString *)contentType
|
|
|
|
inMessage:(TSOutgoingMessage *)message
|
|
|
|
success:(void (^)())successHandler
|
|
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
|
|
{
|
|
|
|
dispatch_async([OWSDispatch attachmentsQueue], ^{
|
|
|
|
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:contentType];
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
[attachmentStream writeData:data error:&error];
|
|
|
|
if (error) {
|
|
|
|
DDLogError(@"%@ Failed to write data for outgoing attachment with error:%@", self.tag, error);
|
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
[attachmentStream save];
|
|
|
|
[message.attachmentIds addObject:attachmentStream.uniqueId];
|
|
|
|
|
|
|
|
message.messageState = TSOutgoingMessageStateAttemptingOut;
|
|
|
|
[message save];
|
|
|
|
|
|
|
|
[self sendMessage:message success:successHandler failure:failureHandler];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)resendMessageFromKeyError:(TSInvalidIdentityKeySendingErrorMessage *)errorMessage
|
|
|
|
success:(void (^)())successHandler
|
|
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
|
|
{
|
|
|
|
TSOutgoingMessage *message = [TSOutgoingMessage fetchObjectWithUniqueID:errorMessage.messageId];
|
|
|
|
|
|
|
|
// Here we remove the existing error message because sending a new message will either
|
|
|
|
// 1.) succeed and create a new successful message in the thread or...
|
|
|
|
// 2.) fail and create a new identical error message in the thread.
|
|
|
|
[errorMessage remove];
|
|
|
|
|
|
|
|
if ([errorMessage.thread isKindOfClass:[TSContactThread class]]) {
|
|
|
|
return [self sendMessage:message success:successHandler failure:failureHandler];
|
|
|
|
}
|
|
|
|
|
|
|
|
// else it's a GroupThread
|
|
|
|
dispatch_async([OWSDispatch sendingQueue], ^{
|
|
|
|
|
|
|
|
// Avoid spamming entire group when resending failed message.
|
|
|
|
SignalRecipient *failedRecipient = [SignalRecipient fetchObjectWithUniqueID:errorMessage.recipientId];
|
|
|
|
|
|
|
|
// Normally marking as unsent is handled in sendMessage happy path, but beacuse we're skipping the common entry
|
|
|
|
// point to message sending in order to send to a single recipient, we have to handle it ourselves.
|
|
|
|
void (^markAndFailureHandler)(NSError *error) = ^(NSError *error) {
|
|
|
|
[self saveMessage:message withError:error];
|
|
|
|
failureHandler(error);
|
|
|
|
};
|
|
|
|
|
|
|
|
[self groupSend:@[ failedRecipient ]
|
|
|
|
message:message
|
|
|
|
thread:message.thread
|
|
|
|
success:successHandler
|
|
|
|
failure:markAndFailureHandler];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSArray<SignalRecipient *> *)getRecipients:(NSArray<NSString *> *)identifiers error:(NSError **)error
|
|
|
|
{
|
|
|
|
NSMutableArray<SignalRecipient *> *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.
|
|
|
|
DDLogError(@"%@ Unknown error finding contacts", self.tag);
|
|
|
|
*error = OWSErrorMakeFailedToSendOutgoingMessageError();
|
|
|
|
}
|
|
|
|
|
|
|
|
return [recipients copy];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)deliverMessage:(TSOutgoingMessage *)message
|
|
|
|
success:(void (^)())successHandler
|
|
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
|
|
{
|
|
|
|
TSThread *thread = message.thread;
|
|
|
|
|
|
|
|
dispatch_async([OWSDispatch sendingQueue], ^{
|
|
|
|
if ([thread isKindOfClass:[TSGroupThread class]]) {
|
|
|
|
TSGroupThread *gThread = (TSGroupThread *)thread;
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
NSArray<SignalRecipient *> *recipients =
|
|
|
|
[self getRecipients:gThread.groupModel.groupMemberIds error:&error];
|
2016-11-08 19:57:05 +01:00
|
|
|
|
|
|
|
if (recipients.count == 0) {
|
|
|
|
if (error) {
|
|
|
|
return failureHandler(error);
|
|
|
|
} else {
|
|
|
|
DDLogError(@"%@ Unknown error finding contacts", self.tag);
|
|
|
|
return failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError());
|
|
|
|
}
|
2016-10-14 23:00:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
[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;
|
|
|
|
|
|
|
|
[self saveMessage:message withState:TSOutgoingMessageStateAttemptingOut];
|
|
|
|
|
|
|
|
if ([contactThread.contactIdentifier isEqualToString:self.storageManager.localNumber]
|
|
|
|
&& ![message isKindOfClass:[OWSOutgoingSyncMessage class]]) {
|
|
|
|
|
|
|
|
[self handleSendToMyself:message];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSString *recipientContactId = [message isKindOfClass:[OWSOutgoingSyncMessage class]]
|
|
|
|
? self.storageManager.localNumber
|
|
|
|
: contactThread.contactIdentifier;
|
|
|
|
|
2016-10-25 15:54:43 +02:00
|
|
|
SignalRecipient *recipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientContactId];
|
2016-10-14 23:00:29 +02:00
|
|
|
if (!recipient) {
|
|
|
|
NSError *error;
|
|
|
|
// possibly returns nil.
|
|
|
|
recipient = [self.contactsUpdater synchronousLookup:contactThread.contactIdentifier error:&error];
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
if (error.code == NOTFOUND_ERROR) {
|
|
|
|
DDLogWarn(@"recipient contact not found with error: %@", error);
|
|
|
|
[self unregisteredRecipient:recipient message:message thread:thread];
|
2016-10-25 15:54:43 +02:00
|
|
|
NSError *error = OWSErrorMakeNoSuchSignalRecipientError();
|
|
|
|
return failureHandler(error);
|
2016-10-14 23:00:29 +02:00
|
|
|
}
|
|
|
|
DDLogError(@"contact lookup failed with error: %@", error);
|
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!recipient) {
|
|
|
|
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
|
|
|
|
DDLogWarn(@"recipient contact still not found after attempting lookup.");
|
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
[self sendMessage:message
|
|
|
|
recipient:recipient
|
|
|
|
thread:thread
|
|
|
|
attempts:OWSMessageSenderRetryAttempts
|
|
|
|
success:successHandler
|
|
|
|
failure:failureHandler];
|
2016-11-04 17:19:13 +01:00
|
|
|
} else {
|
|
|
|
DDLogError(@"%@ Unexpected unhandlable message: %@", self.tag, message);
|
|
|
|
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
|
|
|
|
failureHandler(error);
|
2016-10-14 23:00:29 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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:^{
|
|
|
|
[futureSource trySetResult:@1];
|
|
|
|
}
|
|
|
|
failure:^(NSError *error) {
|
|
|
|
[futureSource trySetFailure:error];
|
|
|
|
}];
|
|
|
|
|
|
|
|
return futureSource.future;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)groupSend:(NSArray<SignalRecipient *> *)recipients
|
|
|
|
message:(TSOutgoingMessage *)message
|
|
|
|
thread:(TSThread *)thread
|
|
|
|
success:(void (^)())successHandler
|
|
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
|
|
{
|
|
|
|
[self saveGroupMessage:message inThread:thread];
|
|
|
|
NSMutableArray<TOCFuture *> *futures = [NSMutableArray array];
|
|
|
|
|
|
|
|
for (SignalRecipient *rec in recipients) {
|
|
|
|
// we don't need to send the message to ourselves, but otherwise we send
|
|
|
|
if (![[rec uniqueId] isEqualToString:[TSStorageManager localNumber]]) {
|
|
|
|
[futures addObject:[self sendMessageFuture:message recipient:rec thread:thread]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TOCFuture *completionFuture = futures.toc_thenAll;
|
|
|
|
|
|
|
|
[completionFuture thenDo:^(id value) {
|
|
|
|
successHandler();
|
|
|
|
}];
|
|
|
|
|
|
|
|
[completionFuture catchDo:^(id failure) {
|
2016-10-24 19:54:00 +02:00
|
|
|
// failure from toc_thenAll yeilds an array of failed Futures, rather than the future's failure.
|
|
|
|
if ([failure isKindOfClass:[NSArray class]]) {
|
2016-10-14 23:00:29 +02:00
|
|
|
NSArray *errors = (NSArray *)failure;
|
2016-10-24 19:54:00 +02:00
|
|
|
for (TOCFuture *failedFuture in errors) {
|
|
|
|
if (!failedFuture.hasFailed) {
|
|
|
|
// If at least one send succeeded, don't show message as failed.
|
|
|
|
// Else user will tap-to-resend to all recipients, including those that already received the
|
|
|
|
// message.
|
|
|
|
return successHandler();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, all recipients must have failed.
|
|
|
|
// But we have all this verbose type checking because TOCFuture doesn't expose type information.
|
|
|
|
id lastError = errors.lastObject;
|
|
|
|
if ([lastError isKindOfClass:[TOCFuture class]]) {
|
|
|
|
TOCFuture *failedFuture = (TOCFuture *)lastError;
|
|
|
|
if (failedFuture.hasFailed) {
|
|
|
|
id failureResult = failedFuture.forceGetFailure;
|
|
|
|
if ([failureResult isKindOfClass:[NSError class]]) {
|
|
|
|
return failureHandler((NSError *)failureResult);
|
|
|
|
}
|
2016-10-14 23:00:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
DDLogWarn(@"%@ Unexpected generic failure: %@", self.tag, failure);
|
|
|
|
return failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError());
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)unregisteredRecipient:(SignalRecipient *)recipient
|
|
|
|
message:(TSOutgoingMessage *)message
|
|
|
|
thread:(TSThread *)thread
|
|
|
|
{
|
|
|
|
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
[recipient removeWithTransaction:transaction];
|
|
|
|
[[TSInfoMessage userNotRegisteredMessageInThread:thread transaction:transaction]
|
|
|
|
saveWithTransaction:transaction];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)sendMessage:(TSOutgoingMessage *)message
|
|
|
|
recipient:(SignalRecipient *)recipient
|
|
|
|
thread:(TSThread *)thread
|
|
|
|
attempts:(int)remainingAttempts
|
|
|
|
success:(void (^)())successHandler
|
|
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
|
|
{
|
|
|
|
if (remainingAttempts <= 0) {
|
|
|
|
// We should always fail with a specific error.
|
|
|
|
DDLogError(@"%@ Unexpected generic failure.", self.tag);
|
|
|
|
return failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError());
|
|
|
|
}
|
|
|
|
remainingAttempts -= 1;
|
|
|
|
|
|
|
|
NSArray<NSDictionary *> *deviceMessages;
|
|
|
|
@try {
|
|
|
|
deviceMessages = [self deviceMessages:message forRecipient:recipient inThread:thread];
|
|
|
|
} @catch (NSException *exception) {
|
|
|
|
deviceMessages = @[];
|
2016-11-10 15:59:07 +01:00
|
|
|
if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
|
|
|
|
[[TSInvalidIdentityKeySendingErrorMessage
|
|
|
|
untrustedKeyWithOutgoingMessage:message
|
|
|
|
inThread:thread
|
|
|
|
forRecipient:exception.userInfo[TSInvalidRecipientKey]
|
|
|
|
preKeyBundle:exception.userInfo[TSInvalidPreKeyBundleKey]] save];
|
|
|
|
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeUntrustedIdentityKey,
|
|
|
|
NSLocalizedString(@"FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_KEY",
|
|
|
|
@"action sheet header when re-sending message which failed because of untrusted identity keys"));
|
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ([exception.name isEqualToString:OWSMessageSenderRateLimitedException]) {
|
|
|
|
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeUntrustedIdentityKey,
|
|
|
|
NSLocalizedString(@"FAILED_SENDING_BECAUSE_RATE_LIMIT",
|
|
|
|
@"action sheet header when re-sending message which failed because of too many attempts"));
|
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
|
2016-10-14 23:00:29 +02:00
|
|
|
if (remainingAttempts == 0) {
|
|
|
|
DDLogWarn(
|
|
|
|
@"%@ Terminal failure to build any device messages. Giving up with exception:%@", self.tag, exception);
|
|
|
|
[self processException:exception outgoingMessage:message inThread:thread];
|
|
|
|
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
|
2016-11-10 15:59:07 +01:00
|
|
|
return failureHandler(error);
|
2016-10-14 23:00:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
|
|
|
|
long statuscode = response.statusCode;
|
|
|
|
NSData *responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
|
|
|
|
|
|
|
|
void (^retrySend)() = ^void() {
|
|
|
|
if (remainingAttempts <= 0) {
|
2016-11-10 15:59:07 +01:00
|
|
|
return failureHandler(error);
|
2016-10-14 23:00:29 +02:00
|
|
|
}
|
2016-10-24 19:54:00 +02:00
|
|
|
|
2016-10-14 23:00:29 +02:00
|
|
|
dispatch_async([OWSDispatch sendingQueue], ^{
|
|
|
|
[self sendMessage:message
|
|
|
|
recipient:recipient
|
|
|
|
thread:thread
|
|
|
|
attempts:remainingAttempts
|
|
|
|
success:successHandler
|
|
|
|
failure:failureHandler];
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
switch (statuscode) {
|
|
|
|
case 404: {
|
|
|
|
[self unregisteredRecipient:recipient message:message thread:thread];
|
2016-10-25 15:54:43 +02:00
|
|
|
NSError *error = OWSErrorMakeNoSuchSignalRecipientError();
|
2016-10-14 23:00:29 +02:00
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
case 409: {
|
|
|
|
// Mismatched devices
|
|
|
|
DDLogWarn(@"%@ Mismatch Devices.", self.tag);
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
NSDictionary *serializedResponse =
|
|
|
|
[NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
|
|
|
|
if (error) {
|
|
|
|
DDLogError(@"%@ Failed to serialize response of mismatched devices: %@", self.tag, error);
|
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
[self handleMismatchedDevices:serializedResponse recipient:recipient];
|
|
|
|
retrySend();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 410: {
|
|
|
|
// staledevices
|
|
|
|
DDLogWarn(@"Stale devices");
|
|
|
|
|
|
|
|
if (!responseData) {
|
|
|
|
DDLogWarn(@"Stale devices but server didn't specify devices in response.");
|
|
|
|
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
|
|
|
|
return failureHandler(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
[self handleStaleDevicesWithResponse:responseData recipientId:recipient.uniqueId];
|
|
|
|
retrySend();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
retrySend();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)handleMismatchedDevices:(NSDictionary *)dictionary recipient:(SignalRecipient *)recipient
|
|
|
|
{
|
|
|
|
NSArray *extraDevices = [dictionary objectForKey:@"extraDevices"];
|
|
|
|
NSArray *missingDevices = [dictionary objectForKey:@"missingDevices"];
|
|
|
|
|
|
|
|
if (extraDevices && extraDevices.count > 0) {
|
|
|
|
for (NSNumber *extraDeviceId in extraDevices) {
|
|
|
|
[self.storageManager deleteSessionForContact:recipient.uniqueId deviceId:extraDeviceId.intValue];
|
|
|
|
}
|
|
|
|
|
|
|
|
[recipient removeDevices:[NSSet setWithArray:extraDevices]];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (missingDevices && missingDevices.count > 0) {
|
|
|
|
[recipient addDevices:[NSSet setWithArray:missingDevices]];
|
|
|
|
}
|
|
|
|
|
|
|
|
[recipient save];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)handleMessageSentLocally:(TSOutgoingMessage *)message
|
|
|
|
{
|
|
|
|
[self saveMessage:message withState:TSOutgoingMessageStateSent];
|
|
|
|
if (message.shouldSyncTranscript) {
|
|
|
|
message.hasSyncedTranscript = YES;
|
|
|
|
[self sendSyncTranscriptForMessage:message];
|
|
|
|
}
|
|
|
|
|
|
|
|
[self.disappearingMessagesJob setExpirationForMessage:message];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)handleMessageSentRemotely:(TSOutgoingMessage *)message sentAt:(uint64_t)sentAt
|
|
|
|
{
|
|
|
|
[self saveMessage:message withState:TSOutgoingMessageStateDelivered];
|
|
|
|
[self becomeConsistentWithDisappearingConfigurationForMessage:message];
|
|
|
|
[self.disappearingMessagesJob setExpirationForMessage:message expirationStartedAt:sentAt];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSOutgoingMessage *)outgoingMessage
|
|
|
|
{
|
|
|
|
[self.disappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage
|
|
|
|
contactsManager:self.contactsManager];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)handleSendToMyself:(TSOutgoingMessage *)outgoingMessage
|
|
|
|
{
|
2016-10-28 19:18:46 +02:00
|
|
|
[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;
|
|
|
|
}
|
|
|
|
|
2016-10-14 23:00:29 +02:00
|
|
|
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
TSContactThread *cThread =
|
|
|
|
[TSContactThread getOrCreateThreadWithContactId:[TSAccountManager localNumber] transaction:transaction];
|
|
|
|
[cThread saveWithTransaction:transaction];
|
|
|
|
TSIncomingMessage *incomingMessage =
|
|
|
|
[[TSIncomingMessage alloc] initWithTimestamp:(outgoingMessage.timestamp + 1)
|
|
|
|
inThread:cThread
|
|
|
|
authorId:[cThread contactIdentifier]
|
|
|
|
messageBody:outgoingMessage.body
|
|
|
|
attachmentIds:outgoingMessage.attachmentIds
|
|
|
|
expiresInSeconds:outgoingMessage.expiresInSeconds];
|
|
|
|
[incomingMessage saveWithTransaction:transaction];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)sendSyncTranscriptForMessage:(TSOutgoingMessage *)message
|
2016-10-08 01:17:38 +02:00
|
|
|
{
|
2016-10-14 23:00:29 +02:00
|
|
|
OWSOutgoingSentMessageTranscript *sentMessageTranscript =
|
|
|
|
[[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message];
|
|
|
|
|
|
|
|
[self sendMessage:sentMessageTranscript
|
|
|
|
recipient:[SignalRecipient selfRecipient]
|
|
|
|
thread:message.thread
|
|
|
|
attempts:OWSMessageSenderRetryAttempts
|
2016-10-08 01:17:38 +02:00
|
|
|
success:^{
|
2016-10-14 23:00:29 +02:00
|
|
|
DDLogInfo(@"Succesfully sent sync transcript.");
|
2016-10-08 01:17:38 +02:00
|
|
|
}
|
2016-10-14 23:00:29 +02:00
|
|
|
failure:^(NSError *error) {
|
|
|
|
DDLogInfo(@"Failed to send sync transcript:%@", error);
|
2016-10-08 01:17:38 +02:00
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
2016-10-14 23:00:29 +02:00
|
|
|
- (NSArray<NSDictionary *> *)deviceMessages:(TSOutgoingMessage *)message
|
|
|
|
forRecipient:(SignalRecipient *)recipient
|
|
|
|
inThread:(TSThread *)thread
|
|
|
|
{
|
|
|
|
NSMutableArray *messagesArray = [NSMutableArray arrayWithCapacity:recipient.devices.count];
|
|
|
|
NSData *plainText = [message buildPlainTextData];
|
|
|
|
|
|
|
|
for (NSNumber *deviceNumber in recipient.devices) {
|
|
|
|
@try {
|
|
|
|
// DEPRECATED - Remove after all clients have been upgraded.
|
|
|
|
BOOL isLegacyMessage = ![message isKindOfClass:[OWSOutgoingSyncMessage class]];
|
|
|
|
|
|
|
|
NSDictionary *messageDict = [self encryptedMessageWithPlaintext:plainText
|
|
|
|
toRecipient:recipient.uniqueId
|
|
|
|
deviceId:deviceNumber
|
|
|
|
keyingStorage:[TSStorageManager sharedManager]
|
|
|
|
legacy:isLegacyMessage];
|
|
|
|
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
|
|
|
|
legacy:(BOOL)isLegacymessage
|
|
|
|
{
|
|
|
|
if (![storage containsSession:identifier deviceId:[deviceNumber intValue]]) {
|
|
|
|
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
|
|
__block PreKeyBundle *bundle;
|
2016-11-03 21:10:06 +01:00
|
|
|
__block NSException *exception;
|
2016-10-14 23:00:29 +02:00
|
|
|
[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) {
|
|
|
|
DDLogError(@"Server replied on PreKeyBundle request with error: %@", error);
|
|
|
|
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
|
|
|
|
if (response.statusCode == 404) {
|
2016-11-03 21:10:06 +01:00
|
|
|
// Can't throw exception from within callback as it's probabably a different thread.
|
|
|
|
exception = [NSException exceptionWithName:OWSMessageSenderInvalidDeviceException
|
|
|
|
reason:@"Device not registered"
|
|
|
|
userInfo:nil];
|
2016-11-10 15:59:07 +01:00
|
|
|
} 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];
|
2016-10-14 23:00:29 +02:00
|
|
|
}
|
|
|
|
dispatch_semaphore_signal(sema);
|
|
|
|
}];
|
|
|
|
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
2016-11-03 21:10:06 +01:00
|
|
|
if (exception) {
|
|
|
|
@throw exception;
|
|
|
|
}
|
2016-10-14 23:00:29 +02:00
|
|
|
|
|
|
|
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:storage
|
|
|
|
recipientId:identifier
|
|
|
|
deviceId:[deviceNumber intValue]];
|
|
|
|
@try {
|
2016-11-01 21:47:22 +01:00
|
|
|
// Mutating session state is not thread safe.
|
|
|
|
@synchronized(self) {
|
|
|
|
[builder processPrekeyBundle:bundle];
|
|
|
|
}
|
2016-10-14 23:00:29 +02:00
|
|
|
} @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:storage
|
|
|
|
recipientId:identifier
|
|
|
|
deviceId:[deviceNumber intValue]];
|
|
|
|
|
2016-11-01 21:47:22 +01:00
|
|
|
// Mutating session state is not thread safe.
|
|
|
|
id<CipherMessage> encryptedMessage;
|
|
|
|
@synchronized (self) {
|
|
|
|
encryptedMessage = [cipher encryptMessage:[plainText paddedMessageBody]];
|
|
|
|
}
|
2016-10-14 23:00:29 +02:00
|
|
|
NSData *serializedMessage = encryptedMessage.serialized;
|
|
|
|
TSWhisperMessageType messageType = [self messageTypeForCipherMessage:encryptedMessage];
|
|
|
|
|
|
|
|
OWSMessageServiceParams *messageParams;
|
|
|
|
// DEPRECATED - Remove after all clients have been upgraded.
|
|
|
|
if (isLegacymessage) {
|
|
|
|
messageParams = [[OWSLegacyMessageServiceParams alloc] initWithType:messageType
|
|
|
|
recipientId:identifier
|
|
|
|
device:[deviceNumber intValue]
|
|
|
|
body:serializedMessage
|
|
|
|
registrationId:cipher.remoteRegistrationId];
|
|
|
|
} else {
|
|
|
|
messageParams = [[OWSMessageServiceParams alloc] initWithType:messageType
|
|
|
|
recipientId:identifier
|
|
|
|
device:[deviceNumber intValue]
|
|
|
|
content:serializedMessage
|
|
|
|
registrationId:cipher.remoteRegistrationId];
|
|
|
|
}
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:messageParams error:&error];
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
DDLogError(@"Error while making JSON dictionary of message: %@", error.debugDescription);
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
return jsonDict;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (TSWhisperMessageType)messageTypeForCipherMessage:(id<CipherMessage>)cipherMessage
|
|
|
|
{
|
|
|
|
if ([cipherMessage isKindOfClass:[PreKeyWhisperMessage class]]) {
|
|
|
|
return TSPreKeyWhisperMessageType;
|
|
|
|
} else if ([cipherMessage isKindOfClass:[WhisperMessage class]]) {
|
|
|
|
return TSEncryptedWhisperMessageType;
|
|
|
|
}
|
|
|
|
return TSUnknownMessageType;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)saveMessage:(TSOutgoingMessage *)message withState:(TSOutgoingMessageState)state
|
|
|
|
{
|
|
|
|
message.messageState = state;
|
|
|
|
[message save];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)saveMessage:(TSOutgoingMessage *)message withError:(NSError *)error
|
|
|
|
{
|
|
|
|
message.messageState = TSOutgoingMessageStateUnsent;
|
|
|
|
[message setSendingError:error];
|
|
|
|
[message save];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread
|
|
|
|
{
|
|
|
|
if (message.groupMetaMessage == TSGroupMessageDeliver) {
|
|
|
|
[self saveMessage:message withState:message.messageState];
|
|
|
|
} 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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)handleStaleDevicesWithResponse:(NSData *)responseData recipientId:(NSString *)identifier
|
|
|
|
{
|
|
|
|
dispatch_async([OWSDispatch sendingQueue], ^{
|
|
|
|
NSDictionary *serialization = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
|
|
|
|
NSArray *devices = serialization[@"staleDevices"];
|
|
|
|
|
|
|
|
if (!([devices count] > 0)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (NSUInteger i = 0; i < [devices count]; i++) {
|
|
|
|
int deviceNumber = [devices[i] intValue];
|
|
|
|
[[TSStorageManager sharedManager] deleteSessionForContact:identifier deviceId:deviceNumber];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)processException:(NSException *)exception
|
|
|
|
outgoingMessage:(TSOutgoingMessage *)message
|
|
|
|
inThread:(TSThread *)thread
|
|
|
|
{
|
|
|
|
DDLogWarn(@"%@ Got exception: %@", self.tag, exception);
|
|
|
|
|
|
|
|
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
TSErrorMessage *errorMessage;
|
|
|
|
|
2016-11-10 15:59:07 +01:00
|
|
|
if (message.groupMetaMessage == TSGroupMessageNone) {
|
2016-10-14 23:00:29 +02:00
|
|
|
// Only update this with exception if it is not a group message as group
|
|
|
|
// messages may except for one group
|
|
|
|
// send but not another and the UI doesn't know how to handle that
|
|
|
|
[message setMessageState:TSOutgoingMessageStateUnsent];
|
|
|
|
[message saveWithTransaction:transaction];
|
|
|
|
}
|
|
|
|
|
|
|
|
[errorMessage saveWithTransaction:transaction];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Logging
|
|
|
|
|
|
|
|
+ (NSString *)tag
|
|
|
|
{
|
|
|
|
return [NSString stringWithFormat:@"[%@]", self.class];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)tag
|
|
|
|
{
|
|
|
|
return self.class.tag;
|
|
|
|
}
|
|
|
|
|
2016-10-08 01:17:38 +02:00
|
|
|
@end
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|