123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // Threema iOS Client
- // Copyright (c) 2013-2020 Threema GmbH
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License, version 3,
- // as published by the Free Software Foundation.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- #import "ContactPhotoSender.h"
- #import "Conversation.h"
- #import "Utils.h"
- #import "NaClCrypto.h"
- #import "MyIdentityStore.h"
- #import "ContactSetPhotoMessage.h"
- #import "ContactDeletePhotoMessage.h"
- #import "ContactRequestPhotoMessage.h"
- #import "MessageQueue.h"
- #import "NSString+Hex.h"
- #import "ActivityIndicatorProxy.h"
- #import "SSLCAHelper.h"
- #import "Contact.h"
- #import "BlobUtil.h"
- #import "ThreemaError.h"
- #import "ContactStore.h"
- #import "EntityManager.h"
- #import "UserSettings.h"
- #ifdef DEBUG
- static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
- #else
- static const DDLogLevel ddLogLevel = DDLogLevelWarning;
- #endif
- #define kTimeToUploadNextBlob -60*60*24*7
- @implementation ContactPhotoSender {
- Contact *toMember;
- NSData *boxImageData;
- NSData *nonce;
- NSData *encryptionKey;
- NSMutableData *receivedData;
- NSURLConnection *uploadConnection;
- void(^onCompletion)(void);
- void(^onError)(NSError *error);
- }
- // MARK: - Profile picture request send
- + (void)sendProfilePictureRequest:(NSString *)toIdentity {
- DDLogVerbose(@"send profile pic request to %@", toIdentity);
- ContactRequestPhotoMessage *msg = [[ContactRequestPhotoMessage alloc] init];
- msg.toIdentity = toIdentity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- }
- // MARK: - Profile picture send/upload
- + (Contact *)shouldSendProfilePictureToContact:(NSString *)identity {
- enum SendProfilePicture preference = [UserSettings sharedUserSettings].sendProfilePicture;
-
- if (preference == SendProfilePictureNone) {
- return nil;
- }
- else if (preference == SendProfilePictureContacts) {
- NSArray *contactIdentities = [UserSettings sharedUserSettings].profilePictureContactList;
- NSSet *selectedContacts = [NSMutableSet setWithArray:contactIdentities];
- if (![selectedContacts containsObject:identity]) {
- return nil;
- }
- }
-
- Contact *contact = [[ContactStore sharedContactStore] contactForIdentity:identity];
-
- if (contact.isGatewayId || contact.isEchoEcho) {
- return nil;
- }
-
- if (!contact.isProfilePictureSended) {
- return contact;
- }
- return nil;
- }
- + (void)sendProfilePicture:(AbstractMessage *)message {
- if ([message allowToSendProfilePicture]) {
- Contact *contact = [ContactPhotoSender shouldSendProfilePictureToContact:message.toIdentity];
- if (contact) {
- // send profile picture
- ContactPhotoSender *sender = [[ContactPhotoSender alloc] init];
- [sender startWithImageToMember:contact onCompletion:^{
- } onError:^(NSError *err) {
- }];
- }
- ContactStore *contactStore = [ContactStore sharedContactStore];
- if ([contactStore existsProfilePictureRequest:message.toIdentity]) {
- // send profile picture request
- [ContactPhotoSender sendProfilePictureRequest:message.toIdentity];
- [contactStore removeProfilePictureRequest:message.toIdentity];
- }
- }
- }
- - (void)startWithImageToMember:(Contact*)_toMember onCompletion:(void (^)(void))_onCompletion onError:(void (^)(NSError *))_onError {
-
- toMember = _toMember;
- onCompletion = _onCompletion;
- onError = _onError;
-
- NSMutableDictionary *profilePicture = [[MyIdentityStore sharedMyIdentityStore] profilePicture];
- if (profilePicture[@"ProfilePicture"] == nil) {
- if (toMember != nil) {
- // save to database
- EntityManager *entityManager = [[EntityManager alloc] init];
- [entityManager performAsyncBlockAndSafe:^{
- toMember.profilePictureSended = YES;
- toMember.profilePictureUpload = [NSDate date];
- }];
- /* send to the specified member only */
- [self sendDeletePhotoMessageToMember:toMember];
- }
-
- dispatch_async(dispatch_get_main_queue(), ^{
- onCompletion();
- });
- } else {
- // check if date is older then 2 weeks when uploading blob, when not send only the blob id in the message
- NSMutableDictionary *profilePicture = [[MyIdentityStore sharedMyIdentityStore] profilePicture];
- NSDate *lastUpload = profilePicture[@"LastUpload"];
- NSDate *blobValidDate = [NSDate dateWithTimeIntervalSinceNow:kTimeToUploadNextBlob];
- if (lastUpload && [lastUpload earlierDate:blobValidDate] == blobValidDate) {
- if (toMember != nil) {
- // save to database
- EntityManager *entityManager = [[EntityManager alloc] init];
- [entityManager performAsyncBlockAndSafe:^{
- toMember.profilePictureSended = YES;
- toMember.profilePictureUpload = [NSDate date];
- }];
- /* send to the specified member only */
- [self sendSetPhotoMessageToMember:toMember withProfilePicture:profilePicture];
- }
-
- dispatch_async(dispatch_get_main_queue(), ^{
- onCompletion();
- });
- } else {
- /* Generate random symmetric key and encrypt */
- encryptionKey = [[NaClCrypto sharedCrypto] randomBytes:kBlobKeyLen];
- boxImageData = [[NaClCrypto sharedCrypto] symmetricEncryptData:profilePicture[@"ProfilePicture"] withKey:encryptionKey nonce:[NSData dataWithBytesNoCopy:kNonce_1 length:sizeof(kNonce_1) freeWhenDone:NO]];
-
- [self startUpload];
- }
- }
- }
- - (void)startUpload {
- [ActivityIndicatorProxy startActivity];
-
- NSURL *blobUploadUrl = [BlobUtil urlForBlobUpload];
-
- NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:blobUploadUrl cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:kBlobUploadTimeout];
- request.HTTPMethod = @"POST";
-
- NSString *boundary = @"---------------------------Boundary_Line";
- NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary];
- [request addValue:contentType forHTTPHeaderField: @"Content-Type"];
-
- NSMutableData *body = [NSMutableData dataWithCapacity:boxImageData.length + 1024];
- [body appendData:[[NSString stringWithFormat:@"--%@\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
- [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"blob\"; filename=\"blob.bin\"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
- [body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
- [body appendData:boxImageData];
- [body appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
-
- [request setHTTPBody:body];
- [request addValue:[NSString stringWithFormat:@"%lu", (unsigned long)[body length]] forHTTPHeaderField:@"Content-Length"];
-
- receivedData = [NSMutableData data];
-
- uploadConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
- }
- - (void)uploadCompletedWithBlobId:(NSData*)blobId {
-
- if (toMember != nil) {
- /* send to the specified member only */
- [self sendSetPhotoMessageToMember:toMember withBlobId:blobId];
- }
-
- dispatch_async(dispatch_get_main_queue(), ^{
- onCompletion();
- });
- }
- - (void)sendSetPhotoMessageToMember:(Contact*)member withBlobId:(NSData*)blobId {
- DDLogVerbose(@"Sending contact photo message to %@", member.identity);
-
- NSMutableDictionary *profilePicture = [[MyIdentityStore sharedMyIdentityStore] profilePicture];
- if (!profilePicture) {
- profilePicture = [NSMutableDictionary new];
- }
- [profilePicture setValue:blobId forKey:@"BlobId"];
- [profilePicture setValue:[NSNumber numberWithUnsignedInteger:boxImageData.length] forKey:@"Size"];
- [profilePicture setValue:encryptionKey forKey:@"EncryptionKey"];
- [profilePicture setValue:[NSDate date] forKey:@"LastUpload"];
- [[MyIdentityStore sharedMyIdentityStore] setProfilePicture:profilePicture];
-
- ContactSetPhotoMessage *msg = [[ContactSetPhotoMessage alloc] init];
- msg.blobId = blobId;
- msg.size = (uint32_t)boxImageData.length;
- msg.encryptionKey = encryptionKey;
- msg.toIdentity = member.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- }
- - (void)sendSetPhotoMessageToMember:(Contact*)member withProfilePicture:(NSMutableDictionary *)profilePicture {
- DDLogVerbose(@"Sending contact photo message to %@", member.identity);
-
- ContactSetPhotoMessage *msg = [[ContactSetPhotoMessage alloc] init];
- msg.blobId = profilePicture[@"BlobId"];
- msg.size = (uint32_t)[profilePicture[@"Size"] unsignedIntValue];
- msg.encryptionKey = profilePicture[@"EncryptionKey"];
- msg.toIdentity = member.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- }
- - (void)sendDeletePhotoMessageToMember:(Contact *)member {
- DDLogVerbose(@"Delete contact photo message to %@", member.identity);
-
- ContactDeletePhotoMessage *msg = [[ContactDeletePhotoMessage alloc] init];
- msg.toIdentity = member.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- }
- #pragma mark - URL connection delegate
- - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
- DDLogVerbose(@"Request succeeded - received %lu bytes", (unsigned long)[receivedData length]);
- [ActivityIndicatorProxy stopActivity];
-
- NSString *blobIdHex = [[NSString alloc] initWithData:receivedData encoding:NSASCIIStringEncoding];
- NSData *blobId = [blobIdHex decodeHex];
-
- if (blobId.length != kBlobIdLen) {
- DDLogError(@"Got invalid blob ID from server: %@", blobId);
- [self connection:connection didFailWithError:[ThreemaError threemaError:@"Got invalid blob ID from server"]];
- return;
- }
-
- // save to database
- EntityManager *entityManager = [[EntityManager alloc] init];
- [entityManager performAsyncBlockAndSafe:^{
- toMember.profilePictureSended = YES;
- toMember.profilePictureUpload = [NSDate date];
- }];
-
- DDLogVerbose(@"Blob ID: %@", blobId);
- [self uploadCompletedWithBlobId:blobId];
- }
- - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
-
- DDLogError(@"Connection failed - error %@ %@",
- [error localizedDescription],
- [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
-
- [ActivityIndicatorProxy stopActivity];
-
- onError(error);
- }
- /* this method ensures that HTTP errors get treated like connection failures etc. as well */
- - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
- [receivedData setLength:0];
-
- NSInteger statusCode = [((NSHTTPURLResponse *)response) statusCode];
- if (statusCode >= 400)
- {
- [connection cancel]; // stop connecting; no more delegate messages
- NSDictionary *errorInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:
- NSLocalizedString(@"Server returned status code %d",@""),
- statusCode]
- forKey:NSLocalizedDescriptionKey];
- NSError *statusError = [NSError errorWithDomain:NSURLErrorDomain
- code:statusCode
- userInfo:errorInfo];
- [self connection:connection didFailWithError:statusError];
- }
- }
- - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
- [receivedData appendData:data];
- }
- - (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
- return [SSLCAHelper connection:connection canAuthenticateAgainstProtectionSpace:protectionSpace];
- }
- - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
- [SSLCAHelper connection:connection didReceiveAuthenticationChallenge:challenge];
- }
- - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
- return nil;
- }
- @end
|