Merge branch 'mkirk/upload-profile-avatar'
This commit is contained in:
commit
620550a462
@ -336,12 +336,15 @@ static const NSInteger kProfileKeyLength = 16;
|
||||
UserProfile *userProfile = self.localUserProfile;
|
||||
OWSAssert(userProfile);
|
||||
|
||||
// If we have a new avatar image, we must first:
|
||||
//
|
||||
// * Encode it to JPEG.
|
||||
// * Write it to disk.
|
||||
// * Upload it to service.
|
||||
if (avatarImage) {
|
||||
|
||||
// If we have a new avatar image, we must first:
|
||||
//
|
||||
// * Encode it to JPEG.
|
||||
// * Write it to disk.
|
||||
// * Encrypt it
|
||||
// * Upload it to asset service
|
||||
// * Send asset service info to Signal Service
|
||||
if (self.localCachedAvatarImage == avatarImage) {
|
||||
OWSAssert(userProfile.avatarUrl.length > 0);
|
||||
OWSAssert(userProfile.avatarDigest.length > 0);
|
||||
@ -400,21 +403,33 @@ static const NSInteger kProfileKeyLength = 16;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: The exact API & encryption scheme for avatars is not yet settled.
|
||||
- (void)uploadAvatarToService:(NSData *)data
|
||||
fileName:(NSString *)fileName
|
||||
- (NSData *)encryptedAvatarData:(NSData *)plainTextData outDigest:(NSData **)outDigest
|
||||
{
|
||||
DDLogError(@"TODO: Profile encryption scheme not yet settled.");
|
||||
|
||||
// server accepts up to 14 base64 chars for digest
|
||||
// 14 <= 4 * ceil(n/3)
|
||||
NSUInteger kAvatarDigestByteLength = 9;
|
||||
*outDigest = [Cryptography computeSHA256Digest:plainTextData truncatedToBytes:kAvatarDigestByteLength];
|
||||
|
||||
return plainTextData;
|
||||
}
|
||||
|
||||
- (void)uploadAvatarToService:(NSData *)avatarData
|
||||
fileName:(NSString *)fileName // TODO do we need filename?
|
||||
success:(void (^)(NSString *avatarUrl, NSData *avatarDigest))successBlock
|
||||
failure:(void (^)())failureBlock
|
||||
{
|
||||
OWSAssert(data.length > 0);
|
||||
OWSAssert(avatarData.length > 0);
|
||||
OWSAssert(fileName.length > 0);
|
||||
OWSAssert(successBlock);
|
||||
OWSAssert(failureBlock);
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// TODO:
|
||||
NSString *avatarUrl = @"avatarUrl";
|
||||
NSData *avatarDigest = [@"avatarDigest" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *encryptedAvatarDigest;
|
||||
NSData *encryptedAvatarData = [self encryptedAvatarData:avatarData outDigest:&encryptedAvatarDigest];
|
||||
OWSAssert(encryptedAvatarData.length > 0);
|
||||
OWSAssert(encryptedAvatarDigest.length > 0);
|
||||
|
||||
// See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html
|
||||
TSProfileAvatarUploadFormRequest *formRequest = [TSProfileAvatarUploadFormRequest new];
|
||||
@ -429,15 +444,6 @@ static const NSInteger kProfileKeyLength = 16;
|
||||
}
|
||||
NSDictionary *responseMap = formResponseObject;
|
||||
DDLogError(@"responseObject: %@", formResponseObject);
|
||||
// acl = private;
|
||||
// algorithm = "AWS4-HMAC-SHA256";
|
||||
// credential =
|
||||
// "AKIAINTYCHN42UH3LGRA/20170804/us-east-1/s3/aws4_request"; date =
|
||||
// 20170804T193927Z; key = PtRO3iSkY6twBA; policy =
|
||||
// eyAiZXhwaXJhdGlvbiI6ICIyMDE3LTA4LTA0VDIwOjA5OjI3LjMwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAic2lnbmFsLXByb2ZpbGVzLXN0YWdpbmcifSwKICAgIHsia2V5IjogIlB0Uk8zaVNrWTZ0d0JBIn0sCiAgICB7ImFjbCI6ICJwcml2YXRlIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRDb250ZW50LVR5cGUiLCAiIl0sCgogICAgeyJ4LWFtei1jcmVkZW50aWFsIjogIkFLSUFJTlRZQ0hONDJVSDNMR1JBLzIwMTcwODA0L3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QifSwKICAgIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwKICAgIHsieC1hbXotZGF0ZSI6ICIyMDE3MDgwNFQxOTM5MjdaIiB9CiAgXQp9;
|
||||
// signature =
|
||||
// 3608fdc9af8ca0d13c754c34eb37014c9995b058c2e0166550468de47b00f316;
|
||||
// url = "profiles-staging.signal.org";
|
||||
|
||||
NSString *formUrl = responseMap[@"url"];
|
||||
if (![formUrl isKindOfClass:[NSString class]] || formUrl.length < 1) {
|
||||
@ -487,87 +493,66 @@ static const NSInteger kProfileKeyLength = 16;
|
||||
failureBlock();
|
||||
return;
|
||||
}
|
||||
NSDictionary<NSString *, NSString *> *parameters = @{
|
||||
@"acl" : formAcl,
|
||||
@"x-amz-algorithm" : formAlgorithm,
|
||||
@"x-amz-credential" : formCredential,
|
||||
@"x-amz-date" : formDate,
|
||||
@"key" : formKey,
|
||||
@"policy" : formPolicy,
|
||||
@"x-amz-signature" : formSignature,
|
||||
|
||||
};
|
||||
AFHTTPSessionManager *profileHttpManager =
|
||||
[[OWSSignalService sharedInstance] profileUploadingSessionManagerWithHostname:formUrl];
|
||||
|
||||
NSString *filePath = [self.profileAvatarsDirPath stringByAppendingPathComponent:fileName];
|
||||
NSInputStream *fileInputStream = [[NSInputStream alloc] initWithFileAtPath:filePath];
|
||||
if (!fileInputStream) {
|
||||
OWSProdFail(@"profile_manager_error_avatar_upload_invalid_file_stream");
|
||||
failureBlock();
|
||||
return;
|
||||
}
|
||||
// Default acceptable content headers are rejected by AWS
|
||||
profileHttpManager.responseSerializer.acceptableContentTypes = nil;
|
||||
|
||||
NSError *error;
|
||||
long long fileSize =
|
||||
[[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize]
|
||||
longLongValue];
|
||||
if (error || fileSize <= 0) {
|
||||
OWSProdFail(@"profile_manager_error_avatar_upload_invalid_file_size");
|
||||
failureBlock();
|
||||
return;
|
||||
}
|
||||
[profileHttpManager POST:@""
|
||||
parameters:nil
|
||||
constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
|
||||
NSData * (^formDataForString)(NSString *formString) = ^(NSString *formString) {
|
||||
return [formString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
};
|
||||
|
||||
NSMutableURLRequest *uploadRequest = [[AFHTTPRequestSerializer serializer]
|
||||
multipartFormRequestWithMethod:@"post"
|
||||
URLString:[@"https://" stringByAppendingString:formUrl]
|
||||
parameters:parameters
|
||||
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
|
||||
// [formData appendPartWithFormData:<#(nonnull NSData *)#> name:@"acl"];
|
||||
// We have to build up the form manually vs. simply passing in a paramaters dict
|
||||
// because AWS is sensitive to the order of 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:encryptedAvatarData name:@"file"];
|
||||
|
||||
[formData appendPartWithInputStream:fileInputStream
|
||||
name:@"file"
|
||||
fileName:fileName
|
||||
length:fileSize
|
||||
mimeType:@"image/jpeg"];
|
||||
}
|
||||
error:&error];
|
||||
|
||||
if (error || !uploadRequest) {
|
||||
OWSProdFail(@"profile_manager_error_avatar_upload_invalid_upload_request");
|
||||
failureBlock();
|
||||
return;
|
||||
}
|
||||
|
||||
[uploadRequest setValue:@"Content-Type: text/html; charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
|
||||
// [uploadRequest setAllHTTPHeaderFields:headerDictionary];
|
||||
|
||||
// TODO: Should we use a special configuration as we do in TSNetworkManager?
|
||||
// TODO: How does censorship circumvention fit in?
|
||||
AFURLSessionManager *manager = [[AFURLSessionManager alloc]
|
||||
initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
|
||||
manager.responseSerializer = [AFXMLParserResponseSerializer new];
|
||||
NSURLSessionUploadTask *uploadTask;
|
||||
uploadTask = [manager uploadTaskWithStreamedRequest:uploadRequest
|
||||
progress:^(NSProgress *_Nonnull uploadProgress) {
|
||||
// This is not called back on the main queue.
|
||||
// You are responsible for dispatching to the main queue for UI updates
|
||||
|
||||
DDLogVerbose(@"%@ Avatar upload progress: %f", self.tag, uploadProgress.fractionCompleted);
|
||||
DDLogVerbose(@"%@ constructed body", self.tag);
|
||||
}
|
||||
completionHandler:^(NSURLResponse *_Nonnull response,
|
||||
id _Nullable uploadResponseObject,
|
||||
NSError *_Nullable uploadError) {
|
||||
progress:^(NSProgress *_Nonnull uploadProgress) {
|
||||
DDLogVerbose(
|
||||
@"%@ avatar upload progress: %.2f%%", self.tag, uploadProgress.fractionCompleted * 100);
|
||||
}
|
||||
success:^(NSURLSessionDataTask *_Nonnull uploadTask, id _Nullable responseObject) {
|
||||
OWSAssert([uploadTask.response isKindOfClass:[NSHTTPURLResponse class]]);
|
||||
NSHTTPURLResponse *response = (NSHTTPURLResponse *)uploadTask.response;
|
||||
|
||||
if (uploadError) {
|
||||
DDLogError(@"%@ Avatar upload failed: %@", self.tag, uploadError);
|
||||
// We could also construct this URL locally from manager.baseUrl + formKey
|
||||
// but the approach of getting it from the remote provider seems a more
|
||||
// robust way to ensure we've actually created the resource where we
|
||||
// think we have.
|
||||
NSString *avatarURL = response.allHeaderFields[@"Location"];
|
||||
if (avatarURL.length == 0) {
|
||||
OWSProdFail(@"profile_manager_error_avatar_upload_no_location_in_response");
|
||||
failureBlock();
|
||||
} else {
|
||||
DDLogVerbose(@"%@ Avatar upload succeeded", self.tag);
|
||||
successBlock(avatarUrl, avatarDigest);
|
||||
return;
|
||||
}
|
||||
|
||||
DDLogVerbose(@"%@ successfully uploaded avatar url: %@ digest: %@",
|
||||
self.tag,
|
||||
avatarURL,
|
||||
encryptedAvatarDigest);
|
||||
successBlock(avatarURL, encryptedAvatarDigest);
|
||||
}
|
||||
failure:^(NSURLSessionDataTask *_Nullable uploadTask, NSError *_Nonnull error) {
|
||||
DDLogVerbose(@"%@ uploading avatar failed with error: %@", self.tag, error);
|
||||
failureBlock();
|
||||
}];
|
||||
[uploadTask resume];
|
||||
}
|
||||
failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||||
DDLogError(@"%@ Failed to get profile avatar upload form: %@", self.tag, error);
|
||||
|
||||
@ -22,11 +22,14 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
if (profileNameEncrypted.length > 0) {
|
||||
self.parameters[@"name"] = [profileNameEncrypted base64EncodedString];
|
||||
}
|
||||
if (avatarUrl.length > 0) {
|
||||
if (avatarUrl.length > 0 && avatarDigest.length > 0) {
|
||||
// TODO why is this base64 encoded?
|
||||
self.parameters[@"avatar"] = [[avatarUrl dataUsingEncoding:NSUTF8StringEncoding] base64EncodedString];
|
||||
}
|
||||
if (avatarDigest.length > 0) {
|
||||
|
||||
self.parameters[@"avatarDigest"] = [avatarDigest base64EncodedString];
|
||||
} else {
|
||||
OWSAssert(avatarUrl.length == 0);
|
||||
OWSAssert(avatarDigest.length == 0);
|
||||
}
|
||||
|
||||
return self;
|
||||
|
||||
@ -61,7 +61,7 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
|
||||
void (^failure)(NSURLSessionDataTask *task, NSError *error) =
|
||||
[TSNetworkManager errorPrettifyingForFailureBlock:failureBlock];
|
||||
|
||||
AFHTTPSessionManager *sessionManager = [OWSSignalService sharedInstance].HTTPSessionManager;
|
||||
AFHTTPSessionManager *sessionManager = [OWSSignalService sharedInstance].signalServiceSessionManager;
|
||||
|
||||
if ([request isKindOfClass:[TSVerifyCodeRequest class]]) {
|
||||
// We plant the Authorization parameter ourselves, no need to double add.
|
||||
|
||||
@ -12,7 +12,9 @@ extern NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidCha
|
||||
|
||||
@interface OWSSignalService : NSObject
|
||||
|
||||
@property (nonatomic, readonly) AFHTTPSessionManager *HTTPSessionManager;
|
||||
@property (nonatomic, readonly) AFHTTPSessionManager *signalServiceSessionManager;
|
||||
|
||||
- (AFHTTPSessionManager *)profileUploadingSessionManagerWithHostname:(NSString *)hostname;
|
||||
|
||||
@property (atomic, readonly) BOOL isCensorshipCircumventionActive;
|
||||
|
||||
|
||||
@ -154,19 +154,20 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange =
|
||||
}
|
||||
}
|
||||
|
||||
- (AFHTTPSessionManager *)HTTPSessionManager
|
||||
- (AFHTTPSessionManager *)signalServiceSessionManager
|
||||
{
|
||||
if (self.isCensorshipCircumventionActive) {
|
||||
DDLogInfo(@"%@ using reflector HTTPSessionManager", self.tag);
|
||||
return self.reflectorHTTPSessionManager;
|
||||
return self.reflectorSignalServiceSessionManager;
|
||||
} else {
|
||||
return self.defaultHTTPSessionManager;
|
||||
return self.defaultSignalServiceSessionManager;
|
||||
}
|
||||
}
|
||||
|
||||
- (AFHTTPSessionManager *)defaultHTTPSessionManager
|
||||
- (AFHTTPSessionManager *)defaultSignalServiceSessionManager
|
||||
{
|
||||
NSURL *baseURL = [[NSURL alloc] initWithString:textSecureServerURL];
|
||||
OWSAssert(baseURL);
|
||||
NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
||||
AFHTTPSessionManager *sessionManager =
|
||||
[[AFHTTPSessionManager alloc] initWithBaseURL:baseURL sessionConfiguration:sessionConf];
|
||||
@ -178,7 +179,7 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange =
|
||||
return sessionManager;
|
||||
}
|
||||
|
||||
- (AFHTTPSessionManager *)reflectorHTTPSessionManager
|
||||
- (AFHTTPSessionManager *)reflectorSignalServiceSessionManager
|
||||
{
|
||||
NSString *localNumber = [TSAccountManager localNumber];
|
||||
OWSAssert(localNumber.length > 0);
|
||||
@ -190,6 +191,7 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange =
|
||||
frontingHost = self.manualCensorshipCircumventionDomain;
|
||||
};
|
||||
NSURL *baseURL = [[NSURL alloc] initWithString:[self.censorshipConfiguration frontingHost:localNumber]];
|
||||
OWSAssert(baseURL);
|
||||
NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
||||
AFHTTPSessionManager *sessionManager =
|
||||
[[AFHTTPSessionManager alloc] initWithBaseURL:baseURL sessionConfiguration:sessionConf];
|
||||
@ -204,6 +206,25 @@ NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange =
|
||||
return sessionManager;
|
||||
}
|
||||
|
||||
#pragma mark - Profile Uploading
|
||||
|
||||
- (AFHTTPSessionManager *)profileUploadingSessionManagerWithHostname:(NSString *)hostname
|
||||
{
|
||||
OWSAssert(hostname.length > 0);
|
||||
if (self.isCensorshipCircumventionActive) {
|
||||
DDLogInfo(@"%@ Profile uploading may not work when under censorship.", self.tag);
|
||||
}
|
||||
|
||||
NSURL *baseURL = [[NSURL alloc] initWithString:[@"https://" stringByAppendingString:hostname]];
|
||||
NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
||||
AFHTTPSessionManager *sessionManager =
|
||||
[[AFHTTPSessionManager alloc] initWithBaseURL:baseURL sessionConfiguration:sessionConf];
|
||||
|
||||
sessionManager.securityPolicy = [OWSHTTPSecurityPolicy sharedPolicy];
|
||||
|
||||
return sessionManager;
|
||||
}
|
||||
|
||||
#pragma mark - Google Pinning Policy
|
||||
|
||||
/**
|
||||
|
||||
@ -24,9 +24,11 @@ typedef enum { kSMSVerification, kPhoneNumberVerification } VerificationTranspor
|
||||
|
||||
#define textSecureWebSocketAPI @"wss://textsecure-service.whispersystems.org/v1/websocket/"
|
||||
#define textSecureServerURL @"https://textsecure-service.whispersystems.org/"
|
||||
#define textSecureProfileServerURL @"https://profiles.signal.org"
|
||||
|
||||
//#define textSecureWebSocketAPI @"wss://textsecure-service-staging.whispersystems.org/v1/websocket/"
|
||||
//#define textSecureServerURL @"https://textsecure-service-staging.whispersystems.org/"
|
||||
//#define textSecureWebSocketAPI @"wss://textsecure-service-staging.whispersystems.org/v1/websocket/"
|
||||
//#define textSecureServerURL @"https://textsecure-service-staging.whispersystems.org/"
|
||||
//#define textSecureProfileServerURL @"https://profiles-staging.signal.org"
|
||||
|
||||
#define textSecureGeneralAPI @"v1"
|
||||
#define textSecureAccountsAPI @"v1/accounts"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user