From 49e2549f1a73c19e99d04074ef5a1d27aa90920d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 15 Apr 2019 11:05:08 -0400 Subject: [PATCH 1/7] Rework profile avatar upload. --- SignalMessaging/profiles/OWSProfileManager.m | 126 ++---------- .../src/Network/API/OWSUploadV2.h | 21 ++ .../src/Network/API/OWSUploadV2.m | 185 ++++++++++++++++++ SignalServiceKit/src/Util/OWSError.h | 1 + 4 files changed, 222 insertions(+), 111 deletions(-) create mode 100644 SignalServiceKit/src/Network/API/OWSUploadV2.h create mode 100644 SignalServiceKit/src/Network/API/OWSUploadV2.m diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index 1c6f7a6261..af73a38af6 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -23,6 +23,7 @@ #import #import #import +#import #import #import #import @@ -407,120 +408,23 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); [userProfile updateWithAvatarUrlPath:nil avatarFileName:nil dbConnection:self.dbConnection completion:nil]; }; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html - TSRequest *formRequest = [OWSRequestFactory profileAvatarUploadFormRequest]; + NSData *_Nullable encryptedAvatarData; + if (avatarData) { + encryptedAvatarData = [self encryptProfileData:avatarData]; + OWSAssertDebug(encryptedAvatarData.length > 0); + } - [self.networkManager makeRequest:formRequest - success:^(NSURLSessionDataTask *task, id formResponseObject) { - if (avatarData == nil) { - OWSLogDebug(@"successfully cleared avatar"); - clearLocalAvatar(); - successBlock(nil); - return; - } + [[OWSUploadV2 uploadAvatarToService:encryptedAvatarData clearLocalAvatar:clearLocalAvatar] + .thenInBackground(^(OWSUploadV2 *upload) { + OWSLogVerbose(@"Upload complete."); - if (![formResponseObject isKindOfClass:[NSDictionary class]]) { - OWSProdFail([OWSAnalyticsEvents profileManagerErrorAvatarUploadFormInvalidResponse]); - return failureBlock( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - NSDictionary *responseMap = formResponseObject; - OWSLogError(@"responseObject: %@", formResponseObject); + successBlock(upload.urlPath); + }) + .catchInBackground(^(NSError *error) { + OWSLogError(@"Failed: %@", error); - NSString *formAcl = responseMap[@"acl"]; - if (![formAcl isKindOfClass:[NSString class]] || formAcl.length < 1) { - OWSProdFail([OWSAnalyticsEvents profileManagerErrorAvatarUploadFormInvalidAcl]); - return failureBlock( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - NSString *formKey = responseMap[@"key"]; - if (![formKey isKindOfClass:[NSString class]] || formKey.length < 1) { - OWSProdFail([OWSAnalyticsEvents profileManagerErrorAvatarUploadFormInvalidKey]); - return failureBlock( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - NSString *formPolicy = responseMap[@"policy"]; - if (![formPolicy isKindOfClass:[NSString class]] || formPolicy.length < 1) { - OWSProdFail([OWSAnalyticsEvents profileManagerErrorAvatarUploadFormInvalidPolicy]); - return failureBlock( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - NSString *formAlgorithm = responseMap[@"algorithm"]; - if (![formAlgorithm isKindOfClass:[NSString class]] || formAlgorithm.length < 1) { - OWSProdFail([OWSAnalyticsEvents profileManagerErrorAvatarUploadFormInvalidAlgorithm]); - return failureBlock( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - NSString *formCredential = responseMap[@"credential"]; - if (![formCredential isKindOfClass:[NSString class]] || formCredential.length < 1) { - OWSProdFail([OWSAnalyticsEvents profileManagerErrorAvatarUploadFormInvalidCredential]); - return failureBlock( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - NSString *formDate = responseMap[@"date"]; - if (![formDate isKindOfClass:[NSString class]] || formDate.length < 1) { - OWSProdFail([OWSAnalyticsEvents profileManagerErrorAvatarUploadFormInvalidDate]); - return failureBlock( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - NSString *formSignature = responseMap[@"signature"]; - if (![formSignature isKindOfClass:[NSString class]] || formSignature.length < 1) { - OWSProdFail([OWSAnalyticsEvents profileManagerErrorAvatarUploadFormInvalidSignature]); - return failureBlock( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - - [self.avatarHTTPManager POST:@"" - parameters:nil - constructingBodyWithBlock:^(id _Nonnull formData) { - NSData * (^formDataForString)(NSString *formString) = ^(NSString *formString) { - return [formString dataUsingEncoding:NSUTF8StringEncoding]; - }; - - // We have to build up the form manually vs. simply passing in a paramaters dict - // because AWS is sensitive to the order of the form params (at least the "key" - // field must occur early on). - // For consistency, all fields are ordered here in a known working order. - [formData appendPartWithFormData:formDataForString(formKey) name:@"key"]; - [formData appendPartWithFormData:formDataForString(formAcl) name:@"acl"]; - [formData appendPartWithFormData:formDataForString(formAlgorithm) name:@"x-amz-algorithm"]; - [formData appendPartWithFormData:formDataForString(formCredential) name:@"x-amz-credential"]; - [formData appendPartWithFormData:formDataForString(formDate) name:@"x-amz-date"]; - [formData appendPartWithFormData:formDataForString(formPolicy) name:@"policy"]; - [formData appendPartWithFormData:formDataForString(formSignature) name:@"x-amz-signature"]; - [formData appendPartWithFormData:formDataForString(OWSMimeTypeApplicationOctetStream) - name:@"Content-Type"]; - NSData *encryptedAvatarData = [self encryptProfileData:avatarData]; - OWSAssertDebug(encryptedAvatarData.length > 0); - [formData appendPartWithFormData:encryptedAvatarData name:@"file"]; - - OWSLogVerbose(@"constructed body"); - } - progress:^(NSProgress *_Nonnull uploadProgress) { - OWSLogVerbose(@"avatar upload progress: %.2f%%", uploadProgress.fractionCompleted * 100); - } - success:^(NSURLSessionDataTask *_Nonnull uploadTask, id _Nullable responseObject) { - OWSLogInfo(@"successfully uploaded avatar with key: %@", formKey); - successBlock(formKey); - } - failure:^(NSURLSessionDataTask *_Nullable uploadTask, NSError *error) { - OWSLogError(@"uploading avatar failed with error: %@", error); - clearLocalAvatar(); - return failureBlock(error); - }]; - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - // Only clear the local avatar if we have a response. Otherwise, we - // had a network failure and probably didn't reach the service. - if (task.response != nil) { - clearLocalAvatar(); - } - - OWSLogError(@"Failed to get profile avatar upload form: %@", error); - return failureBlock(error); - }]; - }); + failureBlock(error); + }) retainUntilComplete]; } - (void)updateServiceWithProfileName:(nullable NSString *)localProfileName diff --git a/SignalServiceKit/src/Network/API/OWSUploadV2.h b/SignalServiceKit/src/Network/API/OWSUploadV2.h new file mode 100644 index 0000000000..76456973c9 --- /dev/null +++ b/SignalServiceKit/src/Network/API/OWSUploadV2.h @@ -0,0 +1,21 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class AnyPromise; + +// This class can be safely accessed and used from any thread. +@interface OWSUploadV2 : NSObject + +@property (nonatomic, nullable) NSString *urlPath; + +// On success, yields an instance of OWSUploadV2. ++ (AnyPromise *)uploadAvatarToService:(NSData *_Nullable)avatarData clearLocalAvatar:(dispatch_block_t)clearLocalAvatar; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/OWSUploadV2.m b/SignalServiceKit/src/Network/API/OWSUploadV2.m new file mode 100644 index 0000000000..333a8719dc --- /dev/null +++ b/SignalServiceKit/src/Network/API/OWSUploadV2.m @@ -0,0 +1,185 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSUploadV2.h" +#import +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSUploadV2 + +#pragma mark - Dependencies + +// TODO: Rename ++ (AFHTTPSessionManager *)uploadHTTPManager +{ + return [OWSSignalService sharedInstance].CDNSessionManager; +} + ++ (TSNetworkManager *)networkManager +{ + return SSKEnvironment.shared.networkManager; +} + +#pragma mark - + +// If avatarData is nil, we are clearing the avatar. ++ (AnyPromise *)uploadAvatarToService:(NSData *_Nullable)avatarData clearLocalAvatar:(dispatch_block_t)clearLocalAvatar +{ + OWSAssertDebug(avatarData == nil || avatarData.length > 0); + + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html + TSRequest *formRequest = [OWSRequestFactory profileAvatarUploadFormRequest]; + + [self.networkManager makeRequest:formRequest + success:^(NSURLSessionDataTask *task, id formResponseObject) { + if (avatarData == nil) { + OWSLogDebug(@"successfully cleared avatar"); + clearLocalAvatar(); + return resolve([OWSUploadV2 new]); + } + + if (![formResponseObject isKindOfClass:[NSDictionary class]]) { + OWSCFailDebug(@"Invalid response."); + return resolve( + OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); + } + NSDictionary *responseMap = formResponseObject; + + // TODO: urlPath? + [[self parseFormAndUpload:responseMap urlPath:@"" uploadData:avatarData] + .thenInBackground(^(OWSUploadV2 *upload) { + resolve(upload); + }) + .catchInBackground(^(NSError *error) { + clearLocalAvatar(); + + resolve(error); + }) retainUntilComplete]; + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + // Only clear the local avatar if we have a response. Otherwise, we + // had a network failure and probably didn't reach the service. + if (task.response != nil) { + clearLocalAvatar(); + } + + OWSLogError(@"Failed to get profile avatar upload form: %@", error); + resolve(error); + }]; + }); + }]; + return promise; +} + ++ (AnyPromise *)parseFormAndUpload:(NSDictionary *)formResponseObject + urlPath:(NSString *)urlPath + uploadData:(NSData *)uploadData +{ + OWSAssertDebug(uploadData.length > 0); + + if (![formResponseObject isKindOfClass:[NSDictionary class]]) { + OWSLogError(@"Invalid upload form."); + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + NSDictionary *responseMap = formResponseObject; + + NSString *formAcl = responseMap[@"acl"]; + if (![formAcl isKindOfClass:[NSString class]] || formAcl.length < 1) { + OWSLogError(@"Invalid upload form: acl."); + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + NSString *formKey = responseMap[@"key"]; + if (![formKey isKindOfClass:[NSString class]] || formKey.length < 1) { + OWSLogError(@"Invalid upload form: key."); + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + NSString *formPolicy = responseMap[@"policy"]; + if (![formPolicy isKindOfClass:[NSString class]] || formPolicy.length < 1) { + OWSLogError(@"Invalid upload form: policy."); + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + NSString *formAlgorithm = responseMap[@"algorithm"]; + if (![formAlgorithm isKindOfClass:[NSString class]] || formAlgorithm.length < 1) { + OWSLogError(@"Invalid upload form: algorithm."); + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + NSString *formCredential = responseMap[@"credential"]; + if (![formCredential isKindOfClass:[NSString class]] || formCredential.length < 1) { + OWSLogError(@"Invalid upload form: credential."); + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + NSString *formDate = responseMap[@"date"]; + if (![formDate isKindOfClass:[NSString class]] || formDate.length < 1) { + OWSLogError(@"Invalid upload form: date."); + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + NSString *formSignature = responseMap[@"signature"]; + if (![formSignature isKindOfClass:[NSString class]] || formSignature.length < 1) { + OWSLogError(@"Invalid upload form: signature."); + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [self.uploadHTTPManager POST:urlPath + parameters:nil + constructingBodyWithBlock:^(id formData) { + NSData * (^formDataForString)(NSString *formString) = ^(NSString *formString) { + return [formString dataUsingEncoding:NSUTF8StringEncoding]; + }; + + // We have to build up the form manually vs. simply passing in a paramaters dict + // because AWS is sensitive to the order of the form params (at least the "key" + // field must occur early on). + // For consistency, all fields are ordered here in a known working order. + [formData appendPartWithFormData:formDataForString(formKey) name:@"key"]; + [formData appendPartWithFormData:formDataForString(formAcl) name:@"acl"]; + [formData appendPartWithFormData:formDataForString(formAlgorithm) name:@"x-amz-algorithm"]; + [formData appendPartWithFormData:formDataForString(formCredential) name:@"x-amz-credential"]; + [formData appendPartWithFormData:formDataForString(formDate) name:@"x-amz-date"]; + [formData appendPartWithFormData:formDataForString(formPolicy) name:@"policy"]; + [formData appendPartWithFormData:formDataForString(formSignature) name:@"x-amz-signature"]; + [formData appendPartWithFormData:formDataForString(OWSMimeTypeApplicationOctetStream) + name:@"Content-Type"]; + [formData appendPartWithFormData:uploadData name:@"file"]; + + OWSLogVerbose(@"constructed body"); + } + progress:^(NSProgress *uploadProgress) { + OWSLogVerbose(@"Upload progress: %.2f%%", uploadProgress.fractionCompleted * 100); + } + success:^(NSURLSessionDataTask *uploadTask, id _Nullable responseObject) { + OWSLogInfo(@"Upload succeeded with key: %@", formKey); + OWSUploadV2 *upload = [OWSUploadV2 new]; + upload.urlPath = formKey; + return resolve(upload); + } + failure:^(NSURLSessionDataTask *_Nullable uploadTask, NSError *error) { + OWSLogError(@"Upload failed with error: %@", error); + resolve(error); + }]; + }]; + return promise; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSError.h b/SignalServiceKit/src/Util/OWSError.h index 5600d449c2..6363624d5e 100644 --- a/SignalServiceKit/src/Util/OWSError.h +++ b/SignalServiceKit/src/Util/OWSError.h @@ -52,6 +52,7 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) { OWSErrorCodeAvatarWriteFailed = 777425, OWSErrorCodeAvatarUploadFailed = 777426, OWSErrorCodeNoSessionForTransientMessage, + OWSErrorCodeUploadFailed, }; extern NSString *const OWSErrorRecipientIdentifierKey; From 2f4a233b6b580d15ec1e5a368a2caa9bf4704995 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 15 Apr 2019 15:12:37 -0400 Subject: [PATCH 2/7] Rework attachment upload. --- SignalMessaging/profiles/OWSProfileManager.m | 9 +- .../Attachments/OWSAttachmentDownloads.m | 2 +- .../src/Network/API/OWSUploadOperation.m | 120 +---- .../src/Network/API/OWSUploadV2.h | 27 +- .../src/Network/API/OWSUploadV2.m | 443 ++++++++++++++---- .../Network/API/Requests/OWSRequestFactory.m | 3 +- 6 files changed, 411 insertions(+), 193 deletions(-) diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index af73a38af6..eab361341c 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -414,8 +414,13 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSAssertDebug(encryptedAvatarData.length > 0); } - [[OWSUploadV2 uploadAvatarToService:encryptedAvatarData clearLocalAvatar:clearLocalAvatar] - .thenInBackground(^(OWSUploadV2 *upload) { + OWSAvatarUploadV2 *upload = [OWSAvatarUploadV2 new]; + [[upload uploadAvatarToService:encryptedAvatarData + clearLocalAvatar:clearLocalAvatar + progressBlock:^(NSProgress *progress){ + // Do nothing. + }] + .thenInBackground(^{ OWSLogVerbose(@"Upload complete."); successBlock(upload.urlPath); diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m index 4ca391c61f..e3f6646a6d 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSAttachmentDownloads.h" diff --git a/SignalServiceKit/src/Network/API/OWSUploadOperation.m b/SignalServiceKit/src/Network/API/OWSUploadOperation.m index 32a071c935..1297680b0f 100644 --- a/SignalServiceKit/src/Network/API/OWSUploadOperation.m +++ b/SignalServiceKit/src/Network/API/OWSUploadOperation.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSUploadOperation.h" @@ -10,10 +10,13 @@ #import "OWSError.h" #import "OWSOperation.h" #import "OWSRequestFactory.h" +#import "OWSUploadV2.h" #import "SSKEnvironment.h" #import "TSAttachmentStream.h" #import "TSNetworkManager.h" +#import #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -82,104 +85,27 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f; [self fireNotificationWithProgress:0]; - OWSLogDebug(@"alloc attachment: %@", self.attachmentId); - TSRequest *request = [OWSRequestFactory allocAttachmentRequest]; - [self.networkManager makeRequest:request - success:^(NSURLSessionDataTask *task, id responseObject) { - if (![responseObject isKindOfClass:[NSDictionary class]]) { - OWSLogError(@"unexpected response from server: %@", responseObject); - NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); + OWSAttachmentUploadV2 *upload = [OWSAttachmentUploadV2 new]; + [[upload uploadAttachmentToService:attachmentStream + progressBlock:^(NSProgress *uploadProgress) { + [self fireNotificationWithProgress:uploadProgress.fractionCompleted]; + }] + .thenInBackground(^{ + attachmentStream.encryptionKey = upload.encryptionKey; + attachmentStream.digest = upload.digest; + attachmentStream.serverId = upload.serverId; + attachmentStream.isUploaded = YES; + [attachmentStream saveAsyncWithCompletionBlock:^{ + [self reportSuccess]; + }]; + }) + .catchInBackground(^(NSError *error) { + OWSLogError(@"Failed: %@", error); + error.isRetryable = YES; + [self reportError:error]; - return; - } - - NSDictionary *responseDict = (NSDictionary *)responseObject; - UInt64 serverId = ((NSDecimalNumber *)[responseDict objectForKey:@"id"]).unsignedLongLongValue; - NSString *location = [responseDict objectForKey:@"location"]; - - dispatch_async([OWSDispatch attachmentsQueue], ^{ - [self uploadWithServerId:serverId location:location attachmentStream:attachmentStream]; - }); - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - OWSLogError(@"Failed to allocate attachment with error: %@", error); - error.isRetryable = YES; - [self reportError:error]; - }]; -} - -- (void)uploadWithServerId:(UInt64)serverId - location:(NSString *)location - attachmentStream:(TSAttachmentStream *)attachmentStream -{ - OWSLogDebug(@"started uploading data for attachment: %@", self.attachmentId); - NSError *error; - NSData *attachmentData = [attachmentStream readDataFromFileWithError:&error]; - if (error) { - OWSLogError(@"Failed to read attachment data with error: %@", error); - error.isRetryable = YES; - [self reportError:error]; - return; - } - - NSData *encryptionKey; - NSData *digest; - NSData *_Nullable encryptedAttachmentData = - [Cryptography encryptAttachmentData:attachmentData outKey:&encryptionKey outDigest:&digest]; - if (!encryptedAttachmentData) { - OWSFailDebug(@"could not encrypt attachment data."); - error = OWSErrorMakeFailedToSendOutgoingMessageError(); - error.isRetryable = YES; - [self reportError:error]; - return; - } - attachmentStream.encryptionKey = encryptionKey; - attachmentStream.digest = digest; - - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:location]]; - request.HTTPMethod = @"PUT"; - [request setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; - - AFURLSessionManager *manager = [[AFURLSessionManager alloc] - initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; - - NSURLSessionUploadTask *uploadTask; - uploadTask = [manager uploadTaskWithRequest:request - fromData:encryptedAttachmentData - progress:^(NSProgress *_Nonnull uploadProgress) { - [self fireNotificationWithProgress:uploadProgress.fractionCompleted]; - } - completionHandler:^(NSURLResponse *_Nonnull response, id _Nullable responseObject, NSError *_Nullable error) { - OWSAssertIsOnMainThread(); - if (error) { - error.isRetryable = YES; - [self reportError:error]; - return; - } - - NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; - BOOL isValidResponse = (statusCode >= 200) && (statusCode < 400); - if (!isValidResponse) { - OWSLogError(@"Unexpected server response: %d", (int)statusCode); - NSError *invalidResponseError = OWSErrorMakeUnableToProcessServerResponseError(); - invalidResponseError.isRetryable = YES; - [self reportError:invalidResponseError]; - return; - } - - OWSLogInfo(@"Uploaded attachment: %p serverId: %llu, byteCount: %u", - attachmentStream.uniqueId, - attachmentStream.serverId, - attachmentStream.byteCount); - attachmentStream.serverId = serverId; - attachmentStream.isUploaded = YES; - [attachmentStream saveAsyncWithCompletionBlock:^{ - [self reportSuccess]; - }]; - }]; - - [uploadTask resume]; + }) retainUntilComplete]; } - (void)fireNotificationWithProgress:(CGFloat)aProgress diff --git a/SignalServiceKit/src/Network/API/OWSUploadV2.h b/SignalServiceKit/src/Network/API/OWSUploadV2.h index 76456973c9..2993a44e7c 100644 --- a/SignalServiceKit/src/Network/API/OWSUploadV2.h +++ b/SignalServiceKit/src/Network/API/OWSUploadV2.h @@ -7,14 +7,35 @@ NS_ASSUME_NONNULL_BEGIN @class AnyPromise; +@class TSAttachmentStream; + +typedef void (^UploadProgressBlock)(NSProgress *progress); // This class can be safely accessed and used from any thread. -@interface OWSUploadV2 : NSObject +@interface OWSAvatarUploadV2 : NSObject +// This is set on success for non-nil uploads. @property (nonatomic, nullable) NSString *urlPath; -// On success, yields an instance of OWSUploadV2. -+ (AnyPromise *)uploadAvatarToService:(NSData *_Nullable)avatarData clearLocalAvatar:(dispatch_block_t)clearLocalAvatar; +- (AnyPromise *)uploadAvatarToService:(NSData *_Nullable)avatarData + clearLocalAvatar:(dispatch_block_t)clearLocalAvatar + progressBlock:(UploadProgressBlock)progressBlock; + +@end + +#pragma mark - + +// This class can be safely accessed and used from any thread. +@interface OWSAttachmentUploadV2 : NSObject + + +// These properties are set on success. +@property (nonatomic, nullable) NSData *encryptionKey; +@property (nonatomic, nullable) NSData *digest; +@property (nonatomic) UInt64 serverId; + +- (AnyPromise *)uploadAttachmentToService:(TSAttachmentStream *)attachmentStream + progressBlock:(UploadProgressBlock)progressBlock; @end diff --git a/SignalServiceKit/src/Network/API/OWSUploadV2.m b/SignalServiceKit/src/Network/API/OWSUploadV2.m index 333a8719dc..391012f693 100644 --- a/SignalServiceKit/src/Network/API/OWSUploadV2.m +++ b/SignalServiceKit/src/Network/API/OWSUploadV2.m @@ -4,62 +4,200 @@ #import "OWSUploadV2.h" #import +#import +#import #import #import #import #import #import #import +#import #import NS_ASSUME_NONNULL_BEGIN -@implementation OWSUploadV2 - -#pragma mark - Dependencies - -// TODO: Rename -+ (AFHTTPSessionManager *)uploadHTTPManager +void AppendMultipartFormPath(id formData, NSString *name, NSString *dataString) { - return [OWSSignalService sharedInstance].CDNSessionManager; -} + NSData *data = [dataString dataUsingEncoding:NSUTF8StringEncoding]; -+ (TSNetworkManager *)networkManager -{ - return SSKEnvironment.shared.networkManager; + [formData appendPartWithFormData:data name:name]; } #pragma mark - +@interface OWSUploadForm : NSObject + +// These properties will bet set for all uploads. +@property (nonatomic) NSString *formAcl; +@property (nonatomic) NSString *formKey; +@property (nonatomic) NSString *formPolicy; +@property (nonatomic) NSString *formAlgorithm; +@property (nonatomic) NSString *formCredential; +@property (nonatomic) NSString *formDate; +@property (nonatomic) NSString *formSignature; + +// These properties will bet set for all attachment uploads. +@property (nonatomic, nullable) NSNumber *attachmentId; +@property (nonatomic, nullable) NSString *attachmentIdString; + +@end + +#pragma mark - + +// See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html +@implementation OWSUploadForm + ++ (nullable OWSUploadForm *)parse:(NSDictionary *)formResponseObject +{ + if (![formResponseObject isKindOfClass:[NSDictionary class]]) { + OWSFailDebug(@"Invalid upload form."); + return nil; + } + NSDictionary *responseMap = formResponseObject; + + NSString *_Nullable formAcl = responseMap[@"acl"]; + if (![formAcl isKindOfClass:[NSString class]] || formAcl.length < 1) { + OWSFailDebug(@"Invalid upload form: acl."); + return nil; + } + NSString *_Nullable formKey = responseMap[@"key"]; + if (![formKey isKindOfClass:[NSString class]] || formKey.length < 1) { + OWSFailDebug(@"Invalid upload form: key."); + return nil; + } + NSString *_Nullable formPolicy = responseMap[@"policy"]; + if (![formPolicy isKindOfClass:[NSString class]] || formPolicy.length < 1) { + OWSFailDebug(@"Invalid upload form: policy."); + return nil; + } + NSString *_Nullable formAlgorithm = responseMap[@"algorithm"]; + if (![formAlgorithm isKindOfClass:[NSString class]] || formAlgorithm.length < 1) { + OWSFailDebug(@"Invalid upload form: algorithm."); + return nil; + } + NSString *_Nullable formCredential = responseMap[@"credential"]; + if (![formCredential isKindOfClass:[NSString class]] || formCredential.length < 1) { + OWSFailDebug(@"Invalid upload form: credential."); + return nil; + } + NSString *_Nullable formDate = responseMap[@"date"]; + if (![formDate isKindOfClass:[NSString class]] || formDate.length < 1) { + OWSFailDebug(@"Invalid upload form: date."); + return nil; + } + NSString *_Nullable formSignature = responseMap[@"signature"]; + if (![formSignature isKindOfClass:[NSString class]] || formSignature.length < 1) { + OWSFailDebug(@"Invalid upload form: signature."); + return nil; + } + + NSNumber *_Nullable attachmentId = responseMap[@"attachmentId"]; + if (attachmentId == nil) { + // This value is optional. + } else if (![attachmentId isKindOfClass:[NSNumber class]]) { + OWSFailDebug(@"Invalid upload form: attachmentId."); + return nil; + } + NSString *_Nullable attachmentIdString = responseMap[@"attachmentIdString"]; + if (attachmentIdString == nil) { + // This value is optional. + } else if (![attachmentIdString isKindOfClass:[NSString class]] || attachmentIdString.length < 1) { + OWSFailDebug(@"Invalid upload form: attachmentIdString."); + return nil; + } + + OWSUploadForm *form = [OWSUploadForm new]; + + // Required properties. + form.formAcl = formAcl; + form.formKey = formKey; + form.formPolicy = formPolicy; + form.formAlgorithm = formAlgorithm; + form.formCredential = formCredential; + form.formDate = formDate; + form.formSignature = formSignature; + + // Optional properties. + form.attachmentId = attachmentId; + form.attachmentIdString = attachmentIdString; + + return form; +} + +- (void)appendToForm:(id)formData +{ + // We have to build up the form manually vs. simply passing in a paramaters dict + // because AWS is sensitive to the order of the form params (at least the "key" + // field must occur early on). + // + // For consistency, all fields are ordered here in a known working order. + AppendMultipartFormPath(formData, @"key", self.formKey); + AppendMultipartFormPath(formData, @"acl", self.formAcl); + AppendMultipartFormPath(formData, @"x-amz-algorithm", self.formAlgorithm); + AppendMultipartFormPath(formData, @"x-amz-credential", self.formCredential); + AppendMultipartFormPath(formData, @"x-amz-date", self.formDate); + AppendMultipartFormPath(formData, @"policy", self.formPolicy); + AppendMultipartFormPath(formData, @"x-amz-signature", self.formSignature); +} + +@end + +#pragma mark - + +@interface OWSAvatarUploadV2 () + +@property (nonatomic, nullable) NSData *avatarData; + +@end + +#pragma mark - + +@implementation OWSAvatarUploadV2 + +#pragma mark - Dependencies + +- (AFHTTPSessionManager *)uploadHTTPManager +{ + return [OWSSignalService sharedInstance].CDNSessionManager; +} + +- (TSNetworkManager *)networkManager +{ + return SSKEnvironment.shared.networkManager; +} + +#pragma mark - Avatars + // If avatarData is nil, we are clearing the avatar. -+ (AnyPromise *)uploadAvatarToService:(NSData *_Nullable)avatarData clearLocalAvatar:(dispatch_block_t)clearLocalAvatar +- (AnyPromise *)uploadAvatarToService:(NSData *_Nullable)avatarData + clearLocalAvatar:(dispatch_block_t)clearLocalAvatar + progressBlock:(UploadProgressBlock)progressBlock { OWSAssertDebug(avatarData == nil || avatarData.length > 0); + self.avatarData = avatarData; + __weak OWSAvatarUploadV2 *weakSelf = self; AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html TSRequest *formRequest = [OWSRequestFactory profileAvatarUploadFormRequest]; - [self.networkManager makeRequest:formRequest success:^(NSURLSessionDataTask *task, id formResponseObject) { + OWSAvatarUploadV2 *_Nullable strongSelf = weakSelf; + if (!strongSelf) { + return; + } + if (avatarData == nil) { OWSLogDebug(@"successfully cleared avatar"); clearLocalAvatar(); - return resolve([OWSUploadV2 new]); + return resolve(@(1)); } - if (![formResponseObject isKindOfClass:[NSDictionary class]]) { - OWSCFailDebug(@"Invalid response."); - return resolve( - OWSErrorWithCodeDescription(OWSErrorCodeAvatarUploadFailed, @"Avatar upload failed.")); - } - NSDictionary *responseMap = formResponseObject; - - // TODO: urlPath? - [[self parseFormAndUpload:responseMap urlPath:@"" uploadData:avatarData] - .thenInBackground(^(OWSUploadV2 *upload) { - resolve(upload); + // TODO: Should we use a non-empty urlPath? + [[strongSelf parseFormAndUpload:formResponseObject urlPath:@"" progressBlock:progressBlock] + .thenInBackground(^{ + return resolve(@(1)); }) .catchInBackground(^(NSError *error) { clearLocalAvatar(); @@ -82,95 +220,224 @@ NS_ASSUME_NONNULL_BEGIN return promise; } -+ (AnyPromise *)parseFormAndUpload:(NSDictionary *)formResponseObject +- (AnyPromise *)parseFormAndUpload:(id)formResponseObject urlPath:(NSString *)urlPath - uploadData:(NSData *)uploadData + progressBlock:(UploadProgressBlock)progressBlock { - OWSAssertDebug(uploadData.length > 0); - if (![formResponseObject isKindOfClass:[NSDictionary class]]) { OWSLogError(@"Invalid upload form."); return [AnyPromise promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; } - NSDictionary *responseMap = formResponseObject; - NSString *formAcl = responseMap[@"acl"]; - if (![formAcl isKindOfClass:[NSString class]] || formAcl.length < 1) { - OWSLogError(@"Invalid upload form: acl."); - return [AnyPromise - promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; - } - NSString *formKey = responseMap[@"key"]; - if (![formKey isKindOfClass:[NSString class]] || formKey.length < 1) { - OWSLogError(@"Invalid upload form: key."); - return [AnyPromise - promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; - } - NSString *formPolicy = responseMap[@"policy"]; - if (![formPolicy isKindOfClass:[NSString class]] || formPolicy.length < 1) { - OWSLogError(@"Invalid upload form: policy."); - return [AnyPromise - promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; - } - NSString *formAlgorithm = responseMap[@"algorithm"]; - if (![formAlgorithm isKindOfClass:[NSString class]] || formAlgorithm.length < 1) { - OWSLogError(@"Invalid upload form: algorithm."); - return [AnyPromise - promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; - } - NSString *formCredential = responseMap[@"credential"]; - if (![formCredential isKindOfClass:[NSString class]] || formCredential.length < 1) { - OWSLogError(@"Invalid upload form: credential."); - return [AnyPromise - promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; - } - NSString *formDate = responseMap[@"date"]; - if (![formDate isKindOfClass:[NSString class]] || formDate.length < 1) { - OWSLogError(@"Invalid upload form: date."); - return [AnyPromise - promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; - } - NSString *formSignature = responseMap[@"signature"]; - if (![formSignature isKindOfClass:[NSString class]] || formSignature.length < 1) { - OWSLogError(@"Invalid upload form: signature."); + OWSUploadForm *_Nullable form = [OWSUploadForm parse:formResponseObject]; + if (!form) { return [AnyPromise promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; } + self.urlPath = form.formKey; + + __weak OWSAvatarUploadV2 *weakSelf = self; AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { [self.uploadHTTPManager POST:urlPath parameters:nil constructingBodyWithBlock:^(id formData) { - NSData * (^formDataForString)(NSString *formString) = ^(NSString *formString) { - return [formString dataUsingEncoding:NSUTF8StringEncoding]; - }; + OWSAvatarUploadV2 *_Nullable strongSelf = weakSelf; + if (!strongSelf) { + return; + } // We have to build up the form manually vs. simply passing in a paramaters dict // because AWS is sensitive to the order of the form params (at least the "key" // field must occur early on). + // // For consistency, all fields are ordered here in a known working order. - [formData appendPartWithFormData:formDataForString(formKey) name:@"key"]; - [formData appendPartWithFormData:formDataForString(formAcl) name:@"acl"]; - [formData appendPartWithFormData:formDataForString(formAlgorithm) name:@"x-amz-algorithm"]; - [formData appendPartWithFormData:formDataForString(formCredential) name:@"x-amz-credential"]; - [formData appendPartWithFormData:formDataForString(formDate) name:@"x-amz-date"]; - [formData appendPartWithFormData:formDataForString(formPolicy) name:@"policy"]; - [formData appendPartWithFormData:formDataForString(formSignature) name:@"x-amz-signature"]; - [formData appendPartWithFormData:formDataForString(OWSMimeTypeApplicationOctetStream) - name:@"Content-Type"]; + [form appendToForm:formData]; + AppendMultipartFormPath(formData, @"Content-Type", OWSMimeTypeApplicationOctetStream); + + NSData *_Nullable uploadData = strongSelf.avatarData; + if (uploadData.length < 1) { + OWSCFailDebug(@"Could not load upload data."); + return resolve( + OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Could not load upload data.")); + } + OWSCAssertDebug(uploadData.length > 0); [formData appendPartWithFormData:uploadData name:@"file"]; OWSLogVerbose(@"constructed body"); } - progress:^(NSProgress *uploadProgress) { - OWSLogVerbose(@"Upload progress: %.2f%%", uploadProgress.fractionCompleted * 100); + progress:^(NSProgress *progress) { + OWSLogVerbose(@"Upload progress: %.2f%%", progress.fractionCompleted * 100); + + progressBlock(progress); } success:^(NSURLSessionDataTask *uploadTask, id _Nullable responseObject) { - OWSLogInfo(@"Upload succeeded with key: %@", formKey); - OWSUploadV2 *upload = [OWSUploadV2 new]; - upload.urlPath = formKey; - return resolve(upload); + OWSLogInfo(@"Upload succeeded with key: %@", form.formKey); + return resolve(@(1)); + } + failure:^(NSURLSessionDataTask *_Nullable uploadTask, NSError *error) { + OWSLogError(@"Upload failed with error: %@", error); + resolve(error); + }]; + }]; + return promise; +} + +@end + +#pragma mark - Attachments + +@interface OWSAttachmentUploadV2 () + +@property (nonatomic) TSAttachmentStream *attachmentStream; + +@end + +#pragma mark - + +@implementation OWSAttachmentUploadV2 + +#pragma mark - Dependencies + +- (AFHTTPSessionManager *)uploadHTTPManager +{ + return [OWSSignalService sharedInstance].CDNSessionManager; +} + +- (TSNetworkManager *)networkManager +{ + return SSKEnvironment.shared.networkManager; +} + +#pragma mark - + +- (nullable NSData *)attachmentData +{ + OWSAssertDebug(self.attachmentStream); + + NSData *encryptionKey; + NSData *digest; + NSError *error; + NSData *attachmentData = [self.attachmentStream readDataFromFileWithError:&error]; + if (error) { + OWSLogError(@"Failed to read attachment data with error: %@", error); + return nil; + } + + NSData *_Nullable encryptedAttachmentData = + [Cryptography encryptAttachmentData:attachmentData outKey:&encryptionKey outDigest:&digest]; + if (!encryptedAttachmentData) { + OWSFailDebug(@"could not encrypt attachment data."); + return nil; + } + + self.encryptionKey = encryptionKey; + self.digest = digest; + + return encryptedAttachmentData; +} + +// On success, yields an instance of OWSUploadV2. +- (AnyPromise *)uploadAttachmentToService:(TSAttachmentStream *)attachmentStream + progressBlock:(UploadProgressBlock)progressBlock +{ + OWSAssertDebug(attachmentStream); + + self.attachmentStream = attachmentStream; + + __weak OWSAttachmentUploadV2 *weakSelf = self; + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + TSRequest *formRequest = [OWSRequestFactory allocAttachmentRequest]; + [self.networkManager makeRequest:formRequest + success:^(NSURLSessionDataTask *task, id formResponseObject) { + OWSAttachmentUploadV2 *_Nullable strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + [[strongSelf parseFormAndUpload:formResponseObject + urlPath:@"attachments/" + progressBlock:progressBlock] + .thenInBackground(^{ + resolve(@(1)); + }) + .catchInBackground(^(NSError *error) { + resolve(error); + }) retainUntilComplete]; + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSLogError(@"Failed to get profile avatar upload form: %@", error); + resolve(error); + }]; + }); + }]; + return promise; +} + +#pragma mark - + +- (AnyPromise *)parseFormAndUpload:(id)formResponseObject + urlPath:(NSString *)urlPath + progressBlock:(UploadProgressBlock)progressBlock +{ + + OWSUploadForm *_Nullable form = [OWSUploadForm parse:formResponseObject]; + if (!form) { + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + UInt64 serverId = form.attachmentId.unsignedLongLongValue; + if (serverId < 1) { + return [AnyPromise + promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; + } + + self.serverId = serverId; + + __weak OWSAttachmentUploadV2 *weakSelf = self; + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [self.uploadHTTPManager POST:urlPath + parameters:nil + constructingBodyWithBlock:^(id formData) { + OWSAttachmentUploadV2 *_Nullable strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + // We have to build up the form manually vs. simply passing in a paramaters dict + // because AWS is sensitive to the order of the form params (at least the "key" + // field must occur early on). + // + // For consistency, all fields are ordered here in a known working order. + [form appendToForm:formData]; + AppendMultipartFormPath(formData, @"Content-Type", OWSMimeTypeApplicationOctetStream); + + NSData *_Nullable uploadData = [strongSelf attachmentData]; + if (uploadData.length < 1) { + OWSCFailDebug(@"Could not load upload data."); + return resolve( + OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Could not load upload data.")); + } + OWSAssertDebug(uploadData.length > 0); + [formData appendPartWithFormData:uploadData name:@"file"]; + + OWSLogVerbose(@"constructed body"); + } + progress:^(NSProgress *progress) { + OWSLogVerbose(@"Upload progress: %.2f%%", progress.fractionCompleted * 100); + + progressBlock(progress); + } + success:^(NSURLSessionDataTask *uploadTask, id _Nullable responseObject) { + OWSAttachmentUploadV2 *_Nullable strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + OWSLogInfo(@"Upload succeeded with key: %@", form.formKey); + return resolve(@(1)); } failure:^(NSURLSessionDataTask *_Nullable uploadTask, NSError *error) { OWSLogError(@"Upload failed with error: %@", error); diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m index 68214a7d71..73ffad3894 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m @@ -141,8 +141,7 @@ NS_ASSUME_NONNULL_BEGIN + (TSRequest *)allocAttachmentRequest { - NSString *path = [NSString stringWithFormat:@"%@", textSecureAttachmentsAPI]; - return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; + return [TSRequest requestWithUrl:[NSURL URLWithString:@"/v2/attachments/form/upload"] method:@"GET" parameters:@{}]; } + (TSRequest *)attachmentRequestWithAttachmentId:(UInt64)attachmentId From 010afcfb61e48218739f573e977a35007e68e653 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 15 Apr 2019 15:21:43 -0400 Subject: [PATCH 3/7] Rework attachment upload. --- .../Messages/Attachments/TSAttachmentStream.h | 5 +++++ .../Messages/Attachments/TSAttachmentStream.m | 22 +++++++++++++++++++ .../src/Network/API/OWSUploadOperation.m | 13 +++++------ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h index 6745e47826..cbade66106 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h @@ -127,6 +127,11 @@ NS_SWIFT_NAME(init(uniqueId:albumMessageId:attachmentSchemaVersion:attachmentTyp #pragma mark - Update With... Methods +- (void)updateAsUploadedWithEncryptionKey:(NSData *)encryptionKey + digest:(NSData *)digest + serverId:(UInt64)serverId + completion:(dispatch_block_t)completion; + - (nullable TSAttachmentStream *)cloneAsThumbnail; #pragma mark - Protobuf diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index a5d4aae868..a799b2be3c 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -847,6 +847,28 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail); #pragma mark - Update With... Methods +- (void)updateAsUploadedWithEncryptionKey:(NSData *)encryptionKey + digest:(NSData *)digest + serverId:(UInt64)serverId + completion:(dispatch_block_t)completion +{ + OWSAssertDebug(encryptionKey.length > 0); + OWSAssertDebug(digest.length > 0); + OWSAssertDebug(serverId > 0); + + [[self dbReadWriteConnection] + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSAttachmentStream *attachment) { + [attachment setEncryptionKey:encryptionKey]; + [attachment setDigest:digest]; + [attachment setServerId:serverId]; + [attachment setIsUploaded:YES]; + }]; + } + completionBlock:completion]; +} + - (nullable TSAttachmentStream *)cloneAsThumbnail { if (!self.isValidVisualMedia) { diff --git a/SignalServiceKit/src/Network/API/OWSUploadOperation.m b/SignalServiceKit/src/Network/API/OWSUploadOperation.m index 1297680b0f..252b49232e 100644 --- a/SignalServiceKit/src/Network/API/OWSUploadOperation.m +++ b/SignalServiceKit/src/Network/API/OWSUploadOperation.m @@ -91,13 +91,12 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f; [self fireNotificationWithProgress:uploadProgress.fractionCompleted]; }] .thenInBackground(^{ - attachmentStream.encryptionKey = upload.encryptionKey; - attachmentStream.digest = upload.digest; - attachmentStream.serverId = upload.serverId; - attachmentStream.isUploaded = YES; - [attachmentStream saveAsyncWithCompletionBlock:^{ - [self reportSuccess]; - }]; + [attachmentStream updateAsUploadedWithEncryptionKey:upload.encryptionKey + digest:upload.digest + serverId:upload.serverId + completion:^{ + [self reportSuccess]; + }]; }) .catchInBackground(^(NSError *error) { OWSLogError(@"Failed: %@", error); From fe8dd2a273e4638765c584e2ceef74e6012362cb Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 15 Apr 2019 15:52:20 -0400 Subject: [PATCH 4/7] Rework attachment download. --- .../Attachments/OWSAttachmentDownloads.m | 96 +++++++------------ 1 file changed, 33 insertions(+), 63 deletions(-) diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m index e3f6646a6d..c76fe8e79d 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m @@ -23,6 +23,7 @@ #import "TSThread.h" #import #import +#import #import #import @@ -100,7 +101,6 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); return SSKEnvironment.shared.networkManager; } - #pragma mark - - (instancetype)init @@ -373,63 +373,28 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); if (attachmentPointer.serverId < 100) { OWSLogError(@"Suspicious attachment id: %llu", (unsigned long long)attachmentPointer.serverId); } - TSRequest *request = [OWSRequestFactory attachmentRequestWithAttachmentId:attachmentPointer.serverId]; - - [self.networkManager makeRequest:request - success:^(NSURLSessionDataTask *task, id responseObject) { - if (![responseObject isKindOfClass:[NSDictionary class]]) { - OWSLogError(@"Failed retrieval of attachment. Response had unexpected format."); - NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); - return markAndHandleFailure(error); + dispatch_async([OWSDispatch attachmentsQueue], ^{ + [self downloadJob:job + success:^(NSString *encryptedDataFilePath) { + [self decryptAttachmentPath:encryptedDataFilePath + attachmentPointer:attachmentPointer + success:markAndHandleSuccess + failure:markAndHandleFailure]; } - NSString *location = [(NSDictionary *)responseObject objectForKey:@"location"]; - if (!location) { - OWSLogError(@"Failed retrieval of attachment. Response had no location."); - NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); - return markAndHandleFailure(error); - } - - dispatch_async([OWSDispatch attachmentsQueue], ^{ - [self downloadFromLocation:location - job:job - success:^(NSString *encryptedDataFilePath) { - [self decryptAttachmentPath:encryptedDataFilePath - attachmentPointer:attachmentPointer - success:markAndHandleSuccess - failure:markAndHandleFailure]; - } - failure:^(NSURLSessionTask *_Nullable task, NSError *error) { - if (attachmentPointer.serverId < 100) { - // This looks like the symptom of the "frequent 404 - // downloading attachments with low server ids". - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; - NSInteger statusCode = [httpResponse statusCode]; - OWSFailDebug(@"%d Failure with suspicious attachment id: %llu, %@", - (int)statusCode, - (unsigned long long)attachmentPointer.serverId, - error); - } - markAndHandleFailure(error); - }]; - }); - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - if (!IsNSErrorNetworkFailure(error)) { - OWSProdError([OWSAnalyticsEvents errorAttachmentRequestFailed]); - } - OWSLogError(@"Failed retrieval of attachment with error: %@", error); - if (attachmentPointer.serverId < 100) { - // This _shouldn't_ be the symptom of the "frequent 404 - // downloading attachments with low server ids". - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; - NSInteger statusCode = [httpResponse statusCode]; - OWSFailDebug(@"%d Failure with suspicious attachment id: %llu, %@", - (int)statusCode, - (unsigned long long)attachmentPointer.serverId, - error); - } - return markAndHandleFailure(error); - }]; + failure:^(NSURLSessionTask *_Nullable task, NSError *error) { + if (attachmentPointer.serverId < 100) { + // This looks like the symptom of the "frequent 404 + // downloading attachments with low server ids". + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; + NSInteger statusCode = [httpResponse statusCode]; + OWSFailDebug(@"%d Failure with suspicious attachment id: %llu, %@", + (int)statusCode, + (unsigned long long)attachmentPointer.serverId, + error); + } + markAndHandleFailure(error); + }]; + }); } - (void)decryptAttachmentPath:(NSString *)encryptedDataFilePath @@ -515,20 +480,25 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); return _serialQueue; } -- (void)downloadFromLocation:(NSString *)location - job:(OWSAttachmentDownloadJob *)job - success:(void (^)(NSString *encryptedDataPath))successHandler - failure:(void (^)(NSURLSessionTask *_Nullable task, NSError *error))failureHandlerParam +- (void)downloadJob:(OWSAttachmentDownloadJob *)job + success:(void (^)(NSString *encryptedDataPath))successHandler + failure:(void (^)(NSURLSessionTask *_Nullable task, NSError *error))failureHandlerParam { OWSAssertDebug(job); + TSAttachmentPointer *attachmentPointer = job.attachmentPointer; - AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; + AFHTTPSessionManager *manager = [OWSSignalService sharedInstance].CDNSessionManager; manager.requestSerializer = [AFHTTPRequestSerializer serializer]; [manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + NSString *urlPath = [NSString stringWithFormat:@"attachments/%llu", attachmentPointer.serverId]; + OWSLogVerbose(@"urlPath: %@", urlPath); + NSURL *url = [[NSURL alloc] initWithString:urlPath relativeToURL:manager.baseURL]; + OWSLogVerbose(@"url.absoluteString: %@", url.absoluteString); + // We want to avoid large downloads from a compromised or buggy service. const long kMaxDownloadSize = 150 * 1024 * 1024; __block BOOL hasCheckedContentLength = NO; @@ -551,7 +521,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); NSString *method = @"GET"; NSError *serializationError = nil; NSMutableURLRequest *request = [manager.requestSerializer requestWithMethod:method - URLString:location + URLString:url.absoluteString parameters:nil error:&serializationError]; if (serializationError) { From 6d0c23719ef9482f713280d6fce04cb59f29f08f Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 15 Apr 2019 16:00:14 -0400 Subject: [PATCH 5/7] Clean up ahead of PR. --- SignalMessaging/profiles/OWSProfileManager.m | 40 ++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index eab361341c..3ef7013ed7 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -408,28 +408,30 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); [userProfile updateWithAvatarUrlPath:nil avatarFileName:nil dbConnection:self.dbConnection completion:nil]; }; - NSData *_Nullable encryptedAvatarData; - if (avatarData) { - encryptedAvatarData = [self encryptProfileData:avatarData]; - OWSAssertDebug(encryptedAvatarData.length > 0); - } + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *_Nullable encryptedAvatarData; + if (avatarData) { + encryptedAvatarData = [self encryptProfileData:avatarData]; + OWSAssertDebug(encryptedAvatarData.length > 0); + } - OWSAvatarUploadV2 *upload = [OWSAvatarUploadV2 new]; - [[upload uploadAvatarToService:encryptedAvatarData - clearLocalAvatar:clearLocalAvatar - progressBlock:^(NSProgress *progress){ - // Do nothing. - }] - .thenInBackground(^{ - OWSLogVerbose(@"Upload complete."); + OWSAvatarUploadV2 *upload = [OWSAvatarUploadV2 new]; + [[upload uploadAvatarToService:encryptedAvatarData + clearLocalAvatar:clearLocalAvatar + progressBlock:^(NSProgress *progress){ + // Do nothing. + }] + .thenInBackground(^{ + OWSLogVerbose(@"Upload complete."); - successBlock(upload.urlPath); - }) - .catchInBackground(^(NSError *error) { - OWSLogError(@"Failed: %@", error); + successBlock(upload.urlPath); + }) + .catchInBackground(^(NSError *error) { + OWSLogError(@"Failed: %@", error); - failureBlock(error); - }) retainUntilComplete]; + failureBlock(error); + }) retainUntilComplete]; + }); } - (void)updateServiceWithProfileName:(nullable NSString *)localProfileName From c19c11b0086cc3dac1b53057a9500754f1361365 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 16 Apr 2019 11:18:55 -0400 Subject: [PATCH 6/7] Respond to CR. --- SignalServiceKit/src/Network/API/OWSUploadV2.m | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/SignalServiceKit/src/Network/API/OWSUploadV2.m b/SignalServiceKit/src/Network/API/OWSUploadV2.m index 391012f693..05af5658dd 100644 --- a/SignalServiceKit/src/Network/API/OWSUploadV2.m +++ b/SignalServiceKit/src/Network/API/OWSUploadV2.m @@ -185,7 +185,7 @@ void AppendMultipartFormPath(id formData, NSString *name, N success:^(NSURLSessionDataTask *task, id formResponseObject) { OWSAvatarUploadV2 *_Nullable strongSelf = weakSelf; if (!strongSelf) { - return; + return resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated")); } if (avatarData == nil) { @@ -224,12 +224,6 @@ void AppendMultipartFormPath(id formData, NSString *name, N urlPath:(NSString *)urlPath progressBlock:(UploadProgressBlock)progressBlock { - if (![formResponseObject isKindOfClass:[NSDictionary class]]) { - OWSLogError(@"Invalid upload form."); - return [AnyPromise - promiseWithValue:OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Invalid upload form.")]; - } - OWSUploadForm *_Nullable form = [OWSUploadForm parse:formResponseObject]; if (!form) { return [AnyPromise @@ -245,7 +239,7 @@ void AppendMultipartFormPath(id formData, NSString *name, N constructingBodyWithBlock:^(id formData) { OWSAvatarUploadV2 *_Nullable strongSelf = weakSelf; if (!strongSelf) { - return; + return resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated")); } // We have to build up the form manually vs. simply passing in a paramaters dict @@ -354,7 +348,7 @@ void AppendMultipartFormPath(id formData, NSString *name, N success:^(NSURLSessionDataTask *task, id formResponseObject) { OWSAttachmentUploadV2 *_Nullable strongSelf = weakSelf; if (!strongSelf) { - return; + return resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated")); } [[strongSelf parseFormAndUpload:formResponseObject @@ -382,7 +376,6 @@ void AppendMultipartFormPath(id formData, NSString *name, N urlPath:(NSString *)urlPath progressBlock:(UploadProgressBlock)progressBlock { - OWSUploadForm *_Nullable form = [OWSUploadForm parse:formResponseObject]; if (!form) { return [AnyPromise @@ -403,7 +396,7 @@ void AppendMultipartFormPath(id formData, NSString *name, N constructingBodyWithBlock:^(id formData) { OWSAttachmentUploadV2 *_Nullable strongSelf = weakSelf; if (!strongSelf) { - return; + return resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated")); } // We have to build up the form manually vs. simply passing in a paramaters dict @@ -433,7 +426,7 @@ void AppendMultipartFormPath(id formData, NSString *name, N success:^(NSURLSessionDataTask *uploadTask, id _Nullable responseObject) { OWSAttachmentUploadV2 *_Nullable strongSelf = weakSelf; if (!strongSelf) { - return; + return resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated")); } OWSLogInfo(@"Upload succeeded with key: %@", form.formKey); From 8f22caa9ee8109e9a591d78ae8b8761e2885d06f Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 22 Apr 2019 14:52:34 -0400 Subject: [PATCH 7/7] Update Cocoapods. --- Pods | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pods b/Pods index e0e6cd88de..413f983f5c 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit e0e6cd88de3842930e91a8e2fe92d79a72c99da2 +Subproject commit 413f983f5c99fc2854990f4f63e1a5cf67b9a9d7