// _____ _
// |_ _| |_ _ _ ___ ___ _ __ __ _
// | | | ' \| '_/ -_) -_) ' \/ _` |_
// |_| |_||_|_| \___\___|_|_|_\__,_(_)
//
// Threema iOS Client
// Copyright (c) 2015-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 .
#import "BlobUploader.h"
#import "ActivityIndicatorProxy.h"
#import "Contact.h"
#import "BlobUtil.h"
#import "BaseMessage.h"
#import "ThreemaError.h"
#import "SSLCAHelper.h"
#import "NSString+Hex.h"
#import "BlobUploadDelegate.h"
#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif
@interface BlobUploader ()
@property NSMutableData *receivedData;
@property NSURLConnection *uploadConnection;
@property id blobUploadDelegate;
@end
@implementation BlobUploader
- (void)startUploadFor:(id)messageProxy {
_blobUploadDelegate = messageProxy;
[ActivityIndicatorProxy startActivity];
[_blobUploadDelegate uploadProgress: [NSNumber numberWithFloat:0]];
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"];
NSUInteger dataLength = [self totalDataLength];
NSMutableData *body = [NSMutableData dataWithCapacity: dataLength];
[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:_data];
_data = nil; // release memory now
if (_thumbnailData) {
[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"blob2\"; filename=\"blob2.bin\"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:_thumbnailData];
_thumbnailData = nil; // release memory now
[body appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
} else {
[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 startImmediately:NO];
[_uploadConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[_uploadConnection start];
}
- (NSUInteger)totalDataLength {
NSUInteger length = _data.length + 1024;
if (_thumbnailData) {
length += _thumbnailData.length;
}
return length;
}
#pragma mark - URL connection delegate
- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
DDLogVerbose(@"totalBytesWritten: %ld, totalBytesExpectedToWrite: %ld", (long)totalBytesWritten, (long)totalBytesExpectedToWrite);
if ([_blobUploadDelegate uploadShouldCancel]) {
DDLogWarn(@"Upload cancelled");
[connection cancel];
[_blobUploadDelegate uploadDidCancel];
return;
}
[_blobUploadDelegate uploadProgress: [NSNumber numberWithFloat:((float)totalBytesWritten / (float)totalBytesExpectedToWrite)]];
}
- (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) {
DDLogVerbose(@"Blob ID: %@", blobId);
[_blobUploadDelegate uploadSucceededWithBlobIds:@[blobId]];
} else if (blobId.length == (2*kBlobIdLen)) {
NSData *blobId1 = [NSData dataWithBytes:blobId.bytes length:kBlobIdLen];
NSData *blobId2 = [NSData dataWithBytes:(blobId.bytes + kBlobIdLen) length:kBlobIdLen];
DDLogVerbose(@"Blob ID: %@ / %@", blobId1, blobId2);
[_blobUploadDelegate uploadSucceededWithBlobIds:@[blobId1, blobId2]];
} else {
DDLogError(@"Got invalid blob ID from server: %@", blobId);
[self connection:connection didFailWithError:[ThreemaError threemaError:@"Got invalid blob ID from server"]];
}
_uploadConnection = nil; // release memory for body etc. now
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
DDLogError(@"Connection failed - error %@ %@",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
[ActivityIndicatorProxy stopActivity];
if ([_blobUploadDelegate uploadShouldCancel]) {
DDLogWarn(@"Upload cancelled");
[_blobUploadDelegate uploadDidCancel];
return;
}
[_blobUploadDelegate uploadFailed];
_uploadConnection = nil; // release memory for body etc. now
}
/* 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