libs-base/Source/GSNativeProtocol.m
Frederik Seiffert 82d9917c2c NSURLSession: implement missing body data for data completion handlers
Now using the previously unused "in-memory" body data drain if a task has a completion handler, which requires the full body to be passed on completion.

Also consolidated private NSURLSessionTask methods, some of which were previously implemented twice in separate categories with the same name, leading to possible undefined runtime behavior.
2023-03-11 20:22:00 +01:00

811 lines
24 KiB
Objective-C

#import "GSURLPrivate.h"
#import "GSNativeProtocol.h"
#import "GSTransferState.h"
#import "GSURLSessionTaskBody.h"
#import "Foundation/NSData.h"
#import "Foundation/NSDictionary.h"
#import "Foundation/NSError.h"
#import "Foundation/NSException.h"
#import "Foundation/NSOperation.h"
#import "Foundation/NSURL.h"
#import "Foundation/NSURLError.h"
#import "Foundation/NSURLSession.h"
#import "Foundation/NSFileHandle.h"
static BOOL isEasyHandlePaused(GSNativeProtocolInternalState state)
{
switch (state)
{
case GSNativeProtocolInternalStateInitial:
return NO;
case GSNativeProtocolInternalStateFulfillingFromCache:
return NO;
case GSNativeProtocolInternalStateTransferReady:
return NO;
case GSNativeProtocolInternalStateTransferInProgress:
return NO;
case GSNativeProtocolInternalStateTransferCompleted:
return NO;
case GSNativeProtocolInternalStateTransferFailed:
return NO;
case GSNativeProtocolInternalStateWaitingForRedirectCompletionHandler:
return NO;
case GSNativeProtocolInternalStateWaitingForResponseCompletionHandler:
return YES;
case GSNativeProtocolInternalStateTaskCompleted:
return NO;
}
}
static BOOL isEasyHandleAddedToMultiHandle(GSNativeProtocolInternalState state)
{
switch (state)
{
case GSNativeProtocolInternalStateInitial:
return NO;
case GSNativeProtocolInternalStateFulfillingFromCache:
return NO;
case GSNativeProtocolInternalStateTransferReady:
return NO;
case GSNativeProtocolInternalStateTransferInProgress:
return YES;
case GSNativeProtocolInternalStateTransferCompleted:
return NO;
case GSNativeProtocolInternalStateTransferFailed:
return NO;
case GSNativeProtocolInternalStateWaitingForRedirectCompletionHandler:
return NO;
case GSNativeProtocolInternalStateWaitingForResponseCompletionHandler:
return YES;
case GSNativeProtocolInternalStateTaskCompleted:
return NO;
}
}
@interface NSURLSession (GSNativeProtocolInternal)
- (void) removeHandle: (GSEasyHandle*)handle;
- (void) addHandle: (GSEasyHandle*)handle;
@end
@implementation NSURLSession (GSNativeProtocolInternal)
- (void) removeHandle: (GSEasyHandle*)handle
{
[_multiHandle removeHandle: handle];
}
- (void) addHandle: (GSEasyHandle*)handle
{
[_multiHandle addHandle: handle];
}
@end
@implementation NSURLSessionTask (GSNativeProtocolInternal)
- (void) setCurrentRequest: (NSURLRequest*)request
{
ASSIGN(_currentRequest, request);
}
- (dispatch_queue_t) workQueue
{
return _workQueue;
}
- (NSUInteger) suspendCount
{
return _suspendCount;
}
- (void) getBodyWithCompletion: (void (^)(GSURLSessionTaskBody *body))completion
{
if (nil != _knownBody)
{
completion(_knownBody);
return;
};
GSURLSessionTaskBody *body = AUTORELEASE([[GSURLSessionTaskBody alloc] init]);
completion(body);
}
- (GSURLSessionTaskBody*) knownBody
{
return _knownBody;
}
- (void) setKnownBody: (GSURLSessionTaskBody*)body
{
ASSIGN(_knownBody, body);
}
- (void) setError: (NSError*)error
{
ASSIGN(_error, error);
}
- (void) setCountOfBytesReceived: (int64_t)count
{
_countOfBytesReceived = count;
}
- (void) setCountOfBytesExpectedToReceive: (int64_t)count
{
_countOfBytesExpectedToReceive = count;
}
- (void) setCountOfBytesExpectedToSend: (int64_t)count
{
_countOfBytesExpectedToSend = count;
}
- (void (^)(NSData *data, NSURLResponse *response, NSError *error)) dataCompletionHandler
{
return _dataCompletionHandler;
}
- (void (^)(NSURL *location, NSURLResponse *response, NSError *error)) downloadCompletionHandler
{
return _downloadCompletionHandler;
}
@end
@implementation GSCompletionAction
- (void) dealloc
{
DESTROY(_redirectRequest);
[super dealloc];
}
- (GSCompletionActionType) type
{
return _type;
}
- (void) setType: (GSCompletionActionType) type
{
_type = type;
}
- (int) errorCode
{
return _errorCode;
}
- (void) setErrorCode: (int)code
{
_errorCode = code;
}
- (NSURLRequest*) redirectRequest
{
return _redirectRequest;
}
- (void) setRedirectRequest: (NSURLRequest*)request
{
ASSIGN(_redirectRequest, request);
}
@end
@implementation GSNativeProtocol
- (instancetype) initWithTask: (NSURLSessionTask*)_task
cachedResponse: (NSCachedURLResponse*)_cachedResponse
client: (id<NSURLProtocolClient>)_client
{
if (nil != (self = [super initWithTask: _task
cachedResponse: _cachedResponse
client: _client]))
{
_internalState = GSNativeProtocolInternalStateInitial;
_easyHandle = [[GSEasyHandle alloc] initWithDelegate: self];
}
return self;
}
- (void) dealloc
{
DESTROY(_easyHandle);
DESTROY(_transferState);
[super dealloc];
}
+ (NSURLRequest*) canonicalRequestForRequest: (NSURLRequest*)request
{
return request;
}
- (void) startLoading
{
[self resume];
}
- (void) stopLoading
{
NSURLSessionTask *task;
if (nil != (task = [self task])
&& NSURLSessionTaskStateSuspended == [task state])
{
[self suspend];
}
else
{
[self setInternalState: GSNativeProtocolInternalStateTransferFailed];
NSAssert(nil != [task error], @"Missing error for failed task");
[self completeTaskWithError: [task error]];
}
}
- (void) setInternalState: (GSNativeProtocolInternalState)newState
{
NSURLSessionTask *task;
GSNativeProtocolInternalState oldState;
if (!isEasyHandlePaused(_internalState) && isEasyHandlePaused(newState))
{
NSAssert(NO, @"Need to solve pausing receive.");
}
if (isEasyHandleAddedToMultiHandle(_internalState)
&& !isEasyHandleAddedToMultiHandle(newState))
{
if (nil != (task = [self task]))
{
[[task session] removeHandle: _easyHandle];
}
}
oldState = _internalState;
_internalState = newState;
if (!isEasyHandleAddedToMultiHandle(oldState)
&& isEasyHandleAddedToMultiHandle(_internalState))
{
if (nil != (task = [self task]))
{
[[task session] addHandle: _easyHandle];
}
}
if (isEasyHandlePaused(oldState) && !isEasyHandlePaused(_internalState))
{
NSAssert(NO, @"Need to solve pausing receive.");
}
}
- (void) startNewTransferWithRequest: (NSURLRequest*)request
{
NSURLSessionTask *task = [self task];
[task setCurrentRequest: request];
NSAssert(nil != [request URL], @"No URL in request.");
[task getBodyWithCompletion: ^(GSURLSessionTaskBody *body)
{
[task setKnownBody: body];
[self setInternalState: GSNativeProtocolInternalStateTransferReady];
ASSIGN(_transferState,
[self createTransferStateWithURL: [request URL]
body: body
workQueue: [task workQueue]]);
[self configureEasyHandleForRequest: request body: body];
if ([task suspendCount] < 1)
{
[self resume];
}
}];
}
- (void) configureEasyHandleForRequest: (NSURLRequest*)request
body: (GSURLSessionTaskBody*)body
{
NSAssert(NO, @"Requires concrete implementation");
}
- (GSTransferState*) createTransferStateWithURL: (NSURL*)url
body: (GSURLSessionTaskBody*)body
workQueue: (dispatch_queue_t)workQueue
{
GSDataDrain *drain = [self createTransferBodyDataDrain];
switch ([body type])
{
case GSURLSessionTaskBodyTypeNone:
return AUTORELEASE([[GSTransferState alloc] initWithURL: url
bodyDataDrain: drain]);
case GSURLSessionTaskBodyTypeData:
{
GSBodyDataSource *source;
source = AUTORELEASE([[GSBodyDataSource alloc] initWithData: [body data]]);
return AUTORELEASE([[GSTransferState alloc] initWithURL: url
bodyDataDrain: drain
bodySource: source]);
}
case GSURLSessionTaskBodyTypeFile:
{
GSBodyFileSource *source;
source = AUTORELEASE([[GSBodyFileSource alloc] initWithFileURL: [body fileURL]
workQueue: workQueue
dataAvailableHandler: ^{
[_easyHandle unpauseSend];
}]);
return AUTORELEASE([[GSTransferState alloc] initWithURL: url
bodyDataDrain: drain
bodySource: source]);
}
case GSURLSessionTaskBodyTypeStream:
{
GSBodyStreamSource *source;
source = AUTORELEASE([[GSBodyStreamSource alloc] initWithInputStream: [body inputStream]]);
return AUTORELEASE([[GSTransferState alloc] initWithURL: url
bodyDataDrain: drain
bodySource: source]);
}
}
}
// The data drain.
// This depends on what the task needs.
- (GSDataDrain*) createTransferBodyDataDrain
{
NSURLSessionTask *task = [self task];
GSDataDrain *dd = AUTORELEASE([[GSDataDrain alloc] init]);
if ([task isKindOfClass: [NSURLSessionDownloadTask class]])
{
// drain to file for download tasks
[dd setType: GSDataDrainTypeToFile];
}
else if ([task dataCompletionHandler])
{
// drain to memory if task has a completion handler, which requires the
// full body to be passed on completion
[dd setType: GSDataDrainInMemory];
}
else
{
// otherwise the data is probably sent to the delegate as it arrives
[dd setType: GSDataDrainTypeIgnore];
}
return dd;
}
- (void) resume
{
NSURLSessionTask *task;
task = [self task];
if (_internalState == GSNativeProtocolInternalStateInitial)
{
NSAssert(nil != [task originalRequest], @"Task has no original request.");
// Check if the cached response is good to use:
if (nil != [self cachedResponse]
&& [self canRespondFromCacheUsing: [self cachedResponse]])
{
[self setInternalState:
GSNativeProtocolInternalStateFulfillingFromCache];
dispatch_async([task workQueue],
^{
id<NSURLProtocolClient> client;
client = [self client];
[client URLProtocol: self
cachedResponseIsValid: [self cachedResponse]];
[client URLProtocol: self
didReceiveResponse: [[self cachedResponse] response]
cacheStoragePolicy: NSURLCacheStorageNotAllowed];
if ([[[self cachedResponse] data] length] > 0)
{
if ([client respondsToSelector:
@selector(URLProtocol:didLoad:)])
{
[client URLProtocol: self
didLoadData: [[self cachedResponse] data]];
}
}
if ([client respondsToSelector:
@selector(URLProtocolDidFinishLoading:)])
{
[client URLProtocolDidFinishLoading: self];
}
[self setInternalState:
GSNativeProtocolInternalStateTaskCompleted];
});
}
else
{
[self startNewTransferWithRequest: [task originalRequest]];
}
}
if (_internalState == GSNativeProtocolInternalStateTransferReady
&& nil != _transferState)
{
[self setInternalState: GSNativeProtocolInternalStateTransferInProgress];
}
}
- (BOOL) canRespondFromCacheUsing: (NSCachedURLResponse*)response
{
// Allows a native protocol to process a cached response.
// If `YES` is returned, the protocol will replay the cached response
// instead of starting a new transfer. The default implementation invalidates
// the response in the cache and returns `NO`.
NSURLCache *cache;
NSURLSessionTask *task;
task = [self task];
cache = [[[task session] configuration] URLCache];
if (nil != cache && [task isKindOfClass: [NSURLSessionDataTask class]])
{
[cache removeCachedResponseForDataTask: (NSURLSessionDataTask*)task];
}
return NO;
}
- (void) suspend
{
if (_internalState == GSNativeProtocolInternalStateTransferInProgress)
{
[self setInternalState: GSNativeProtocolInternalStateTransferReady];
}
}
- (void) completeTaskWithError: (NSError*)error
{
[[self task] setError: error];
NSAssert(_internalState == GSNativeProtocolInternalStateTransferFailed,
@"Trying to complete the task, but its transfer isn't complete / failed.");
// We don't want a timeout to be triggered after this.
// The timeout timer needs to be cancelled.
[_easyHandle setTimeoutTimer: nil];
[self setInternalState: GSNativeProtocolInternalStateTaskCompleted];
}
- (GSEasyHandleAction) didReceiveData: (NSData*)data
{
NSURLResponse *response;
NSAssert(GSNativeProtocolInternalStateTransferInProgress == _internalState,
@"Received body data, but no transfer in progress.");
response = [self validateHeaderCompleteTransferState: _transferState];
if (nil != response)
{
[_transferState setResponse: response];
}
[self notifyDelegateAboutReceivedData: data];
_internalState = GSNativeProtocolInternalStateTransferInProgress;
ASSIGN(_transferState, [_transferState byAppendingBodyData: data]);
return GSEasyHandleActionProceed;
}
- (NSURLResponse*) validateHeaderCompleteTransferState: (GSTransferState*)ts
{
if (![ts isHeaderComplete])
{
NSAssert(NO, @"Received body data, but the header is not complete, yet.");
}
return nil;
}
- (void) notifyDelegateAboutReceivedData: (NSData*)data
{
NSURLSession *session;
NSURLSessionTask *task;
id<NSURLSessionDelegate> delegate;
task = [self task];
NSAssert(nil != task, @"Cannot notify");
session = [task session];
NSAssert(nil != session, @"Missing session");
/* Calculate received data length */
[task setCountOfBytesReceived: (int64_t)[data length] + [task countOfBytesReceived]];
delegate = [session delegate];
if (nil != delegate
&& [task isKindOfClass: [NSURLSessionDataTask class]]
&& [delegate respondsToSelector: @selector(URLSession:dataTask:didReceiveData:)])
{
id<NSURLSessionDataDelegate> dataDelegate;
NSURLSessionDataTask *dataTask;
dataDelegate = (id<NSURLSessionDataDelegate>)delegate;
dataTask = (NSURLSessionDataTask*)task;
[[session delegateQueue] addOperationWithBlock:
^{
[dataDelegate URLSession: session
dataTask: dataTask
didReceiveData: data];
}];
}
if (nil != delegate
&& [task isKindOfClass: [NSURLSessionDownloadTask class]]
&& [delegate respondsToSelector: @selector
(URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)])
{
id<NSURLSessionDownloadDelegate> downloadDelegate;
NSURLSessionDownloadTask *downloadTask;
downloadDelegate = (id<NSURLSessionDownloadDelegate>)delegate;
downloadTask = (NSURLSessionDownloadTask*)task;
[[session delegateQueue] addOperationWithBlock:
^{
[downloadDelegate URLSession: session
downloadTask: downloadTask
didWriteData: (int64_t)[data length]
totalBytesWritten: [task countOfBytesReceived]
totalBytesExpectedToWrite: [task countOfBytesExpectedToReceive]];
}];
}
}
- (void) notifyDelegateAboutUploadedDataCount: (int64_t)count
{
NSURLSessionTask *task;
id<NSURLSessionDelegate> delegate;
task = [self task];
NSAssert(nil != task, @"Cannot notify");
delegate = [[task session] delegate];
if (nil != delegate
&& [task isKindOfClass: [NSURLSessionUploadTask class]]
&& [delegate respondsToSelector: @selector(URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)])
{
id<NSURLSessionTaskDelegate> taskDelegate;
NSURLSession *session;
session = [task session];
NSAssert(nil != session, @"Missing session");
taskDelegate = (id<NSURLSessionTaskDelegate>)delegate;
[[session delegateQueue] addOperationWithBlock:
^{
[taskDelegate URLSession: session
task: task
didSendBodyData: count
totalBytesSent: [task countOfBytesSent]
totalBytesExpectedToSend: [task countOfBytesExpectedToSend]];
}];
}
}
- (GSEasyHandleAction) didReceiveHeaderData: (NSData*)data
contentLength: (int64_t)contentLength
{
NSAssert(NO, @"Require concrete implementation");
return GSEasyHandleActionAbort;
}
- (void) fillWriteBufferLength: (NSInteger)length
result: (void (^)(GSEasyHandleWriteBufferResult result, NSInteger length, NSData *data))result
{
id<GSURLSessionTaskBodySource> source;
NSAssert(GSNativeProtocolInternalStateTransferInProgress == _internalState,
@"Requested to fill write buffer, but transfer isn't in progress.");
source = [_transferState requestBodySource];
NSAssert(nil != source,
@"Requested to fill write buffer, but transfer state has no body source.");
if (nil == result)
{
return;
}
[source getNextChunkWithLength: length
completionHandler: ^(GSBodySourceDataChunk chunk, NSData *_Nullable data)
{
switch (chunk)
{
case GSBodySourceDataChunkData:
{
NSUInteger count = [data length];
[self notifyDelegateAboutUploadedDataCount: (int64_t)count];
result(GSEasyHandleWriteBufferResultBytes, count, data);
break;
}
case GSBodySourceDataChunkDone:
result(GSEasyHandleWriteBufferResultBytes, 0, nil);
break;
case GSBodySourceDataChunkRetryLater:
// At this point we'll try to pause the easy handle. The body
// source is responsible for un-pausing the handle once data
// becomes available.
result(GSEasyHandleWriteBufferResultPause, -1, nil);
break;
case GSBodySourceDataChunkError:
result(GSEasyHandleWriteBufferResultAbort, -1, nil);
break;
}
}];
}
- (void) transferCompletedWithError: (NSError*)error
{
/* At this point the transfer is complete and we can decide what to do.
* If everything went well, we will simply forward the resulting data
* to the delegate. But in case of redirects etc. we might send another
* request.
*/
NSURLRequest *request;
NSURLResponse *response;
GSCompletionAction *action;
if (nil != error)
{
[self setInternalState: GSNativeProtocolInternalStateTransferFailed];
[self failWithError: error request: [self request]];
return;
}
NSAssert(_internalState == GSNativeProtocolInternalStateTransferInProgress,
@"Transfer completed, but it wasn't in progress.");
request = [[self task] currentRequest];
NSAssert(nil != request,
@"Transfer completed, but there's no current request.");
if (nil != [[self task] response])
{
[_transferState setResponse: [[self task] response]];
}
response = [_transferState response];
NSAssert(nil != response, @"Transfer completed, but there's no response.");
[self setInternalState: GSNativeProtocolInternalStateTransferCompleted];
action = [self completeActionForCompletedRequest: request response: response];
switch ([action type])
{
case GSCompletionActionTypeCompleteTask:
[self completeTask];
break;
case GSCompletionActionTypeFailWithError:
[self setInternalState: GSNativeProtocolInternalStateTransferFailed];
error = [NSError errorWithDomain: NSURLErrorDomain
code: [action errorCode]
userInfo: nil];
[self failWithError: error request: request];
break;
case GSCompletionActionTypeRedirectWithRequest:
[self redirectForRequest: [action redirectRequest]];
break;
}
}
- (GSCompletionAction*) completeActionForCompletedRequest: (NSURLRequest*)request
response: (NSURLResponse*)response
{
GSCompletionAction *action;
action = AUTORELEASE([[GSCompletionAction alloc] init]);
[action setType: GSCompletionActionTypeCompleteTask];
return action;
}
- (void) completeTask
{
NSURLSessionTask *task;
GSDataDrain *bodyDataDrain;
id<NSURLProtocolClient> client;
NSAssert(_internalState == GSNativeProtocolInternalStateTransferCompleted,
@"Trying to complete the task, but its transfer isn't complete.");
task = [self task];
[task setResponse: [_transferState response]];
client = [self client];
// We don't want a timeout to be triggered after this. The timeout timer
// needs to be cancelled.
[_easyHandle setTimeoutTimer: nil];
[self setInternalState: GSNativeProtocolInternalStateTaskCompleted];
// Add complete data to NSURLRequestProperties if the task has a data
// completion handler
bodyDataDrain = [_transferState bodyDataDrain];
if (GSDataDrainInMemory == [bodyDataDrain type])
{
NSData *data = AUTORELEASE([[bodyDataDrain data] copy]);
[[self request] _setProperty: data
forKey: @"tempData"];
}
// Add temporary file URL to NSURLRequest properties
// and close the fileHandle
if ([task isKindOfClass: [NSURLSessionDownloadTask class]])
{
[[bodyDataDrain fileHandle] closeFile];
[[self request] _setProperty: [bodyDataDrain fileURL]
forKey: @"tempFileURL"];
}
if ([client respondsToSelector: @selector(URLProtocolDidFinishLoading:)])
{
[client URLProtocolDidFinishLoading: self];
}
}
- (void) redirectForRequest: (NSURLRequest*)request
{
NSAssert(NO, @"Require concrete implementation");
}
- (void) failWithError: (NSError*)error request: (NSURLRequest*)request
{
NSDictionary *info;
NSError *urlError;
id<NSURLProtocolClient> client;
info = [NSDictionary dictionaryWithObjectsAndKeys:
error, NSUnderlyingErrorKey,
[request URL], NSURLErrorFailingURLErrorKey,
[[request URL] absoluteString], NSURLErrorFailingURLStringErrorKey,
[error localizedDescription], NSLocalizedDescriptionKey, nil];
urlError = [NSError errorWithDomain: NSURLErrorDomain
code: [error code]
userInfo: info];
[self completeTaskWithError: urlError];
client = [self client];
if ([client respondsToSelector: @selector(URLProtocol:didFailWithError:)])
{
[client URLProtocol: self didFailWithError: urlError];
}
}
- (BOOL) seekInputStreamToPosition: (uint64_t)position
{
//TODO implement seek for NSURLSessionUploadTask
return NO;
}
- (void) updateProgressMeterWithTotalBytesSent: (int64_t)totalBytesSent
totalBytesExpectedToSend: (int64_t)totalBytesExpectedToSend
totalBytesReceived: (int64_t)totalBytesReceived
totalBytesExpectedToReceive: (int64_t)totalBytesExpectedToReceive
{
// TODO: Update progress. Note that a single NSURLSessionTask might
// perform multiple transfers. The values in `progress` are only for
// the current transfer.
}
@end