blob: f0e1cc5077869d6e845472c006f4371ae264448c [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
/*
The base64 Encoding part of this class is based on the PostController.m class
of the sample app 'SimpleURLConnections' provided by Apple.
http://developer.apple.com/library/ios/#samplecode/SimpleURLConnections/Introduction/Intro.html
*/
#import "CMISHttpUploadRequest.h"
#import "CMISBase64Encoder.h"
#import "CMISAtomEntryWriter.h"
#import "CMISLog.h"
#import "CMISErrors.h"
/**
this is the buffer size for the input/output stream pair containing the base64 encoded data
*/
const NSUInteger kFullBufferSize = 32768;
/**
this is the buffer size for the raw data. It must be an integer multiple of 3. Base64 encoding uses
4 bytes for each 3 bytes of raw data. Therefore, the amount of raw data we take is
kFullBufferSize/4 * 3.
*/
const NSUInteger kRawBufferSize = 24576;
/**
A category that extends the NSStream class in order to pair an inputstream with an outputstream.
The input stream will be used by NSURLSession via the HTTPBodyStream property of the URL request.
The paired output stream will buffer base64 encoded as well as XML data.
NOTE: the original sample code also provides a method for backward compatibility w.r.t iOS versions below 5.0
However, since the CMIS library is only to be used with iOS version 5.1 and higher, this code is obsolete and has
been omitted here.
*/
@interface NSStream (StreamPair)
+ (void)createBoundInputStream:(NSInputStream **)inputStreamPtr
outputStream:(NSOutputStream **)outputStreamPtr;
@end
@implementation NSStream (StreamPair)
+ (void)createBoundInputStream:(NSInputStream **)inputStreamPtr
outputStream:(NSOutputStream **)outputStreamPtr
{
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
assert((inputStreamPtr != NULL) || (outputStreamPtr != NULL));
readStream = NULL;
writeStream = NULL;
CFStreamCreateBoundPair(NULL,
((inputStreamPtr != nil) ? &readStream : NULL),
((outputStreamPtr != nil) ? &writeStream : NULL),
(CFIndex)kFullBufferSize);
if (inputStreamPtr != NULL) {
*inputStreamPtr = CFBridgingRelease(readStream);
}
if (outputStreamPtr != NULL) {
*outputStreamPtr = CFBridgingRelease(writeStream);
}
}
@end
@interface CMISHttpUploadRequest ()
@property (nonatomic, assign) unsigned long long bytesUploaded;
@property (nonatomic, copy) void (^progressBlock)(unsigned long long bytesUploaded, unsigned long long bytesTotal);
@property (nonatomic, assign) BOOL useCombinedInputStream;
@property (nonatomic, assign) BOOL base64Encoding;
@property (nonatomic, assign) BOOL transferCompleted;
@property (nonatomic, strong) NSInputStream *combinedInputStream;
@property (nonatomic, strong) NSOutputStream *encoderStream;
@property (nonatomic, strong) NSData *streamStartData;
@property (nonatomic, strong) NSData *streamEndData;
@property (nonatomic, assign) unsigned long long encodedLength;
@property (nonatomic, strong) NSData *dataBuffer;
@property (nonatomic, assign, readwrite) size_t bufferOffset;
@property (nonatomic, assign, readwrite) size_t bufferLimit;
@end
@implementation CMISHttpUploadRequest
+ (id)startRequest:(NSMutableURLRequest *)urlRequest
httpMethod:(CMISHttpRequestMethod)httpRequestMethod
inputStream:(NSInputStream*)inputStream
headers:(NSDictionary*)additionalHeaders
bytesExpected:(unsigned long long)bytesExpected
session:(CMISBindingSession *)session
completionBlock:(void (^)(CMISHttpResponse *httpResponse, NSError *error))completionBlock
progressBlock:(void (^)(unsigned long long bytesUploaded, unsigned long long bytesTotal))progressBlock
{
CMISHttpUploadRequest *httpRequest = [[self alloc] initWithHttpMethod:httpRequestMethod
completionBlock:completionBlock
progressBlock:progressBlock];
httpRequest.inputStream = inputStream;
httpRequest.additionalHeaders = additionalHeaders;
httpRequest.bytesExpected = bytesExpected;
httpRequest.session = session;
httpRequest.useCombinedInputStream = NO;
httpRequest.combinedInputStream = nil;
httpRequest.encoderStream = nil;
if (![httpRequest startRequest:urlRequest]) {
httpRequest = nil;
}
return httpRequest;
}
+ (id)startRequest:(NSMutableURLRequest *)urlRequest
httpMethod:(CMISHttpRequestMethod)httpRequestMethod
inputStream:(NSInputStream *)inputStream
headers:(NSDictionary *)additionalHeaders
bytesExpected:(unsigned long long)bytesExpected
session:(CMISBindingSession *)session
startData:(NSData *)startData
endData:(NSData *)endData
useBase64Encoding:(BOOL)useBase64Encoding
completionBlock:(void (^)(CMISHttpResponse *, NSError *))completionBlock
progressBlock:(void (^)(unsigned long long, unsigned long long))progressBlock
{
CMISHttpUploadRequest *httpRequest = [[self alloc] initWithHttpMethod:httpRequestMethod
completionBlock:completionBlock
progressBlock:progressBlock];
httpRequest.inputStream = inputStream;
httpRequest.streamStartData = startData;
httpRequest.streamEndData = endData;
httpRequest.additionalHeaders = additionalHeaders;
httpRequest.bytesExpected = bytesExpected;
httpRequest.useCombinedInputStream = YES;
httpRequest.base64Encoding = useBase64Encoding;
httpRequest.session = session;
[httpRequest prepareStreams];
if (![httpRequest startRequest:urlRequest]) {
httpRequest = nil;
}
return httpRequest;
}
- (id)initWithHttpMethod:(CMISHttpRequestMethod)httpRequestMethod
completionBlock:(void (^)(CMISHttpResponse *httpResponse, NSError *error))completionBlock
progressBlock:(void (^)(unsigned long long bytesUploaded, unsigned long long bytesTotal))progressBlock
{
self = [super initWithHttpMethod:httpRequestMethod
completionBlock:completionBlock];
if (self) {
_progressBlock = progressBlock;
_transferCompleted = NO;
}
return self;
}
/**
if we are using on-the-go base64 encoding, we will use the combinedInputStream in URL connections/request.
In this case a little extra work is required: i.e. we need to provide the length of the encoded data stream (including
the XML data).
*/
- (BOOL)startRequest:(NSMutableURLRequest*)urlRequest
{
if (self.useCombinedInputStream && self.combinedInputStream && self.base64Encoding) {
NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:self.additionalHeaders];
[headers setValue:[NSString stringWithFormat:@"%llu", self.encodedLength] forKey:@"Content-Length"];
self.additionalHeaders = [NSDictionary dictionaryWithDictionary:headers];
}
BOOL startSuccess = [super startRequest:urlRequest];
if (self.useCombinedInputStream) {
[self.encoderStream open];
}
return startSuccess;
}
- (NSURLSessionTask *)taskForRequest:(NSURLRequest *)request
{
return [self.urlSession uploadTaskWithStreamedRequest:request];
}
#pragma mark CMISCancellableRequest method
- (void)cancel
{
if (self.transferCompleted) {
return;
}
self.progressBlock = nil;
[super cancel];
if (self.useCombinedInputStream) {
[self stopSendWithStatus:@"connection has been cancelled."];
}
}
#pragma mark Session delegate methods
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *))completionHandler
{
if (self.combinedInputStream) {
completionHandler(self.combinedInputStream);
} else {
completionHandler(self.inputStream);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
[super URLSession:session task:task didCompleteWithError:error];
if (self.useCombinedInputStream) {
if (error) {
[self stopSendWithStatus:@"connection is being terminated with error."];
} else {
[self stopSendWithStatus:@"Connection finished as expected."];
}
}
self.progressBlock = nil;
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
self.bytesUploaded = 0;
[super URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
if (self.progressBlock) {
if (self.useCombinedInputStream && self.base64Encoding) {
// Show the actual transmitted raw data size to the user, not the base64 encoded size
totalBytesSent = [CMISHttpUploadRequest rawEncodedLength:totalBytesSent];
if (totalBytesSent > totalBytesExpectedToSend) {
totalBytesSent = totalBytesExpectedToSend;
}
}
if (self.bytesExpected == 0) {
if (totalBytesSent >= totalBytesExpectedToSend) {
self.transferCompleted = YES;
}
// pass progress to progressBlock, on the original thread
if (self.originalThread) {
[self performSelector:@selector(executeProgressBlock:) onThread:self.originalThread withObject:@[@(totalBytesSent), @(totalBytesExpectedToSend)] waitUntilDone:NO];
}
} else {
if (totalBytesSent >= self.bytesExpected) {
self.transferCompleted = YES;
}
// pass progress to progressBlock, on the original thread
if (self.originalThread) {
[self performSelector:@selector(executeProgressBlock:) onThread:self.originalThread withObject:@[@(totalBytesSent), @(self.bytesExpected)] waitUntilDone:NO];
}
}
}
}
#pragma mark NSStreamDelegate method
/**
For encoding base64 data - this is the meat of this class.
The action is in the case where the eventCode == NSStreamEventHasSpaceAvailable
Note 1:
The output stream (encoderStream) is paired with the encoded input stream (combinedInputStream) which is the one
the active URL connection uses to read from. Thereby any data made available to the outputstream will be available to this input stream as well.
Any action on the output stream (like close) will also affect this combinedInputStream.
Note 2:
since we are encoding "on the fly" we are dealing with 2 different buffer sizes. The encoded buffer size kFullBufferSize, and the
buffer size of the raw/non-encoded data kRawBufferSize.
Note 3:
the reading from the source input stream, as well as the writing to the encoderStream is regulated via 2 variables: bufferLimit and bufferOffset
bufferLimit is the size of the XML data or the No of bytes read in from the raw data set (inputstream)
At each readIn, the bufferOffset will be reset to 0 to indicate a free buffer to write to.
When the data are finally written to the output stream, both bufferLimit and bufferOffset should be having the same value (unless we attempt to
write more bytes than are available in the buffer).
Once we reach the end of the raw data set, both bufferLimit and bufferOffset are set to 0. This indicates that the outputStream (and its paired
input stream) can be closed.
(Final Note:The Apple source code discourages removing the stream from the runloop in this method as it can cause random crashes.)
*/
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode){
case NSStreamEventOpenCompleted:{
#if TARGET_OS_IPHONE
#ifndef __IPHONE_8_0
// this workaround breaks POST requests on MacOS targets and iOS 8
if (self.combinedInputStream.streamStatus != NSStreamStatusOpen) {
[self.combinedInputStream open]; // this seems to work around the 'Stream ... is sending an event before being opened' Apple bug
}
#endif
#endif
}
break;
case NSStreamEventHasBytesAvailable: {
}
break;
case NSStreamEventHasSpaceAvailable: {
if (self.combinedInputStream) {
NSStreamStatus inputStatus = self.combinedInputStream.streamStatus;
if (inputStatus == NSStreamStatusClosed) {
CMISLogTrace(@"combinedInputStream %@ is closed", self.combinedInputStream);
} else if (inputStatus == NSStreamStatusAtEnd){
CMISLogTrace(@"combinedInputStream %@ has reached the end", self.combinedInputStream);
} else if (inputStatus == NSStreamStatusError){
CMISLogTrace(@"combinedInputStream %@ input stream error: %@", self.combinedInputStream, self.combinedInputStream.streamError);
[self stopSendWithStatus:@"Network read error"];
}
}
if (self.bufferOffset == self.bufferLimit) {
if (self.streamStartData != nil) {
self.streamStartData = nil;
self.bufferOffset = 0;
self.bufferLimit = 0;
}
if (self.inputStream != nil) {
NSInteger rawBytesRead;
uint8_t rawBuffer[kRawBufferSize];
rawBytesRead = [self.inputStream read:rawBuffer maxLength:kRawBufferSize];
if (-1 == rawBytesRead) {
[self stopSendWithStatus:@"Error while reading from source input stream"];
} else if (0 != rawBytesRead) {
NSData *encodedBuffer;
if (self.base64Encoding) {
encodedBuffer = [CMISBase64Encoder dataByEncodingText:[NSData dataWithBytes:rawBuffer length:rawBytesRead]];
} else {
encodedBuffer = [NSData dataWithBytes:rawBuffer length:rawBytesRead];
}
self.dataBuffer = [NSData dataWithData:encodedBuffer];
self.bufferOffset = 0;
self.bufferLimit = encodedBuffer.length;
} else {
[self.inputStream close];
self.inputStream = nil;
self.bufferOffset = 0;
self.bufferLimit = self.streamEndData.length;
self.dataBuffer = [NSData dataWithData:self.streamEndData];
self.streamEndData = nil;
}
if ((self.bufferLimit == self.bufferOffset) && self.encoderStream != nil) {
self.encoderStream.delegate = nil;
[self.encoderStream close];
}
} else if (self.streamEndData != nil) {
self.bufferOffset = 0;
self.bufferLimit = self.streamEndData.length;
self.dataBuffer = [NSData dataWithData:self.streamEndData];
self.streamEndData = nil;
}
if ((self.bufferOffset == self.bufferLimit) && (self.encoderStream != nil)) {
self.encoderStream.delegate = nil;
[self.encoderStream close];
}
}
if (self.bufferOffset != self.bufferLimit) {
NSUInteger length = self.dataBuffer.length;
uint8_t buffer[length];
[self.dataBuffer getBytes:buffer length:length];
NSInteger bytesWritten;
bytesWritten = [self.encoderStream write:&buffer[self.bufferOffset] maxLength:self.bufferLimit - self.bufferOffset];
if (bytesWritten <= 0) {
[self stopSendWithStatus:@"Network write error"];
NSError *cmisError = [CMISErrors createCMISErrorWithCode:kCMISErrorCodeConnection detailedDescription:@"Network write error"];
[self URLSession:nil task:nil didCompleteWithError:cmisError];
} else {
self.bufferOffset += bytesWritten;
}
}
}
break;
case NSStreamEventErrorOccurred: {
[self stopSendWithStatus:@"Stream open error"];
}
break;
case NSStreamEventEndEncountered: {
}
break;
default:
break;
}
}
#pragma mark Private methods
- (void)prepareStreams
{
self.bufferOffset = 0;
self.bufferLimit = self.streamStartData.length;
self.dataBuffer = [NSData dataWithData:self.streamStartData];
unsigned long long bytesExpected = self.bytesExpected;
if (self.base64Encoding) {
// if base64 encoding is being used we need to adjust the bytesExpected
bytesExpected = [CMISHttpUploadRequest base64EncodedLength:self.bytesExpected];
}
unsigned long long encodedLength = bytesExpected;
encodedLength += self.streamStartData.length;
encodedLength += self.streamEndData.length;
// update the originally provided expected bytes with encoded length
self.bytesExpected = encodedLength;
self.encodedLength = self.bytesExpected;
if (self.inputStream.streamStatus != NSStreamStatusOpen) {
[self.inputStream open];
}
NSInputStream *requestInputStream;
NSOutputStream *outputStream;
[NSStream createBoundInputStream:&requestInputStream outputStream:&outputStream];
assert(requestInputStream != nil);
assert(outputStream != nil);
self.combinedInputStream = requestInputStream;
self.encoderStream = outputStream;
self.encoderStream.delegate = self;
[self.encoderStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.encoderStream open];
}
+ (unsigned long long)base64EncodedLength:(unsigned long long)contentSize
{
if (0 == contentSize) {
return 0;
}
unsigned long long adjustedThirdPartOfSize = (contentSize / 3) + ( (0 == contentSize % 3 ) ? 0 : 1 );
return 4 * adjustedThirdPartOfSize;
}
+ (unsigned long long)rawEncodedLength:(unsigned long long)base64EncodedSize
{
if (0 == base64EncodedSize) {
return 0;
}
unsigned long long adjustedFourthPartOfSize = (base64EncodedSize / 4) + ( (0 == base64EncodedSize % 4 ) ? 0 : 1 );
return 3 * adjustedFourthPartOfSize;
}
- (void)stopSendWithStatus:(NSString *)statusString
{
if (nil != statusString) {
CMISLogTrace(@"Upload request terminated: Message is %@", statusString);
}
self.bufferOffset = 0;
self.bufferLimit = 0;
self.dataBuffer = nil;
if (self.urlSession != nil) {
[self.urlSession invalidateAndCancel];
self.urlSession = nil;
}
if (self.encoderStream != nil) {
self.encoderStream.delegate = nil;
[self.encoderStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.encoderStream close];
self.encoderStream = nil;
}
self.combinedInputStream = nil;
if(self.inputStream != nil){
[self.inputStream close];
self.inputStream = nil;
}
self.streamEndData = nil;
self.streamStartData = nil;
}
- (void)executeProgressBlock:(NSArray*)valueArray {
if (self.progressBlock) {
self.progressBlock([valueArray[0] unsignedLongLongValue], [valueArray[1] unsignedLongLongValue]);
}
}
@end