libs-base/Source/NSURLSessionTask.m

1630 lines
48 KiB
Mathematica
Raw Normal View History

/**
NSURLSessionTask.m
Copyright (C) 2017-2024 Free Software Foundation, Inc.
Written by: Hugo Melder <hugo@algoriddim.com>
Date: May 2024
This file is part of GNUStep-base
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
If you are interested in a warranty or support for this source code,
contact Scott Christley <scottc@net-community.com> for more information.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110 USA.
*/
#import "NSURLSessionPrivate.h"
#include <curl/curl.h>
#include <dispatch/dispatch.h>
#import "NSURLSessionTaskPrivate.h"
#import "Foundation/NSOperation.h"
#import "Foundation/NSPathUtilities.h"
#import "Foundation/NSFileManager.h"
#import "Foundation/NSFileHandle.h"
#import "Foundation/NSCharacterSet.h"
#import "Foundation/NSDictionary.h"
#import "Foundation/NSError.h"
#import "Foundation/NSData.h"
#import "Foundation/NSUUID.h"
#import "Foundation/NSValue.h"
#import "Foundation/NSURL.h"
#import "Foundation/NSURLError.h"
#import "Foundation/NSURLResponse.h"
#import "Foundation/NSHTTPCookie.h"
#import "Foundation/NSStream.h"
#import "GNUstepBase/NSDebug+GNUstepBase.h" /* For NSDebugMLLog */
#import "GNUstepBase/NSObject+GNUstepBase.h" /* For -[NSObject notImplemented] */
#import "GSURLPrivate.h"
@interface _GSInsensitiveDictionary : NSDictionary
@end
@interface _GSMutableInsensitiveDictionary : NSMutableDictionary
@end
GS_DECLARE const float NSURLSessionTaskPriorityDefault = 0.5;
GS_DECLARE const float NSURLSessionTaskPriorityLow = 0.0;
GS_DECLARE const float NSURLSessionTaskPriorityHigh = 1.0;
GS_DECLARE const int64_t NSURLSessionTransferSizeUnknown = -1;
/* Initialised in +[NSURLSessionTask initialize] */
static Class dataTaskClass;
static Class downloadTaskClass;
static SEL didReceiveDataSel;
static SEL didReceiveResponseSel;
static SEL didCompleteWithErrorSel;
static SEL didFinishDownloadingToURLSel;
static SEL didWriteDataSel;
static SEL needNewBodyStreamSel;
static SEL willPerformHTTPRedirectionSel;
static NSString *taskTransferDataKey = @"transferData";
static NSString *taskTemporaryFileLocationKey = @"tempFileLocation";
static NSString *taskTemporaryFileHandleKey = @"tempFileHandle";
static NSString *taskInputStreamKey = @"inputStream";
static NSString *taskUploadData = @"uploadData";
/* Translate WinSock2 Error Codes */
#ifdef _WIN32
static inline NSInteger
translateWinSockToPOSIXError(NSInteger err)
{
switch (err)
{
case WSAEADDRINUSE:
err = EADDRINUSE;
break;
case WSAEADDRNOTAVAIL:
err = EADDRNOTAVAIL;
break;
case WSAEINPROGRESS:
err = EINPROGRESS;
break;
case WSAECONNRESET:
err = ECONNRESET;
break;
case WSAECONNABORTED:
err = ECONNABORTED;
break;
case WSAECONNREFUSED:
err = ECONNREFUSED;
break;
case WSAEHOSTUNREACH:
err = EHOSTUNREACH;
break;
case WSAENETUNREACH:
err = ENETUNREACH;
break;
case WSAETIMEDOUT:
err = ETIMEDOUT;
break;
default:
break;
}
return err;
}
#endif
static inline NSError *
errorForCURLcode(CURL *handle, CURLcode code, char errorBuffer[CURL_ERROR_SIZE])
{
NSString *curlErrorString;
NSString *errorString;
NSDictionary *userInfo;
NSError *error;
NSInteger urlError = NSURLErrorUnknown;
NSInteger posixError;
NSInteger osError = 0;
if (NULL == handle || CURLE_OK == code)
{
return NULL;
}
errorString = [[NSString alloc] initWithCString:errorBuffer];
curlErrorString = [[NSString alloc] initWithCString:curl_easy_strerror(code)];
/* Get errno number from the last connect failure.
*
* libcurl errors that may have saved errno are:
* - CURLE_COULDNT_CONNECT
* - CURLE_FAILED_INIT
* - CURLE_INTERFACE_FAILED
* - CURLE_OPERATION_TIMEDOUT
* - CURLE_RECV_ERROR
* - CURLE_SEND_ERROR
*/
curl_easy_getinfo(handle, CURLINFO_OS_ERRNO, &osError);
#ifdef _WIN32
posixError = translateWinSockToPOSIXError(osError);
#else
posixError = osError;
#endif
/* Translate libcurl to NSURLError codes */
switch (code)
{
case CURLE_UNSUPPORTED_PROTOCOL:
urlError = NSURLErrorUnsupportedURL;
break;
case CURLE_URL_MALFORMAT:
urlError = NSURLErrorBadURL;
break;
/* Connection Errors */
case CURLE_COULDNT_RESOLVE_PROXY:
case CURLE_COULDNT_RESOLVE_HOST:
urlError = NSURLErrorDNSLookupFailed;
break;
case CURLE_QUIC_CONNECT_ERROR:
case CURLE_COULDNT_CONNECT:
urlError = NSURLErrorCannotConnectToHost;
break;
case CURLE_OPERATION_TIMEDOUT:
urlError = NSURLErrorTimedOut;
break;
case CURLE_FILESIZE_EXCEEDED:
urlError = NSURLErrorDataLengthExceedsMaximum;
break;
case CURLE_LOGIN_DENIED:
urlError = NSURLErrorUserAuthenticationRequired;
break;
/* Response Errors */
case CURLE_WEIRD_SERVER_REPLY:
urlError = NSURLErrorBadServerResponse;
break;
case CURLE_REMOTE_ACCESS_DENIED:
urlError = NSURLErrorNoPermissionsToReadFile;
break;
case CURLE_GOT_NOTHING:
urlError = NSURLErrorZeroByteResource;
break;
case CURLE_RECV_ERROR:
urlError = NSURLErrorResourceUnavailable;
break;
/* Callback Errors */
case CURLE_ABORTED_BY_CALLBACK:
case CURLE_WRITE_ERROR:
errorString = @"Transfer aborted by user";
urlError = NSURLErrorCancelled;
break;
/* SSL Errors */
case CURLE_SSL_CACERT_BADFILE:
case CURLE_SSL_PINNEDPUBKEYNOTMATCH:
case CURLE_SSL_CONNECT_ERROR:
urlError = NSURLErrorSecureConnectionFailed;
break;
case CURLE_SSL_CERTPROBLEM:
urlError = NSURLErrorClientCertificateRejected;
break;
case CURLE_SSL_INVALIDCERTSTATUS:
case CURLE_SSL_ISSUER_ERROR:
urlError = NSURLErrorServerCertificateUntrusted;
break;
default:
urlError = NSURLErrorUnknown;
break;
}
/* Adjust error based on underlying OS error if available */
if (code == CURLE_COULDNT_CONNECT || code == CURLE_RECV_ERROR
|| code == CURLE_SEND_ERROR)
{
switch (posixError)
{
case EADDRINUSE:
urlError = NSURLErrorCannotConnectToHost;
break;
case EADDRNOTAVAIL:
urlError = NSURLErrorCannotFindHost;
break;
case ECONNREFUSED:
urlError = NSURLErrorCannotConnectToHost;
break;
case ENETUNREACH:
urlError = NSURLErrorDNSLookupFailed;
break;
case ETIMEDOUT:
urlError = NSURLErrorTimedOut;
break;
default: /* Do not alter urlError if we have no match */
break;
}
}
userInfo = @{
@"_curlErrorCode" : [NSNumber numberWithInteger:code],
@"_curlErrorString" : curlErrorString,
/* This is the raw POSIX error or WinSock2 Error Code depending on OS */
@"_errno" : [NSNumber numberWithInteger:osError],
NSLocalizedDescriptionKey : errorString
};
error = [NSError errorWithDomain:NSURLErrorDomain
code:urlError
userInfo:userInfo];
[curlErrorString release];
[errorString release];
return error;
}
/* CURLOPT_PROGRESSFUNCTION: progress reports by libcurl */
static int
progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
curl_off_t ultotal, curl_off_t ulnow)
{
NSURLSessionTask *task = clientp;
/* Returning -1 from this callback makes libcurl abort the transfer and return
* CURLE_ABORTED_BY_CALLBACK.
*/
if (YES == [task _shouldStopTransfer])
{
return -1;
}
[task _setCountOfBytesReceived:dlnow];
[task _setCountOfBytesSent:ulnow];
[task _setCountOfBytesExpectedToSend:ultotal];
[task _setCountOfBytesExpectedToReceive:dltotal];
return 0;
}
/* CURLOPT_HEADERFUNCTION: callback for received headers
*
* This function is called for each header line and is called
* again when a redirect or authentication occurs.
*
* libcurl does not unfold HTTP "folded headers" (deprecated since RFC 7230).
*/
size_t
header_callback(char *ptr, size_t size, size_t nitems, void *userdata)
{
NSURLSessionTask *task;
NSMutableDictionary *taskData;
NSMutableDictionary *headerFields;
NSString *headerLine;
NSInteger headerCallbackCount;
NSRange range;
NSCharacterSet *set;
task = (NSURLSessionTask *) userdata;
taskData = [task _taskData];
headerFields = [taskData objectForKey:@"headers"];
headerCallbackCount = [task _headerCallbackCount] + 1;
set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
[task _setHeaderCallbackCount:headerCallbackCount];
if (nil == headerFields)
{
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ Could not find 'headers' key in taskData", task);
return 0;
}
headerLine = [[NSString alloc] initWithBytes:ptr
length:nitems
encoding:NSUTF8StringEncoding];
// First line is the HTTP Version
if (1 == headerCallbackCount)
{
[taskData setObject:headerLine forKey:@"version"];
[headerLine release];
return size * nitems;
}
/* Header fields can be extended over multiple lines by preceding
* each extra line with at least one SP or HT (RFC 2616).
*
* This is known as line folding. We append the value to the
* previous header's value.
*/
if ((ptr[0] == ' ') || (ptr[0] == '\t'))
{
NSString *key;
if (nil != (key = [taskData objectForKey:@"lastHeaderKey"]))
{
NSString *value;
NSString *trimmedLine;
value = [headerFields objectForKey:key];
if (!value)
{
NSError *error;
NSString *errorDescription;
errorDescription = [NSString
stringWithFormat:@"Header is line folded but previous header "
@"key '%@' does not have an entry",
key];
error =
[NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorCancelled
userInfo:@{
NSLocalizedDescriptionKey : errorDescription
}];
[taskData setObject:error forKey:NSUnderlyingErrorKey];
[headerLine release];
return 0;
}
trimmedLine = [headerLine stringByTrimmingCharactersInSet:set];
value = [value stringByAppendingString:trimmedLine];
[headerFields setObject:value forKey:key];
}
[headerLine release];
return size * nitems;
}
range = [headerLine rangeOfString:@":"];
if (NSNotFound != range.location)
{
NSString *key;
NSString *value;
key = [headerLine substringToIndex:range.location];
value = [headerLine substringFromIndex:range.location + 1];
/* Remove LWS from key and value */
key = [key stringByTrimmingCharactersInSet:set];
value = [value stringByTrimmingCharactersInSet:set];
[headerFields setObject:value forKey:key];
/* Used for line unfolding */
[taskData setObject:key forKey:@"lastHeaderKey"];
[headerLine release];
return size * nitems;
}
[headerLine release];
/* Final Header Line:
*
* If this is the initial request (not a redirect) and delegate updates are
* enabled, notify the delegate about the initial response.
*/
if (nitems > 1 && (ptr[0] == '\r') && (ptr[1] == '\n'))
{
NSURLSession *session;
id delegate;
NSHTTPURLResponse *response;
NSString *version;
NSString *urlString;
NSURL *url;
CURL *handle;
char *effURL;
NSInteger numberOfRedirects = 0;
NSInteger statusCode = 0;
session = [task _session];
delegate = [task delegate];
handle = [task _easyHandle];
numberOfRedirects = [task _numberOfRedirects] + 1;
[task _setNumberOfRedirects:numberOfRedirects];
[task _setHeaderCallbackCount:0];
curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &statusCode);
curl_easy_getinfo(handle, CURLINFO_EFFECTIVE_URL, &effURL);
if (nil == (version = [taskData objectForKey:@"version"]))
{
/* Default to HTTP/1.0 if no data is available */
version = @"HTTP/1.0";
}
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ version=%@ status=%ld found %ld headers", task,
version, statusCode, [headerFields count]);
urlString = [[NSString alloc] initWithCString:effURL];
url = [NSURL URLWithString:urlString];
response = [[NSHTTPURLResponse alloc] initWithURL:url
statusCode:statusCode
HTTPVersion:version
headerFields:[headerFields copy]];
[task _setCookiesFromHeaders:headerFields];
[task _setResponse:response];
/* URL redirection handling for 3xx status codes, if delegate updates are
* enabled.
*
* NOTE: The URLSession API does not provide a way to limit redirection
* attempts.
*/
if ([task _properties] & GSURLSessionUpdatesDelegate && statusCode >= 300
&& statusCode < 400)
{
NSString *location;
/*
* RFC 7231: 7.1.2 Location [Header]
* Location = URI-reference
*
* The field value consists of a single URI-reference. When it has
* the form of a relative reference ([RFC3986], Section 4.2), the
* final value is computed by resolving it against the effective
* request URI
* ([RFC3986], Section 5).
*/
location = [headerFields objectForKey:@"Location"];
if (nil != location)
{
NSURL *redirectURL;
NSMutableURLRequest *newRequest;
/* baseURL is only used, if location is a relative reference */
redirectURL = [NSURL URLWithString:location relativeToURL:url];
newRequest = [[task originalRequest] mutableCopy];
[newRequest setURL:redirectURL];
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ status=%ld has Location header. Prepare "
@"for redirection with url=%@",
task, statusCode, redirectURL);
if ([delegate respondsToSelector:willPerformHTTPRedirectionSel])
{
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ ask delegate for redirection "
@"permission. Pausing handle.",
task);
curl_easy_pause(handle, CURLPAUSE_ALL);
[[session delegateQueue] addOperationWithBlock:^{
void (^completionHandler)(NSURLRequest *) = ^(
NSURLRequest *userRequest) {
/* Changes are dispatched onto workqueue */
dispatch_async([session _workQueue], ^{
if (NULL == userRequest)
{
curl_easy_pause(handle, CURLPAUSE_CONT);
[task _setShouldStopTransfer:YES];
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ willPerformHTTPRedirection "
@"completionHandler called with nil "
@"request",
task);
}
else
{
NSString *newURLString;
newURLString = [[userRequest URL] absoluteString];
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ willPerformHTTPRedirection "
@"delegate completionHandler called "
@"with new URL %@",
task, newURLString);
/* Remove handle for reconfiguration */
[session _removeHandle:handle];
/* Reset statistics */
[task _setCountOfBytesReceived:0];
[task _setCountOfBytesSent:0];
[task _setCountOfBytesExpectedToReceive:0];
[task _setCountOfBytesExpectedToSend:0];
[task _setCurrentRequest:userRequest];
/* Update URL in easy handle */
curl_easy_setopt(handle, CURLOPT_URL,
[newURLString UTF8String]);
curl_easy_pause(handle, CURLPAUSE_CONT);
[session _addHandle:handle];
}
});
};
[delegate URLSession:session
task:task
willPerformHTTPRedirection:response
newRequest:newRequest
completionHandler:completionHandler];
}];
[headerFields removeAllObjects];
return size * nitems;
}
else
{
NSDebugLLog(
GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ status=%ld has Location header but "
@"delegate does not respond to "
@"willPerformHTTPRedirection:. Redirecting to Location %@",
task, statusCode, redirectURL);
/* Remove handle for reconfiguration */
[session _removeHandle:handle];
curl_easy_setopt(handle, CURLOPT_URL,
[[redirectURL absoluteString] UTF8String]);
/* Reset statistics */
[task _setCountOfBytesReceived:0];
[task _setCountOfBytesSent:0];
[task _setCountOfBytesExpectedToReceive:0];
[task _setCountOfBytesExpectedToSend:0];
[task _setCurrentRequest:newRequest];
/* Re-add handle to session */
[session _addHandle:handle];
}
[headerFields removeAllObjects];
return size * nitems;
}
else
{
NSError *error;
NSString *errorString;
errorString = [NSString
stringWithFormat:@"task=%@ status=%ld has no Location header",
task, statusCode];
error = [NSError
errorWithDomain:NSURLErrorDomain
code:NSURLErrorBadServerResponse
userInfo:@{NSLocalizedDescriptionKey : errorString}];
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY, @"%@", errorString);
[taskData setObject:error forKey:NSUnderlyingErrorKey];
return 0;
}
}
[headerFields removeAllObjects];
/* URLSession:dataTask:didReceiveResponse:completionHandler:
* is called *after* all potential redirections are handled.
*
* FIXME: Enforce this and implement a custom redirect system
*/
if ([task _properties] & GSURLSessionUpdatesDelegate &&
[task isKindOfClass:dataTaskClass] &&
[delegate respondsToSelector:didReceiveResponseSel])
{
dispatch_queue_t queue;
queue = [session _workQueue];
/* Pause until the completion handler is called */
curl_easy_pause(handle, CURLPAUSE_ALL);
[[session delegateQueue] addOperationWithBlock:^{
[delegate URLSession:session
dataTask:(NSURLSessionDataTask *) task
didReceiveResponse:response
completionHandler:^(
NSURLSessionResponseDisposition disposition) {
/* FIXME: Implement NSURLSessionResponseBecomeDownload */
if (disposition == NSURLSessionResponseCancel)
{
[task _setShouldStopTransfer:YES];
}
/* Unpause easy handle */
dispatch_async(queue, ^{
curl_easy_pause(handle, CURLPAUSE_CONT);
});
}];
}];
}
[urlString release];
[response release];
}
return size * nitems;
}
/* CURLOPT_READFUNCTION: read callback for data uploads */
size_t
read_callback(char *buffer, size_t size, size_t nitems, void *userdata)
{
NSURLSession *session;
NSURLSessionTask *task;
NSMutableDictionary *taskData;
NSInputStream *stream;
NSInteger bytesWritten;
task = (NSURLSessionTask *) userdata;
session = [task _session];
taskData = [task _taskData];
stream = [taskData objectForKey:taskInputStreamKey];
if (nil == stream)
{
id<NSURLSessionTaskDelegate> delegate = [task delegate];
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ requesting new body stream from delegate", task);
if ([delegate respondsToSelector:needNewBodyStreamSel])
{
[[[task _session] delegateQueue] addOperationWithBlock:^{
[delegate URLSession:session
task:task
needNewBodyStream:^(NSInputStream *bodyStream) {
/* Add input stream to task data */
[taskData setObject:bodyStream forKey:taskInputStreamKey];
/* Continue with the transfer */
curl_easy_pause([task _easyHandle], CURLPAUSE_CONT);
}];
}];
return CURL_READFUNC_PAUSE;
}
else
{
NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY,
@"task=%@ no input stream was given and delegate does "
@"not respond to URLSession:task:needNewBodyStream:",
task);
return CURL_READFUNC_ABORT;
}
}
bytesWritten = [stream read:(uint8_t *) buffer maxLength:(size * nitems)];
/* An error occured while reading from the inputStream */
if (bytesWritten < 0)
{
NSError *error;
error = [NSError
errorWithDomain:NSURLErrorDomain
code:NSURLErrorCancelled
userInfo:@{
NSLocalizedDescriptionKey :
@"An error occured while reading from the body stream",
NSUnderlyingErrorKey : [stream streamError]
}];
[taskData setObject:error forKey:NSUnderlyingErrorKey];
return CURL_READFUNC_ABORT;
}
return bytesWritten;
}
/* CURLOPT_WRITEFUNCTION: callback for writing received data from easy handle */
static size_t
write_callback(char *ptr, size_t size, size_t nmemb, void *userdata)
{
NSURLSessionTask *task;
NSURLSession *session;
NSMutableDictionary *taskData;
NSData *dataFragment;
NSInteger properties;
task = (NSURLSessionTask *) userdata;
session = [task _session];
taskData = [task _taskData];
dataFragment = [[NSData alloc] initWithBytes:ptr length:(size * nmemb)];
properties = [task _properties];
if (properties & GSURLSessionStoresDataInMemory)
{
NSMutableData *data;
data = [taskData objectForKey:taskTransferDataKey];
if (!data)
{
data = [[NSMutableData alloc] init];
/* Strong reference maintained by taskData */
[taskData setObject:data forKey:taskTransferDataKey];
[data release];
}
[data appendData:dataFragment];
}
else if (properties & GSURLSessionWritesDataToFile)
{
NSFileHandle *handle;
NSError *error = NULL;
// Get a temporary file path and create a file handle
if (nil == (handle = [taskData objectForKey:taskTemporaryFileHandleKey]))
{
handle = [task _createTemporaryFileHandleWithError:&error];
/* We add the error to taskData as an underlying error */
if (NULL != error)
{
[taskData setObject:error forKey:NSUnderlyingErrorKey];
[dataFragment release];
return 0;
}
}
[handle writeData:dataFragment];
}
/* Notify delegate */
if (properties & GSURLSessionUpdatesDelegate)
{
id delegate = [task delegate];
if ([task isKindOfClass:dataTaskClass] &&
[delegate respondsToSelector:didReceiveDataSel])
{
[[session delegateQueue] addOperationWithBlock:^{
[delegate URLSession:session
dataTask:(NSURLSessionDataTask *) task
didReceiveData:dataFragment];
}];
}
/* Notify delegate about the download process */
if ([task isKindOfClass:downloadTaskClass] &&
[delegate respondsToSelector:didWriteDataSel])
{
NSURLSessionDownloadTask *downloadTask;
int64_t bytesWritten;
int64_t totalBytesWritten;
int64_t totalBytesExpectedToReceive;
downloadTask = (NSURLSessionDownloadTask *) task;
bytesWritten = [dataFragment length];
[downloadTask _updateCountOfBytesWritten:bytesWritten];
totalBytesWritten = [downloadTask _countOfBytesWritten];
totalBytesExpectedToReceive =
[downloadTask countOfBytesExpectedToReceive];
[[session delegateQueue] addOperationWithBlock:^{
[delegate URLSession:session
downloadTask:downloadTask
didWriteData:bytesWritten
totalBytesWritten:totalBytesWritten
totalBytesExpectedToWrite:totalBytesExpectedToReceive];
}];
}
}
[dataFragment release];
return size * nmemb;
}
@implementation NSURLSessionTask
{
_Atomic(BOOL) _shouldStopTransfer;
/* Opaque value for storing task specific properties */
NSInteger _properties;
/* Internal task data */
NSMutableDictionary *_taskData;
NSInteger _numberOfRedirects;
NSInteger _headerCallbackCount;
NSUInteger _suspendCount;
char _curlErrorBuffer[CURL_ERROR_SIZE];
struct curl_slist *_headerList;
CURL *_easyHandle;
NSURLSession *_session;
}
+ (void)initialize
{
dataTaskClass = [NSURLSessionDataTask class];
downloadTaskClass = [NSURLSessionDownloadTask class];
didReceiveDataSel = @selector(URLSession:dataTask:didReceiveData:);
didReceiveResponseSel =
@selector(URLSession:dataTask:didReceiveResponse:completionHandler:);
didCompleteWithErrorSel = @selector(URLSession:task:didCompleteWithError:);
didFinishDownloadingToURLSel =
@selector(URLSession:downloadTask:didFinishDownloadingToURL:);
didWriteDataSel = @selector
(URLSession:
downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:);
needNewBodyStreamSel = @selector(URLSession:task:needNewBodyStream:);
willPerformHTTPRedirectionSel = @selector
(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:);
}
- (instancetype)initWithSession:(NSURLSession *)session
request:(NSURLRequest *)request
taskIdentifier:(NSUInteger)identifier
{
self = [super init];
if (self)
{
NSString *httpMethod;
NSData *certificateBlob;
NSURL *url;
NSDictionary *immConfigHeaders;
NSURLSessionConfiguration *configuration;
NSHTTPCookieStorage *storage;
_GSMutableInsensitiveDictionary *requestHeaders = nil;
_GSMutableInsensitiveDictionary *configHeaders = nil;
_taskIdentifier = identifier;
_taskData = [[NSMutableDictionary alloc] init];
_shouldStopTransfer = NO;
_numberOfRedirects = -1;
_headerCallbackCount = 0;
ASSIGNCOPY(_originalRequest, request);
ASSIGNCOPY(_currentRequest, request);
httpMethod = [[_originalRequest HTTPMethod] lowercaseString];
url = [_originalRequest URL];
requestHeaders = [[_originalRequest _insensitiveHeaders] mutableCopy];
configuration = [session configuration];
/* Only retain the session once the -resume method is called
* and release the session as the last thing done once the
* task has completed. This avoids a retain loop causing
* session and tasks to be leaked.
*/
_session = session;
_suspendCount = 0;
_state = NSURLSessionTaskStateSuspended;
_curlErrorBuffer[0] = '\0';
/* Configure initial task data
*/
[_taskData setObject:[NSMutableDictionary new] forKey:@"headers"];
/* Easy Handle Configuration
*/
_easyHandle = curl_easy_init();
if ([@"head" isEqualToString:httpMethod])
{
curl_easy_setopt(_easyHandle, CURLOPT_NOBODY, 1L);
}
/* Setup upload data if a HTTPBody or HTTPBodyStream is present in the
* URLRequest
*/
if (nil != [_originalRequest HTTPBody])
{
NSData *body = [_originalRequest HTTPBody];
curl_easy_setopt(_easyHandle, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(_easyHandle, CURLOPT_POSTFIELDSIZE_LARGE,
[body length]);
curl_easy_setopt(_easyHandle, CURLOPT_POSTFIELDS, [body bytes]);
}
else if (nil != [_originalRequest HTTPBodyStream])
{
NSInputStream *stream = [_originalRequest HTTPBodyStream];
[_taskData setObject:stream forKey:taskInputStreamKey];
curl_easy_setopt(_easyHandle, CURLOPT_READFUNCTION, read_callback);
curl_easy_setopt(_easyHandle, CURLOPT_READDATA, self);
curl_easy_setopt(_easyHandle, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(_easyHandle, CURLOPT_POSTFIELDSIZE, -1);
}
/* Configure HTTP method and URL */
curl_easy_setopt(_easyHandle, CURLOPT_CUSTOMREQUEST,
[[_originalRequest HTTPMethod] UTF8String]);
curl_easy_setopt(_easyHandle, CURLOPT_URL,
[[url absoluteString] UTF8String]);
/* This callback function gets called by libcurl as soon as there is data
* received that needs to be saved. For most transfers, this callback gets
* called many times and each invoke delivers another chunk of data.
*
* This is directly mapped to -[NSURLSessionDataDelegate
* URLSession:dataTask:didReceiveData:].
*/
curl_easy_setopt(_easyHandle, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(_easyHandle, CURLOPT_WRITEDATA, self);
/* Retrieve the header data
*
* If the delegate conforms to the NSURLSessionDataDelegate
* - URLSession:dataTask:didReceiveResponse:completionHandler:
* we can notify it about the header response.
*/
curl_easy_setopt(_easyHandle, CURLOPT_HEADERFUNCTION, header_callback);
curl_easy_setopt(_easyHandle, CURLOPT_HEADERDATA, self);
curl_easy_setopt(_easyHandle, CURLOPT_ERRORBUFFER, _curlErrorBuffer);
/* The task is now associated with the easy handle and can be accessed
* using curl_easy_getinfo with CURLINFO_PRIVATE.
*/
curl_easy_setopt(_easyHandle, CURLOPT_PRIVATE, self);
/* Disable libcurl's build-in progress reporting */
curl_easy_setopt(_easyHandle, CURLOPT_NOPROGRESS, 0L);
/* Specifiy our own progress function with the user pointer being the
* current object
*/
curl_easy_setopt(_easyHandle, CURLOPT_XFERINFOFUNCTION,
progress_callback);
curl_easy_setopt(_easyHandle, CURLOPT_XFERINFODATA, self);
/* Do not Follow redirects by default
*
* libcurl does not provide a direct interface
* for redirect notification. We have implemented our own redirection
* system in header_callback.
*/
curl_easy_setopt(_easyHandle, CURLOPT_FOLLOWLOCATION, 0L);
/* Set timeout in connect phase */
curl_easy_setopt(_easyHandle, CURLOPT_CONNECTTIMEOUT,
(NSInteger)[request timeoutInterval]);
/* Set overall timeout */
curl_easy_setopt(_easyHandle, CURLOPT_TIMEOUT,
[configuration timeoutIntervalForResource]);
/* Set to HTTP/3 if requested */
if ([request assumesHTTP3Capable])
{
curl_easy_setopt(_easyHandle, CURLOPT_HTTP_VERSION,
CURL_HTTP_VERSION_3);
}
/* Configure the custom CA certificate if available */
if (nil != (certificateBlob = [_session _certificateBlob]))
{
// CURLOPT_CAINFO_BLOB was added in 7.77.0
#if LIBCURL_VERSION_NUM >= 0x074D00
struct curl_blob blob;
blob.data = (void *) [certificateBlob bytes];
blob.len = [certificateBlob length];
/* Session becomes a strong reference when task is resumed until the
* end of transfer. */
blob.flags = CURL_BLOB_NOCOPY;
curl_easy_setopt(_easyHandle, CURLOPT_CAINFO_BLOB, &blob);
#else
curl_easy_setopt(_easyHandle, CURLOPT_CAINFO,
[_session _certificatePath]);
#endif
}
/* Process config headers */
immConfigHeaders = [configuration HTTPAdditionalHeaders];
if (nil != immConfigHeaders)
{
configHeaders = [[_GSMutableInsensitiveDictionary alloc]
initWithDictionary:immConfigHeaders
copyItems:NO];
/* Merge Headers.
*
* If the same header appears in both the configuration's
* HTTPAdditionalHeaders and the request object (where applicable),
* the request object’s value takes precedence.
*/
[configHeaders
addEntriesFromDictionary:(NSDictionary *) requestHeaders];
requestHeaders = configHeaders;
}
/* Use stored cookies is instructed to do so
*/
storage = [configuration HTTPCookieStorage];
if (nil != storage && [configuration HTTPShouldSetCookies])
{
NSDictionary *cookieHeaders;
NSArray<NSHTTPCookie *> *cookies;
/* No headers were set */
if (nil == requestHeaders)
{
requestHeaders = [_GSMutableInsensitiveDictionary new];
}
cookies = [storage cookiesForURL:url];
if ([cookies count] > 0)
{
cookieHeaders =
[NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
[requestHeaders addEntriesFromDictionary:cookieHeaders];
}
}
/* Append Headers to the libcurl header list
*/
[requestHeaders
enumerateKeysAndObjectsUsingBlock:^(id<NSCopying> key, id object,
BOOL *stop) {
NSString *headerLine;
headerLine = [NSString stringWithFormat:@"%@: %@", key, object];
/* We have removed all reserved headers in NSURLRequest */
_headerList = curl_slist_append(_headerList, [headerLine UTF8String]);
}];
curl_easy_setopt(_easyHandle, CURLOPT_HTTPHEADER, _headerList);
}
return self;
}
- (void)_enableAutomaticRedirects:(BOOL)flag
{
curl_easy_setopt(_easyHandle, CURLOPT_FOLLOWLOCATION, flag ? 1L : 0L);
}
- (void)_enableUploadWithData:(NSData *)data
{
curl_easy_setopt(_easyHandle, CURLOPT_UPLOAD, 1L);
/* Retain data */
[_taskData setObject:data forKey:taskUploadData];
curl_easy_setopt(_easyHandle, CURLOPT_POSTFIELDSIZE_LARGE, [data length]);
curl_easy_setopt(_easyHandle, CURLOPT_POSTFIELDS, [data bytes]);
/* The method is overwritten by CURLOPT_UPLOAD. Change it back. */
curl_easy_setopt(_easyHandle, CURLOPT_CUSTOMREQUEST,
[[_originalRequest HTTPMethod] UTF8String]);
}
- (void)_enableUploadWithSize:(NSInteger)size
{
curl_easy_setopt(_easyHandle, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(_easyHandle, CURLOPT_READFUNCTION, read_callback);
curl_easy_setopt(_easyHandle, CURLOPT_READDATA, self);
if (size > 0)
{
curl_easy_setopt(_easyHandle, CURLOPT_POSTFIELDSIZE_LARGE, size);
}
else
{
curl_easy_setopt(_easyHandle, CURLOPT_POSTFIELDSIZE, -1);
}
/* The method is overwritten by CURLOPT_UPLOAD. Change it back. */
curl_easy_setopt(_easyHandle, CURLOPT_CUSTOMREQUEST,
[[_originalRequest HTTPMethod] UTF8String]);
}
- (CURL *)_easyHandle
{
return _easyHandle;
}
- (void)_setVerbose:(BOOL)flag
{
dispatch_async([_session _workQueue], ^{
curl_easy_setopt(_easyHandle, CURLOPT_VERBOSE, flag ? 1L : 0L);
});
}
- (void)_setBodyStream:(NSInputStream *)stream
{
[_taskData setObject:stream forKey:taskInputStreamKey];
}
- (void)_setOriginalRequest:(NSURLRequest *)request
{
ASSIGNCOPY(_originalRequest, request);
}
- (void)_setCurrentRequest:(NSURLRequest *)request
{
ASSIGNCOPY(_currentRequest, request);
}
- (void)_setResponse:(NSURLResponse *)response
{
NSURLResponse *oldResponse = _response;
_response = [response retain];
[oldResponse release];
}
- (void)_setCountOfBytesSent:(int64_t)count
{
_countOfBytesSent = count;
}
- (void)_setCountOfBytesReceived:(int64_t)count
{
_countOfBytesReceived = count;
}
- (void)_setCountOfBytesExpectedToSend:(int64_t)count
{
_countOfBytesExpectedToSend = count;
}
- (void)_setCountOfBytesExpectedToReceive:(int64_t)count
{
_countOfBytesExpectedToReceive = count;
}
- (NSMutableDictionary *)_taskData
{
return _taskData;
}
- (NSInteger)_properties
{
return _properties;
}
- (void)_setProperties:(NSInteger)properties
{
_properties = properties;
}
- (NSURLSession *)_session
{
return _session;
}
- (BOOL)_shouldStopTransfer
{
return _shouldStopTransfer;
}
- (void)_setShouldStopTransfer:(BOOL)flag
{
_shouldStopTransfer = flag;
}
- (NSInteger)_numberOfRedirects
{
return _numberOfRedirects;
}
- (void)_setNumberOfRedirects:(NSInteger)redirects
{
_numberOfRedirects = redirects;
}
- (NSInteger)_headerCallbackCount
{
return _headerCallbackCount;
}
- (void)_setHeaderCallbackCount:(NSInteger)count
{
_headerCallbackCount = count;
}
/* Creates a temporary file and opens a file handle for writing */
- (NSFileHandle *)_createTemporaryFileHandleWithError:(NSError **)error
{
NSFileManager *mgr;
NSFileHandle *handle;
NSString *path;
NSURL *url;
mgr = [NSFileManager defaultManager];
path = NSTemporaryDirectory();
path = [path stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
url = [NSURL fileURLWithPath:path];
[_taskData setObject:url forKey:taskTemporaryFileLocationKey];
if (![mgr createFileAtPath:path contents:nil attributes:nil])
{
if (error)
{
NSString *errorDescription = [NSString
stringWithFormat:@"Failed to create temporary file at path %@",
path];
*error = [NSError
errorWithDomain:NSCocoaErrorDomain
code:NSURLErrorCannotCreateFile
userInfo:@{NSLocalizedDescriptionKey : errorDescription}];
}
return nil;
}
handle = [NSFileHandle fileHandleForWritingAtPath:path];
[_taskData setObject:handle forKey:taskTemporaryFileHandleKey];
return handle;
}
/* Called in _checkForCompletion */
- (void)_transferFinishedWithCode:(CURLcode)code
{
NSError *error = errorForCURLcode(_easyHandle, code, _curlErrorBuffer);
if (_properties & GSURLSessionWritesDataToFile)
{
NSFileHandle *handle;
if (nil != (handle = [_taskData objectForKey:taskTemporaryFileHandleKey]))
{
[handle closeFile];
}
}
if (_properties & GSURLSessionUpdatesDelegate)
{
if (_properties & GSURLSessionWritesDataToFile &&
[_delegate respondsToSelector:didFinishDownloadingToURLSel])
{
NSURL *url = [_taskData objectForKey:taskTemporaryFileLocationKey];
[[_session delegateQueue] addOperationWithBlock:^{
[(id<NSURLSessionDownloadDelegate>) _delegate
URLSession:_session
downloadTask:(NSURLSessionDownloadTask *) self
didFinishDownloadingToURL:url];
}];
}
if ([_delegate respondsToSelector:didCompleteWithErrorSel])
{
[[_session delegateQueue] addOperationWithBlock:^{
[_delegate URLSession:_session
task:self
didCompleteWithError:error];
}];
}
}
/* NSURLSessionUploadTask is a subclass of a NSURLSessionDataTask with the
* same completion handler signature. It thus follows the same code path.
*/
if ((_properties & GSURLSessionStoresDataInMemory)
&& (_properties & GSURLSessionHasCompletionHandler) &&
[self isKindOfClass:dataTaskClass])
{
NSURLSessionDataTask *dataTask;
NSData *data;
dataTask = (NSURLSessionDataTask *) self;
data = [_taskData objectForKey:taskTransferDataKey];
[[_session delegateQueue] addOperationWithBlock:^{
[dataTask _completionHandler](data, _response, error);
}];
}
else if ((_properties & GSURLSessionWritesDataToFile)
&& (_properties & GSURLSessionHasCompletionHandler) &&
[self isKindOfClass:downloadTaskClass])
{
NSURLSessionDownloadTask *downloadTask;
NSURL *tempFile;
downloadTask = (NSURLSessionDownloadTask *) self;
tempFile = [_taskData objectForKey:taskTemporaryFileLocationKey];
[[_session delegateQueue] addOperationWithBlock:^{
[downloadTask _completionHandler](tempFile, _response, error);
}];
}
RELEASE(_session);
}
/* Called in header_callback */
- (void)_setCookiesFromHeaders:(NSDictionary *)headers
{
NSURL *url;
NSArray *cookies;
NSURLSessionConfiguration *config;
config = [_session configuration];
url = [_currentRequest URL];
/* FIXME: Implement NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain */
if (NSHTTPCookieAcceptPolicyNever != [config HTTPCookieAcceptPolicy]
&& nil != [config HTTPCookieStorage])
{
cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers
forURL:url];
if ([cookies count] > 0)
{
[[config HTTPCookieStorage] setCookies:cookies
forURL:url
mainDocumentURL:nil];
}
}
}
#pragma mark - Public Methods
- (void)suspend
{
_suspendCount += 1;
if (_suspendCount == 1)
{
/* If there is an active transfer associated with this task, it will be
* aborted in the next libcurl progress_callback.
*
* TODO: Pause the easy handle put do not abort the full transfer!
* . What if the handle is currently paused?
*/
_shouldStopTransfer = YES;
}
}
- (void)resume
{
/* Only resume a transfer if the task is not suspended and in suspended state
*/
if (_suspendCount == 0 && [self state] == NSURLSessionTaskStateSuspended)
{
/*
* Properly retain the session to keep a reference
* to the task. This ensures correct API behaviour.
*/
RETAIN(_session);
_state = NSURLSessionTaskStateRunning;
[_session _resumeTask:self];
return;
}
_suspendCount -= 1;
}
- (void)cancel
{
/* Transfer is aborted in the next libcurl progress_callback
*
* If a NSURLSessionTask delegate is set and this is not a convenience task,
* URLSession:task:didCompleteWithError: is called after receiving
* CURLMSG_DONE in -[NSURLSessionTask _checkForCompletion].
*/
dispatch_async([_session _workQueue], ^{
/* Unpause the easy handle if previously paused */
curl_easy_pause(_easyHandle, CURLPAUSE_CONT);
_shouldStopTransfer = YES;
_state = NSURLSessionTaskStateCanceling;
});
}
- (float)priority
{
return _priority;
}
- (void)setPriority:(float)priority
{
_priority = priority;
}
- (id)copyWithZone:(NSZone *)zone
{
NSURLSessionTask *copy = [[[self class] alloc] init];
if (copy)
{
copy->_originalRequest = [_originalRequest copyWithZone:zone];
copy->_currentRequest = [_currentRequest copyWithZone:zone];
copy->_response = [_response copyWithZone:zone];
/* FIXME: Seems like copyWithZone: is not implemented for NSProgress */
copy->_progress = [_progress copy];
copy->_earliestBeginDate = [_earliestBeginDate copyWithZone:zone];
copy->_taskDescription = [_taskDescription copyWithZone:zone];
copy->_taskData = [_taskData copyWithZone:zone];
copy->_easyHandle = curl_easy_duphandle(_easyHandle);
}
return copy;
}
#pragma mark - Getter and Setter
- (NSUInteger)taskIdentifier
{
return _taskIdentifier;
}
- (NSURLRequest *)originalRequest
{
return AUTORELEASE([_originalRequest copy]);
}
- (NSURLRequest *)currentRequest
{
return AUTORELEASE([_currentRequest copy]);
}
- (NSURLResponse *)response
{
return AUTORELEASE([_response copy]);
}
- (NSURLSessionTaskState)state
{
return _state;
}
- (NSProgress *)progress
{
return _progress;
}
- (NSError *)error
{
return _error;
}
- (id<NSURLSessionTaskDelegate>)delegate
{
return _delegate;
}
- (void)setDelegate:(id<NSURLSessionTaskDelegate>)delegate
{
id<NSURLSessionTaskDelegate> oldDelegate = _delegate;
_delegate = RETAIN(delegate);
RELEASE(oldDelegate);
}
- (NSDate *)earliestBeginDate
{
return _earliestBeginDate;
}
- (void)setEarliestBeginDate:(NSDate *)date
{
NSDate *oldDate = _earliestBeginDate;
_earliestBeginDate = RETAIN(date);
RELEASE(oldDate);
}
- (int64_t)countOfBytesClientExpectsToSend
{
return _countOfBytesClientExpectsToSend;
}
- (int64_t)countOfBytesClientExpectsToReceive
{
return _countOfBytesClientExpectsToReceive;
}
- (int64_t)countOfBytesSent
{
return _countOfBytesSent;
}
- (int64_t)countOfBytesReceived
{
return _countOfBytesReceived;
}
- (int64_t)countOfBytesExpectedToSend
{
return _countOfBytesExpectedToSend;
}
- (int64_t)countOfBytesExpectedToReceive
{
return _countOfBytesExpectedToReceive;
}
- (NSString *)taskDescription
{
return _taskDescription;
}
- (void)setTaskDescription:(NSString *)description
{
NSString *oldDescription = _taskDescription;
_taskDescription = [description copy];
RELEASE(oldDescription);
}
- (void)dealloc
{
/* The session retains this task until the transfer is complete and the easy
* handle removed from the multi handle.
*
* It is save to release the curl handle here.
*/
curl_easy_cleanup(_easyHandle);
curl_slist_free_all(_headerList);
RELEASE(_originalRequest);
RELEASE(_currentRequest);
RELEASE(_response);
RELEASE(_progress);
RELEASE(_earliestBeginDate);
RELEASE(_taskDescription);
RELEASE(_taskData);
[super dealloc];
}
@end /* NSURLSessionTask */
@implementation NSURLSessionDataTask
- (GSNSURLSessionDataCompletionHandler)_completionHandler
{
return _completionHandler;
}
- (void)_setCompletionHandler:(GSNSURLSessionDataCompletionHandler)handler
{
_completionHandler = _Block_copy(handler);
}
- (void)dealloc
{
_Block_release(_completionHandler);
[super dealloc];
}
@end
@implementation NSURLSessionUploadTask
@end
@implementation NSURLSessionDownloadTask
- (GSNSURLSessionDownloadCompletionHandler)_completionHandler
{
return _completionHandler;
}
- (void)_setCompletionHandler:(GSNSURLSessionDownloadCompletionHandler)handler
{
_completionHandler = _Block_copy(handler);
}
- (int64_t)_countOfBytesWritten
{
return _countOfBytesWritten;
};
- (void)_updateCountOfBytesWritten:(int64_t)count
{
_countOfBytesWritten += count;
}
- (void)dealloc
{
_Block_release(_completionHandler);
[super dealloc];
}
@end
@implementation NSURLSessionStreamTask
@end