Merge branch 'charlesmchen/uploadV2'

This commit is contained in:
Matthew Chen 2019-04-22 14:52:46 -04:00
commit d56beb4478
10 changed files with 593 additions and 273 deletions

2
Pods

@ -1 +1 @@
Subproject commit e0e6cd88de3842930e91a8e2fe92d79a72c99da2
Subproject commit 413f983f5c99fc2854990f4f63e1a5cf67b9a9d7

View File

@ -23,6 +23,7 @@
#import <SignalServiceKit/OWSProfileKeyMessage.h>
#import <SignalServiceKit/OWSRequestBuilder.h>
#import <SignalServiceKit/OWSSignalService.h>
#import <SignalServiceKit/OWSUploadV2.h>
#import <SignalServiceKit/SSKEnvironment.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSGroupThread.h>
@ -408,118 +409,28 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
};
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;
}
OWSAvatarUploadV2 *upload = [OWSAvatarUploadV2 new];
[[upload uploadAvatarToService:encryptedAvatarData
clearLocalAvatar:clearLocalAvatar
progressBlock:^(NSProgress *progress){
// Do nothing.
}]
.thenInBackground(^{
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<AFMultipartFormData> _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];
});
}

View File

@ -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"
@ -23,6 +23,7 @@
#import "TSThread.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/Cryptography.h>
#import <SignalServiceKit/OWSSignalService.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <YapDatabase/YapDatabaseConnection.h>
@ -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) {

View File

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

View File

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

View File

@ -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 <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/Cryptography.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <YapDatabase/YapDatabaseConnection.h>
NS_ASSUME_NONNULL_BEGIN
@ -82,104 +85,26 @@ 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 updateAsUploadedWithEncryptionKey:upload.encryptionKey
digest:upload.digest
serverId:upload.serverId
completion:^{
[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

View File

@ -0,0 +1,42 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <Foundation/Foundation.h>
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 OWSAvatarUploadV2 : NSObject
// This is set on success for non-nil uploads.
@property (nonatomic, nullable) NSString *urlPath;
- (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
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,445 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSUploadV2.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/Cryptography.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalServiceKit/MIMETypeUtil.h>
#import <SignalServiceKit/OWSError.h>
#import <SignalServiceKit/OWSRequestFactory.h>
#import <SignalServiceKit/OWSSignalService.h>
#import <SignalServiceKit/SSKEnvironment.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSNetworkManager.h>
NS_ASSUME_NONNULL_BEGIN
void AppendMultipartFormPath(id<AFMultipartFormData> formData, NSString *name, NSString *dataString)
{
NSData *data = [dataString dataUsingEncoding:NSUTF8StringEncoding];
[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<AFMultipartFormData>)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
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), ^{
TSRequest *formRequest = [OWSRequestFactory profileAvatarUploadFormRequest];
[self.networkManager makeRequest:formRequest
success:^(NSURLSessionDataTask *task, id formResponseObject) {
OWSAvatarUploadV2 *_Nullable strongSelf = weakSelf;
if (!strongSelf) {
return resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated"));
}
if (avatarData == nil) {
OWSLogDebug(@"successfully cleared avatar");
clearLocalAvatar();
return resolve(@(1));
}
// TODO: Should we use a non-empty urlPath?
[[strongSelf parseFormAndUpload:formResponseObject urlPath:@"" progressBlock:progressBlock]
.thenInBackground(^{
return resolve(@(1));
})
.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:(id)formResponseObject
urlPath:(NSString *)urlPath
progressBlock:(UploadProgressBlock)progressBlock
{
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<AFMultipartFormData> formData) {
OWSAvatarUploadV2 *_Nullable strongSelf = weakSelf;
if (!strongSelf) {
return resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated"));
}
// 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.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 *progress) {
OWSLogVerbose(@"Upload progress: %.2f%%", progress.fractionCompleted * 100);
progressBlock(progress);
}
success:^(NSURLSessionDataTask *uploadTask, id _Nullable responseObject) {
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 resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated"));
}
[[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<AFMultipartFormData> formData) {
OWSAttachmentUploadV2 *_Nullable strongSelf = weakSelf;
if (!strongSelf) {
return resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated"));
}
// 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 resolve(OWSErrorWithCodeDescription(OWSErrorCodeUploadFailed, @"Upload deallocated"));
}
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
NS_ASSUME_NONNULL_END

View File

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

View File

@ -52,6 +52,7 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) {
OWSErrorCodeAvatarWriteFailed = 777425,
OWSErrorCodeAvatarUploadFailed = 777426,
OWSErrorCodeNoSessionForTransientMessage,
OWSErrorCodeUploadFailed,
};
extern NSString *const OWSErrorRecipientIdentifierKey;