diff --git a/.clang-format b/.clang-format index a1fffd463..cabf64fd3 100644 --- a/.clang-format +++ b/.clang-format @@ -65,7 +65,6 @@ SortIncludes: false SpaceAfterCStyleCast: true SpaceBeforeParens: ControlStatements SpacesBeforeTrailingComments: 1 -UseTab: Always AlignEscapedNewlines: Right AlignTrailingComments: true AllowShortFunctionsOnASingleLine: All diff --git a/.gitignore b/.gitignore index d5362eaed..056c6a9a9 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,12 @@ DerivedData/ ### Xcode Patch ### **/xcshareddata/WorkspaceSettings.xcsettings +# clangd cache +.cache + +# compile commands +compile_commands.json + # End of https://www.gitignore.io/api/xcode .cache diff --git a/ChangeLog b/ChangeLog index 057a58858..90af0da59 100644 --- a/ChangeLog +++ b/ChangeLog @@ -86,6 +86,59 @@ the GNUSTEP_MAKEFILES environment variable or get the output of the gnustep-config script directly. +2024-07-02 Hugo Melder + + * Source/GSEasyHandle.h: + * Source/GSEasyHandle.m: + * Source/GSHTTPURLProtocol.h: + * Source/GSHTTPURLProtocol.m: + * Source/GSMultiHandle.h: + * Source/GSMultiHandle.m: + * Source/GSNativeProtocol.h: + * Source/GSNativeProtocol.m: + * Source/GSTaskRegistry.h: + * Source/GSTaskRegistry.m: + * Source/GSTimeoutSource.h: + * Source/GSTimeoutSource.m: + * Source/GSTransferState.h: + * Source/GSTransferState.m: + * Source/GSURLPrivate.h: + * Source/GSURLSessionTaskBody.h: + * Source/GSURLSessionTaskBody.m: + * Source/GSURLSessionTaskBodySource.h: + * Source/GSURLSessionTaskBodySource.m: + Deleted old NSURLSession implementation. + + * Headers/Foundation/NSURLSession.h: + * Source/NSURLSessionPrivate.h: + * Source/NSURLSession.m: + * Source/NSURLSessionConfiguration.m: + * Source/NSURLSessionTaskPrivate.h: + * Source/NSURLSessionTask.m: + * Source/NSURLSessionConfiguration.m: + The new NSURLSession implementation was reduced to just two main classes: + NSURLSession and NSURLSessionTask. URLSession manages a libcurl multi + handle and the required plumming via libdispatch. Timeout and socket actions + are handled in the libdispatch workqueue. Delegate messages and completion + handlers are submitted to a seperate serial NSOperationQueue. + + * Headers/Foundation/NSUserDefaults.h: + * Source/externs.m: + Added GSCACertificateFilePath for custom CA certificate file configuration. + + * Source/NSHTTPCookie.m: + Reworked cookie expiration date parsing. + + * Source/NSOperation.m: + Clear all blocks in _executionBlocks after completion. + + * Headers/Foundation/NSURLRequest.h: + * Source/NSURLRequest.m: + Implemented -[NSURLRequest assumesHTTP3Capable] and -[NSURLRequest + setAssumesHTTP3Capable:] respectively. The internal case-insensitive header + dictionary is now exposed for private usage with -[NSURLRequest + _insensitiveHeaders]. + 2024-06-21 Richard Frith-Macdonald * Source/NSDistantObject.m: Use standard method name conventions diff --git a/Headers/Foundation/NSURLRequest.h b/Headers/Foundation/NSURLRequest.h index da536c1b4..0b8fcfa3a 100644 --- a/Headers/Foundation/NSURLRequest.h +++ b/Headers/Foundation/NSURLRequest.h @@ -271,6 +271,16 @@ GS_EXPORT_CLASS */ - (NSString *) valueForHTTPHeaderField: (NSString *)field; +#if OS_API_VERSION(MAC_OS_VERSION_11_0, GS_API_LATEST) +/** + * Indicates whether the URL loading system assumes the host is HTTP/3 capable. + * + * This method returns the current assumption of the URL loading system regarding + * the server's HTTP capabilities. + */ +- (BOOL) assumesHTTP3Capable; +#endif + @end @@ -327,6 +337,17 @@ GS_EXPORT_CLASS */ - (void) setValue: (NSString *)value forHTTPHeaderField: (NSString *)field; +#if OS_API_VERSION(MAC_OS_VERSION_11_0, GS_API_LATEST) +/** + * Sets whether the URL loading system should assume the host is HTTP/3 capable. + * + * This method configures the URL loading system's assumptions about the + * server's HTTP capabilities, optimizing the connection process if HTTP/3 is + * supported. + */ +- (void) setAssumesHTTP3Capable: (BOOL)capable; +#endif + @end @protocol GSLogDelegate; diff --git a/Headers/Foundation/NSURLSession.h b/Headers/Foundation/NSURLSession.h index ad549a63b..ef9df4aa9 100644 --- a/Headers/Foundation/NSURLSession.h +++ b/Headers/Foundation/NSURLSession.h @@ -1,11 +1,10 @@ /** NSURLSession.h - Copyright (C) 2017-2023 Free Software Foundation, Inc. + Copyright (C) 2017-2024 Free Software Foundation, Inc. - Written by: Daniel Ferreira - Date: November 2017 - Author: Richard Frith-Macdonald + Written by: Hugo Melder + Date: May 2024 This file is part of GNUStep-base @@ -34,14 +33,18 @@ #import #import #import +#import +#import +#import +#import +#import #if GS_HAVE_NSURLSESSION -#if OS_API_VERSION(MAC_OS_X_VERSION_10_9,GS_API_LATEST) +#if OS_API_VERSION(MAC_OS_X_VERSION_10_9, GS_API_LATEST) + @protocol NSURLSessionDelegate; @protocol NSURLSessionTaskDelegate; -@class GSMultiHandle; -@class GSURLSessionTaskBody; @class NSError; @class NSHTTPURLResponse; @class NSOperationQueue; @@ -58,6 +61,15 @@ @class NSURLSessionUploadTask; @class NSURLSessionDownloadTask; +NS_ASSUME_NONNULL_BEGIN + +typedef void (^GSNSURLSessionDataCompletionHandler)( + NSData *_Nullable data, NSURLResponse *_Nullable response, + NSError *_Nullable error); + +typedef void (^GSNSURLSessionDownloadCompletionHandler)( + NSURL *_Nullable location, NSURLResponse *_Nullable response, + NSError *_Nullable error); /** * NSURLSession is a replacement API for NSURLConnection. It provides @@ -67,18 +79,18 @@ * * An NSURLSession may be bound to a delegate object. The delegate is * invoked for certain events during the lifetime of a session. - * + * * NSURLSession instances are threadsafe. * * An NSURLSession creates NSURLSessionTask objects which represent the * action of a resource being loaded. - * + * * NSURLSessionTask objects are always created in a suspended state and * must be sent the -resume message before they will execute. * * Subclasses of NSURLSessionTask are used to syntactically * differentiate between data and file downloads. - * + * * An NSURLSessionDataTask receives the resource as a series of calls to * the URLSession:dataTask:didReceiveData: delegate method. This is type of * task most commonly associated with retrieving objects for immediate parsing @@ -87,125 +99,156 @@ GS_EXPORT_CLASS @interface NSURLSession : NSObject { - NSOperationQueue *_delegateQueue; - id _delegate; - NSURLSessionConfiguration *_configuration; - NSString *_sessionDescription; - GSMultiHandle *_multiHandle; +@private + NSOperationQueue *_delegateQueue; + id _delegate; + NSURLSessionConfiguration *_configuration; + + NSString *_sessionDescription; } -+ (NSURLSession*) sharedSession; ++ (NSURLSession *)sharedSession; -+ (NSURLSession*) sessionWithConfiguration: (NSURLSessionConfiguration*)configuration; ++ (NSURLSession *)sessionWithConfiguration: + (NSURLSessionConfiguration *)configuration; /* * Customization of NSURLSession occurs during creation of a new session. * If you do specify a delegate, the delegate will be retained until after * the delegate has been sent the URLSession:didBecomeInvalidWithError: message. */ -+ (NSURLSession*) sessionWithConfiguration: (NSURLSessionConfiguration*)configuration - delegate: (id )delegate - delegateQueue: (NSOperationQueue*)queue; ++ (NSURLSession *) + sessionWithConfiguration:(NSURLSessionConfiguration *)configuration + delegate:(nullable id)delegate + delegateQueue:(nullable NSOperationQueue *)queue; -- (NSOperationQueue*) delegateQueue; - -- (id ) delegate; - -- (NSURLSessionConfiguration*) configuration; - -- (NSString*) sessionDescription; - -- (void) setSessionDescription: (NSString*)sessionDescription; - -/* -finishTasksAndInvalidate returns immediately and existing tasks will be +/* -finishTasksAndInvalidate returns immediately and existing tasks will be * allowed to run to completion. New tasks may not be created. The session - * will continue to make delegate callbacks until - * URLSession:didBecomeInvalidWithError: has been issued. + * will continue to make delegate callbacks until + * URLSession:didBecomeInvalidWithError: has been issued. * - * When invalidating a background session, it is not safe to create another - * background session with the same identifier until + * When invalidating a background session, it is not safe to create another + * background session with the same identifier until * URLSession:didBecomeInvalidWithError: has been issued. */ -- (void) finishTasksAndInvalidate; +- (void)finishTasksAndInvalidate; /* -invalidateAndCancel acts as -finishTasksAndInvalidate, but issues - * -cancel to all outstanding tasks for this session. Note task + * -cancel to all outstanding tasks for this session. Note task * cancellation is subject to the state of the task, and some tasks may - * have already have completed at the time they are sent -cancel. + * have already have completed at the time they are sent -cancel. */ -- (void) invalidateAndCancel; +- (void)invalidateAndCancel; -/* +/* * NSURLSessionTask objects are always created in a suspended state and * must be sent the -resume message before they will execute. */ -/* Creates a data task with the given request. +/** Creates a data task with the given request. * The request may have a body stream. */ -- (NSURLSessionDataTask*) dataTaskWithRequest: (NSURLRequest*)request; +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request; -/* Creates a data task to retrieve the contents of the given URL. */ -- (NSURLSessionDataTask*) dataTaskWithURL: (NSURL*)url; +/** Creates a data task to retrieve the contents of the given URL. */ +- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url; -/* Not implemented */ -- (NSURLSessionUploadTask*) uploadTaskWithRequest: (NSURLRequest*)request - fromFile: (NSURL*)fileURL; +- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request + fromFile:(NSURL *)fileURL; -/* Not implemented */ -- (NSURLSessionUploadTask*) uploadTaskWithRequest: (NSURLRequest*)request - fromData: (NSData*)bodyData; +- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request + fromData:(NSData *)bodyData; -/* Not implemented */ -- (NSURLSessionUploadTask*) uploadTaskWithStreamedRequest: (NSURLRequest*)request; +- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest: + (NSURLRequest *)request; /* Creates a download task with the given request. */ -- (NSURLSessionDownloadTask*) downloadTaskWithRequest: (NSURLRequest*)request; +- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request; /* Creates a download task to download the contents of the given URL. */ -- (NSURLSessionDownloadTask*) downloadTaskWithURL: (NSURL*)url; +- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url; -/* Not implemented */ -- (NSURLSessionDownloadTask *) downloadTaskWithResumeData: (NSData *)resumeData; +- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData; -- (void) getTasksWithCompletionHandler: (void (^)(GS_GENERIC_CLASS(NSArray, NSURLSessionDataTask*) *dataTasks, GS_GENERIC_CLASS(NSArray, NSURLSessionUploadTask*) *uploadTasks, GS_GENERIC_CLASS(NSArray, NSURLSessionDownloadTask*) *downloadTasks))completionHandler; +- (void)getTasksWithCompletionHandler: + (void (^)(NSArray *dataTasks, + NSArray *uploadTasks, + NSArray *downloadTasks)) + completionHandler; -- (void) getAllTasksWithCompletionHandler: (void (^)(GS_GENERIC_CLASS(NSArray, __kindof NSURLSessionTask*) *tasks))completionHandler; +- (void)getAllTasksWithCompletionHandler: + (void (^)(GS_GENERIC_CLASS(NSArray, __kindof NSURLSessionTask *) * tasks)) + completionHandler; + +/** + * This serial NSOperationQueue queue is used for dispatching delegate messages + * and completion handlers. + */ +- (NSOperationQueue *)delegateQueue; + +/** + * The delegate for the session. This is the object to which delegate messages + * will be sent. + * + * The session keeps a strong reference to the delegate. + */ +- (nullable id)delegate; + +/** + * The configuration object used to create the session. + * + * A copy of the configuration object is made. + * Changes to the configuration object after the session is created have no + * effect. + */ +- (NSURLSessionConfiguration *)configuration; + +/** + * An App-specific description of the session. + */ +- (nullable NSString *)sessionDescription; + +/** + * Sets an app-specific description of the session. + */ +- (void)setSessionDescription:(NSString *)description; @end /* - * NSURLSession convenience routines deliver results to + * NSURLSession convenience routines deliver results to * a completion handler block. These convenience routines * are not available to NSURLSessions that are configured * as background sessions. * - * Task objects are always created in a suspended state and + * Task objects are always created in a suspended state and * must be sent the -resume message before they will execute. */ -@interface NSURLSession (NSURLSessionAsynchronousConvenience) +@interface +NSURLSession (NSURLSessionAsynchronousConvenience) /* * data task convenience methods. These methods create tasks that * bypass the normal delegate calls for response and data delivery, * and provide a simple cancelable asynchronous interface to receiving - * data. Errors will be returned in the NSURLErrorDomain, + * data. Errors will be returned in the NSURLErrorDomain, * see . The delegate, if any, will still be * called for authentication challenges. */ -- (NSURLSessionDataTask*) dataTaskWithRequest: (NSURLRequest*)request - completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; -- (NSURLSessionDataTask*) dataTaskWithURL: (NSURL*)url - completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; +- (NSURLSessionDataTask *) + dataTaskWithRequest:(NSURLRequest *)request + completionHandler:(GSNSURLSessionDataCompletionHandler)completionHandler; +- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url + completionHandler:(GSNSURLSessionDataCompletionHandler) + completionHandler; +- (NSURLSessionUploadTask *) + uploadTaskWithRequest:(NSURLRequest *)request + fromFile:(NSURL *)fileURL + completionHandler:(GSNSURLSessionDataCompletionHandler)completionHandler; -/* Not implemented */ -- (NSURLSessionUploadTask*) uploadTaskWithRequest: (NSURLRequest*)request - fromFile: (NSURL*)fileURL - completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; - -/* Not implemented */ -- (NSURLSessionUploadTask*) uploadTaskWithRequest: (NSURLRequest*)request - fromData: (NSData*)bodyData - completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; +- (NSURLSessionUploadTask *) + uploadTaskWithRequest:(NSURLRequest *)request + fromData:(NSData *)bodyData + completionHandler:(GSNSURLSessionDataCompletionHandler)completionHandler; /* * download task convenience methods. When a download successfully @@ -213,27 +256,32 @@ GS_EXPORT_CLASS * copied during the invocation of the completion routine. The file * will be removed automatically. */ -- (NSURLSessionDownloadTask*) downloadTaskWithRequest: (NSURLRequest*)request - completionHandler: (void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler; -- (NSURLSessionDownloadTask*) downloadTaskWithURL: (NSURL *)url - completionHandler: (void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler; +- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request + completionHandler: + (GSNSURLSessionDownloadCompletionHandler) + completionHandler; +- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url + completionHandler: + (GSNSURLSessionDownloadCompletionHandler) + completionHandler; -/* Not implemented */ -- (NSURLSessionDownloadTask*) downloadTaskWithResumeData: (NSData*)resumeData - completionHandler: (void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler; +- (NSURLSessionDownloadTask *) + downloadTaskWithResumeData:(NSData *)resumeData + completionHandler: + (GSNSURLSessionDownloadCompletionHandler)completionHandler; @end typedef NS_ENUM(NSUInteger, NSURLSessionTaskState) { /* The task is currently being serviced by the session */ - NSURLSessionTaskStateRunning = 0, + NSURLSessionTaskStateRunning = 0, NSURLSessionTaskStateSuspended = 1, - /* The task has been told to cancel. + /* The task has been told to cancel. * The session will receive URLSession:task:didCompleteWithError:. */ - NSURLSessionTaskStateCanceling = 2, - /* The task has completed and the session will receive no more + NSURLSessionTaskStateCanceling = 2, + /* The task has completed and the session will receive no more * delegate notifications */ - NSURLSessionTaskStateCompleted = 3, + NSURLSessionTaskStateCompleted = 3, }; GS_EXPORT const float NSURLSessionTaskPriorityDefault; @@ -247,125 +295,81 @@ GS_EXPORT const int64_t NSURLSessionTransferSizeUnknown; * of processing a given request. */ GS_EXPORT_CLASS -@interface NSURLSessionTask : NSObject +@interface NSURLSessionTask : NSObject { - /** An identifier for this task, assigned by and unique - * to the owning session - */ NSUInteger _taskIdentifier; + NSURLRequest *_originalRequest; - /** The request this task was created to handle. - */ - NSURLRequest *_originalRequest; + id _delegate; + NSURLSessionTaskState _state; + NSURLRequest *_currentRequest; + NSURLResponse *_response; + NSProgress *_progress; + NSDate *_earliestBeginDate; - /** The request this task is currently handling. This may differ from - * originalRequest due to http server redirection - */ - NSURLRequest *_currentRequest; + _Atomic(int64_t) _countOfBytesClientExpectsToSend; + _Atomic(int64_t) _countOfBytesClientExpectsToReceive; + _Atomic(int64_t) _countOfBytesSent; + _Atomic(int64_t) _countOfBytesReceived; + _Atomic(int64_t) _countOfBytesExpectedToSend; + _Atomic(int64_t) _countOfBytesExpectedToReceive; + _Atomic(double) _priority; - /** The response to the current request, which may be nil if no response - * has been received - */ - NSURLResponse *_response; - - /** number of body bytes already received - */ - int64_t _countOfBytesReceived; - - /** number of body bytes already sent - */ - int64_t _countOfBytesSent; - - /** number of body bytes we expect to send, derived from - * the Content-Length of the HTTP request - */ - int64_t _countOfBytesExpectedToSend; - - /** number of byte bytes we expect to receive, usually derived from the - * Content-Length header of an HTTP response. - */ - int64_t _countOfBytesExpectedToReceive; - - /** a description of the current task for diagnostic purposes - */ - NSString *_taskDescription; - - /** The current state of the task within the session. - */ - NSURLSessionTaskState _state; - - /** The error, if any, delivered via -URLSession:task:didCompleteWithError: - * This is nil until an error has occured. - */ - NSError *_error; - - /** The dispatch queue used to handle this request/response. - * This is actualy a libdispatch queue of type dispatch_queue_t, but on all - * known implementations this is a pointer, so void* is the correct size. - */ - void *_workQueue; - - NSUInteger _suspendCount; - - GSURLSessionTaskBody *_knownBody; - - void (^_dataCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); - void (^_downloadCompletionHandler)(NSURL *location, NSURLResponse *response, NSError *error); + NSString *_taskDescription; + NSError *_error; } -- (NSUInteger) taskIdentifier; +- (NSUInteger)taskIdentifier; -- (NSURLRequest*) originalRequest; +- (nullable NSURLRequest *)originalRequest; +- (nullable NSURLRequest *)currentRequest; +- (nullable NSURLResponse *)response; -- (NSURLRequest*) currentRequest; +- (NSURLSessionTaskState)state; +- (NSProgress *)progress; +- (nullable NSError *)error; -- (NSURLResponse*) response; -- (void) setResponse: (NSURLResponse*)response; +- (nullable id)delegate; +- (void)setDelegate:(nullable id)delegate; -- (int64_t) countOfBytesReceived; +- (nullable NSDate *)earliestBeginDate; +- (void)setEarliestBeginDate:(nullable NSDate *)date; -- (int64_t) countOfBytesSent; +- (int64_t)countOfBytesClientExpectsToSend; +- (int64_t)countOfBytesClientExpectsToReceive; +- (int64_t)countOfBytesSent; +- (int64_t)countOfBytesReceived; +- (int64_t)countOfBytesExpectedToSend; +- (int64_t)countOfBytesExpectedToReceive; -- (int64_t) countOfBytesExpectedToSend; - -- (int64_t) countOfBytesExpectedToReceive; - -- (NSString*) taskDescription; - -- (void) setTaskDescription: (NSString*)taskDescription; - -- (NSURLSessionTaskState) state; - -- (NSError*) error; - -- (NSURLSession*) session; - -/* -cancel returns immediately, but marks a task as being canceled. - * The task will signal -URLSession:task:didCompleteWithError: with an - * error value of { NSURLErrorDomain, NSURLErrorCancelled }. In some - * cases, the task may signal other work before it acknowledges the - * cancelation. -cancel may be sent to a task that has been suspended. +/** + * App-specific description of the task. */ -- (void) cancel; +- (nullable NSString *)taskDescription; -/* - * Suspending a task will prevent the NSURLSession from continuing to - * load data. There may still be delegate calls made on behalf of - * this task (for instance, to report data received while suspending) - * but no further transmissions will be made on behalf of the task - * until -resume is sent. The timeout timer associated with the task - * will be disabled while a task is suspended. +/** + * Sets an app-specific description of the task. */ -- (void) suspend; -- (void) resume; +- (void)setTaskDescription:(nullable NSString *)description; -- (float) priority; -- (void) setPriority: (float)priority; +/** + * Cancels the task and the ongoing transfer. + */ +- (void)cancel; + +- (void)suspend; +- (void)resume; + +- (float)priority; +- (void)setPriority:(float)priority; @end GS_EXPORT_CLASS @interface NSURLSessionDataTask : NSURLSessionTask +{ + void *_completionHandler; +} @end GS_EXPORT_CLASS @@ -374,9 +378,13 @@ GS_EXPORT_CLASS GS_EXPORT_CLASS @interface NSURLSessionDownloadTask : NSURLSessionTask +{ + void *_completionHandler; + int64_t _countOfBytesWritten; +} @end -#if OS_API_VERSION(MAC_OS_X_VERSION_10_11,GS_API_LATEST) +#if OS_API_VERSION(MAC_OS_X_VERSION_10_11, GS_API_LATEST) GS_EXPORT_CLASS @interface NSURLSessionStreamTask : NSURLSessionTask @end @@ -390,65 +398,139 @@ GS_EXPORT_CLASS GS_EXPORT_CLASS @interface NSURLSessionConfiguration : NSObject { - NSString *_identifier; - NSURLCache *_URLCache; + NSString *_identifier; + NSURLCache *_URLCache; NSURLRequestCachePolicy _requestCachePolicy; - NSArray *_protocolClasses; + NSArray *_protocolClasses; NSInteger _HTTPMaximumConnectionLifetime; NSInteger _HTTPMaximumConnectionsPerHost; BOOL _HTTPShouldUsePipelining; NSHTTPCookieAcceptPolicy _HTTPCookieAcceptPolicy; - NSHTTPCookieStorage *_HTTPCookieStorage; - NSURLCredentialStorage *_URLCredentialStorage; + NSHTTPCookieStorage *_HTTPCookieStorage; + NSURLCredentialStorage *_URLCredentialStorage; BOOL _HTTPShouldSetCookies; - NSDictionary *_HTTPAdditionalHeaders; + NSDictionary *_HTTPAdditionalHeaders; + NSTimeInterval _timeoutIntervalForRequest; + NSTimeInterval _timeoutIntervalForResource; } -- (NSURLRequest*) configureRequest: (NSURLRequest*)request; +- (NSURLRequest *)configureRequest:(NSURLRequest *)request; -+ (NSURLSessionConfiguration*) defaultSessionConfiguration; -+ (NSURLSessionConfiguration*) ephemeralSessionConfiguration; -+ (NSURLSessionConfiguration*) backgroundSessionConfigurationWithIdentifier:(NSString*)identifier; ++ (NSURLSessionConfiguration *)defaultSessionConfiguration; ++ (NSURLSessionConfiguration *)ephemeralSessionConfiguration; ++ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier: + (NSString *)identifier; -- (NSDictionary*) HTTPAdditionalHeaders; -- (void) setHTTPAdditionalHeaders: (NSDictionary*)headers; +- (nullable NSDictionary *)HTTPAdditionalHeaders; +- (void)setHTTPAdditionalHeaders:(NSDictionary *)headers; -- (NSHTTPCookieAcceptPolicy) HTTPCookieAcceptPolicy; -- (void) setHTTPCookieAcceptPolicy: (NSHTTPCookieAcceptPolicy)policy; +/** + * Gets the timeout interval to use when waiting for additional data to arrive. + * The request timeout interval controls how long (in seconds) a task should + * wait for additional data to arrive before giving up. The timer is reset + * whenever new data arrives. When the request timer reaches the specified + * interval without receiving any new data, it triggers a timeout. + * + * Currently not used by NSURLSession. + */ +- (NSTimeInterval)timeoutIntervalForRequest; +/** + * Sets the timeout interval to use when waiting for additional data to arrive. + */ +- (void)setTimeoutIntervalForRequest:(NSTimeInterval)interval; -- (NSHTTPCookieStorage*) HTTPCookieStorage; -- (void) setHTTPCookieStorage: (NSHTTPCookieStorage*)storage; +/** + * Gets the maximum amount of time that a resource request should be allowed to + * take. The resource timeout interval controls how long (in seconds) to wait + * for an entire resource to transfer before giving up. The resource timer + * starts when the request is initiated and counts until either the request + * completes or this timeout interval is reached, whichever comes first. + */ +- (NSTimeInterval)timeoutIntervalForResource; +/** + * Sets the maximum amount of time that a resource request should be allowed to + * take. + */ +- (void)setTimeoutIntervalForResource:(NSTimeInterval)interval; -- (NSInteger) HTTPMaximumConnectionsPerHost; -- (void) setHTTPMaximumConnectionsPerHost: (NSInteger)n; +- (NSHTTPCookieAcceptPolicy)HTTPCookieAcceptPolicy; +- (void)setHTTPCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)policy; -- (BOOL) HTTPShouldSetCookies; -- (void) setHTTPShouldSetCookies: (BOOL)flag; +- (nullable NSHTTPCookieStorage *)HTTPCookieStorage; +- (void)setHTTPCookieStorage:(NSHTTPCookieStorage *)storage; -- (BOOL) HTTPShouldUsePipelining; -- (void) setHTTPShouldUsePipelining: (BOOL)flag; +- (NSInteger)HTTPMaximumConnectionsPerHost; +- (void)setHTTPMaximumConnectionsPerHost:(NSInteger)n; -- (NSString*) identifier; +/** + * Indicates whether the session should set cookies. + * + * This property controls whether tasks within sessions based on this + * configuration should automatically include cookies from the shared cookie + * store when making requests. + * + * If set to NO, you must manually provide cookies by adding a Cookie header + * through the session's HTTPAdditionalHeaders property or on a per-request + * basis using a custom NSURLRequest object. + * + * The default value is YES. + * + * See Also: + * - HTTPCookieAcceptPolicy + * - HTTPCookieStorage + * - NSHTTPCookieStorage + * - NSHTTPCookie + */ +- (BOOL)HTTPShouldSetCookies; -- (NSArray*) protocolClasses; +/** + * Sets whether the session should set cookies. + * + * This method controls whether tasks within sessions based on this + * configuration should automatically include cookies from the shared cookie + * store when making requests. + * + * If set to NO, you must manually provide cookies by adding a Cookie header + * through the session's HTTPAdditionalHeaders property or on a per-request + * basis using a custom NSURLRequest object. + * + * The default value is YES. + * + * See Also: + * - HTTPCookieAcceptPolicy + * - HTTPCookieStorage + * - NSHTTPCookieStorage + * - NSHTTPCookie + */ +- (void)setHTTPShouldSetCookies:(BOOL)flag; -- (NSURLRequestCachePolicy) requestCachePolicy; -- (void) setRequestCachePolicy: (NSURLRequestCachePolicy)policy; +/** + * HTTP/1.1 pipelining is not implemented. This flag is ignored. + */ +- (BOOL)HTTPShouldUsePipelining; +- (void)setHTTPShouldUsePipelining:(BOOL)flag; -- (NSURLCache*) URLCache; -- (void) setURLCache: (NSURLCache*)cache; +- (nullable NSString *)identifier; -- (NSURLCredentialStorage*) URLCredentialStorage; -- (void) setURLCredentialStorage: (NSURLCredentialStorage*)storage; +- (nullable NSArray *)protocolClasses; -#if !NO_GNUSTEP +- (NSURLRequestCachePolicy)requestCachePolicy; +- (void)setRequestCachePolicy:(NSURLRequestCachePolicy)policy; + +- (nullable NSURLCache *)URLCache; +- (void)setURLCache:(NSURLCache *)cache; + +- (nullable NSURLCredentialStorage *)URLCredentialStorage; +- (void)setURLCredentialStorage:(NSURLCredentialStorage *)storage; + +#if !NO_GNUSTEP /** Permits a session to be configured so that older connections are reused. * A value of zero or less uses the default behavior where connections are * reused as long as they are not older than 118 seconds, which is reasonable * for the vast majority if situations. */ -- (NSInteger) HTTPMaximumConnectionLifetime; -- (void) setHTTPMaximumConnectionLifetime: (NSInteger)n; +- (NSInteger)HTTPMaximumConnectionLifetime; +- (void)setHTTPMaximumConnectionLifetime:(NSInteger)n; #endif @end @@ -464,7 +546,7 @@ typedef NS_ENUM(NSInteger, NSURLSessionResponseDisposition) { NSURLSessionResponseCancel = 0, NSURLSessionResponseAllow = 1, NSURLSessionResponseBecomeDownload = 2, - NSURLSessionResponseBecomeStream = 3 + NSURLSessionResponseBecomeStream = 3 }; @protocol NSURLSessionDelegate @@ -473,58 +555,72 @@ typedef NS_ENUM(NSInteger, NSURLSessionResponseDisposition) { * invalid because of a systemic error or when it has been * explicitly invalidated, in which case the error parameter will be nil. */ -- (void) URLSession: (NSURLSession*)session - didBecomeInvalidWithError: (NSError*)error; +- (void)URLSession:(NSURLSession *)session + didBecomeInvalidWithError:(nullable NSError *)error; /* Implementing this method permits a delegate to provide authentication * credentials in response to a challenge from the remote server. */ -- (void) URLSession: (NSURLSession*)session -didReceiveChallenge: (NSURLAuthenticationChallenge*)challenge - completionHandler: (void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))handler; +- (void)URLSession:(NSURLSession *)session + didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler: + (void (^)(NSURLSessionAuthChallengeDisposition disposition, + NSURLCredential *credential))handler; @end @protocol NSURLSessionTaskDelegate @optional + +#if OS_API_VERSION(MAC_OS_VERSION_13_0, GS_API_LATEST) +- (void)URLSession:(NSURLSession *)session + didCreateTask:(NSURLSessionTask *)task; +#endif + /* Sent as the last message related to a specific task. Error may be - * nil, which implies that no error occurred and this task is complete. + * nil, which implies that no error occurred and this task is complete. */ -- (void ) URLSession: (NSURLSession*)session - task: (NSURLSessionTask*)task - didCompleteWithError: (NSError*)error; - +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didCompleteWithError:(nullable NSError *)error; + /* Called to request authentication credentials from the delegate when * an authentication request is received from the server which is specific * to this task. */ -- (void) URLSession: (NSURLSession*)session - task: (NSURLSessionTask*)task -didReceiveChallenge: (NSURLAuthenticationChallenge*)challenge - completionHandler: (void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))handler; +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler: + (void (^)(NSURLSessionAuthChallengeDisposition disposition, + NSURLCredential *credential))handler; -/* Periodically informs the delegate of the progress of sending body content +/* Periodically informs the delegate of the progress of sending body content * to the server. */ -- (void) URLSession: (NSURLSession*)session - task: (NSURLSessionTask*)task - didSendBodyData: (int64_t)bytesSent - totalBytesSent: (int64_t)totalBytesSent - totalBytesExpectedToSend: (int64_t)totalBytesExpectedToSend; +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didSendBodyData:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent + totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend; /* An HTTP request is attempting to perform a redirection to a different * URL. You must invoke the completion routine to allow the * redirection, allow the redirection with a modified request, or - * pass nil to the completionHandler to cause the body of the redirection + * pass nil to the completionHandler to cause the body of the redirection * response to be delivered as the payload of this request. The default - * is to follow redirections. + * is to follow redirections. * */ -- (void) URLSession: (NSURLSession*)session - task: (NSURLSessionTask*)task - willPerformHTTPRedirection: (NSHTTPURLResponse*)response - newRequest: (NSURLRequest*)request - completionHandler: (void (^)(NSURLRequest*))completionHandler; +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLRequest *))completionHandler; + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler; @end @@ -532,51 +628,54 @@ didReceiveChallenge: (NSURLAuthenticationChallenge*)challenge @optional /* Sent when data is available for the delegate to consume. */ -- (void) URLSession: (NSURLSession*)session - dataTask: (NSURLSessionDataTask*)dataTask - didReceiveData: (NSData*)data; +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data; /** Informs the delegate of a response. This message is sent when all the * response headers have arrived, before the body of the response arrives. */ -- (void) URLSession: (NSURLSession*)session - dataTask: (NSURLSessionDataTask*)dataTask - didReceiveResponse: (NSURLResponse*)response - completionHandler: (void (^)(NSURLSessionResponseDisposition disposition))completionHandler; +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveResponse:(NSURLResponse *)response + completionHandler: + (void (^)(NSURLSessionResponseDisposition disposition))completionHandler; @end @protocol NSURLSessionDownloadDelegate -/* Sent when a download task that has completed a download. The delegate should - * copy or move the file at the given location to a new location as it will be - * removed when the delegate message returns. URLSession:task:didCompleteWithError: will - * still be called. +/* Sent when a download task that has completed a download. The delegate should + * copy or move the file at the given location to a new location as it will be + * removed when the delegate message returns. + * URLSession:task:didCompleteWithError: will still be called. */ -- (void) URLSession: (NSURLSession *)session - downloadTask: (NSURLSessionDownloadTask *)downloadTask - didFinishDownloadingToURL: (NSURL *)location; +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didFinishDownloadingToURL:(NSURL *)location; @optional /* Sent periodically to notify the delegate of download progress. */ -- (void) URLSession: (NSURLSession *)session - downloadTask: (NSURLSessionDownloadTask *)downloadTask - didWriteData: (int64_t)bytesWritten - totalBytesWritten: (int64_t)totalBytesWritten - totalBytesExpectedToWrite: (int64_t)totalBytesExpectedToWrite; +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didWriteData:(int64_t)bytesWritten + totalBytesWritten:(int64_t)totalBytesWritten + totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite; /* Sent when a download has been resumed. If a download failed with an * error, the -userInfo dictionary of the error will contain an * NSURLSessionDownloadTaskResumeData key, whose value is the resume - * data. + * data. */ -- (void) URLSession: (NSURLSession *)session - downloadTask: (NSURLSessionDownloadTask *)downloadTask - didResumeAtOffset: (int64_t)fileOffset - expectedTotalBytes: (int64_t)expectedTotalBytes; +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didResumeAtOffset:(int64_t)fileOffset + expectedTotalBytes:(int64_t)expectedTotalBytes; @end -#endif -#endif -#endif +NS_ASSUME_NONNULL_END + +#endif /* MAC_OS_X_VERSION_10_9 */ +#endif /* GS_HAVE_NSURLSESSION */ +#endif /* __NSURLSession_h_GNUSTEP_BASE_INCLUDE */ diff --git a/Headers/Foundation/NSUserDefaults.h b/Headers/Foundation/NSUserDefaults.h index 49cb777d6..61f508e2e 100644 --- a/Headers/Foundation/NSUserDefaults.h +++ b/Headers/Foundation/NSUserDefaults.h @@ -192,6 +192,11 @@ GS_EXPORT NSString* const GSLocale; #endif #endif +#if !NO_GNUSTEP +/* Path to PEM Encoded Certificate File (NSString). Used by NSURLSession if set. */ +GS_EXPORT NSString *const GSCACertificateFilePath; +#endif + /* General implementation notes: OpenStep spec currently is neither complete nor consistent. Therefore diff --git a/Headers/GNUstepBase/config.h.in b/Headers/GNUstepBase/config.h.in index 95b594907..043ad5e70 100644 --- a/Headers/GNUstepBase/config.h.in +++ b/Headers/GNUstepBase/config.h.in @@ -221,6 +221,9 @@ */ #undef HAVE_DIRENT_H +/* Define to 1 if you have the `dispatch_cancel' function. */ +#undef HAVE_DISPATCH_CANCEL + /* Define to 1 if you have the header file. */ #undef HAVE_DISPATCH_DISPATCH_H diff --git a/Source/GNUmakefile b/Source/GNUmakefile index 1ee4b1f0f..d16c9a753 100644 --- a/Source/GNUmakefile +++ b/Source/GNUmakefile @@ -374,16 +374,9 @@ ifeq ($(HAVE_BLOCKS), 1) ifeq ($(GNUSTEP_BASE_HAVE_LIBDISPATCH), 1) ifeq ($(GNUSTEP_BASE_HAVE_LIBCURL), 1) BASE_MFILES += \ - GSEasyHandle.m \ - GSHTTPURLProtocol.m \ - GSMultiHandle.m \ - GSNativeProtocol.m \ - GSTaskRegistry.m \ - GSTimeoutSource.m \ - GSTransferState.m \ - GSURLSessionTaskBody.m \ - GSURLSessionTaskBodySource.m \ - NSURLSession.m + NSURLSession.m \ + NSURLSessionTask.m \ + NSURLSessionConfiguration.m endif endif endif diff --git a/Source/GSEasyHandle.h b/Source/GSEasyHandle.h deleted file mode 100644 index ffe0de589..000000000 --- a/Source/GSEasyHandle.h +++ /dev/null @@ -1,236 +0,0 @@ -#ifndef INCLUDED_GSEASYHANDLE_H -#define INCLUDED_GSEASYHANDLE_H - -#import "common.h" -#import - -@class NSData; -@class NSError; -@class NSURL; -@class NSURLSessionConfiguration; -@class NSURLSessionTask; -@class GSTimeoutSource; - -typedef NS_ENUM(NSUInteger, GSEasyHandleAction) { - GSEasyHandleActionAbort, - GSEasyHandleActionProceed, - GSEasyHandleActionPause, -}; - -typedef NS_ENUM(NSUInteger, GSEasyHandleWriteBufferResult) { - GSEasyHandleWriteBufferResultAbort, - GSEasyHandleWriteBufferResultPause, - GSEasyHandleWriteBufferResultBytes, -}; - -@protocol GSEasyHandleDelegate - -/* - * Handle data read from the network. - * - returns: the action to be taken: abort, proceed, or pause. - */ -- (GSEasyHandleAction) didReceiveData: (NSData*)data; - -/* - * Handle header data read from the network. - * - returns: the action to be taken: abort, proceed, or pause. - */ -- (GSEasyHandleAction) didReceiveHeaderData: (NSData*)data - contentLength: (int64_t)contentLength; - -/* - * Fill a buffer with data to be sent. - * - parameter data: The buffer to fill - * - returns: the number of bytes written to the `data` buffer, or `nil` - * to stop the current transfer immediately. - */ -- (void) fillWriteBufferLength: (NSInteger)length - result: (void (^)(GSEasyHandleWriteBufferResult result, NSInteger length, NSData *data))result; - -/* - * The transfer for this handle completed. - * - parameter errorCode: An NSURLError code, or `nil` if no error occurred. - */ -- (void) transferCompletedWithError: (NSError*)error; - -/* - * Seek the input stream to the given position - */ -- (BOOL) seekInputStreamToPosition: (uint64_t)position; - -/* - * Gets called during the transfer to update progress. - */ -- (void) updateProgressMeterWithTotalBytesSent: (int64_t)totalBytesSent - totalBytesExpectedToSend: (int64_t)totalBytesExpectedToSend - totalBytesReceived: (int64_t)totalBytesReceived - totalBytesExpectedToReceive: (int64_t)totalBytesExpectedToReceive; - -@end - - -/* - * Minimal wrapper around the curl easy interface - * (https://curl.haxx.se/libcurl/c/) - * - * An *easy handle* manages the state of a transfer inside libcurl. - * - * As such the easy handle's responsibility is implementing the HTTP - * protocol while the *multi handle* is in charge of managing sockets and - * reading from / writing to these sockets. - * - * An easy handle is added to a multi handle in order to associate it with - * an actual socket. The multi handle will then feed bytes into the easy - * handle and read bytes from the easy handle. But this process is opaque - * to use. It is further worth noting, that with HTTP/1.1 persistent - * connections and with HTTP/2 there's a 1-to-many relationship between - * TCP streams and HTTP transfers / easy handles. A single TCP stream and - * its socket may be shared by multiple easy handles. - * - * A single HTTP request-response exchange (refered to here as a - * *transfer*) corresponds directly to an easy handle. Hence anything that - * needs to be configured for a specific transfer (e.g. the URL) will be - * configured on an easy handle. - * - * A single `NSURLSessionTask` may do multiple, consecutive transfers, and - * as a result it will have to reconfigure its easy handle between - * transfers. An easy handle can be re-used once its transfer has - * completed. - * - * Note: All code assumes that it is being called on a single thread, - * it is intentionally **not** thread safe. - */ -@interface GSEasyHandle : NSObject -{ - CURL *_rawHandle; - char *_errorBuffer; - id _delegate; - GSTimeoutSource *_timeoutTimer; - NSURL *_URL; -} - -- (CURL*) rawHandle; - -- (char*) errorBuffer; - -/* - * Set error buffer for error messages - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_ERRORBUFFER.html - */ -- (void) setErrorBuffer: (char*)buffer; - -- (GSTimeoutSource*) timeoutTimer; - -- (void) setTimeoutTimer: (GSTimeoutSource*)timer; - -- (NSURL*) URL; - -/* - * URL to use in the request - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_URL.html - */ -- (void) setURL: (NSURL*)URL; - -- (void) setPipeWait: (BOOL)flag; - -- (instancetype) initWithDelegate: (id)delegate; - -- (void) transferCompletedWithError: (NSError*)error; - -- (int) urlErrorCodeWithEasyCode: (int)easyCode; - -- (void) setVerboseMode: (BOOL)flag; - -- (void) setDebugOutput: (BOOL)flag - task: (NSURLSessionTask*)task; - -- (void) setPassHeadersToDataStream: (BOOL)flag; - -/* - * Follow any Location: header that the server sends as part of a HTTP header - * in a 3xx response - */ -- (void) setFollowLocation: (BOOL)flag; - -/* - * Switch off the progress meter. - */ -- (void) setProgressMeterOff: (BOOL)flag; - -/* - * Skip all signal handling - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_NOSIGNAL.html - */ -- (void) setSkipAllSignalHandling: (BOOL)flag; - -/* - * Request failure on HTTP response >= 400 - */ -- (void) setFailOnHTTPErrorCode: (BOOL)flag; - -- (void) setConnectToHost: (NSString*)host - port: (NSInteger)port; - -- (void) setSessionConfig: (NSURLSessionConfiguration*)config; - -- (void) setAllowedProtocolsToHTTPAndHTTPS; - -/* - * set preferred receive buffer size - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_BUFFERSIZE.html - */ -- (void) setPreferredReceiveBufferSize: (NSInteger)size; - -/* - * Set custom HTTP headers - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html - */ -- (void) setCustomHeaders: (NSArray*)headers; - -/* - * Enable automatic decompression of HTTP downloads - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTP_CONTENT_DECODING.html - */ -- (void) setAutomaticBodyDecompression: (BOOL)flag; - -/* - * Set request method - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_CUSTOMREQUEST.html - */ -- (void) setRequestMethod:(NSString*)method; - -/* - * Download request without body - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_NOBODY.html - */ -- (void) setNoBody: (BOOL)flag; - -/* - * Enable data upload - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_UPLOAD.html - */ -- (void) setUpload: (BOOL)flag; - -/* - * Set size of the request body to send - * - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_INFILESIZE_LARGE.html - */ -- (void) setRequestBodyLength: (int64_t)length; - -- (void) setTimeout: (NSInteger)timeout; - -- (void) setProxy; - -- (double) getTimeoutIntervalSpent; - - -- (void) pauseReceive; -- (void) unpauseReceive; - -- (void) pauseSend; -- (void) unpauseSend; - -@end - -#endif diff --git a/Source/GSEasyHandle.m b/Source/GSEasyHandle.m deleted file mode 100644 index 74667be2f..000000000 --- a/Source/GSEasyHandle.m +++ /dev/null @@ -1,773 +0,0 @@ -#import "GSURLPrivate.h" -#import "GSEasyHandle.h" -#import "GSTimeoutSource.h" -#import "Foundation/NSCharacterSet.h" -#import "Foundation/NSURLSession.h" - -typedef NS_OPTIONS(NSUInteger, GSEasyHandlePauseState) { - GSEasyHandlePauseStateReceive = 1 << 0, - GSEasyHandlePauseStateSend = 1 << 1 -}; - -@interface GSEasyHandle () - -- (void) resetTimer; - -- (NSInteger) didReceiveData: (char*)data - size: (NSInteger)size - nmemb:(NSInteger)nmemb; - -- (NSInteger) fillWriteBuffer: (char *)buffer - size: (NSInteger)size - nmemb: (NSInteger)nmemb; - -- (NSInteger) didReceiveHeaderData: (char*)headerData - size: (NSInteger)size - nmemb: (NSInteger)nmemb - contentLength: (double)contentLength; - -- (int) seekInputStreamWithOffset: (int64_t)offset - origin: (NSInteger)origin; - -@end - -static void -handleEasyCode(int code) -{ - if (CURLE_OK != code) - { - NSString *reason; - NSException *e; - - reason = [NSString stringWithFormat: @"An error occurred, CURLcode is %d", - code]; - e = [NSException exceptionWithName: @"libcurl.easy" - reason: reason - userInfo: nil]; - [e raise]; - } -} - -static size_t -curl_write_function(char *data, size_t size, size_t nmemb, void *userdata) -{ - GSEasyHandle *handle; - - if (!userdata) - { - return 0; - } - - handle = (GSEasyHandle*)userdata; - - [handle resetTimer]; //FIXME should be deffered after the function returns? - - return [handle didReceiveData:data size:size nmemb:nmemb]; -} - -static size_t -curl_read_function(char *data, size_t size, size_t nmemb, void *userdata) -{ - GSEasyHandle *handle; - - if (!userdata) - { - return 0; - } - - handle = (GSEasyHandle*)userdata; - - [handle resetTimer]; //FIXME should be deffered after the function returns? - - return [handle fillWriteBuffer: data size: size nmemb: nmemb]; -} - -size_t -curl_header_function(char *data, size_t size, size_t nmemb, void *userdata) -{ - GSEasyHandle *handle; - double length; - - if (!userdata) - { - return 0; - } - - handle = (GSEasyHandle*)userdata; - - [handle resetTimer]; //FIXME should be deffered after the function returns? - - handleEasyCode(curl_easy_getinfo(handle.rawHandle, - CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length)); - - return [handle didReceiveHeaderData: data - size: size - nmemb: nmemb - contentLength: length]; -} - -static int -curl_seek_function(void *userdata, curl_off_t offset, int origin) -{ - GSEasyHandle *handle; - - if (!userdata) - { - return CURL_SEEKFUNC_FAIL; - } - - handle = (GSEasyHandle*)userdata; - - return [handle seekInputStreamWithOffset: offset origin: origin]; -} - -static int -curl_debug_function(CURL *handle, curl_infotype type, char *data, - size_t size, void *userptr) -{ - NSURLSessionTask *task; - NSString *text; - NSURLRequest *o; - NSURLRequest *r; - id d; - - if (!userptr) - { - return 0; - } - - if (CURLINFO_SSL_DATA_OUT == type || CURLINFO_SSL_DATA_IN == type) - { - return 0; // Don't log encrypted data here - } - - task = (NSURLSessionTask*)userptr; - text = @""; - o = [task originalRequest]; - r = [task currentRequest]; - d = [(nil == r ? o : r) _debugLogDelegate]; - - if (d != nil) - { - if (CURLINFO_DATA_IN == type || CURLINFO_HEADER_IN == type) - { - if ([d getBytes: (const uint8_t *)data ofLength: size byHandle: o]) - { - return 0; // Handled - } - } - if (CURLINFO_DATA_OUT == type || CURLINFO_HEADER_OUT == type) - { - if ([d putBytes: (const uint8_t *)data ofLength: size byHandle: o]) - { - return 0; // Handled - } - } - } - if (data) - { - text = [NSString stringWithUTF8String: data]; - } - - NSLog(@"%p %lu %d %@", o, (unsigned long)[task taskIdentifier], type, text); - - return 0; -} - -static int -curl_socket_function(void *userdata, curl_socket_t fd, curlsocktype type) -{ - return 0; -} - -@implementation GSEasyHandle -{ - NSURLSessionConfiguration *_config; - GSEasyHandlePauseState _pauseState; - struct curl_slist *_headerList; -} - -- (instancetype) initWithDelegate: (id)delegate -{ - if (nil != (self = [super init])) - { - char *eb; - - _rawHandle = curl_easy_init(); - _delegate = delegate; - - eb = (char *)malloc(sizeof(char) * (CURL_ERROR_SIZE + 1)); - _errorBuffer = memset(eb, 0, sizeof(char) * (CURL_ERROR_SIZE + 1)); - - [self setupCallbacks]; - } - - return self; -} - -- (void) dealloc -{ - curl_easy_cleanup(_rawHandle); - curl_slist_free_all(_headerList); - free(_errorBuffer); - DESTROY(_config); - [_timeoutTimer cancel]; - DESTROY(_timeoutTimer); - DESTROY(_URL); - [super dealloc]; -} - -- (CURL*) rawHandle -{ - return _rawHandle; -} - -- (char*) errorBuffer -{ - return _errorBuffer; -} - -- (GSTimeoutSource*) timeoutTimer -{ - return _timeoutTimer; -} - -- (void) setTimeoutTimer: (GSTimeoutSource*)timer -{ - [_timeoutTimer cancel]; - ASSIGN(_timeoutTimer, timer); -} - -- (NSURL*) URL -{ - return _URL; -} - -- (void) transferCompletedWithError: (NSError*)error -{ - [_delegate transferCompletedWithError: error]; -} - -- (void) resetTimer -{ - [_timeoutTimer setTimeout: [_timeoutTimer timeout]]; -} - -- (void) setupCallbacks -{ - // write - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_WRITEDATA, self)); - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_WRITEFUNCTION, - curl_write_function)); - - // read - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_READDATA, self)); - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_READFUNCTION, - curl_read_function)); - - // header - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_HEADERDATA, self)); - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_HEADERFUNCTION, - curl_header_function)); - - // socket options - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_SOCKOPTDATA, self)); - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_SOCKOPTFUNCTION, - curl_socket_function)); - - // seeking in input stream - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_SEEKDATA, self)); - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_SEEKFUNCTION, - curl_seek_function)); -} - -- (int) urlErrorCodeWithEasyCode: (int)easyCode -{ - int failureErrno = (int)[self connectFailureErrno]; - - if (easyCode == CURLE_OK) - { - return 0; - } - else if (failureErrno == ECONNREFUSED) - { - return NSURLErrorCannotConnectToHost; - } - else if (easyCode == CURLE_UNSUPPORTED_PROTOCOL) - { - return NSURLErrorUnsupportedURL; - } - else if (easyCode == CURLE_URL_MALFORMAT) - { - return NSURLErrorBadURL; - } - else if (easyCode == CURLE_COULDNT_RESOLVE_HOST) - { - return NSURLErrorCannotFindHost; - } - else if (easyCode == CURLE_RECV_ERROR && failureErrno == ECONNRESET) - { - return NSURLErrorNetworkConnectionLost; - } - else if (easyCode == CURLE_SEND_ERROR && failureErrno == ECONNRESET) - { - return NSURLErrorNetworkConnectionLost; - } - else if (easyCode == CURLE_GOT_NOTHING) - { - return NSURLErrorBadServerResponse; - } - else if (easyCode == CURLE_ABORTED_BY_CALLBACK) - { - return NSURLErrorUnknown; - } - else if (easyCode == CURLE_COULDNT_CONNECT && failureErrno == ETIMEDOUT) - { - return NSURLErrorTimedOut; - } - else if (easyCode == CURLE_COULDNT_CONNECT) - { - return NSURLErrorCannotConnectToHost; - } - else if (easyCode == CURLE_OPERATION_TIMEDOUT) - { - return NSURLErrorTimedOut; - } - else - { - return NSURLErrorUnknown; - } -} - -- (void) setVerboseMode: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_VERBOSE, flag ? 1 : 0)); -} - -- (void) setDebugOutput: (BOOL)flag - task: (NSURLSessionTask*)task -{ - if (flag) - { - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_DEBUGDATA, task)); - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_DEBUGFUNCTION, - curl_debug_function)); - } - else - { - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_DEBUGDATA, NULL)); - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_DEBUGFUNCTION, NULL)); - } -} - -- (void) setPassHeadersToDataStream: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_HEADER, - flag ? 1 : 0)); -} - -- (void) setFollowLocation: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_FOLLOWLOCATION, - flag ? 1 : 0)); -} - -- (void) setProgressMeterOff: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_NOPROGRESS, - flag ? 1 : 0)); -} - -- (void) setSkipAllSignalHandling: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_NOSIGNAL, - flag ? 1 : 0)); -} - -- (void) setErrorBuffer: (char*)buffer -{ - char *b = buffer ? buffer : _errorBuffer; - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_ERRORBUFFER, b)); -} - -- (void) setFailOnHTTPErrorCode: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_FAILONERROR, - flag ? 1 : 0)); -} - -- (void) setURL: (NSURL *)URL -{ - ASSIGN(_URL, URL); - if (nil != [URL absoluteString]) - { - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_URL, - [[URL absoluteString] UTF8String])); - } -} - -- (void) setPipeWait: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_PIPEWAIT, flag ? 1 : 0)); -} - -- (void) setConnectToHost: (NSString*)host port: (NSInteger)port -{ - if (nil != host) - { - NSString *originHost = [_URL host]; - NSString *value; - struct curl_slist *connect_to; - - if (0 == port) - { - value = [NSString stringWithFormat: @"%@::%@", originHost, host]; - } - else - { - value = [NSString stringWithFormat: @"%@:%lu:%@", - originHost, (unsigned long)port, host]; - } - - connect_to = curl_slist_append(NULL, [value UTF8String]); - handleEasyCode( - curl_easy_setopt(_rawHandle, CURLOPT_CONNECT_TO, connect_to)); - } -} - -- (void) setSessionConfig: (NSURLSessionConfiguration*)config -{ - ASSIGN(_config, config); -#if defined(CURLOPT_MAXAGE_CONN) - /* This specifies the maximum age of a connection if it is to be considered - * a candidate for re-use. By default curl currently uses 118 seconds, so - * this is what we will get if the configuration does not contain a positive - * number of seconds. - */ - if ([config HTTPMaximumConnectionLifetime] > 0) - { - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_MAXAGE_CONN, - (long)[config HTTPMaximumConnectionLifetime])); - } -#endif -} - -- (void) setAllowedProtocolsToHTTPAndHTTPS -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_PROTOCOLS, - CURLPROTO_HTTP | CURLPROTO_HTTPS)); - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_REDIR_PROTOCOLS, - CURLPROTO_HTTP | CURLPROTO_HTTPS)); -} - -- (void) setPreferredReceiveBufferSize: (NSInteger)size -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_BUFFERSIZE, - MIN(size, CURL_MAX_WRITE_SIZE))); -} - -- (void) setCustomHeaders: (NSArray*)headers -{ - NSEnumerator *e; - NSString *h; - - e = [headers objectEnumerator]; - while (nil != (h = [e nextObject])) - { - _headerList = curl_slist_append(_headerList, [h UTF8String]); - } - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_HTTPHEADER, _headerList)); -} - -- (void) setAutomaticBodyDecompression: (BOOL)flag -{ - if (flag) - { - handleEasyCode(curl_easy_setopt(_rawHandle, - CURLOPT_ACCEPT_ENCODING, "")); - handleEasyCode(curl_easy_setopt(_rawHandle, - CURLOPT_HTTP_CONTENT_DECODING, 1)); - } - else - { - handleEasyCode(curl_easy_setopt(_rawHandle, - CURLOPT_ACCEPT_ENCODING, NULL)); - handleEasyCode(curl_easy_setopt(_rawHandle, - CURLOPT_HTTP_CONTENT_DECODING, 0)); - } -} - -- (void) setRequestMethod: (NSString*)method -{ - if (nil == method) - { - return; - } - - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_CUSTOMREQUEST, - [method UTF8String])); -} - -- (void) setNoBody: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_NOBODY, - flag ? 1 : 0)); -} - -- (void) setUpload: (BOOL)flag -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_UPLOAD, flag ? 1 : 0)); -} - -- (void) setRequestBodyLength: (int64_t)length -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_INFILESIZE_LARGE, - length)); -} - -- (void) setTimeout: (NSInteger)timeout -{ - handleEasyCode(curl_easy_setopt(_rawHandle, CURLOPT_TIMEOUT, - (long)timeout)); -} - -- (void) setProxy -{ - //TODO setup proxy -} - -- (void) updatePauseState: (GSEasyHandlePauseState)pauseState -{ - NSUInteger send = pauseState & GSEasyHandlePauseStateSend; - NSUInteger receive = pauseState & GSEasyHandlePauseStateReceive; - int bitmask; - - bitmask = 0 - | (send ? CURLPAUSE_SEND : CURLPAUSE_SEND_CONT) - | (receive ? CURLPAUSE_RECV : CURLPAUSE_RECV_CONT); - handleEasyCode(curl_easy_pause(_rawHandle, bitmask)); -} - -- (double) getTimeoutIntervalSpent -{ - double timeSpent; - curl_easy_getinfo(_rawHandle, CURLINFO_TOTAL_TIME, &timeSpent); - return timeSpent / 1000; -} - -- (long) connectFailureErrno -{ - long _errno; - handleEasyCode(curl_easy_getinfo(_rawHandle, CURLINFO_OS_ERRNO, &_errno)); - return _errno; -} - -- (void) pauseSend -{ - if (_pauseState & GSEasyHandlePauseStateSend) - { - return; - } - - _pauseState = _pauseState | GSEasyHandlePauseStateSend; - [self updatePauseState: _pauseState]; -} - -- (void) unpauseSend -{ - if (!(_pauseState & GSEasyHandlePauseStateSend)) - { - return; - } - - _pauseState = _pauseState ^ GSEasyHandlePauseStateSend; - [self updatePauseState: _pauseState]; -} - -- (void) pauseReceive -{ - if (_pauseState & GSEasyHandlePauseStateReceive) - { - return; - } - - _pauseState = _pauseState | GSEasyHandlePauseStateReceive; - [self updatePauseState: _pauseState]; -} - -- (void) unpauseReceive -{ - if (!(_pauseState & GSEasyHandlePauseStateReceive)) - { - return; - } - - _pauseState = _pauseState ^ GSEasyHandlePauseStateReceive; - [self updatePauseState: _pauseState]; -} - -- (NSInteger) didReceiveData: (char*)data - size: (NSInteger)size - nmemb: (NSInteger)nmemb -{ - NSData *buffer; - GSEasyHandleAction action; - NSUInteger bytes; - - if (![_delegate respondsToSelector: @selector(didReceiveData:)]) - { - return 0; - } - - bytes = size * nmemb; - buffer = AUTORELEASE([[NSData alloc] initWithBytes: data length: bytes]); - action = [_delegate didReceiveData: buffer]; - switch (action) - { - case GSEasyHandleActionProceed: - return bytes; - case GSEasyHandleActionAbort: - return 0; - case GSEasyHandleActionPause: - _pauseState = _pauseState | GSEasyHandlePauseStateReceive; - return CURL_WRITEFUNC_PAUSE; - } -} - -- (NSInteger) didReceiveHeaderData: (char*)headerData - size: (NSInteger)size - nmemb: (NSInteger)nmemb - contentLength: (double)contentLength -{ - NSData *buffer; - GSEasyHandleAction action; - NSInteger bytes = size * nmemb; - - buffer = [NSData dataWithBytes: headerData length: bytes]; - - [self setCookiesWithHeaderData: buffer]; - - if (![_delegate respondsToSelector: - @selector(didReceiveHeaderData:contentLength:)]) - { - return 0; - } - - action = [_delegate didReceiveHeaderData: buffer - contentLength: (int64_t)contentLength]; - switch (action) - { - case GSEasyHandleActionProceed: - return bytes; - case GSEasyHandleActionAbort: - return 0; - case GSEasyHandleActionPause: - _pauseState = _pauseState | GSEasyHandlePauseStateReceive; - return CURL_WRITEFUNC_PAUSE; - } -} - -- (NSInteger) fillWriteBuffer: (char*)buffer - size: (NSInteger)size - nmemb: (NSInteger)nmemb -{ - __block NSInteger d; - - if (![_delegate respondsToSelector: @selector(fillWriteBufferLength:result:)]) - { - return CURL_READFUNC_ABORT; - } - - [_delegate fillWriteBufferLength: size * nmemb - result: ^(GSEasyHandleWriteBufferResult result, NSInteger length, NSData *data) - { - switch (result) - { - case GSEasyHandleWriteBufferResultPause: - _pauseState = _pauseState | GSEasyHandlePauseStateSend; - d = CURL_READFUNC_PAUSE; - break; - case GSEasyHandleWriteBufferResultAbort: - d = CURL_READFUNC_ABORT; - break; - case GSEasyHandleWriteBufferResultBytes: - memcpy(buffer, [data bytes], length); - d = length; - break; - } - }]; - - return d; -} - -- (int) seekInputStreamWithOffset: (int64_t)offset - origin: (NSInteger)origin -{ - NSAssert(SEEK_SET == origin, @"Unexpected 'origin' in seek."); - - if (![_delegate respondsToSelector: @selector(seekInputStreamToPosition:)]) - { - return CURL_SEEKFUNC_CANTSEEK; - } - - if ([_delegate seekInputStreamToPosition: offset]) - { - return CURL_SEEKFUNC_OK; - } - else - { - return CURL_SEEKFUNC_CANTSEEK; - } -} - -- (void) setCookiesWithHeaderData: (NSData*)data -{ - NSString *headerLine; - NSRange r; - NSString *head; - NSString *tail; - NSCharacterSet *set; - NSString *key; - NSString *value; - NSArray *cookies; - NSDictionary *d; - - if (nil != _config - && NSHTTPCookieAcceptPolicyNever != [_config HTTPCookieAcceptPolicy] - && nil != [_config HTTPCookieStorage]) - { - headerLine = [[NSString alloc] initWithData: data - encoding: NSUTF8StringEncoding]; - if (0 == [headerLine length]) - { - RELEASE(headerLine); - return; - } - - r = [headerLine rangeOfString: @":"]; - if (NSNotFound != r.location) - { - head = [headerLine substringToIndex:r.location]; - tail = [headerLine substringFromIndex:r.location + 1]; - set = [NSCharacterSet whitespaceAndNewlineCharacterSet]; - key = [head stringByTrimmingCharactersInSet:set]; - value = [tail stringByTrimmingCharactersInSet:set]; - - if (nil != key && nil != value) - { - d = [NSDictionary dictionaryWithObject: value forKey: key]; - cookies = [NSHTTPCookie cookiesWithResponseHeaderFields: d - forURL: _URL]; - if ([cookies count] > 0) - { - [[_config HTTPCookieStorage] setCookies: cookies - forURL: _URL - mainDocumentURL: nil]; - } - } - } - RELEASE(headerLine); - } -} - -@end diff --git a/Source/GSHTTPURLProtocol.h b/Source/GSHTTPURLProtocol.h deleted file mode 100644 index 78f8ff2da..000000000 --- a/Source/GSHTTPURLProtocol.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef INCLUDED_GSHTTPURLPROTOCOL_H -#define INCLUDED_GSHTTPURLPROTOCOL_H - -#import "GSNativeProtocol.h" - -@interface GSHTTPURLProtocol : GSNativeProtocol -@end - -#endif diff --git a/Source/GSHTTPURLProtocol.m b/Source/GSHTTPURLProtocol.m deleted file mode 100644 index bb85b9ef5..000000000 --- a/Source/GSHTTPURLProtocol.m +++ /dev/null @@ -1,1028 +0,0 @@ -#import "GSURLPrivate.h" -#import "GSHTTPURLProtocol.h" -#import "GSTransferState.h" -#import "GSURLSessionTaskBody.h" -#import "GSTimeoutSource.h" - -#import "Foundation/FoundationErrors.h" -#import "Foundation/NSCharacterSet.h" -#import "Foundation/NSDateFormatter.h" -#import "Foundation/NSError.h" -#import "Foundation/NSOperation.h" -#import "Foundation/NSSet.h" -#import "Foundation/NSStream.h" -#import "Foundation/NSURL.h" -#import "Foundation/NSURLError.h" -#import "Foundation/NSValue.h" - -@interface GSURLCacherHelper : NSObject - -+ (BOOL) canCacheResponse: (NSCachedURLResponse*)response - request: (NSURLRequest*)request; - -@end - -static NSDate* -dateFromString(NSString *v) -{ - // https://tools.ietf.org/html/rfc2616#section-3.3.1 - NSDateFormatter *df; - NSDate *d; - - df = AUTORELEASE([[NSDateFormatter alloc] init]); - - // RFC 822 - [df setDateFormat: @"EEE, dd MMM yyyy HH:mm:ss zzz"]; - d = [df dateFromString: v]; - if (nil != d) - { - return d; - } - - // RFC 850 - [df setDateFormat: @"EEEE, dd-MMM-yy HH:mm:ss zzz"]; - d = [df dateFromString: v]; - if (nil != d) - { - return d; - } - - // ANSI C's asctime() format - [df setDateFormat: @"EEE MMM dd HH:mm:ss yy"]; - d = [df dateFromString: v]; - if (nil != d) - { - return d; - } - - return nil; -} - -static NSInteger -parseArgumentPart(NSString *part, NSString *name) -{ - NSString *prefix; - - prefix = [NSString stringWithFormat: @"%@=", name]; - if ([part hasPrefix: prefix]) - { - NSArray *split; - - split = [part componentsSeparatedByString: @"="]; - if (split && [split count] == 2) - { - NSString *argument = split[1]; - - if ([argument hasPrefix: @"\""] && [argument hasSuffix: @"\""]) - { - if ([argument length] >= 2) - { - NSRange range = NSMakeRange(1, [argument length] - 2); - argument = [argument substringWithRange: range]; - return [argument integerValue]; - } - else - { - return 0; - } - } - else - { - return [argument integerValue]; - } - } - } - - return 0; -} - - -@implementation GSURLCacherHelper - -+ (BOOL) canCacheResponse: (NSCachedURLResponse*)response - request: (NSURLRequest*)request -{ - NSURLRequest *httpRequest = request; - NSHTTPURLResponse *httpResponse = nil; - NSDate *now; - NSDate *expirationStart; - NSString *dateString; - NSDictionary *headers; - BOOL hasCacheControl = NO; - BOOL hasMaxAge = NO; - NSString *cacheControl; - NSString *pragma; - NSString *expires; - - if (nil == httpRequest) - { - return NO; - } - - if ([[response response] isKindOfClass: [NSHTTPURLResponse class]]) - { - httpResponse = (NSHTTPURLResponse*)[response response]; - } - - if (nil == httpResponse) - { - return NO; - } - - // HTTP status codes: https://tools.ietf.org/html/rfc7231#section-6.1 - switch ([httpResponse statusCode]) - { - case 200: - case 203: - case 204: - case 206: - case 300: - case 301: - case 404: - case 405: - case 410: - case 414: - case 501: - break; - - default: - return NO; - } - - headers = [httpResponse allHeaderFields]; - - // Vary: https://tools.ietf.org/html/rfc7231#section-7.1.4 - if (nil != [headers objectForKey: @"Vary"]) - { - return NO; - } - - now = [NSDate date]; - dateString = [headers objectForKey: @"Date"]; - if (nil != dateString) - { - expirationStart = dateFromString(dateString); - } - else - { - return NO; - } - - // We opt not to cache any requests or responses that contain authorization headers. - if ([headers objectForKey: @"WWW-Authenticate"] - || [headers objectForKey: @"Proxy-Authenticate"] - || [headers objectForKey: @"Authorization"] - || [headers objectForKey: @"Proxy-Authorization"]) - { - return NO; - } - - // HTTP Methods: https://tools.ietf.org/html/rfc7231#section-4.2.3 - if ([[httpRequest HTTPMethod] isEqualToString: @"GET"]) - { - } - else if ([[httpRequest HTTPMethod] isEqualToString: @"HEAD"]) - { - if ([response data] && [[response data] length] > 0) - { - return NO; - } - } - else - { - return NO; - } - - // Cache-Control: https://tools.ietf.org/html/rfc7234#section-5.2 - cacheControl = [headers objectForKey: @"Cache-Control"]; - if (nil != cacheControl) - { - NSInteger maxAge = 0; - NSInteger sharedMaxAge = 0; - BOOL noCache = NO; - BOOL noStore = NO; - - [self getCacheControlDeirectivesFromHeaderValue: cacheControl - maxAge: &maxAge - sharedMaxAge: &sharedMaxAge - noCache: &noCache - noStore: &noStore]; - if (noCache || noStore) - { - return NO; - } - - if (maxAge > 0) - { - NSDate *expiration; - - hasMaxAge = YES; - - expiration = [expirationStart dateByAddingTimeInterval: maxAge]; - if ([now timeIntervalSince1970] >= [expiration timeIntervalSince1970]) - { - return NO; - } - } - - if (sharedMaxAge) - { - hasMaxAge = YES; - } - - hasCacheControl = YES; - } - - // Pragma: https://tools.ietf.org/html/rfc7234#section-5.4 - pragma = [headers objectForKey: @"Pragma"]; - if (!hasCacheControl && nil != pragma) - { - NSArray *cs = [pragma componentsSeparatedByString: @","]; - NSMutableArray *components = [NSMutableArray arrayWithCapacity: [cs count]]; - NSString *c; - - for (int i = 0; i < [cs count]; i++) - { - c = [cs objectAtIndex: i]; - c = [c stringByTrimmingCharactersInSet: - [NSCharacterSet whitespaceCharacterSet]]; - c = [c lowercaseString]; - [components setObject: c atIndexedSubscript: i]; - } - - if ([components containsObject: @"no-cache"]) - { - return NO; - } - } - - // Expires: - // We should not cache a response that has already expired. - // We MUST ignore this if we have Cache-Control: max-age or s-maxage. - expires = [headers objectForKey: @"Expires"]; - if (!hasMaxAge && nil != expires) - { - NSDate *expiration = dateFromString(expires); - if (nil == expiration) - { - return NO; - } - - if ([now timeIntervalSince1970] >= [expiration timeIntervalSince1970]) - { - return NO; - } - } - - if (!hasCacheControl) - { - return NO; - } - - return YES; -} - -+ (void) getCacheControlDeirectivesFromHeaderValue: (NSString*)headerValue - maxAge: (NSInteger*)maxAge - sharedMaxAge: (NSInteger*)sharedMaxAge - noCache: (BOOL*)noCache - noStore: (BOOL*)noStore -{ - NSArray *components; - NSEnumerator *e; - NSString *part; - - components = [headerValue componentsSeparatedByString: @","]; - e = [components objectEnumerator]; - while (nil != (part = [e nextObject])) - { - part = [part stringByTrimmingCharactersInSet: - [NSCharacterSet whitespaceCharacterSet]]; - part = [part lowercaseString]; - - if ([part isEqualToString: @"no-cache"]) - { - *noCache = YES; - } - else if ([part isEqualToString: @"no-store"]) - { - *noStore = YES; - } - else if ([part containsString: @"max-age"]) - { - *maxAge = parseArgumentPart(part, @"max-age"); - } - else if ([part containsString: @"s-maxage"]) - { - *sharedMaxAge = parseArgumentPart(part, @"s-maxage"); - } - } -} - -@end - -@implementation GSHTTPURLProtocol - -+ (BOOL) canInitWithTask: (NSURLSessionTask*)task -{ - NSURLRequest *request = [task currentRequest]; - NSURL *url; - - if (nil != (url = [request URL]) - && ([[url scheme] isEqualToString: @"http"] - || [[url scheme] isEqualToString: @"https"])) - { - return YES; - } - else - { - return NO; - } -} - -+ (id) _ProtocolClient -{ - return AUTORELEASE([[_NSURLProtocolClient alloc] init]); -} - -- (GSEasyHandleAction) didReceiveHeaderData: (NSData*)data - contentLength: (int64_t)contentLength -{ - NSURLSessionTask *task; - GSTransferState *newTS; - NSError *error = NULL; - - NSAssert(_internalState == GSNativeProtocolInternalStateTransferInProgress, - @"Received header data, but no transfer in progress."); - - task = [self task]; - NSAssert(nil != task, @"Received header data but no task available."); - - newTS = [_transferState byAppendingHTTPHeaderLineData: data error: &error]; - if (nil != newTS && NULL == error) - { - BOOL didCompleteHeader; - - didCompleteHeader = ![_transferState isHeaderComplete] - && [newTS isHeaderComplete]; - [self setInternalState: GSNativeProtocolInternalStateTransferInProgress]; - ASSIGN(_transferState, newTS); - if (didCompleteHeader) - { - // The header is now complete, but wasn't before. - NSHTTPURLResponse *response; - NSString *contentEncoding; - - response = (NSHTTPURLResponse*)[newTS response]; - contentEncoding = [[response allHeaderFields] - objectForKey: @"Content-Encoding"]; - if (nil != contentEncoding - && ![contentEncoding isEqual: @"identity"]) - { - // compressed responses do not report expected size - [task setCountOfBytesExpectedToReceive: -1]; - } - else - { - [task setCountOfBytesExpectedToReceive: - (contentLength > 0 ? contentLength : -1)]; - } - [self didReceiveResponse]; - } - return GSEasyHandleActionProceed; - } - else - { - return GSEasyHandleActionAbort; - } -} - -- (BOOL) canRespondFromCacheUsing: (NSCachedURLResponse*)response -{ - BOOL canCache; - NSURLSessionTask *task; - - task = [self task]; - - canCache = [GSURLCacherHelper canCacheResponse: response - request: [task currentRequest]]; - if (!canCache) - { - // If somehow cached a response that shouldn't have been, - // we should remove it. - NSURLCache *cache; - - cache = [[[task session] configuration] URLCache]; - if (nil != cache) - { - [cache removeCachedResponseForRequest: [task currentRequest]]; - } - - return NO; - } - - return YES; -} - -/// Set options on the easy handle to match the given request. -/// This performs a series of `curl_easy_setopt()` calls. -- (void) configureEasyHandleForRequest: (NSURLRequest*)request - body: (GSURLSessionTaskBody*)body -{ - NSURLSessionTask *task = [self task]; - NSURLSession *session = [task session]; - NSURLSessionConfiguration *config = [session configuration]; - BOOL debugLibcurl; - - if ([[request HTTPMethod] isEqualToString:@"GET"]) - { - if ([body type] != GSURLSessionTaskBodyTypeNone) - { - NSError *error; - NSDictionary *info; - - info = [NSDictionary dictionaryWithObjectsAndKeys: - @"resource exceeds maximum size", NSLocalizedDescriptionKey, - [[request URL] description], NSURLErrorFailingURLStringErrorKey, - nil]; - error = [NSError errorWithDomain: NSURLErrorDomain - code: NSURLErrorDataLengthExceedsMaximum - userInfo: info]; - [self setInternalState: GSNativeProtocolInternalStateTransferFailed]; - [self transferCompletedWithError: error]; - return; - } - } - - debugLibcurl = [[[NSProcessInfo processInfo] environment] - objectForKey: @"URLSessionDebugLibcurl"] ? YES : NO; - - /* Programatically turning debug on in the request supercedes any - * environment variable setting. - */ - if ([request _debug]) - { - debugLibcurl = YES; - } - [_easyHandle setVerboseMode: debugLibcurl]; - - BOOL debugOutput = [[[NSProcessInfo processInfo] environment] - objectForKey: @"URLSessionDebug"] ? YES : NO; - [_easyHandle setDebugOutput: debugOutput task: task]; - - [_easyHandle setPassHeadersToDataStream: NO]; - [_easyHandle setProgressMeterOff: YES]; - [_easyHandle setSkipAllSignalHandling: YES]; - - // Error Options: - [_easyHandle setErrorBuffer: NULL]; - [_easyHandle setFailOnHTTPErrorCode: NO]; - - NSAssert(nil != [request URL], @"No URL in request."); - [_easyHandle setURL: [request URL]]; - - [_easyHandle setPipeWait: [config HTTPShouldUsePipelining]]; - - [_easyHandle setSessionConfig: config]; - [_easyHandle setAllowedProtocolsToHTTPAndHTTPS]; - [_easyHandle setPreferredReceiveBufferSize: NSIntegerMax]; - - NSError *e = nil; - NSNumber *bodySize = [body getBodyLengthWithError: &e]; - if (nil != e) - { - NSInteger errorCode; - NSError *error; - - [self setInternalState: GSNativeProtocolInternalStateTransferFailed]; - errorCode = [self errorCodeFromFileSystemError: e]; - error = [NSError errorWithDomain: NSURLErrorDomain - code: errorCode - userInfo: @{NSLocalizedDescriptionKey : @"File system error"}]; - [self failWithError: error request: request]; - return; - } - - if ([body type] == GSURLSessionTaskBodyTypeNone) - { - if ([[request HTTPMethod] isEqualToString: @"GET"]) - { - [_easyHandle setUpload: NO]; - [_easyHandle setRequestBodyLength: 0]; - } - else - { - [_easyHandle setUpload: YES]; - [_easyHandle setRequestBodyLength: 0]; - } - } - else if (bodySize != nil) - { - [task setCountOfBytesExpectedToSend: [bodySize longLongValue]]; - [_easyHandle setUpload: YES]; - [_easyHandle setRequestBodyLength: [bodySize unsignedLongLongValue]]; - } - else if (bodySize == nil) - { - [_easyHandle setUpload: YES]; - [_easyHandle setRequestBodyLength:-1]; - } - - [_easyHandle setFollowLocation: NO]; - - /* The httpAdditionalHeaders from session configuration has to be added to - * the request. The request.allHTTPHeaders can override the - * httpAdditionalHeaders elements. Add the httpAdditionalHeaders from session - * configuration first and then append/update the request.allHTTPHeaders - * so that request.allHTTPHeaders can override httpAdditionalHeaders. - */ - NSMutableDictionary *hh; - NSDictionary *HTTPAdditionalHeaders; - NSDictionary *HTTPHeaders; - - hh = [NSMutableDictionary dictionary]; - HTTPAdditionalHeaders - = [[[task session] configuration] HTTPAdditionalHeaders]; - if (nil == HTTPAdditionalHeaders) - { - HTTPAdditionalHeaders = [NSDictionary dictionary]; - } - HTTPHeaders = [request allHTTPHeaderFields]; - if (nil == HTTPHeaders) - { - HTTPHeaders = [NSDictionary dictionary]; - } - - [hh addEntriesFromDictionary: - [self transformLowercaseKeyForHTTPHeaders: HTTPAdditionalHeaders]]; - [hh addEntriesFromDictionary: - [self transformLowercaseKeyForHTTPHeaders: HTTPHeaders]]; - - NSMutableArray *curlHeaders = [self curlHeadersForHTTPHeaders: hh]; - if ([[request HTTPMethod] isEqualToString:@"POST"] - && [[request HTTPBody] length] > 0 - && [request valueForHTTPHeaderField: @"Content-Type"] == nil) - { - [curlHeaders addObject: @"Content-Type:application/x-www-form-urlencoded"]; - } - [_easyHandle setCustomHeaders: curlHeaders]; - - NSInteger timeoutInterval = [request timeoutInterval] * 1000; - GSTimeoutSource *timeoutTimer; - - timeoutTimer = [[GSTimeoutSource alloc] initWithQueue: [task workQueue] - handler: - ^{ - NSError *urlError; - id client; - - [self setInternalState: GSNativeProtocolInternalStateTransferFailed]; - urlError = [NSError errorWithDomain: NSURLErrorDomain - code: NSURLErrorTimedOut - userInfo: nil]; - [self completeTaskWithError: urlError]; - if (nil != (client = [self client]) - && [client respondsToSelector: @selector(URLProtocol:didFailWithError:)]) - { - [client URLProtocol: self didFailWithError: urlError]; - } - }]; - [timeoutTimer setTimeout: timeoutInterval]; - [_easyHandle setTimeoutTimer: timeoutTimer]; - RELEASE(timeoutTimer); - - [_easyHandle setAutomaticBodyDecompression: YES]; - [_easyHandle setRequestMethod: - [request HTTPMethod] ? [request HTTPMethod] : @"GET"]; - - // always set the status as it may change if a HEAD is converted to a GET - [_easyHandle setNoBody: [[request HTTPMethod] isEqualToString: @"HEAD"]]; - [_easyHandle setProxy]; -} - -- (GSCompletionAction*) completeActionForCompletedRequest: (NSURLRequest*)request - response: (NSURLResponse*)response -{ - GSCompletionAction *action; - NSHTTPURLResponse *httpResponse; - NSURLRequest *redirectRequest; - - NSAssert([response isKindOfClass: [NSHTTPURLResponse class]], - @"Response was not NSHTTPURLResponse"); - httpResponse = (NSHTTPURLResponse*)response; - - redirectRequest = [self redirectRequestForResponse: httpResponse - fromRequest: request]; - - action = AUTORELEASE([[GSCompletionAction alloc] init]); - - if (nil != redirectRequest) - { - [action setType: GSCompletionActionTypeRedirectWithRequest]; - [action setRedirectRequest: redirectRequest]; - } - else - { - [action setType: GSCompletionActionTypeCompleteTask]; - } - - return action; -} - -/* If the response is a redirect, return the new request - * - * RFC 7231 section 6.4 defines redirection behavior for HTTP/1.1 - * - * - SeeAlso: - */ -- (NSURLRequest*) redirectRequestForResponse: (NSHTTPURLResponse*)response - fromRequest: (NSURLRequest*)fromRequest -{ - NSString *method = nil; - NSURL *targetURL; - NSString *location; - NSMutableURLRequest *request; - - if (nil == [response allHeaderFields]) - { - return nil; - } - - location = [[response allHeaderFields] objectForKey: @"Location"]; - targetURL = [NSURL URLWithString: location]; - if (nil == location && nil == targetURL) - { - return nil; - } - - switch ([response statusCode]) - { - case 301: - case 302: - method = [[fromRequest HTTPMethod] isEqualToString:@"POST"] ? - @"GET" : [fromRequest HTTPMethod]; - break; - case 303: - method = @"GET"; - break; - case 305: - case 306: - case 307: - case 308: - method = nil != [fromRequest HTTPMethod] ? - [fromRequest HTTPMethod] : @"GET"; - break; - default: - return nil; - } - - request = AUTORELEASE([fromRequest mutableCopy]); - [request setHTTPMethod: method]; - - if (nil != [targetURL scheme] && nil != [targetURL host]) - { - [request setURL: targetURL]; - return request; - } - - NSString *scheme = [[request URL] scheme]; - NSString *host = [[request URL] host]; - NSNumber *port = [[request URL] port]; - - NSURLComponents *components = [[NSURLComponents alloc] init]; - [components setScheme: scheme]; - [components setHost: host]; - - /* Use the original port if the new URL does not contain a host - * ie Location: /foo => :/Foo - * but Location: newhost/foo will ignore the original port - */ - if ([targetURL host] == nil) - { - [components setPort: port]; - } - - /* The path must either begin with "/" or be an empty string. - */ - if (![[targetURL relativePath] hasPrefix:@"/"]) - { - [components setPath: - [NSString stringWithFormat:@"/%@", [targetURL relativePath]]]; - } - else - { - [components setPath: [targetURL relativePath]]; - } - - NSString *urlString = [components string]; - RELEASE(components); - if (nil == urlString) - { - return nil; - } - - [request setURL: [NSURL URLWithString:urlString]]; - double timeSpent = [_easyHandle getTimeoutIntervalSpent]; - [request setTimeoutInterval: [fromRequest timeoutInterval] - timeSpent]; - return request; -} - -- (void) redirectForRequest: (NSURLRequest*)request -{ - NSURLSessionTask *task; - NSURLSession *session; - id delegate; - - NSAssert(_internalState == GSNativeProtocolInternalStateTransferCompleted, - @"Trying to redirect, but the transfer is not complete."); - - task = [self task]; - session = [task session]; - delegate = [session delegate]; - - if (nil != delegate - && [delegate respondsToSelector:@selector(selectr)]) - { - // At this point we need to change the internal state to note - // that we're waiting for the delegate to call the completion - // handler. Then we'll call the delegate callback - // (willPerformHTTPRedirection). The task will then switch out of - // its internal state once the delegate calls the completion - // handler. - [self setInternalState: GSNativeProtocolInternalStateWaitingForRedirectCompletionHandler]; - [[session delegateQueue] addOperationWithBlock: - ^{ - id taskDelegate = - (id)delegate; - [taskDelegate URLSession: session - task: task - willPerformHTTPRedirection: (NSHTTPURLResponse*)[_transferState response] - newRequest: request - completionHandler: ^(NSURLRequest *_Nullable request) { - dispatch_async([task workQueue], ^{ - NSAssert(_internalState == GSNativeProtocolInternalStateWaitingForRedirectCompletionHandler, - @"Received callback for HTTP redirection, but we're not waiting " - @"for it. Was it called multiple times?"); - - // If the request is `nil`, we're supposed to treat the current response - // as the final response, i.e. not do any redirection. - // Otherwise, we'll start a new transfer with the passed in request. - if (nil != request) - { - [self startNewTransferWithRequest: request]; - } - else - { - [self setInternalState: GSNativeProtocolInternalStateTransferCompleted]; - [self completeTask]; - } - }); - }]; - }]; - } - else - { - NSURLRequest *configuredRequest; - - configuredRequest = [[session configuration] configureRequest: request]; - [self startNewTransferWithRequest: configuredRequest]; - } -} - -- (NSURLResponse*) validateHeaderCompleteTransferState: (GSTransferState*)ts -{ - if (![_transferState isHeaderComplete]) - { - /* we received body data before CURL tells us that the headers are complete, - that happens for HTTP/0.9 simple responses, see - - https://www.w3.org/Protocols/HTTP/1.0/spec.html#Message-Types - - https://github.com/curl/curl/issues/467 - */ - return AUTORELEASE([[NSHTTPURLResponse alloc] - initWithURL: [ts URL] - statusCode: 200 - HTTPVersion: @"HTTP/0.9" - headerFields: [NSDictionary dictionary]]); - } - - return nil; -} - -- (NSDictionary*) transformLowercaseKeyForHTTPHeaders: (NSDictionary*)HTTPHeaders -{ - NSMutableDictionary *result; - NSEnumerator *e; - NSString *k; - - if (nil == HTTPHeaders) - { - return nil; - } - - result = [NSMutableDictionary dictionary]; - e = [HTTPHeaders keyEnumerator]; - while (nil != (k = [e nextObject])) - { - [result setObject: [HTTPHeaders objectForKey: k] - forKey: [k lowercaseString]]; - } - - return AUTORELEASE([result copy]); -} - -// These are a list of headers that should be passed to libcurl. -// -// Headers will be returned as `Accept: text/html` strings for -// setting fields, `Accept:` for disabling the libcurl default header, or -// `Accept;` for a header with no content. This is the format that libcurl -// expects. -// -// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html -- (NSMutableArray*) curlHeadersForHTTPHeaders: (NSDictionary*)HTTPHeaders -{ - NSMutableArray *result = [NSMutableArray array]; - NSMutableSet *names = [NSMutableSet set]; - NSEnumerator *e; - NSString *k; - NSDictionary *curlHeadersToSet; - NSArray *curlHeadersToRemove; - - if (nil == HTTPHeaders) - { - return nil; - } - - e = [HTTPHeaders keyEnumerator]; - while (nil != (k = [e nextObject])) - { - NSString *name = [k lowercaseString]; - NSString *value = [HTTPHeaders objectForKey: k]; - - if ([names containsObject: name]) - { - break; - } - - [names addObject: name]; - - if ([value length] == 0) - { - [result addObject: [NSString stringWithFormat: @"%@;", k]]; - } - else - { - [result addObject: [NSString stringWithFormat: @"%@: %@", k, value]]; - } - } - - curlHeadersToSet = [self curlHeadersToSet]; - e = [curlHeadersToSet keyEnumerator]; - while (nil != (k = [e nextObject])) - { - NSString *name = [k lowercaseString]; - NSString *value = [curlHeadersToSet objectForKey: k]; - - if ([names containsObject: name]) - { - break; - } - - [names addObject: name]; - - if ([value length] == 0) - { - [result addObject: [NSString stringWithFormat: @"%@;", k]]; - } - else - { - [result addObject: [NSString stringWithFormat: @"%@: %@", k, value]]; - } - } - - curlHeadersToRemove = [self curlHeadersToRemove]; - e = [curlHeadersToRemove objectEnumerator]; - while (nil != (k = [e nextObject])) - { - NSString *name = [k lowercaseString]; - - if ([names containsObject: name]) - { - break; - } - - [names addObject:name]; - - [result addObject: [NSString stringWithFormat: @"%@:", k]]; - } - - return result; -} - -// Any header values that should be passed to libcurl -// -// These will only be set if not already part of the request. -// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html -- (NSDictionary*) curlHeadersToSet -{ - return [NSDictionary dictionaryWithObjectsAndKeys: - @"keep-alive", @"Connection", - [self userAgentString], @"User-Agent", - nil]; -} - -// Any header values that should be removed from the ones set by libcurl -// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html -- (NSArray*) curlHeadersToRemove -{ - if ([[self task] knownBody] == nil) - { - return [NSArray array]; - } - else if ([[[self task] knownBody] type] == GSURLSessionTaskBodyTypeNone) - { - return [NSArray array]; - } - - return [NSArray arrayWithObject: @"Expect"]; -} - -- (NSString*) userAgentString -{ - NSProcessInfo *processInfo = [NSProcessInfo processInfo]; - NSString *name = [processInfo processName]; - curl_version_info_data *curlInfo = curl_version_info(CURLVERSION_NOW); - - return [NSString stringWithFormat: @"%@ (unknown version) curl/%d.%d.%d", - name, - curlInfo->version_num >> 16 & 0xff, - curlInfo->version_num >> 8 & 0xff, - curlInfo->version_num & 0xff]; -} - -- (NSInteger) errorCodeFromFileSystemError: (NSError*)error -{ - if ([error domain] == NSCocoaErrorDomain) - { - switch (error.code) - { - case NSFileReadNoSuchFileError: - return NSURLErrorFileDoesNotExist; - case NSFileReadNoPermissionError: - return NSURLErrorNoPermissionsToReadFile; - default: - return NSURLErrorUnknown; - } - } - else - { - return NSURLErrorUnknown; - } -} - -// Whenever we receive a response (i.e. a complete header) from libcurl, -// this method gets called. -- (void) didReceiveResponse -{ - NSURLSessionDataTask *task; - NSHTTPURLResponse *response; - - task = (NSURLSessionDataTask*)[self task]; - - if (![task isKindOfClass: [NSURLSessionDataTask class]]) - { - return; - } - - NSAssert(_internalState == GSNativeProtocolInternalStateTransferInProgress, - @"Transfer not in progress."); - - NSAssert([[_transferState response] isKindOfClass: [NSHTTPURLResponse class]], - @"Header complete, but not URL response."); - - response = (NSHTTPURLResponse*)[_transferState response]; - - if (nil != [[task session] delegate]) - { - switch ([response statusCode]) - { - case 301: - case 302: - case 303: - case 307: - break; - default: - { - id client = [self client]; - - if (nil != client) - { - [client URLProtocol: self - didReceiveResponse: response - cacheStoragePolicy: NSURLCacheStorageNotAllowed]; - } - } - } - } -} - -@end diff --git a/Source/GSMultiHandle.h b/Source/GSMultiHandle.h deleted file mode 100644 index c075dd9e8..000000000 --- a/Source/GSMultiHandle.h +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef INCLUDED_GSMULTIHANDLE_H -#define INCLUDED_GSMULTIHANDLE_H - -#import "common.h" -#import -#import "GSDispatch.h" - -@class NSURLSessionConfiguration; -@class GSEasyHandle; - -/* - * Minimal wrapper around curl multi interface - * (https://curl.haxx.se/libcurl/c/libcurl-multi.html). - * - * The the *multi handle* manages the sockets for easy handles - * (`GSEasyHandle`), and this implementation uses - * libdispatch to listen for sockets being read / write ready. - * - * Using `dispatch_source_t` allows this implementation to be - * non-blocking and all code to run on the same thread - * thus keeping is simple. - * - * - SeeAlso: GSEasyHandle - */ -@interface GSMultiHandle : NSObject -{ - CURLM *_rawHandle; -} - -- (CURLM*) rawHandle; -- (instancetype) initWithConfiguration: (NSURLSessionConfiguration*)configuration - workQueue: (dispatch_queue_t)workQueque; -- (void) addHandle: (GSEasyHandle*)easyHandle; -- (void) removeHandle: (GSEasyHandle*)easyHandle; -- (void) updateTimeoutTimerToValue: (NSInteger)value; - -@end - -// What read / write ready event to register / unregister. -typedef NS_ENUM(NSUInteger, GSSocketRegisterActionType) { - GSSocketRegisterActionTypeNone = 0, - GSSocketRegisterActionTypeRegisterRead, - GSSocketRegisterActionTypeRegisterWrite, - GSSocketRegisterActionTypeRegisterReadAndWrite, - GSSocketRegisterActionTypeUnregister, -}; - -@interface GSSocketRegisterAction : NSObject -{ - GSSocketRegisterActionType _type; -} - -- (instancetype) initWithRawValue: (int)rawValue; -- (GSSocketRegisterActionType) type; -- (BOOL) needsReadSource; -- (BOOL) needsWriteSource; -- (BOOL) needsSource; - -@end - -/* - * Read and write libdispatch sources for a specific socket. - * - * A simple helper that combines two sources -- both being optional. - * - * This info is stored into the socket using `curl_multi_assign()`. - * - * - SeeAlso: GSSocketRegisterAction - */ -@interface GSSocketSources : NSObject -{ - dispatch_source_t _readSource; - dispatch_source_t _writeSource; -} - -- (void) createSourcesWithAction: (GSSocketRegisterAction *)action - socket: (curl_socket_t)socket - queue: (dispatch_queue_t)queue - handler: (dispatch_block_t)handler; -- (dispatch_source_t) createSourceWithType: (dispatch_source_type_t)type - socket: (curl_socket_t)socket - queue: (dispatch_queue_t)queue - handler: (dispatch_block_t)handler; - -+ (instancetype) from: (void*)socketSourcePtr; - -@end - -#endif diff --git a/Source/GSMultiHandle.m b/Source/GSMultiHandle.m deleted file mode 100644 index a7b3d7b44..000000000 --- a/Source/GSMultiHandle.m +++ /dev/null @@ -1,485 +0,0 @@ -#import "GSMultiHandle.h" -#import "GSTimeoutSource.h" -#import "GSEasyHandle.h" - -#import "Foundation/NSArray.h" -#import "Foundation/NSDictionary.h" -#import "Foundation/NSError.h" -#import "Foundation/NSException.h" -#import "Foundation/NSURLError.h" -#import "Foundation/NSURLSession.h" -#import "Foundation/NSValue.h" - -@interface GSMultiHandle () -- (void) readAndWriteAvailableDataOnSocket: (curl_socket_t)socket; -- (void) readMessages; -- (void) completedTransferForEasyHandle: (CURL*)rawEasyHandle - easyCode: (int)easyCode; -- (int32_t) registerWithSocket: (curl_socket_t)socket - what: (int)what - socketSourcePtr: (void *)socketSourcePtr; -@end - -static void handleEasyCode(int code) -{ - if (CURLE_OK != code) - { - NSString *reason; - NSException *e; - - reason = [NSString stringWithFormat: @"An error occurred, CURLcode is %d", - code]; - e = [NSException exceptionWithName: @"libcurl.easy" - reason: reason - userInfo: nil]; - [e raise]; - } -} - -static void handleMultiCode(int code) -{ - if (CURLM_OK != code) - { - NSString *reason; - NSException *e; - - reason = [NSString stringWithFormat: @"An error occurred, CURLcode is %d", - code]; - e = [NSException exceptionWithName: @"libcurl.multi" - reason: reason - userInfo: nil]; - [e raise]; - } -} - -static int curl_socket_function(CURL *easyHandle, curl_socket_t socket, int what, void *userdata, void *socketptr) -{ - GSMultiHandle *handle = (GSMultiHandle*)userdata; - - return [handle registerWithSocket: socket - what: what - socketSourcePtr: socketptr]; -} - -static int curl_timer_function(CURL *easyHandle, int timeout, void *userdata) { - GSMultiHandle *handle = (GSMultiHandle*)userdata; - - [handle updateTimeoutTimerToValue: timeout]; - - return 0; -} - -@implementation GSMultiHandle -{ - NSMutableArray *_easyHandles; - dispatch_queue_t _queue; - GSTimeoutSource *_timeoutSource; -} - -- (CURLM*) rawHandle -{ - return _rawHandle; -} - -- (instancetype) initWithConfiguration: (NSURLSessionConfiguration*)conf - workQueue: (dispatch_queue_t)aQueue -{ - if (nil != (self = [super init])) - { - _rawHandle = curl_multi_init(); - _easyHandles = [[NSMutableArray alloc] init]; -#if HAVE_DISPATCH_QUEUE_CREATE_WITH_TARGET - _queue = dispatch_queue_create_with_target("GSMultiHandle.isolation", - DISPATCH_QUEUE_SERIAL, aQueue); -#else - _queue = dispatch_queue_create("GSMultiHandle.isolation", - DISPATCH_QUEUE_SERIAL); - dispatch_set_target_queue(_queue, aQueue); -#endif - [self setupCallbacks]; - [self configureWithConfiguration: conf]; - } - - return self; -} - -- (void) dealloc -{ - NSEnumerator *e; - GSEasyHandle *handle; - - [_timeoutSource cancel]; - DESTROY(_timeoutSource); - - dispatch_release(_queue); - - e = [_easyHandles objectEnumerator]; - while (nil != (handle = [e nextObject])) - { - curl_multi_remove_handle([handle rawHandle], _rawHandle); - } - DESTROY(_easyHandles); - - curl_multi_cleanup(_rawHandle); - - [super dealloc]; -} - -- (void) configureWithConfiguration: (NSURLSessionConfiguration*)configuration -{ - handleEasyCode(curl_multi_setopt(_rawHandle, CURLMOPT_MAX_HOST_CONNECTIONS, [configuration HTTPMaximumConnectionsPerHost])); - handleEasyCode(curl_multi_setopt(_rawHandle, CURLMOPT_PIPELINING, [configuration HTTPShouldUsePipelining] ? CURLPIPE_MULTIPLEX : CURLPIPE_NOTHING)); -} - -- (void)setupCallbacks -{ - handleEasyCode(curl_multi_setopt(_rawHandle, CURLMOPT_SOCKETDATA, (void*)self)); - handleEasyCode(curl_multi_setopt(_rawHandle, CURLMOPT_SOCKETFUNCTION, curl_socket_function)); - - handleEasyCode(curl_multi_setopt(_rawHandle, CURLMOPT_TIMERDATA, (__bridge void *)self)); - handleEasyCode(curl_multi_setopt(_rawHandle, CURLMOPT_TIMERFUNCTION, curl_timer_function)); -} - -- (void) addHandle: (GSEasyHandle*)easyHandle -{ - // If this is the first handle being added, we need to `kick` the - // underlying multi handle by calling `timeoutTimerFired` as - // described in - // . - // That will initiate the registration for timeout timer and socket - // readiness. - BOOL needsTimeout = false; - - if ([_easyHandles count] == 0) - { - needsTimeout = YES; - } - - [_easyHandles addObject: easyHandle]; - - handleMultiCode(curl_multi_add_handle(_rawHandle, [easyHandle rawHandle])); - - if (needsTimeout) - { - [self timeoutTimerFired]; - } -} - -- (void) removeHandle: (GSEasyHandle*)easyHandle -{ - NSEnumerator *e; - int idx = 0; - BOOL found = NO; - GSEasyHandle *h; - - e = [_easyHandles objectEnumerator]; - while (nil != (h = [e nextObject])) - { - if ([h rawHandle] == [easyHandle rawHandle]) - { - found = YES; - break; - } - idx++; - } - - NSAssert(found, @"Handle not in list."); - - handleMultiCode(curl_multi_remove_handle(_rawHandle, [easyHandle rawHandle])); - [_easyHandles removeObjectAtIndex: idx]; -} - -- (void) updateTimeoutTimerToValue: (NSInteger)value -{ - // A timeout_ms value of -1 passed to this callback means you should delete - // the timer. All other values are valid expire times in number - // of milliseconds. - if (-1 == value) - { - [_timeoutSource suspend]; - } - else - { - if (!_timeoutSource) - { - _timeoutSource = [[GSTimeoutSource alloc] initWithQueue: _queue - handler: ^{ - [self timeoutTimerFired]; - }]; - } - [_timeoutSource setTimeout: value]; - } -} - -- (void) timeoutTimerFired -{ - [self readAndWriteAvailableDataOnSocket: CURL_SOCKET_TIMEOUT]; -} - -- (void) readAndWriteAvailableDataOnSocket: (curl_socket_t)socket -{ - int runningHandlesCount = 0; - - handleMultiCode(curl_multi_socket_action(_rawHandle, socket, 0, &runningHandlesCount)); - - [self readMessages]; -} - -/// Check the status of all individual transfers. -/// -/// libcurl refers to this as “read multi stack informationals”. -/// Check for transfers that completed. -- (void) readMessages -{ - while (true) - { - int count = 0; - CURLMsg *msg; - CURL *easyHandle; - int code; - - msg = curl_multi_info_read(_rawHandle, &count); - - if (NULL == msg || CURLMSG_DONE != msg->msg || !msg->easy_handle) break; - - easyHandle = msg->easy_handle; - code = msg->data.result; - [self completedTransferForEasyHandle: easyHandle easyCode: code]; - } -} - -- (void) completedTransferForEasyHandle: (CURL*)rawEasyHandle - easyCode: (int)easyCode -{ - NSEnumerator *e; - GSEasyHandle *h; - GSEasyHandle *handle = nil; - NSError *err = nil; - int errCode; - - e = [_easyHandles objectEnumerator]; - while (nil != (h = [e nextObject])) - { - if ([h rawHandle] == rawEasyHandle) - { - handle = h; - break; - } - } - - NSAssert(nil != handle, @"Transfer completed for easy handle" - @", but it is not in the list of added handles."); - - errCode = [handle urlErrorCodeWithEasyCode: easyCode]; - if (0 != errCode) - { - NSString *d = nil; - - if ([handle errorBuffer][0] == 0) - { - const char *description = curl_easy_strerror(errCode); - d = [[NSString alloc] initWithCString: description - encoding: NSUTF8StringEncoding]; - } - else - { - d = [[NSString alloc] initWithCString: [handle errorBuffer] - encoding: NSUTF8StringEncoding]; - } - err = [NSError errorWithDomain: NSURLErrorDomain - code: errCode - userInfo: @{NSLocalizedDescriptionKey : d, NSUnderlyingErrorKey: [NSNumber numberWithInt:easyCode]}]; - RELEASE(d); - } - - [handle transferCompletedWithError: err]; -} - -- (int32_t) registerWithSocket: (curl_socket_t)socket - what: (int)what - socketSourcePtr: (void *)socketSourcePtr -{ - // We get this callback whenever we need to register or unregister a - // given socket with libdispatch. - // The `action` / `what` defines if we should register or unregister - // that we're interested in read and/or write readiness. We will do so - // through libdispatch (DispatchSource) and store the source(s) inside - // a `SocketSources` which we in turn store inside libcurl's multi handle - // by means of curl_multi_assign() -- we retain the object first. - - GSSocketRegisterAction *action; - GSSocketSources *socketSources; - - action = [[GSSocketRegisterAction alloc] initWithRawValue: what]; - socketSources = [GSSocketSources from: socketSourcePtr]; - - if (nil == socketSources && [action needsSource]) - { - GSSocketSources *s; - - s = [[GSSocketSources alloc] init]; - curl_multi_assign(_rawHandle, socket, (void*)s); - socketSources = s; - } - else if (nil != socketSources - && GSSocketRegisterActionTypeUnregister == [action type]) - { - DESTROY(socketSources); - curl_multi_assign(_rawHandle, socket, NULL); - } - - if (nil != socketSources) - { - [socketSources createSourcesWithAction: action - socket: socket - queue: _queue - handler: ^{ - [self readAndWriteAvailableDataOnSocket: socket]; - }]; - } - - RELEASE(action); - - return 0; -} - -@end - -@implementation GSSocketRegisterAction - -- (instancetype) initWithRawValue: (int)rawValue -{ - if (nil != (self = [super init])) - { - switch (rawValue) { - case CURL_POLL_NONE: - _type = GSSocketRegisterActionTypeNone; - break; - case CURL_POLL_IN: - _type = GSSocketRegisterActionTypeRegisterRead; - break; - case CURL_POLL_OUT: - _type = GSSocketRegisterActionTypeRegisterWrite; - break; - case CURL_POLL_INOUT: - _type = GSSocketRegisterActionTypeRegisterReadAndWrite; - break; - case CURL_POLL_REMOVE: - _type = GSSocketRegisterActionTypeUnregister; - break; - default: - NSAssert(NO, @"Invalid CURL_POLL value"); - } - } - - return self; -} - -- (GSSocketRegisterActionType) type -{ - return _type; -} - -- (BOOL) needsReadSource -{ - switch (self.type) - { - case GSSocketRegisterActionTypeRegisterRead: - case GSSocketRegisterActionTypeRegisterReadAndWrite: - return YES; - default: - return NO; - } -} - -- (BOOL) needsWriteSource -{ - switch (self.type) - { - case GSSocketRegisterActionTypeRegisterWrite: - case GSSocketRegisterActionTypeRegisterReadAndWrite: - return YES; - default: - return NO; - } -} - -- (BOOL)needsSource -{ - return [self needsReadSource] || [self needsWriteSource]; -} - -@end - -@implementation GSSocketSources - -- (void) dealloc -{ - if (_readSource) - { - dispatch_source_cancel(_readSource); - } - _readSource = NULL; - - if (_writeSource) - { - dispatch_source_cancel(_writeSource); - } - _writeSource = NULL; - - [super dealloc]; -} - -- (void) createSourcesWithAction: (GSSocketRegisterAction*)action - socket: (curl_socket_t)socket - queue: (dispatch_queue_t)queue - handler: (dispatch_block_t)handler -{ - if (!_readSource && [action needsReadSource]) - { - _readSource = [self createSourceWithType: DISPATCH_SOURCE_TYPE_READ - socket: socket - queue: queue - handler: handler]; - } - - if (!_writeSource && [action needsWriteSource]) - { - _writeSource = [self createSourceWithType: DISPATCH_SOURCE_TYPE_WRITE - socket: socket - queue: queue - handler: handler]; - } -} - -- (dispatch_source_t) createSourceWithType: (dispatch_source_type_t)type - socket: (curl_socket_t)socket - queue: (dispatch_queue_t)queue - handler: (dispatch_block_t)handler -{ - dispatch_source_t source; - - source = dispatch_source_create(type, socket, 0, queue); - dispatch_source_set_event_handler(source, handler); - dispatch_source_set_cancel_handler(source, ^{ - dispatch_release(source); - }); - dispatch_resume(source); - - return source; -} - - -+ (instancetype) from: (void*)socketSourcePtr -{ - if (!socketSourcePtr) - { - return nil; - } - else - { - return (GSSocketSources*)socketSourcePtr; - } -} - -@end diff --git a/Source/GSNativeProtocol.h b/Source/GSNativeProtocol.h deleted file mode 100644 index d1a0be7a0..000000000 --- a/Source/GSNativeProtocol.h +++ /dev/null @@ -1,120 +0,0 @@ -#ifndef INCLUDED_GSNATIVEPROTOCOL_H -#define INCLUDED_GSNATIVEPROTOCOL_H - -#import "GSDispatch.h" -#import "GSEasyHandle.h" -#import "Foundation/NSURLProtocol.h" -#import "Foundation/NSURLSession.h" - -@class GSTransferState; - -@interface NSURLSessionTask (GSNativeProtocolInternal) - -- (void) setCurrentRequest: (NSURLRequest*)request; - -- (dispatch_queue_t) workQueue; - -- (NSUInteger) suspendCount; - -- (void) getBodyWithCompletion: (void (^)(GSURLSessionTaskBody *body))completion; - -- (GSURLSessionTaskBody*) knownBody; -- (void) setKnownBody: (GSURLSessionTaskBody*)body; - -- (void) setError: (NSError*)error; - -- (void) setCountOfBytesReceived: (int64_t)count; - -- (void) setCountOfBytesExpectedToReceive: (int64_t)count; - -- (void) setCountOfBytesExpectedToSend: (int64_t)count; - -- (void (^)(NSData *data, NSURLResponse *response, NSError *error)) dataCompletionHandler; - -- (void (^)(NSURL *location, NSURLResponse *response, NSError *error)) downloadCompletionHandler; - -@end - -typedef NS_ENUM(NSUInteger, GSCompletionActionType) { - GSCompletionActionTypeCompleteTask, - GSCompletionActionTypeFailWithError, - GSCompletionActionTypeRedirectWithRequest, -}; - -// Action to be taken after a transfer completes -@interface GSCompletionAction : NSObject -{ - GSCompletionActionType _type; - int _errorCode; - NSURLRequest *_redirectRequest; -} - -- (GSCompletionActionType) type; -- (void) setType: (GSCompletionActionType) type; - -- (int) errorCode; -- (void) setErrorCode: (int)code; - -- (NSURLRequest*) redirectRequest; -- (void) setRedirectRequest: (NSURLRequest*)request; - -@end - -typedef NS_ENUM(NSUInteger, GSNativeProtocolInternalState) { - // Task has been created, but nothing has been done, yet - GSNativeProtocolInternalStateInitial, - // The task is being fulfilled from the cache rather than the network. - GSNativeProtocolInternalStateFulfillingFromCache, - // The easy handle has been fully configured. But it is not added to - // the multi handle. - GSNativeProtocolInternalStateTransferReady, - // The easy handle is currently added to the multi handle - GSNativeProtocolInternalStateTransferInProgress, - // The transfer completed. - // The easy handle has been removed from the multi handle. This does - // not necessarily mean the task completed. A task that gets - // redirected will do multiple transfers. - GSNativeProtocolInternalStateTransferCompleted, - // The transfer failed. - // Same as `GSNativeProtocolInternalStateTransferCompleted`, - // but without response / body data - GSNativeProtocolInternalStateTransferFailed, - // Waiting for the completion handler of the HTTP redirect callback. - // When we tell the delegate that we're about to perform an HTTP - // redirect, we need to wait for the delegate to let us know what - // action to take. - GSNativeProtocolInternalStateWaitingForRedirectCompletionHandler, - // Waiting for the completion handler of the 'did receive response' callback. - // When we tell the delegate that we received a response (i.e. when - // we received a complete header), we need to wait for the delegate to - // let us know what action to take. In this state the easy handle is - // paused in order to suspend delegate callbacks. - GSNativeProtocolInternalStateWaitingForResponseCompletionHandler, - // The task is completed - // Contrast this with `GSNativeProtocolInternalStateTransferCompleted`. - GSNativeProtocolInternalStateTaskCompleted, -}; - -// This abstract class has the common implementation of Native protocols like -// HTTP, FTP, etc. -// These are libcurl helpers for the URLSession API code. -@interface GSNativeProtocol : NSURLProtocol -{ - GSEasyHandle *_easyHandle; - GSNativeProtocolInternalState _internalState; - GSTransferState *_transferState; -} - -- (void) setInternalState: (GSNativeProtocolInternalState)newState; - -- (void) failWithError: (NSError*)error request: (NSURLRequest*)request; - -- (void) completeTaskWithError: (NSError*)error; - -- (void) completeTask; - -- (void) startNewTransferWithRequest: (NSURLRequest*)request; - -@end - -#endif diff --git a/Source/GSNativeProtocol.m b/Source/GSNativeProtocol.m deleted file mode 100644 index a0b38df58..000000000 --- a/Source/GSNativeProtocol.m +++ /dev/null @@ -1,818 +0,0 @@ -#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 -{ - GSURLSessionTaskBody *body; - - if (nil != _knownBody) - { - completion(_knownBody); - return; - }; - - 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 - -+ (BOOL) canInitWithRequest: (NSURLRequest*)request -{ - return NO; -} - -- (instancetype) initWithTask: (NSURLSessionTask*)_task - cachedResponse: (NSCachedURLResponse*)_cachedResponse - client: (id)_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 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 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 dataDelegate; - NSURLSessionDataTask *dataTask; - - dataDelegate = (id)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 downloadDelegate; - NSURLSessionDownloadTask *downloadTask; - - downloadDelegate = (id)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 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 taskDelegate; - NSURLSession *session; - - session = [task session]; - NSAssert(nil != session, @"Missing session"); - taskDelegate = (id)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 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 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 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 - - diff --git a/Source/GSTaskRegistry.h b/Source/GSTaskRegistry.h deleted file mode 100644 index 610339a44..000000000 --- a/Source/GSTaskRegistry.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef INCLUDED_GSTASKREGISTRY_H -#define INCLUDED_GSTASKREGISTRY_H - -#import "common.h" - -@class NSArray; -@class NSURLSessionTask; - -/* - * This helper class keeps track of all tasks. - * - * Each `NSURLSession` has a `GSTaskRegistry` for its running tasks. - * - * - Note: This must **only** be accessed on the owning session's work queue. - */ -@interface GSTaskRegistry : NSObject - -- (void ) addTask: (NSURLSessionTask*)task; - -- (void) removeTask: (NSURLSessionTask*)task; - -- (void) notifyOnTasksCompletion: (void (^)(void))tasksCompletion; - -- (NSArray*) allTasks; - -- (BOOL) isEmpty; - -@end - -#endif diff --git a/Source/GSTaskRegistry.m b/Source/GSTaskRegistry.m deleted file mode 100644 index 07e99f655..000000000 --- a/Source/GSTaskRegistry.m +++ /dev/null @@ -1,98 +0,0 @@ -#import "GSTaskRegistry.h" -#import "Foundation/NSDictionary.h" -#import "Foundation/NSException.h" -#import "Foundation/NSURLSession.h" - - -@implementation GSTaskRegistry -{ - NSMutableDictionary *_tasks; - void (^_tasksCompletion)(void); -} - -- (instancetype) init -{ - if (nil != (self = [super init])) - { - _tasks = [[NSMutableDictionary alloc] init]; - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_tasks); - [super dealloc]; -} - -- (NSArray*) allTasks -{ - return [_tasks allValues]; -} - -- (BOOL) isEmpty -{ - return [_tasks count] == 0; -} - -- (void) notifyOnTasksCompletion: (void (^)(void))tasksCompletion -{ - _tasksCompletion = tasksCompletion; -} - -- (void) addTask: (NSURLSessionTask*)task -{ - NSString *identifier; - NSUInteger taskIdentifier; - NSURLSessionTask *t; - - taskIdentifier = [task taskIdentifier]; - - NSAssert(taskIdentifier != 0, @"Invalid task identifier"); - - identifier = [NSString stringWithFormat: @"%lu", taskIdentifier]; - - if (nil != (t = [_tasks objectForKey: identifier])) - { - if ([t isEqual: task]) - { - NSAssert(NO, - @"Trying to re-insert a task that's already in the registry."); - } - else - { - NSAssert(NO, - @"Trying to insert a task, but a different task with the same" - @" identifier is already in the registry."); - } - } - - [_tasks setObject: task forKey: identifier]; -} - -- (void) removeTask: (NSURLSessionTask*)task -{ - NSString *identifier; - NSUInteger taskIdentifier; - - taskIdentifier = [task taskIdentifier]; - - NSAssert(taskIdentifier != 0, @"Invalid task identifier"); - - identifier = [NSString stringWithFormat: @"%lu", taskIdentifier]; - - if (nil == [_tasks objectForKey: identifier]) - { - NSAssert(NO, @"Trying to remove task, but it's not in the registry."); - } - - [_tasks removeObjectForKey: identifier]; - - if (nil != _tasksCompletion && [self isEmpty]) - { - _tasksCompletion(); - } -} - -@end diff --git a/Source/GSTimeoutSource.h b/Source/GSTimeoutSource.h deleted file mode 100644 index 2399e7a2a..000000000 --- a/Source/GSTimeoutSource.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef INCLUDED_GSTIMEOUTSOURCE_H -#define INCLUDED_GSTIMEOUTSOURCE_H - -#import "common.h" -#import "GSDispatch.h" - -/* - * A helper class that wraps a libdispatch timer. - * - * Used to implement the timeout of `GSMultiHandle` and `GSEasyHandle` - */ -@interface GSTimeoutSource : NSObject -{ - dispatch_source_t _timer; - NSInteger _timeoutMs; - bool _isSuspended; -} - - -- (instancetype) initWithQueue: (dispatch_queue_t)queue - handler: (dispatch_block_t)handler; - -- (NSInteger) timeout; -- (void) setTimeout: (NSInteger)timeoutMs; - -- (void) suspend; - -- (void) cancel; - -@end - -#endif diff --git a/Source/GSTimeoutSource.m b/Source/GSTimeoutSource.m deleted file mode 100644 index 5c5e049e4..000000000 --- a/Source/GSTimeoutSource.m +++ /dev/null @@ -1,76 +0,0 @@ -#import "GSTimeoutSource.h" - -@implementation GSTimeoutSource - -- (instancetype) initWithQueue: (dispatch_queue_t)queue - handler: (dispatch_block_t)handler -{ - if (nil != (self = [super init])) - { - dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); - dispatch_source_set_event_handler(timer, handler); - dispatch_source_set_cancel_handler(timer, ^{ - dispatch_release(timer); - }); - - _timer = timer; - _timeoutMs = -1; - _isSuspended = YES; - } - return self; -} - -- (void) dealloc -{ - [self cancel]; - [super dealloc]; -} - -- (NSInteger) timeout -{ - return _timeoutMs; -} - -- (void) setTimeout: (NSInteger)timeoutMs -{ - if (timeoutMs >= 0) - { - _timeoutMs = timeoutMs; - - dispatch_source_set_timer(_timer, - dispatch_time(DISPATCH_TIME_NOW, timeoutMs * NSEC_PER_MSEC), - DISPATCH_TIME_FOREVER, // don't repeat - timeoutMs * 0.05); // 5% leeway - - if (_isSuspended) - { - _isSuspended = NO; - dispatch_resume(_timer); - } - } - else - { - [self suspend]; - } -} - -- (void)suspend -{ - if (!_isSuspended) - { - _isSuspended = YES; - _timeoutMs = -1; - dispatch_suspend(_timer); - } -} - -- (void) cancel -{ - if (_timer) - { - dispatch_source_cancel(_timer); - _timer = NULL; // released in cancel handler - } -} - -@end \ No newline at end of file diff --git a/Source/GSTransferState.h b/Source/GSTransferState.h deleted file mode 100644 index 5de46551e..000000000 --- a/Source/GSTransferState.h +++ /dev/null @@ -1,140 +0,0 @@ -#ifndef INCLUDED_GSTRANSFERTSTATE_H -#define INCLUDED_GSTRANSFERTSTATE_H - -#import "GSURLSessionTaskBodySource.h" - - -@class GSURLSessionTaskBodySource; -@class NSArray; -@class NSData; -@class NSMutableData; -@class NSFileHandle; -@class NSHTTPURLResponse; -@class NSURL; -@class NSURLResponse; - -typedef NS_ENUM(NSUInteger, GSParsedResponseHeaderType) { - GSParsedResponseHeaderTypePartial, - GSParsedResponseHeaderTypeComplete -}; - -/* - * A native protocol like HTTP header being parsed. - * - * It can either be complete (i.e. the final CR LF CR LF has been - * received), or partial. - */ -@interface GSParsedResponseHeader: NSObject -{ - NSArray *_lines; - GSParsedResponseHeaderType _type; -} - -- (GSParsedResponseHeaderType) type; - -/* - * Parse a header line passed by libcurl. - * - * These contain the ending and the final line contains nothing but - * that ending. - * - Returns: Returning nil indicates failure. Otherwise returns a new - * `GSParsedResponseHeader` with the given line added. - */ -- (instancetype) byAppendingHeaderLine: (NSData*)data; - -- (NSHTTPURLResponse*) createHTTPURLResponseForURL: (NSURL*)URL; - -@end - -typedef NS_ENUM(NSUInteger, GSDataDrainType) { - // Concatenate in-memory - GSDataDrainInMemory, - // Write to file - GSDataDrainTypeToFile, - // Do nothing. Might be forwarded to delegate - GSDataDrainTypeIgnore, -}; - -@interface GSDataDrain: NSObject -{ - GSDataDrainType _type; - NSMutableData *_data; - NSURL *_fileURL; - NSFileHandle *_fileHandle; -} - -- (GSDataDrainType) type; -- (void) setType: (GSDataDrainType)type; - -- (NSMutableData*) data; - -- (NSURL*) fileURL; - -- (NSFileHandle*) fileHandle; - -@end - -/* - * State related to an ongoing transfer. - * - * This contains headers received so far, body data received so far, etc. - * - * There's a strict 1-to-1 relationship between a `GSEasyHandle` and a - * `GSTransferState`. - */ -@interface GSTransferState: NSObject -{ - NSURL *_url; // The URL that's being requested - GSParsedResponseHeader *_parsedResponseHeader; // Raw headers received. - NSURLResponse *_response; // Once the headers is complete, this will contain the response - id _requestBodySource; // The body data to be sent in the request - GSDataDrain *_bodyDataDrain; // Body data received - BOOL _isHeaderComplete; -} - -// Transfer state that can receive body data, but will not send body data. -- (instancetype) initWithURL: (NSURL*)url - bodyDataDrain: (GSDataDrain*)bodyDataDrain; - -// Transfer state that sends body data and can receive body data. -- (instancetype) initWithURL: (NSURL*)url - bodyDataDrain: (GSDataDrain*)bodyDataDrain - bodySource: (id)bodySource; - -- (instancetype) initWithURL: (NSURL*)url - parsedResponseHeader: (GSParsedResponseHeader*)parsedResponseHeader - response: (NSURLResponse*)response - bodySource: (id)bodySource - bodyDataDrain: (GSDataDrain*)bodyDataDrain; - -/* - * Append body data - */ -- (instancetype) byAppendingBodyData: (NSData*)bodyData; - -/* - * Appends a header line - * - * Will set the complete response once the header is complete, i.e. the - * return value's `isHeaderComplete` will then by `YES`. - * - * When a parsing error occurs `error` will be set. - */ -- (instancetype) byAppendingHTTPHeaderLineData: (NSData*)data - error: (NSError**)error; - -- (NSURLResponse*) response; - -- (void) setResponse: (NSURLResponse*)response; - -- (BOOL) isHeaderComplete; - -- (id) requestBodySource; - -- (GSDataDrain*) bodyDataDrain; - -- (NSURL*) URL; - -@end - -#endif diff --git a/Source/GSTransferState.m b/Source/GSTransferState.m deleted file mode 100644 index f1be36c3a..000000000 --- a/Source/GSTransferState.m +++ /dev/null @@ -1,487 +0,0 @@ -#import "GSTransferState.h" -#import "GSURLSessionTaskBodySource.h" -#import "Foundation/NSArray.h" -#import "Foundation/NSCharacterSet.h" -#import "Foundation/NSData.h" -#import "Foundation/NSDictionary.h" -#import "Foundation/NSFileHandle.h" -#import "Foundation/NSError.h" -#import "Foundation/NSURL.h" -#import "Foundation/NSURLError.h" -#import "Foundation/NSURLResponse.h" -#import "Foundation/NSURLSession.h" -#import "Foundation/NSUUID.h" -#import "Foundation/NSFileManager.h" -#import "Foundation/NSPathUtilities.h" - -#define GS_DELIMITERS_CR 0x0d -#define GS_DELIMITERS_LR 0x0a - -@implementation GSParsedResponseHeader - -- (instancetype) init -{ - if (nil != (self = [super init])) - { - _lines = [[NSMutableArray alloc] init]; - _type = GSParsedResponseHeaderTypePartial; - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_lines); - [super dealloc]; -} - -- (GSParsedResponseHeaderType) type -{ - return _type; -} - -- (void) setType: (GSParsedResponseHeaderType)type -{ - _type = type; -} - -- (void) setLines: (NSArray*)lines -{ - ASSIGN(_lines, lines); -} - -- (instancetype) byAppendingHeaderLine: (NSData*)data -{ - NSUInteger length = [data length]; - - if (length >= 2) - { - uint8_t last2; - uint8_t last1; - - [data getBytes: &last2 range: NSMakeRange(length - 2, 1)]; - [data getBytes: &last1 range: NSMakeRange(length - 1, 1)]; - - if (GS_DELIMITERS_CR == last2 && GS_DELIMITERS_LR == last1) - { - NSData *lineBuffer; - NSString *line; - - lineBuffer = [data subdataWithRange: NSMakeRange(0, length - 2)]; - line = AUTORELEASE([[NSString alloc] initWithData: lineBuffer - encoding: NSUTF8StringEncoding]); - - if (nil == line) - { - return nil; - } - - return [self _byAppendingHeaderLine: line]; - } - } - - return nil; -} - -- (NSHTTPURLResponse*) createHTTPURLResponseForURL: (NSURL*)URL -{ - NSArray *tail; - NSArray *startLine; - NSDictionary *headerFields; - NSString *head; - NSString *s, *v; - - head = [_lines firstObject]; - if (nil == head) - { - return nil; - } - if ([_lines count] == 0) - { - return nil; - } - - tail = [_lines subarrayWithRange: NSMakeRange(1, [_lines count] - 1)]; - - startLine = [self statusLineFromLine: head]; - if (nil == startLine) - { - return nil; - } - - headerFields = [self createHeaderFieldsFromLines: tail]; - - v = [startLine objectAtIndex: 0]; - s = [startLine objectAtIndex: 1]; - - return AUTORELEASE([[NSHTTPURLResponse alloc] initWithURL: URL - statusCode: [s integerValue] - HTTPVersion: v - headerFields: headerFields]); -} - -- (NSArray*) statusLineFromLine: (NSString*)line -{ - NSArray *a; - NSString *s; - NSInteger status; - - a = [line componentsSeparatedByString: @" "]; - if ([a count] < 3) - { - return nil; - } - - s = [a objectAtIndex: 1]; - - status = [s integerValue]; - if (status >= 100 && status <= 999) - { - return a; - } - else - { - return nil; - } -} - -- (NSDictionary *) createHeaderFieldsFromLines: (NSArray *)lines -{ - NSMutableDictionary *headerFields = nil; - NSEnumerator *e; - NSString *line; - - e = [_lines objectEnumerator]; - while (nil != (line = [e nextObject])) - { - NSRange r; - NSString *head; - NSString *tail; - NSCharacterSet *set; - NSString *key; - NSString *value; - NSString *v; - - r = [line rangeOfString: @":"]; - if (r.location != NSNotFound) - { - head = [line substringToIndex: r.location]; - tail = [line substringFromIndex: r.location + 1]; - set = [NSCharacterSet whitespaceAndNewlineCharacterSet]; - key = [head stringByTrimmingCharactersInSet: set]; - value = [tail stringByTrimmingCharactersInSet: set]; - if (nil != key && nil != value) - { - if (nil == headerFields) - { - headerFields = [NSMutableDictionary dictionary]; - } - if (nil != [headerFields objectForKey: key]) - { - v = [NSString stringWithFormat:@"%@, %@", - [headerFields objectForKey: key], value]; - [headerFields setObject: v forKey: key]; - } - else - { - [headerFields setObject: value forKey: key]; - } - } - } - else - { - continue; - } - } - - return AUTORELEASE([headerFields copy]); -} - -- (instancetype) _byAppendingHeaderLine: (NSString*)line -{ - GSParsedResponseHeader *header; - - header = AUTORELEASE([[GSParsedResponseHeader alloc] init]); - - if ([line length] == 0) - { - switch (_type) - { - case GSParsedResponseHeaderTypePartial: - { - [header setType: GSParsedResponseHeaderTypeComplete]; - [header setLines: _lines]; - - return header; - } - case GSParsedResponseHeaderTypeComplete: - return header; - } - } - else - { - NSMutableArray *lines = [[self partialResponseHeader] mutableCopy]; - - [lines addObject:line]; - - [header setType: GSParsedResponseHeaderTypePartial]; - [header setLines: lines]; - - RELEASE(lines); - - return header; - } -} - -- (NSArray*) partialResponseHeader -{ - switch (_type) - { - case GSParsedResponseHeaderTypeComplete: - return [NSArray array]; - - case GSParsedResponseHeaderTypePartial: - return _lines; - } -} - -@end - -@implementation GSDataDrain - -- (void) dealloc -{ - DESTROY(_data); - DESTROY(_fileURL); - DESTROY(_fileHandle); - [super dealloc]; -} - -- (GSDataDrainType) type -{ - return _type; -} - -- (void) setType: (GSDataDrainType)type -{ - _type = type; -} - -- (NSMutableData*) data -{ - if (!_data) - { - _data = [[NSMutableData alloc] init]; - } - - return _data; -} - -- (NSURL*) fileURL -{ - /* Generate a random fileURL if fileURL is not initialized. - * It would be nice to have this implemented in an initializer. */ - if (!_fileURL) - { - NSUUID *randomUUID; - NSString *fileName; - NSURL *tempURL; - - randomUUID = [NSUUID UUID]; - fileName = [[randomUUID UUIDString] stringByAppendingPathExtension: @"tmp"]; - tempURL = [NSURL fileURLWithPath: NSTemporaryDirectory()]; - - _fileURL = RETAIN([NSURL fileURLWithPath: fileName relativeToURL: tempURL]); - } - - return _fileURL; -} - -- (NSFileHandle*) fileHandle -{ - /* Create temporary file and open a fileHandle for writing. */ - if (!_fileHandle) - { - NSFileManager *fileManager = [NSFileManager defaultManager]; - [fileManager createFileAtPath: [[self fileURL] relativePath] - contents: nil - attributes: nil]; - - _fileHandle = RETAIN([NSFileHandle fileHandleForWritingToURL: [self fileURL] error: NULL]); - } - - return _fileHandle; -} - -@end - -@implementation GSTransferState - -- (instancetype) initWithURL: (NSURL*)url - bodyDataDrain: (GSDataDrain*)bodyDataDrain -{ - return [self initWithURL: url - parsedResponseHeader: nil - response: nil - bodySource: nil - bodyDataDrain: bodyDataDrain]; -} - -- (instancetype) initWithURL: (NSURL*)url - bodyDataDrain: (GSDataDrain*)bodyDataDrain - bodySource: (id)bodySource -{ - return [self initWithURL: url - parsedResponseHeader: nil - response: nil - bodySource: bodySource - bodyDataDrain: bodyDataDrain]; -} - -- (instancetype) initWithURL: (NSURL*)url - parsedResponseHeader: (GSParsedResponseHeader*)parsedResponseHeader - response: (NSURLResponse*)response - bodySource: (id)bodySource - bodyDataDrain: (GSDataDrain*)bodyDataDrain -{ - if (nil != (self = [super init])) - { - ASSIGN(_url, url); - if (nil != parsedResponseHeader) - { - ASSIGN(_parsedResponseHeader, parsedResponseHeader); - } - else - { - _parsedResponseHeader = [[GSParsedResponseHeader alloc] init]; - } - ASSIGN(_response, response); - ASSIGN(_requestBodySource, bodySource); - ASSIGN(_bodyDataDrain, bodyDataDrain); - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_url); - DESTROY(_parsedResponseHeader); - DESTROY(_response); - DESTROY(_requestBodySource); - DESTROY(_bodyDataDrain); - [super dealloc]; -} - -- (instancetype) byAppendingBodyData: (NSData*)bodyData -{ - switch ([_bodyDataDrain type]) - { - case GSDataDrainInMemory: - [[_bodyDataDrain data] appendData: bodyData]; - return self; - case GSDataDrainTypeToFile: - { - NSFileHandle *fileHandle; - - fileHandle = [_bodyDataDrain fileHandle]; - [fileHandle seekToEndOfFile]; - [fileHandle writeData: bodyData]; - - return self; - } - case GSDataDrainTypeIgnore: - return self; - } -} - -- (instancetype) byAppendingHTTPHeaderLineData: (NSData*)data - error: (NSError**)error -{ - GSParsedResponseHeader *h; - - h = [_parsedResponseHeader byAppendingHeaderLine: data]; - if (nil == h) - { - if (error != NULL) - { - *error = [NSError errorWithDomain: NSURLErrorDomain - code: -1 - userInfo: nil]; - } - - return nil; - } - - if ([h type] == GSParsedResponseHeaderTypeComplete) - { - NSHTTPURLResponse *response; - GSParsedResponseHeader *ph; - GSTransferState *ts; - - response = [h createHTTPURLResponseForURL: _url]; - if (nil == response) - { - if (error != NULL) - { - *error = [NSError errorWithDomain: NSURLErrorDomain - code: -1 - userInfo: nil]; - } - - return nil; - } - - ph = AUTORELEASE([[GSParsedResponseHeader alloc] init]); - ts = [[GSTransferState alloc] initWithURL: _url - parsedResponseHeader: ph - response: response - bodySource: _requestBodySource - bodyDataDrain: _bodyDataDrain]; - return AUTORELEASE(ts); - } - else - { - GSTransferState *ts; - - ts = [[GSTransferState alloc] initWithURL: _url - parsedResponseHeader: h - response: nil - bodySource: _requestBodySource - bodyDataDrain: _bodyDataDrain]; - return AUTORELEASE(ts); - } -} - -- (BOOL) isHeaderComplete -{ - return _response != nil; -} - -- (NSURLResponse*) response -{ - return _response; -} - -- (void) setResponse: (NSURLResponse*)response -{ - ASSIGN(_response, response); -} - -- (id) requestBodySource -{ - return _requestBodySource; -} - -- (GSDataDrain*) bodyDataDrain -{ - return _bodyDataDrain; -} - -- (NSURL*) URL -{ - return _url; -} - -@end diff --git a/Source/GSURLPrivate.h b/Source/GSURLPrivate.h index afb5a1e09..69a31464d 100644 --- a/Source/GSURLPrivate.h +++ b/Source/GSURLPrivate.h @@ -61,6 +61,7 @@ - (id) _debugLogDelegate; - (id) _propertyForKey: (NSString*)key; - (void) _setProperty: (id)value forKey: (NSString*)key; +- (NSDictionary *) _insensitiveHeaders; @end diff --git a/Source/GSURLSessionTaskBody.h b/Source/GSURLSessionTaskBody.h deleted file mode 100644 index 681fa5819..000000000 --- a/Source/GSURLSessionTaskBody.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef INCLUDED_GSURLSESSIONTASKBODY_H -#define INCLUDED_GSURLSESSIONTASKBODY_H - -#import "common.h" - -@class NSData; -@class NSError; -@class NSInputStream; -@class NSNumber; -@class NSURL; - -typedef NS_ENUM(NSUInteger, GSURLSessionTaskBodyType) { - GSURLSessionTaskBodyTypeNone, - GSURLSessionTaskBodyTypeData, - // Body data is read from the given file URL - GSURLSessionTaskBodyTypeFile, - // Body data is read from the given input stream - GSURLSessionTaskBodyTypeStream, -}; - -@interface GSURLSessionTaskBody : NSObject -{ - GSURLSessionTaskBodyType _type; - NSData *_data; - NSURL *_fileURL; - NSInputStream *_inputStream; -} - -- (instancetype) init; -- (instancetype) initWithData: (NSData*)data; -- (instancetype) initWithFileURL: (NSURL*)fileURL; -- (instancetype) initWithInputStream: (NSInputStream*)inputStream; - -- (GSURLSessionTaskBodyType) type; - -- (NSData*) data; - -- (NSURL*) fileURL; - -- (NSInputStream*) inputStream; - -- (NSNumber*) getBodyLengthWithError: (NSError**)error; - -@end - -#endif diff --git a/Source/GSURLSessionTaskBody.m b/Source/GSURLSessionTaskBody.m deleted file mode 100644 index 34c5e32c9..000000000 --- a/Source/GSURLSessionTaskBody.m +++ /dev/null @@ -1,111 +0,0 @@ -#import "GSURLSessionTaskBody.h" -#import "Foundation/NSData.h" -#import "Foundation/NSFileManager.h" -#import "Foundation/NSStream.h" -#import "Foundation/NSURL.h" -#import "Foundation/NSValue.h" - -@implementation GSURLSessionTaskBody - -- (instancetype) init -{ - if (nil != (self = [super init])) - { - _type = GSURLSessionTaskBodyTypeNone; - } - - return self; -} - -- (instancetype) initWithData: (NSData*)data -{ - if (nil != (self = [super init])) - { - _type = GSURLSessionTaskBodyTypeData; - ASSIGN(_data, data); - } - - return self; -} - -- (instancetype) initWithFileURL: (NSURL*)fileURL -{ - if (nil != (self = [super init])) - { - _type = GSURLSessionTaskBodyTypeFile; - ASSIGN(_fileURL, fileURL); - } - - return self; -} - -- (instancetype) initWithInputStream: (NSInputStream*)inputStream -{ - if (nil != (self = [super init])) - { - _type = GSURLSessionTaskBodyTypeStream; - ASSIGN(_inputStream, inputStream); - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_data); - DESTROY(_fileURL); - DESTROY(_inputStream); - [super dealloc]; -} - -- (GSURLSessionTaskBodyType) type -{ - return _type; -} - -- (NSData*) data -{ - return _data; -} - -- (NSURL*) fileURL -{ - return _fileURL; -} - -- (NSInputStream*) inputStream -{ - return _inputStream; -} - -- (NSNumber*) getBodyLengthWithError: (NSError**)error -{ - switch (_type) - { - case GSURLSessionTaskBodyTypeNone: - return [NSNumber numberWithInt: 0]; - case GSURLSessionTaskBodyTypeData: - return [NSNumber numberWithUnsignedInteger: [_data length]]; - case GSURLSessionTaskBodyTypeFile: - { - NSDictionary *attributes; - - attributes = [[NSFileManager defaultManager] - attributesOfItemAtPath: [_fileURL path] - error: error]; - if (!error) - { - NSNumber *size = [attributes objectForKey: NSFileSize]; - return size; - } - else - { - return nil; - } - } - case GSURLSessionTaskBodyTypeStream: - return nil; - } -} - -@end diff --git a/Source/GSURLSessionTaskBodySource.h b/Source/GSURLSessionTaskBodySource.h deleted file mode 100644 index 142dc31db..000000000 --- a/Source/GSURLSessionTaskBodySource.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef INCLUDED_GSURLSESSIONTASKBODYSOURCE_H -#define INCLUDED_GSURLSESSIONTASKBODYSOURCE_H - -#import "common.h" -#import "GSDispatch.h" - -@class NSFileHandle; -@class NSInputStream; - -typedef NS_ENUM(NSUInteger, GSBodySourceDataChunk) { - GSBodySourceDataChunkData, - // The source is depleted. - GSBodySourceDataChunkDone, - // Retry later to get more data. - GSBodySourceDataChunkRetryLater, - GSBodySourceDataChunkError -}; - -/* - * A (non-blocking) source for body data. - */ -@protocol GSURLSessionTaskBodySource - -/* - * Get the next chunck of data. - */ -- (void) getNextChunkWithLength: (NSInteger)length - completionHandler: (void (^)(GSBodySourceDataChunk chunk, NSData *data))completionHandler; - -@end - -@interface GSBodyStreamSource : NSObject - -- (instancetype) initWithInputStream: (NSInputStream*)inputStream; - -@end - -@interface GSBodyDataSource : NSObject - -- (instancetype)initWithData:(NSData *)data; - -@end - -@interface GSBodyFileSource : NSObject - -- (instancetype) initWithFileURL: (NSURL*)fileURL - workQueue: (dispatch_queue_t)workQueue - dataAvailableHandler: (void (^)(void))dataAvailableHandler; - -@end - -#endif diff --git a/Source/GSURLSessionTaskBodySource.m b/Source/GSURLSessionTaskBodySource.m deleted file mode 100644 index 8d4ddaedc..000000000 --- a/Source/GSURLSessionTaskBodySource.m +++ /dev/null @@ -1,152 +0,0 @@ -#import "GSURLSessionTaskBodySource.h" -#import "Foundation/NSData.h" -#import "Foundation/NSStream.h" - - -@implementation GSBodyStreamSource -{ - NSInputStream *_inputStream; -} - -- (instancetype) initWithInputStream: (NSInputStream*)inputStream -{ - if (nil != (self = [super init])) - { - ASSIGN(_inputStream, inputStream); - if ([_inputStream streamStatus] == NSStreamStatusNotOpen) - { - [_inputStream open]; - } - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_inputStream); - [super dealloc]; -} - -- (void) getNextChunkWithLength: (NSInteger)length - completionHandler: (void (^)(GSBodySourceDataChunk, NSData*))completionHandler -{ - if (nil == completionHandler) - { - return; - } - - if (![_inputStream hasBytesAvailable]) - { - completionHandler(GSBodySourceDataChunkDone, nil); - return; - } - - uint8_t buffer[length]; - NSInteger readBytes; - NSData *data; - - readBytes = [_inputStream read: buffer maxLength: length]; - if (readBytes > 0) - { - data = AUTORELEASE([[NSData alloc] initWithBytes: buffer - length: readBytes]); - completionHandler(GSBodySourceDataChunkData, data); - } - else if (readBytes == 0) - { - completionHandler(GSBodySourceDataChunkDone, nil); - } - else - { - completionHandler(GSBodySourceDataChunkError, nil); - } -} - -@end - -@implementation GSBodyDataSource -{ - NSData *_data; -} - -- (instancetype) initWithData: (NSData*)data -{ - if (nil != (self = [super init])) - { - ASSIGN(_data, data); - } - return self; -} - -- (void) dealloc -{ - DESTROY(_data); - [super dealloc]; -} - -- (void) getNextChunkWithLength: (NSInteger)length - completionHandler: (void (^)(GSBodySourceDataChunk, NSData*))completionHandler -{ - if (nil == completionHandler) - { - return; - } - - NSUInteger remaining = [_data length]; - if (remaining == 0) - { - completionHandler(GSBodySourceDataChunkDone, nil); - } - else if (remaining <= length) - { - NSData *r = AUTORELEASE([[NSData alloc] initWithData: _data]); - DESTROY(_data); - completionHandler(GSBodySourceDataChunkData, r); - } - else - { - NSData *chunk = [_data subdataWithRange: NSMakeRange(0, length)]; - NSData *remainder = [_data subdataWithRange: - NSMakeRange(length - 1, [_data length] - length)]; - ASSIGN(_data, remainder); - completionHandler(GSBodySourceDataChunkData, chunk); - } -} - -@end - -@implementation GSBodyFileSource -{ - NSURL *_fileURL; - dispatch_queue_t _workQueue; - void (^_dataAvailableHandler)(void); -} - -- (instancetype) initWithFileURL: (NSURL*)fileURL - workQueue: (dispatch_queue_t)workQueue - dataAvailableHandler: (void (^)(void))dataAvailableHandler -{ - if (nil != (self = [super init])) - { - ASSIGN(_fileURL, fileURL); - _workQueue = workQueue; - _dataAvailableHandler = dataAvailableHandler; - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_fileURL); - [super dealloc]; -} - -- (void) getNextChunkWithLength: (NSInteger)length - completionHandler: (void (^)(GSBodySourceDataChunk chunk, NSData *data))completionHandler -{ - //FIXME -} - -@end diff --git a/Source/NSHTTPCookie.m b/Source/NSHTTPCookie.m index dc8933b1f..c3c6da6a5 100644 --- a/Source/NSHTTPCookie.m +++ b/Source/NSHTTPCookie.m @@ -43,7 +43,9 @@ #import "Foundation/NSSet.h" #import "Foundation/NSValue.h" #import "Foundation/NSString.h" -#import "Foundation/NSCalendarDate.h" +#import "Foundation/NSDateFormatter.h" +#import "Foundation/NSLocale.h" +#import "Foundation/NSTimeZone.h" #import "GNUstepBase/Unicode.h" static NSString * const HTTPCookieHTTPOnly = @"HTTPOnly"; @@ -681,10 +683,22 @@ _setCookieKey(NSMutableDictionary *dict, NSString *key, NSString *value) else if ([[key lowercaseString] isEqual: @"expires"]) { NSDate *expireDate; - expireDate = [NSCalendarDate dateWithString: value - calendarFormat: @"%a, %d-%b-%Y %I:%M:%S %Z"]; + NSDateFormatter *formatter; + NSLocale *locale; + NSTimeZone *gmtTimeZone; + + locale = [NSLocale localeWithLocaleIdentifier: @"en_US"]; + gmtTimeZone = [NSTimeZone timeZoneWithAbbreviation: @"GMT"]; + + formatter = [[NSDateFormatter alloc] init]; + [formatter setDateFormat: @"EEE, dd-MMM-yyyy HH:mm:ss zzz"]; + [formatter setLocale: locale]; + [formatter setTimeZone: gmtTimeZone]; + + expireDate = [formatter dateFromString:value]; if (expireDate) [dict setObject: expireDate forKey: NSHTTPCookieExpires]; + RELEASE(formatter); } else if ([[key lowercaseString] isEqual: @"max-age"]) [dict setObject: value forKey: NSHTTPCookieMaximumAge]; @@ -697,7 +711,7 @@ _setCookieKey(NSMutableDictionary *dict, NSString *key, NSString *value) else if ([[key lowercaseString] isEqual: @"secure"]) [dict setObject: [NSNumber numberWithBool: YES] forKey: NSHTTPCookieSecure]; - else if ([[key lowercaseString] isEqual:@"httponly"]) + else if ([[key lowercaseString] isEqual: @"httponly"]) [dict setObject: [NSNumber numberWithBool: YES] forKey: HTTPCookieHTTPOnly]; else if ([[key lowercaseString] isEqual: @"version"]) diff --git a/Source/NSOperation.m b/Source/NSOperation.m index a2d2c1244..a30876a80 100644 --- a/Source/NSOperation.m +++ b/Source/NSOperation.m @@ -593,13 +593,15 @@ static NSArray *empty = nil; - (void) main { - NSEnumerator *en = [[self executionBlocks] objectEnumerator]; + NSEnumerator *en = [_executionBlocks objectEnumerator]; GSBlockOperationBlock theBlock; while ((theBlock = (GSBlockOperationBlock)[en nextObject]) != NULL) { CALL_NON_NULL_BLOCK_NO_ARGS(theBlock); } + + [_executionBlocks removeAllObjects]; } @end diff --git a/Source/NSURLRequest.m b/Source/NSURLRequest.m index bd034cdef..2725423c8 100644 --- a/Source/NSURLRequest.m +++ b/Source/NSURLRequest.m @@ -39,6 +39,7 @@ typedef struct { NSMutableDictionary *headers; BOOL shouldHandleCookies; BOOL debug; + BOOL assumesHTTP3Capable; id ioDelegate; NSURL *URL; NSURL *mainDocumentURL; @@ -56,6 +57,9 @@ typedef struct { @interface _GSMutableInsensitiveDictionary : NSMutableDictionary @end +@interface _GSInsensitiveDictionary : NSMutableDictionary +@end + @implementation NSURLRequest + (id) allocWithZone: (NSZone*)z @@ -116,6 +120,7 @@ typedef struct { ASSIGN(inst->bodyStream, this->bodyStream); ASSIGN(inst->method, this->method); inst->shouldHandleCookies = this->shouldHandleCookies; + inst->assumesHTTP3Capable = this->assumesHTTP3Capable; inst->debug = this->debug; inst->ioDelegate = this->ioDelegate; inst->headers = [this->headers mutableCopy]; @@ -375,6 +380,11 @@ typedef struct { return [this->headers objectForKey: field]; } +- (BOOL) assumesHTTP3Capable +{ + return this->assumesHTTP3Capable; +} + @end @@ -454,6 +464,11 @@ typedef struct { } } +- (void)setAssumesHTTP3Capable:(BOOL)capable +{ + this->assumesHTTP3Capable = capable; +} + @end @implementation NSURLRequest (Private) @@ -468,6 +483,11 @@ typedef struct { return this->ioDelegate; } +- (NSDictionary *) _insensitiveHeaders +{ + return [this->headers copy]; +} + - (id) _propertyForKey: (NSString*)key { return [this->properties objectForKey: key]; diff --git a/Source/NSURLSession.m b/Source/NSURLSession.m index 017a626d4..dd47cb6f9 100644 --- a/Source/NSURLSession.m +++ b/Source/NSURLSession.m @@ -1,11 +1,11 @@ /** NSURLSession.m - Copyright (C) 2017-2023 Free Software Foundation, Inc. + Copyright (C) 2017-2024 Free Software Foundation, Inc. - Written by: Daniel Ferreira - Date: November 2017 - Author: Richard Frith-Macdonald + Written by: Hugo Melder + Date: May 2024 + Author: Hugo Melder This file is part of GNUStep-base @@ -28,104 +28,29 @@ Boston, MA 02110 USA. */ -#import "GSURLPrivate.h" -#import +#import "NSURLSessionPrivate.h" +#import "NSURLSessionTaskPrivate.h" +#import "Foundation/NSString.h" +#import "Foundation/NSArray.h" +#import "Foundation/NSStream.h" +#import "Foundation/NSUserDefaults.h" +#import "Foundation/NSBundle.h" +#import "Foundation/NSData.h" -#import "GSDispatch.h" -#import "GSEasyHandle.h" -#import "GSHTTPURLProtocol.h" -#import "GSMultiHandle.h" -#import "GSPThread.h" -#import "GSTaskRegistry.h" -#import "GSURLSessionTaskBody.h" +#import "GNUstepBase/NSDebug+GNUstepBase.h" /* For NSDebugMLLog */ +#import "GNUstepBase/NSObject+GNUstepBase.h" /* For -notImplemented */ +#import "GSPThread.h" /* For nextSessionIdentifier() */ +#import "GSDispatch.h" /* For dispatch compatibility */ -#import "Foundation/NSError.h" -#import "Foundation/NSException.h" -#import "Foundation/NSOperation.h" -#import "Foundation/NSPredicate.h" -#import "Foundation/NSURLError.h" -#import "Foundation/NSURLSession.h" -#import "Foundation/NSURLRequest.h" -#import "Foundation/NSValue.h" +NSString *GS_NSURLSESSION_DEBUG_KEY = @"NSURLSession"; -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; - -/* NSURLSession API implementation overview - * - * This implementation uses libcurl for the HTTP layer implementation. At a - * high level, the [NSURLSession] keeps a curl *multi handle*, and each - * [NSURLSessionTask] has an *easy handle*. This way these two APIs somewhat - * have a 1-to-1 mapping. - * - * The [NSURLSessionTask] class is in charge of configuring its *easy handle* - * and adding it to the owning session’s *multi handle*. Adding / removing - * the handle effectively resumes / suspends the transfer. - * - * The [NSURLSessionTasks] class has subclasses, but this design puts all the - * logic into the parent [NSURLSessionTask]. - * - * The session class uses the [GSTaskRegistry] to keep track of its tasks. - * - * The task class uses an GSInternalState type together with GSTransferState - * to keep track of its state and each transfer’s state. - * NB. a single task may do multiple transfers (e.g. as the result of a - * redirect). - * - * The [NSURLSession] has a libdispatch *work queue*, and all internal work is - * done on that queue, such that the code doesn't have to deal with thread - * safety beyond that. All work inside a [NSURLSessionTask] will run on this - * work queue, and so will code manipulating the session's *multi handle*. - * - * Delegate callbacks are, however, done on the passed in delegateQueue. - * Any calls into this API need to switch onto the *work queue* as needed. - * - * Most of HTTP is defined in [RFC 2616](https://tools.ietf.org/html/rfc2616). - * While libcurl handles many of these details, some are handled by this - * NSURLSession implementation. +/* We need a globably unique label for the NSURLSession workQueues. */ -@interface NSURLSession () - -- (dispatch_queue_t) workQueue; - -@end - -@interface NSURLSessionTask() -- (instancetype) initWithSession: (NSURLSession*)session - request: (NSURLRequest*)request - taskIdentifier: (NSUInteger)identifier; - -- (void) getProtocolWithCompletion: (void (^)(NSURLProtocol* protocol))completion; - -- (void) setState: (NSURLSessionTaskState)state; - -- (void) invalidateProtocol; - -- (void (^)(NSData *data, NSURLResponse *response, NSError *error)) dataCompletionHandler; -- (void) setDataCompletionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; - -- (void (^)(NSURL *location, NSURLResponse *response, NSError *error)) downloadCompletionHandler; -- (void) setDownloadCompletionHandler: (void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler; -@end - -@interface NSURLSessionTask (URLProtocolClient) - -@end - -typedef NS_ENUM(NSUInteger, NSURLSessionTaskProtocolState) { - NSURLSessionTaskProtocolStateToBeCreated = 0, - NSURLSessionTaskProtocolStateAwaitingCacheReply = 1, - NSURLSessionTaskProtocolStateExisting = 2, - NSURLSessionTaskProtocolStateInvalidated = 3, -}; - -static unsigned nextSessionIdentifier() +static NSUInteger +nextSessionIdentifier() { static gs_mutex_t lock = GS_MUTEX_INIT_STATIC; - static unsigned sessionCounter = 0; + static NSUInteger sessionCounter = 0; GS_MUTEX_LOCK(lock); sessionCounter += 1; @@ -134,1591 +59,1007 @@ static unsigned nextSessionIdentifier() return sessionCounter; } -@implementation NSURLSession +#pragma mark - libcurl callbacks + +/* CURLMOPT_TIMERFUNCTION: Callback to receive timer requests from libcurl */ +static int +timer_callback(CURLM *multi, /* multi handle */ + long timeout_ms, /* timeout in number of ms */ + void *clientp) /* private callback pointer */ { - dispatch_queue_t _workQueue; - NSUInteger _nextTaskIdentifier; - BOOL _invalidated; - GSTaskRegistry *_taskRegistry; + NSURLSession *session = (NSURLSession *) clientp; + + NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Timer Callback for Session %@: multi=%p timeout_ms=%ld", + session, multi, timeout_ms); + + /* + * if timeout_ms is -1, just delete the timer + * + * For all other values of timeout_ms, this should set or *update* the timer + * to the new value + */ + if (timeout_ms == -1) + [session _suspendTimer]; + else + [session _setTimer:timeout_ms]; + return 0; } - -+ (NSURLSession*) sharedSession +/* CURLMOPT_SOCKETFUNCTION: libcurl requests socket monitoring using this + * callback */ +static int +socket_callback(CURL *easy, /* easy handle */ + curl_socket_t s, /* socket */ + int what, /* describes the socket */ + void *clientp, /* private callback pointer */ + void *socketp) /* private socket pointer */ { - static NSURLSession *session = nil; + NSURLSession *session = clientp; + const char *whatstr[] = {"none", "IN", "OUT", "INOUT", "REMOVE"}; + + NSDebugLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Socket Callback for Session %@: socket=%d easy:%p what=%s", + session, s, easy, whatstr[what]); + + if (NULL == socketp) + { + return [session _addSocket:s easyHandle:easy what:what]; + } + else if (CURL_POLL_REMOVE == what) + { + [session _removeSocket:(struct SourceInfo *) socketp]; + return 0; + } + else + { + return [session _setSocket:s + sources:(struct SourceInfo *) socketp + what:what]; + } +} + +#pragma mark - NSURLSession Implementation + +@implementation NSURLSession +{ + /* The libcurl multi handle associated with this session. + * We use the curl_multi_socket_action API as we utilise our + * own event-handling system based on libdispatch. + * + * Event creation and deletion is driven by the various callbacks + * registered during initialisation of the multi handle. + */ + CURLM *_multiHandle; + /* A serial work queue for timer and socket sources + * created on libcurl's behalf. + */ + dispatch_queue_t _workQueue; + /* This timer is driven by libcurl and used by + * libcurl's multi API. + * + * The handler notifies libcurl using curl_multi_socket_action + * and checks for completed requests by calling + * _checkForCompletion. + * + * See https://curl.se/libcurl/c/CURLMOPT_TIMERFUNCTION.html + * and https://curl.se/libcurl/c/curl_multi_socket_action.html + * respectively. + */ + dispatch_source_t _timer; + + /* The timer may be suspended upon request by libcurl. + */ + BOOL _isTimerSuspended; + + /* Only set when session originates from +[NSURLSession sharedSession] */ + BOOL _isSharedSession; + BOOL _invalidated; + + /* + * Number of currently running handles. + * This number is updated by curl_multi_socket_action + * in the socket source handlers. + */ + int _stillRunning; + + /* List of active tasks. Access is synchronised via the _workQueue. + */ + NSMutableArray *_tasks; + + /* PEM encoded blob of one or more certificates. + * + * See GSCACertificateFilePath in NSUserDefaults.h + */ + NSData *_certificateBlob; + /* Path to PEM encoded CA certificate file. */ + NSString *_certificatePath; + + /* The task identifier for the next task + */ + _Atomic(NSInteger) _taskIdentifier; + /* Lock for _taskIdentifier and _tasks + */ + gs_mutex_t _taskLock; +} + ++ (NSURLSession *)sharedSession +{ + static NSURLSession *session = nil; static dispatch_once_t predicate; - + dispatch_once(&predicate, ^{ - NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - session = [[NSURLSession alloc] initWithConfiguration: configuration - delegate: nil - delegateQueue: nil]; + NSURLSessionConfiguration *configuration = + [NSURLSessionConfiguration defaultSessionConfiguration]; + session = [[NSURLSession alloc] initWithConfiguration:configuration + delegate:nil + delegateQueue:nil]; + [session _setSharedSession:YES]; }); return session; } -+ (NSURLSession*) sessionWithConfiguration: (NSURLSessionConfiguration*)configuration ++ (NSURLSession *)sessionWithConfiguration: + (NSURLSessionConfiguration *)configuration { NSURLSession *session; - session = [[NSURLSession alloc] initWithConfiguration: configuration - delegate: nil - delegateQueue: nil]; + session = [[NSURLSession alloc] initWithConfiguration:configuration + delegate:nil + delegateQueue:nil]; return AUTORELEASE(session); } -+ (NSURLSession*) sessionWithConfiguration: (NSURLSessionConfiguration*)configuration - delegate: (id )delegate - delegateQueue: (NSOperationQueue*)queue ++ (NSURLSession *)sessionWithConfiguration: + (NSURLSessionConfiguration *)configuration + delegate:(id)delegate + delegateQueue:(NSOperationQueue *)queue { NSURLSession *session; - session = [[NSURLSession alloc] initWithConfiguration: configuration - delegate: delegate - delegateQueue: queue]; + session = [[NSURLSession alloc] initWithConfiguration:configuration + delegate:delegate + delegateQueue:queue]; return AUTORELEASE(session); } -- (instancetype) initWithConfiguration: (NSURLSessionConfiguration*)configuration - delegate: (id )delegate - delegateQueue: (NSOperationQueue*)queue +- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration + delegate:(id)delegate + delegateQueue:(NSOperationQueue *)queue { - if (nil != (self = [super init])) + self = [super init]; + + if (self) { - char label[30]; - dispatch_queue_t targetQueue; + NSString *queueLabel; + NSString *caPath; + NSUInteger sessionIdentifier; - _taskRegistry = [[GSTaskRegistry alloc] init]; -#if defined(CURLSSLBACKEND_GNUTLS) - curl_global_sslset(CURLSSLBACKEND_GNUTLS, NULL, NULL)l -#endif - curl_global_init(CURL_GLOBAL_SSL); - sprintf(label, "NSURLSession %u", nextSessionIdentifier()); - targetQueue - = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); -#if HAVE_DISPATCH_QUEUE_CREATE_WITH_TARGET - _workQueue = dispatch_queue_create_with_target(label, - DISPATCH_QUEUE_SERIAL, targetQueue); -#else - _workQueue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL); - dispatch_set_target_queue(_workQueue, targetQueue); -#endif - if (nil != queue) + /* To avoid a retain cycle in blocks referencing this object */ + __block typeof(self) this = self; + + sessionIdentifier = nextSessionIdentifier(); + queueLabel = [[NSString alloc] + initWithFormat:@"org.gnustep.NSURLSession.WorkQueue%ld", + sessionIdentifier]; + ASSIGN(_delegate, delegate); + ASSIGNCOPY(_configuration, configuration); + + _tasks = [[NSMutableArray alloc] init]; + GS_MUTEX_INIT(_taskLock); + + /* label is strdup'ed by libdispatch */ + _workQueue + = dispatch_queue_create([queueLabel UTF8String], DISPATCH_QUEUE_SERIAL); + [queueLabel release]; + if (!_workQueue) + return nil; + + _isTimerSuspended = YES; + _timer + = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _workQueue); + if (!_timer) { - ASSIGN(_delegateQueue, queue); + return nil; + } + + dispatch_source_set_cancel_handler(_timer, ^{ + dispatch_release(this->_timer); + }); + + // Called after timeout set by libcurl is reached + dispatch_source_set_event_handler(_timer, ^{ + // TODO: Check for return values + curl_multi_socket_action(this->_multiHandle, CURL_SOCKET_TIMEOUT, 0, + &this->_stillRunning); + [this _checkForCompletion]; + }); + + /* Use the provided delegateQueue if available */ + if (queue) + { + _delegateQueue = queue; } else { + /* This (serial) NSOperationQueue is only used for dispatching + * delegate callbacks and is orthogonal to the workQueue. + */ _delegateQueue = [[NSOperationQueue alloc] init]; - [_delegateQueue setMaxConcurrentOperationCount: 1]; + [_delegateQueue setMaxConcurrentOperationCount:1]; + } + + /* libcurl Configuration */ + curl_global_init(CURL_GLOBAL_SSL); + + _multiHandle = curl_multi_init(); + + // Set up CURL multi callbacks + curl_multi_setopt(_multiHandle, CURLMOPT_SOCKETFUNCTION, socket_callback); + curl_multi_setopt(_multiHandle, CURLMOPT_SOCKETDATA, self); + curl_multi_setopt(_multiHandle, CURLMOPT_TIMERFUNCTION, timer_callback); + curl_multi_setopt(_multiHandle, CURLMOPT_TIMERDATA, self); + + // Configure Multi Handle + curl_multi_setopt(_multiHandle, CURLMOPT_MAX_HOST_CONNECTIONS, + [_configuration HTTPMaximumConnectionsPerHost]); + + /* Check if GSCACertificateFilePath is set */ + + caPath = [[NSUserDefaults standardUserDefaults] + objectForKey:GSCACertificateFilePath]; + if (caPath) + { + NSDebugMLLog( + GS_NSURLSESSION_DEBUG_KEY, + @"Found a GSCACertificateFilePath entry in UserDefaults"); + + _certificateBlob = [[NSData alloc] initWithContentsOfFile:caPath]; + if (!_certificateBlob) + { + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Could not open file at GSCACertificateFilePath=%@", + caPath); + } + else + { + ASSIGN(_certificatePath, caPath); + } } - _delegate = delegate; - ASSIGN(_configuration, configuration); - _nextTaskIdentifier = 1; - _invalidated = NO; - _multiHandle = [[GSMultiHandle alloc] initWithConfiguration: configuration - workQueue: _workQueue]; - [NSURLProtocol registerClass: [GSHTTPURLProtocol class]]; } return self; } -- (void) dealloc +#pragma mark - Private Methods + +- (NSData *)_certificateBlob { - DESTROY(_taskRegistry); - DESTROY(_configuration); - DESTROY(_delegateQueue); - DESTROY(_multiHandle); - [super dealloc]; + return _certificateBlob; } -- (dispatch_queue_t) workQueue +- (NSString *)_certificatePath +{ + return _certificatePath; +} + +- (void)_setSharedSession:(BOOL)flag +{ + _isSharedSession = flag; +} + +- (NSInteger)_nextTaskIdentifier +{ + NSInteger identifier; + + GS_MUTEX_LOCK(_taskLock); + identifier = _taskIdentifier; + _taskIdentifier += 1; + GS_MUTEX_UNLOCK(_taskLock); + + return identifier; +} + +- (void)_resumeTask:(NSURLSessionTask *)task +{ + dispatch_async(_workQueue, ^{ + CURLMcode code; + CURLM *multiHandle = _multiHandle; + + code = curl_multi_add_handle(multiHandle, [task _easyHandle]); + + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Added task=%@ easy=%p to multi=%p with return value %d", + task, [task _easyHandle], multiHandle, code); + }); +} + +- (void)_addHandle:(CURL *)easy +{ + curl_multi_add_handle(_multiHandle, easy); +} +- (void)_removeHandle:(CURL *)easy +{ + curl_multi_remove_handle(_multiHandle, easy); +} + +- (void)_setTimer:(NSInteger)timeoutMs +{ + dispatch_source_set_timer(_timer, + dispatch_time(DISPATCH_TIME_NOW, + timeoutMs * NSEC_PER_MSEC), + DISPATCH_TIME_FOREVER, // don't repeat + timeoutMs * 0.05); // 5% leeway + + if (_isTimerSuspended) + { + _isTimerSuspended = NO; + dispatch_resume(_timer); + } +} + +- (void)_suspendTimer +{ + if (!_isTimerSuspended) + { + _isTimerSuspended = YES; + dispatch_suspend(_timer); + } +} + +- (dispatch_queue_t)_workQueue { return _workQueue; } -- (NSOperationQueue*) delegateQueue +/* This method is called when receiving CURL_POLL_REMOVE in socket_callback. + * We cancel all active dispatch sources and release the SourceInfo structure + * previously allocated in _addSocket:easyHandle:what: + */ +- (void)_removeSocket:(struct SourceInfo *)sources { - return _delegateQueue; + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, @"Remove socket with SourceInfo: %p", + sources); + + if (sources->readSocket) + { + dispatch_source_cancel(sources->readSocket); + dispatch_release(sources->readSocket); + } + if (sources->writeSocket) + { + dispatch_source_cancel(sources->writeSocket); + dispatch_release(sources->writeSocket); + } + + free(sources); } -- (id ) delegate +/* A socket needs to be configured and the private socket pointer + * (socketp) in socket_callback is NULL, meaning we first need to + * allocate our SourceInfo structure. + */ +- (int)_addSocket:(curl_socket_t)socket easyHandle:(CURL *)easy what:(int)what { - return _delegate; + struct SourceInfo *info; + + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, @"Add Socket: %d easy: %p", socket, + easy); + + /* Allocate a new SourceInfo structure on the heap */ + if (!(info = calloc(1, sizeof(struct SourceInfo)))) + { + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Failed to allocate SourceInfo structure!"); + return -1; + } + + /* We can now configure the dispatch sources */ + if (-1 == [self _setSocket:socket sources:info what:what]) + { + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, @"Failed to setup sockets!"); + return -1; + } + /* Assign the SourceInfo for access in subsequent socket_callback calls */ + curl_multi_assign(_multiHandle, socket, info); + return 0; } -- (NSURLSessionConfiguration*) configuration +- (int)_setSocket:(curl_socket_t)socket + sources:(struct SourceInfo *)sources + what:(int)what { - return _configuration; -} - -- (NSString*) sessionDescription -{ - return _sessionDescription; -} - -- (void) setSessionDescription: (NSString*)sessionDescription -{ - ASSIGN(_sessionDescription, sessionDescription); -} - -- (void) finishTasksAndInvalidate -{ - dispatch_async(_workQueue, - ^{ - _invalidated = YES; - - void (^invalidateSessionCallback)(void) = - ^{ - if (nil == _delegate) return; - - [[self delegateQueue] addOperationWithBlock: - ^{ - if ([_delegate respondsToSelector: @selector(URLSession:didBecomeInvalidWithError:)]) - { - [_delegate URLSession: self didBecomeInvalidWithError: nil]; - } - _delegate = nil; - }]; - }; - - if (![_taskRegistry isEmpty]) + /* Create a Reading Dispatch Source that listens on socket */ + if (CURL_POLL_IN == what || CURL_POLL_INOUT == what) + { + /* Reset Dispatch Source if previously initialised */ + if (sources->readSocket) { - [_taskRegistry notifyOnTasksCompletion: invalidateSessionCallback]; - } - else - { - invalidateSessionCallback(); - } - }); -} - -- (void) invalidateAndCancel -{ - NSEnumerator *e; - NSURLSessionTask *task; - - dispatch_sync(_workQueue, - ^{ - _invalidated = YES; - }); - - e = [[_taskRegistry allTasks] objectEnumerator]; - while (nil != (task = [e nextObject])) - { - [task cancel]; - } - - dispatch_async(_workQueue, - ^{ - if (nil == _delegate) - { - return; - } - - [[self delegateQueue] addOperationWithBlock: - ^{ - if ([_delegate respondsToSelector: @selector(URLSession:didBecomeInvalidWithError:)]) - { - [_delegate URLSession: self didBecomeInvalidWithError: nil]; - } - _delegate = nil; - }]; - }); -} - -- (NSURLSessionDataTask*) dataTaskWithRequest: (NSURLRequest*)request -{ - NSURLSessionDataTask *task; - - if (_invalidated) - { - return nil; - } - - task = [[NSURLSessionDataTask alloc] initWithSession: self - request: request - taskIdentifier: _nextTaskIdentifier++]; - - [self addTask: task]; - - return AUTORELEASE(task); -} - -- (NSURLSessionDataTask*) dataTaskWithURL: (NSURL*)url -{ - NSMutableURLRequest *request; - - request = [NSMutableURLRequest requestWithURL: url]; - [request setHTTPMethod: @"POST"]; - - return [self dataTaskWithRequest: request]; -} - -- (NSURLSessionUploadTask*) uploadTaskWithRequest: (NSURLRequest*)request - fromFile: (NSURL*)fileURL -{ - return [self notImplemented: _cmd]; -} - -- (NSURLSessionUploadTask*) uploadTaskWithRequest: (NSURLRequest*)request - fromData: (NSData*)bodyData -{ - return [self notImplemented: _cmd]; -} - -- (NSURLSessionUploadTask*) uploadTaskWithStreamedRequest: (NSURLRequest*)request -{ - return [self notImplemented: _cmd]; -} - -- (NSURLSessionDownloadTask*) downloadTaskWithRequest: (NSURLRequest*)request -{ - NSURLSessionDownloadTask *task; - - if (_invalidated) - { - return nil; - } - - task = [[NSURLSessionDownloadTask alloc] initWithSession: self - request: request - taskIdentifier: _nextTaskIdentifier++]; - - [self addTask: task]; - - return AUTORELEASE(task); -} - -- (NSURLSessionDownloadTask*) downloadTaskWithURL: (NSURL*)url -{ - NSMutableURLRequest *request; - - request = [NSMutableURLRequest requestWithURL: url]; - [request setHTTPMethod: @"GET"]; - - return [self downloadTaskWithRequest: request]; -} - -- (NSURLSessionDownloadTask*) downloadTaskWithResumeData: (NSData*)resumeData -{ - return [self notImplemented: _cmd]; -} - -- (void) getTasksWithCompletionHandler: (void (^)(GS_GENERIC_CLASS(NSArray, NSURLSessionDataTask*) *dataTasks, GS_GENERIC_CLASS(NSArray, NSURLSessionUploadTask*) *uploadTasks, GS_GENERIC_CLASS(NSArray, NSURLSessionDownloadTask*) *downloadTasks))completionHandler -{ - NSArray *allTasks, *dataTasks, *uploadTasks, *downloadTasks; - - allTasks = [_taskRegistry allTasks]; - dataTasks = [allTasks filteredArrayUsingPredicate: - [NSPredicate predicateWithBlock:^BOOL(id task, NSDictionary* bindings) { - return [task isKindOfClass:[NSURLSessionDataTask class]]; - }]]; - uploadTasks = [allTasks filteredArrayUsingPredicate: - [NSPredicate predicateWithBlock:^BOOL(id task, NSDictionary* bindings) { - return [task isKindOfClass:[NSURLSessionUploadTask class]]; - }]]; - downloadTasks = [allTasks filteredArrayUsingPredicate: - [NSPredicate predicateWithBlock:^BOOL(id task, NSDictionary* bindings) { - return [task isKindOfClass:[NSURLSessionDownloadTask class]]; - }]]; - - [[self delegateQueue] addOperationWithBlock: - ^{ - completionHandler(dataTasks, uploadTasks, downloadTasks); - }]; -} - -- (void) getAllTasksWithCompletionHandler: (void (^)(GS_GENERIC_CLASS(NSArray, __kindof NSURLSessionTask*) *tasks))completionHandler -{ - NSArray *allTasks = [_taskRegistry allTasks]; - - [[self delegateQueue] addOperationWithBlock: - ^{ - completionHandler(allTasks); - }]; -} - -- (void) addTask: (NSURLSessionTask*)task -{ - [_taskRegistry addTask: task]; -} - -- (void) removeTask: (NSURLSessionTask*)task -{ - [_taskRegistry removeTask: task]; -} - -@end - -@implementation NSURLSession (NSURLSessionAsynchronousConvenience) - -- (NSURLSessionDataTask*) dataTaskWithRequest: (NSURLRequest*)request - completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler -{ - NSURLSessionDataTask *task; - - if (_invalidated) - { - return nil; - } - - task = [[NSURLSessionDataTask alloc] initWithSession: self - request: request - taskIdentifier: _nextTaskIdentifier++]; - [task setDataCompletionHandler: completionHandler]; - - [self addTask: task]; - - return AUTORELEASE(task); -} - -- (NSURLSessionDataTask*) dataTaskWithURL: (NSURL*)url - completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler -{ - NSMutableURLRequest *request; - - request = [NSMutableURLRequest requestWithURL: url]; - [request setHTTPMethod: @"POST"]; - - return [self dataTaskWithRequest: request - completionHandler: completionHandler]; -} - -- (NSURLSessionUploadTask*) uploadTaskWithRequest: (NSURLRequest*)request - fromFile: (NSURL*)fileURL - completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler -{ - return [self notImplemented: _cmd]; -} - -- (NSURLSessionUploadTask*) uploadTaskWithRequest: (NSURLRequest*)request - fromData: (NSData*)bodyData - completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler -{ - return [self notImplemented: _cmd]; -} - -- (NSURLSessionDownloadTask*) downloadTaskWithRequest: (NSURLRequest*)request - completionHandler: (void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler -{ - NSURLSessionDataTask *task; - - if (_invalidated) - { - return nil; - } - - task = [[NSURLSessionDataTask alloc] initWithSession: self - request: request - taskIdentifier: _nextTaskIdentifier++]; - [task setDownloadCompletionHandler: completionHandler]; - - [self addTask: task]; - - return AUTORELEASE(task); -} - -- (NSURLSessionDownloadTask*) downloadTaskWithURL: (NSURL*)url - completionHandler: (void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler -{ - NSMutableURLRequest *request; - - request = [NSMutableURLRequest requestWithURL: url]; - [request setHTTPMethod: @"GET"]; - - return [self downloadTaskWithRequest: request - completionHandler: completionHandler]; -} - -- (NSURLSessionDownloadTask*) downloadTaskWithResumeData: (NSData*)resumeData - completionHandler: (void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler -{ - return [self notImplemented: _cmd]; -} - -@end - - -@implementation _NSURLProtocolClient - -- (instancetype) init -{ - if (nil != (self = [super init])) - { - _cachePolicy = NSURLCacheStorageNotAllowed; - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_cacheableData); - DESTROY(_cacheableResponse); - [super dealloc]; -} - -- (void) URLProtocol: (NSURLProtocol *)protocol - cachedResponseIsValid: (NSCachedURLResponse *)cachedResponse -{ - -} - -- (void) URLProtocol: (NSURLProtocol *)protocol - didFailWithError: (NSError *)error -{ - NSURLSessionTask *task = [protocol task]; - - NSAssert(nil != task, @"Missing task"); - - [self task: task didFailWithError: error]; -} - -- (void) task: (NSURLSessionTask *)task - didFailWithError: (NSError *)error -{ - NSURLSession *session; - NSOperationQueue *delegateQueue; - id delegate; - void (^dataCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); - void (^downloadCompletionHandler)(NSURL *location, NSURLResponse *response, NSError *error); - - session = [task session]; - NSAssert(nil != session, @"Missing session"); - - delegateQueue = [session delegateQueue]; - delegate = [session delegate]; - dataCompletionHandler = [task dataCompletionHandler]; - downloadCompletionHandler = [task downloadCompletionHandler]; - - if (nil != delegate) - { - [delegateQueue addOperationWithBlock: - ^{ - if (NSURLSessionTaskStateCompleted == [task state]) - { - return; - } - - if ([delegate respondsToSelector: - @selector(URLSession:task:didCompleteWithError:)]) - { - [(id)delegate URLSession: session - task: task - didCompleteWithError: error]; - } - - [task setState: NSURLSessionTaskStateCompleted]; - dispatch_async([session workQueue], - ^{ - [session removeTask: task]; - }); - }]; - } - else if (nil != dataCompletionHandler) - { - [delegateQueue addOperationWithBlock: - ^{ - if (NSURLSessionTaskStateCompleted == [task state]) - { - return; - } - - dataCompletionHandler(nil, nil, error); - - [task setState: NSURLSessionTaskStateCompleted]; - - dispatch_async([session workQueue], - ^{ - [session removeTask: task]; - }); - }]; - } - else if (nil != downloadCompletionHandler) - { - [delegateQueue addOperationWithBlock: - ^{ - if (NSURLSessionTaskStateCompleted == [task state]) - { - return; - } - - downloadCompletionHandler(nil, nil, error); - - [task setState: NSURLSessionTaskStateCompleted]; - - dispatch_async([session workQueue], - ^{ - [session removeTask: task]; - }); - }]; - } - else - { - if (NSURLSessionTaskStateCompleted == [task state]) - { - return; + dispatch_source_cancel(sources->readSocket); + dispatch_release(sources->readSocket); + sources->readSocket = NULL; } - [task setState: NSURLSessionTaskStateCompleted]; - dispatch_async([session workQueue], - ^{ - [session removeTask: task]; - }); - } - - [task invalidateProtocol]; -} + NSDebugMLLog( + GS_NSURLSESSION_DEBUG_KEY, + @"Creating a reading dispatch source: socket=%d sources=%p what=%d", + socket, sources, what); -- (void) URLProtocol: (NSURLProtocol *)protocol - didLoadData: (NSData *)data -{ - NSURLSessionTask *task = [protocol task]; - NSURLSession *session; - id delegate; - - NSAssert(nil != task, @"Missing task"); - - session = [task session]; - delegate = [session delegate]; - - switch (_cachePolicy) - { - case NSURLCacheStorageAllowed: - case NSURLCacheStorageAllowedInMemoryOnly: + sources->readSocket = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + socket, 0, _workQueue); + if (!sources->readSocket) { - if (nil != _cacheableData) - { - [_cacheableData addObject: data]; - } - break; + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Unable to create dispatch source for read socket!"); + return -1; } - case NSURLCacheStorageNotAllowed: - break; + dispatch_source_set_event_handler(sources->readSocket, ^{ + int action; + + action = CURL_CSELECT_IN; + curl_multi_socket_action(_multiHandle, socket, action, &_stillRunning); + + /* Check if the transfer is complete */ + [self _checkForCompletion]; + /* When _stillRunning reaches zero, all transfers are complete/done */ + if (_stillRunning <= 0) + { + [self _suspendTimer]; + } + }); + + dispatch_resume(sources->readSocket); } - if (nil != delegate - && [task isKindOfClass: [NSURLSessionDataTask class]] - && [delegate respondsToSelector: - @selector(URLSession:dataTask:didReceiveData:)]) + /* Create a Writing Dispatch Source that listens on socket */ + if (CURL_POLL_OUT == what || CURL_POLL_INOUT == what) { - [[session delegateQueue] addOperationWithBlock: - ^{ - [(id)delegate URLSession: session - dataTask: (NSURLSessionDataTask*)task - didReceiveData: data]; - }]; + /* Reset Dispatch Source if previously initialised */ + if (sources->writeSocket) + { + dispatch_source_cancel(sources->writeSocket); + dispatch_release(sources->writeSocket); + sources->writeSocket = NULL; + } + + NSDebugMLLog( + GS_NSURLSESSION_DEBUG_KEY, + @"Creating a writing dispatch source: socket=%d sources=%p what=%d", + socket, sources, what); + + sources->writeSocket = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, + socket, 0, _workQueue); + if (!sources->writeSocket) + { + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Unable to create dispatch source for write socket!"); + return -1; + } + + dispatch_source_set_event_handler(sources->writeSocket, ^{ + int action; + + action = CURL_CSELECT_OUT; + curl_multi_socket_action(_multiHandle, socket, action, &_stillRunning); + + /* Check if the tranfer is complete */ + [self _checkForCompletion]; + + /* When _stillRunning reaches zero, all transfers are complete/done */ + if (_stillRunning <= 0) + { + [self _suspendTimer]; + } + }); + + dispatch_resume(sources->writeSocket); + } + + return 0; +} + +/* Called by a socket event handler or by a firing timer set by timer_callback. + * + * The socket event handler is executed on the _workQueue. + */ +- (void)_checkForCompletion +{ + CURLMsg *msg; + int msgs_left; + CURL *easyHandle; + CURLcode res; + char *eff_url = NULL; + NSURLSessionTask *task = nil; + + /* Ask the multi handle if there are any messages from the individual + * transfers. + * + * Remove the associated easy handle and release the task if the transfer is + * done. This completes the life-cycle of a task added to NSURLSession. + */ + while ((msg = curl_multi_info_read(_multiHandle, &msgs_left))) + { + if (msg->msg == CURLMSG_DONE) + { + CURLcode rc; + easyHandle = msg->easy_handle; + res = msg->data.result; + + /* Get the NSURLSessionTask instance */ + rc = curl_easy_getinfo(easyHandle, CURLINFO_PRIVATE, &task); + if (CURLE_OK != rc) + { + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Failed to retrieve task from easy handle %p using " + @"CURLINFO_PRIVATE", + easyHandle); + } + rc = curl_easy_getinfo(easyHandle, CURLINFO_EFFECTIVE_URL, &eff_url); + if (CURLE_OK != rc) + { + NSDebugMLLog( + GS_NSURLSESSION_DEBUG_KEY, + @"Failed to retrieve effective URL from easy handle %p using " + @"CURLINFO_PRIVATE", + easyHandle); + } + + NSDebugMLLog(GS_NSURLSESSION_DEBUG_KEY, + @"Transfer finished for Task %@ with effective url %s " + @"and CURLcode: %s", + task, eff_url, curl_easy_strerror(res)); + + curl_multi_remove_handle(_multiHandle, easyHandle); + + /* This session might be released in _transferFinishedWithCode. Better + * retain it first. */ + RETAIN(self); + + RETAIN(task); + [_tasks removeObject:task]; + [task _transferFinishedWithCode:res]; + RELEASE(task); + + /* Send URLSession:didBecomeInvalidWithError: to delegate if this + * session was invalidated */ + if (_invalidated && [_tasks count] == 0 && + [_delegate respondsToSelector:@selector(URLSession: + didBecomeInvalidWithError:)]) + { + [_delegateQueue addOperationWithBlock:^{ + /* We only support explicit Invalidation for now. Error is set + * to nil in this case. */ + [_delegate URLSession:self didBecomeInvalidWithError:nil]; + }]; + } + + RELEASE(self); + } } } -- (void) URLProtocol: (NSURLProtocol *)protocol - didReceiveAuthenticationChallenge: (NSURLAuthenticationChallenge *)challenge +/* Adds task to _tasks and updates the delegate */ +- (void)_didCreateTask:(NSURLSessionTask *)task { - //FIXME + dispatch_async(_workQueue, ^{ + [_tasks addObject:task]; + }); + + if ([_delegate respondsToSelector:@selector(URLSession:didCreateTask:)]) + { + [_delegateQueue addOperationWithBlock:^{ + [(id) _delegate URLSession:self + didCreateTask:task]; + }]; + } } -- (void) URLProtocol: (NSURLProtocol *)protocol - didReceiveResponse: (NSURLResponse *)response - cacheStoragePolicy: (NSURLCacheStoragePolicy)policy +#pragma mark - Public API + +- (void)finishTasksAndInvalidate { - NSURLSessionTask *task = [protocol task]; - NSURLSession *session; - id delegate; - - NSAssert(nil != task, @"Missing task"); - - [task setResponse: response]; - - session = [task session]; - - if (![task isKindOfClass: [NSURLSessionDataTask class]]) + if (_isSharedSession) { return; } - _cachePolicy = policy; - - if (nil != [[session configuration] URLCache]) - { - switch (policy) - { - case NSURLCacheStorageAllowed: - case NSURLCacheStorageAllowedInMemoryOnly: - ASSIGN(_cacheableData, [NSMutableArray array]); - ASSIGN(_cacheableResponse, response); - break; - case NSURLCacheStorageNotAllowed: - break; - } - } - - delegate = [session delegate]; - if (nil != delegate) - { - [[session delegateQueue] addOperationWithBlock: - ^{ - if ([delegate respondsToSelector: @selector - (URLSession:dataTask:didReceiveResponse:completionHandler:)]) - { - NSURLSessionDataTask *dataTask = (NSURLSessionDataTask*)task; - - [(id)delegate URLSession: session - dataTask: dataTask - didReceiveResponse: response - completionHandler: - ^(NSURLSessionResponseDisposition disposition) { - if (disposition != NSURLSessionResponseAllow) - { - NSLog(@"Warning: ignoring disposition %d from completion handler", - (int)disposition); - } - }]; - } - }]; - } + dispatch_async(_workQueue, ^{ + _invalidated = YES; + }); } -- (void) URLProtocol: (NSURLProtocol *)protocol - wasRedirectedToRequest: (NSURLRequest *)request - redirectResponse: (NSURLResponse *)redirectResponse +- (void)invalidateAndCancel { - NSAssert(NO, @"The NSURLSession implementation doesn't currently handle redirects directly."); + if (_isSharedSession) + { + return; + } + + dispatch_async(_workQueue, ^{ + _invalidated = YES; + + /* Cancel all tasks */ + for (NSURLSessionTask *task in _tasks) + { + [task cancel]; + } + }); } -- (NSURLProtectionSpace*) _protectionSpaceFrom: (NSHTTPURLResponse*)response +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request { - NSURLProtectionSpace *space = nil; - NSString *auth; + NSURLSessionDataTask *task; + NSInteger identifier; - auth = [[response allHeaderFields] objectForKey: @"WWW-Authenticate"]; - if (nil != auth) - { - NSURL *url = [response URL]; - NSString *host = [url host]; - NSNumber *port = [url port]; - NSString *scheme = [url scheme]; - NSRange range; - NSString *realm; - NSString *method; + identifier = [self _nextTaskIdentifier]; + task = [[NSURLSessionDataTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; - if (nil == host) host = @""; - if (nil == port) port = [NSNumber numberWithInt: 80]; - method = [[auth componentsSeparatedByString: @" "] firstObject]; - range = [auth rangeOfString: @"realm="]; - realm = range.length > 0 - ? [auth substringFromIndex: NSMaxRange(range)] : @""; - space = AUTORELEASE([[NSURLProtectionSpace alloc] - initWithHost: host - port: [port integerValue] - protocol: scheme - realm: realm - authenticationMethod: method]); - } - return space; + /* We use the session delegate by default. NSURLSessionTaskDelegate + * is a purely optional protocol. + */ + [task setDelegate:(id) _delegate]; + + [task _setProperties:GSURLSessionUpdatesDelegate]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); } -- (void) URLProtocolDidFinishLoading: (NSURLProtocol *)protocol +- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url { - NSURLSessionTask *task = [protocol task]; - NSURLSession *session; - NSHTTPURLResponse *urlResponse; - NSURLCache *cache; - NSOperationQueue *delegateQueue; - id delegate; - void (^dataCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); - void (^downloadCompletionHandler)(NSURL *location, NSURLResponse *response, NSError *error); + NSURLRequest *request; - NSAssert(nil != task, @"Missing task"); - - session = [task session]; - urlResponse = (NSHTTPURLResponse*)[task response]; - - if ([urlResponse statusCode] == 401) - { - [self _protectionSpaceFrom: urlResponse]; - } - - delegate = [session delegate]; - delegateQueue = [session delegateQueue]; - dataCompletionHandler = [task dataCompletionHandler]; - downloadCompletionHandler = [task downloadCompletionHandler]; - - if (nil != (cache = [[session configuration] URLCache]) - && [task isKindOfClass: [NSURLSessionDataTask class]] - && nil != _cacheableData - && nil != _cacheableResponse) - { - NSCachedURLResponse *cacheable; - NSMutableData *data; - NSEnumerator *e; - NSData *d; - - data = [NSMutableData data]; - e = [_cacheableData objectEnumerator]; - while (nil != (d = [e nextObject])) - { - [data appendData: d]; - } - - cacheable = [[NSCachedURLResponse alloc] initWithResponse: urlResponse - data: data - userInfo: nil - storagePolicy: _cachePolicy]; - [cache storeCachedResponse: cacheable - forDataTask: (NSURLSessionDataTask*)task]; - RELEASE(cacheable); - } - - if (nil != dataCompletionHandler) - { - NSData *data = [NSURLProtocol propertyForKey: @"tempData" - inRequest: [protocol request]]; - - [delegateQueue addOperationWithBlock: - ^{ - if (NSURLSessionTaskStateCompleted == [task state]) - { - return; - } - - dataCompletionHandler(data, urlResponse, nil); - - [task setState: NSURLSessionTaskStateCompleted]; - - dispatch_async([session workQueue], - ^{ - [session removeTask: task]; - }); - }]; - } - else if (nil != downloadCompletionHandler) - { - NSURL *fileURL = [NSURLProtocol propertyForKey: @"tempFileURL" - inRequest: [protocol request]]; - - [delegateQueue addOperationWithBlock: - ^{ - if (NSURLSessionTaskStateCompleted == [task state]) - { - return; - } - - downloadCompletionHandler(fileURL, urlResponse, nil); - - [task setState: NSURLSessionTaskStateCompleted]; - - dispatch_async([session workQueue], - ^{ - [session removeTask: task]; - }); - }]; - } - else if (nil != delegate) - { - // Send delegate with temporary fileURL - if ([task isKindOfClass: [NSURLSessionDownloadTask class]] - && [delegate respondsToSelector: @selector(URLSession:downloadTask:didFinishDownloadingToURL:)]) - { - id downloadDelegate; - NSURLSessionDownloadTask *downloadTask; - NSURL *fileURL; - - downloadDelegate = (id)delegate; - downloadTask = (NSURLSessionDownloadTask *)task; - fileURL = [NSURLProtocol propertyForKey: @"tempFileURL" - inRequest: [protocol request]]; - - [delegateQueue addOperationWithBlock: - ^{ - [downloadDelegate URLSession: session - downloadTask: downloadTask - didFinishDownloadingToURL: fileURL]; - }]; - } - - [delegateQueue addOperationWithBlock: - ^{ - if (NSURLSessionTaskStateCompleted == [task state]) - { - return; - } - - if ([delegate respondsToSelector: - @selector(URLSession:task:didCompleteWithError:)]) - { - [(id)delegate URLSession: session - task: task - didCompleteWithError: nil]; - } - - [task setState: NSURLSessionTaskStateCompleted]; - - dispatch_async([session workQueue], - ^{ - [session removeTask: task]; - }); - }]; - } - else - { - if (NSURLSessionTaskStateCompleted != [task state]) - { - [task setState: NSURLSessionTaskStateCompleted]; - dispatch_async([session workQueue], - ^{ - [session removeTask: task]; - }); - } - } - - [task invalidateProtocol]; + request = [NSURLRequest requestWithURL:url]; + return [self dataTaskWithRequest:request]; } -- (void) URLProtocol: (NSURLProtocol *)protocol - didCancelAuthenticationChallenge: (NSURLAuthenticationChallenge *)challenge +- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request + fromFile:(NSURL *)fileURL { - //FIXME + NSURLSessionUploadTask *task; + NSInputStream *stream; + NSInteger identifier; + + identifier = [self _nextTaskIdentifier]; + stream = [NSInputStream inputStreamWithURL:fileURL]; + task = [[NSURLSessionUploadTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; + + /* We use the session delegate by default. NSURLSessionTaskDelegate + * is a purely optional protocol. + */ + [task setDelegate:(id) _delegate]; + [task + _setProperties:GSURLSessionUpdatesDelegate | GSURLSessionHasInputStream]; + [task _setBodyStream:stream]; + [task _enableUploadWithSize:0]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); } -@end - -@implementation NSURLSessionTask +- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request + fromData:(NSData *)bodyData { - NSURLSession *_session; - NSLock *_protocolLock; - NSURLSessionTaskProtocolState _protocolState; - NSURLProtocol *_protocol; - NSMutableArray *_protocolBag; - Class _protocolClass; - BOOL _hasTriggeredResume; - float _priority; + NSURLSessionUploadTask *task; + NSInteger identifier; + + identifier = [self _nextTaskIdentifier]; + task = [[NSURLSessionUploadTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; + + /* We use the session delegate by default. NSURLSessionTaskDelegate + * is a purely optional protocol. + */ + [task setDelegate:(id) _delegate]; + [task _setProperties:GSURLSessionUpdatesDelegate]; + [task _enableUploadWithData:bodyData]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); } -- (instancetype) initWithSession: (NSURLSession*)session - request: (NSURLRequest*)request - taskIdentifier: (NSUInteger)identifier +- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest: + (NSURLRequest *)request { - NSEnumerator *e; - Class protocolClass; + NSURLSessionUploadTask *task; + NSInteger identifier; - if (nil != (self = [super init])) - { - NSData *data; - NSInputStream *stream; + identifier = [self _nextTaskIdentifier]; + task = [[NSURLSessionUploadTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; - /* - * 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; - ASSIGN(_originalRequest, request); - ASSIGN(_currentRequest, request); - if ([(data = [request HTTPBody]) length] > 0) - { - _knownBody = [[GSURLSessionTaskBody alloc] initWithData: data]; - } - else if (nil != (stream = [request HTTPBodyStream])) - { - _knownBody = [[GSURLSessionTaskBody alloc] - initWithInputStream: stream]; - } - _taskIdentifier = identifier; -#if HAVE_DISPATCH_QUEUE_CREATE_WITH_TARGET - _workQueue = dispatch_queue_create_with_target("org.gnustep.NSURLSessionTask.WorkQueue", DISPATCH_QUEUE_SERIAL, [session workQueue]); + /* We use the session delegate by default. NSURLSessionTaskDelegate + * is a purely optional protocol. + */ + [task setDelegate:(id) _delegate]; + [task + _setProperties:GSURLSessionUpdatesDelegate | GSURLSessionHasInputStream]; + [task _enableUploadWithSize:0]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); +} + +- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request +{ + NSURLSessionDownloadTask *task; + NSInteger identifier; + + identifier = [self _nextTaskIdentifier]; + task = [[NSURLSessionDownloadTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; + + /* We use the session delegate by default. NSURLSessionTaskDelegate + * is a purely optional protocol. + */ + [task setDelegate:(id) _delegate]; + [task + _setProperties:GSURLSessionWritesDataToFile | GSURLSessionUpdatesDelegate]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); +} + +- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url +{ + NSURLRequest *request; + + request = [NSURLRequest requestWithURL:url]; + return [self downloadTaskWithRequest:request]; +} + +- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData +{ + return [self notImplemented:_cmd]; +} + +- (void)getTasksWithCompletionHandler: + (void (^)(NSArray *dataTasks, + NSArray *uploadTasks, + NSArray *downloadTasks)) + completionHandler +{ + dispatch_async(_workQueue, ^{ + NSMutableArray *dataTasks; + NSMutableArray *uploadTasks; + NSMutableArray *downloadTasks; + NSInteger numberOfTasks; + + Class dataTaskClass; + Class uploadTaskClass; + Class downloadTaskClass; + + numberOfTasks = [_tasks count]; + dataTasks = [NSMutableArray arrayWithCapacity:numberOfTasks / 2]; + uploadTasks = [NSMutableArray arrayWithCapacity:numberOfTasks / 2]; + downloadTasks = [NSMutableArray arrayWithCapacity:numberOfTasks / 2]; + + dataTaskClass = [NSURLSessionDataTask class]; + uploadTaskClass = [NSURLSessionUploadTask class]; + downloadTaskClass = [NSURLSessionDownloadTask class]; + + for (NSURLSessionTask *task in _tasks) + { + if ([task isKindOfClass:dataTaskClass]) + { + [dataTasks addObject:(NSURLSessionDataTask *) task]; + } + else if ([task isKindOfClass:uploadTaskClass]) + { + [uploadTasks addObject:(NSURLSessionUploadTask *) task]; + } + else if ([task isKindOfClass:downloadTaskClass]) + { + [downloadTasks addObject:(NSURLSessionDownloadTask *) task]; + } + } + + completionHandler(dataTasks, uploadTasks, downloadTasks); + }); +} + +- (void)getAllTasksWithCompletionHandler: + (void (^)(NSArray<__kindof NSURLSessionTask *> *tasks))completionHandler +{ + dispatch_async(_workQueue, ^{ + completionHandler(_tasks); + }); +} + +#pragma mark - Getter and Setter + +- (NSOperationQueue *)delegateQueue +{ + return _delegateQueue; +} + +- (id)delegate +{ + return _delegate; +} + +- (NSURLSessionConfiguration *)configuration +{ + return AUTORELEASE([_configuration copy]); +} + +- (NSString *)sessionDescription +{ + return _sessionDescription; +} + +- (void)setSessionDescription:(NSString *)sessionDescription +{ + ASSIGNCOPY(_sessionDescription, sessionDescription); +} + +- (void)dealloc +{ + RELEASE(_delegateQueue); + RELEASE(_delegate); + RELEASE(_configuration); + RELEASE(_tasks); + RELEASE(_certificateBlob); + RELEASE(_certificatePath); + + curl_multi_cleanup(_multiHandle); + +#if defined(HAVE_DISPATCH_CANCEL) + dispatch_cancel(_timer); #else - _workQueue = dispatch_queue_create("org.gnustep.NSURLSessionTask.WorkQueue", DISPATCH_QUEUE_SERIAL); - dispatch_set_target_queue(_workQueue, [session workQueue]); + dispatch_source_cancel(_timer); #endif - _state = NSURLSessionTaskStateSuspended; - _suspendCount = 1; - _protocolLock = [[NSLock alloc] init]; - _protocolState = NSURLSessionTaskProtocolStateToBeCreated; - _protocol = nil; - _hasTriggeredResume = NO; - _priority = NSURLSessionTaskPriorityDefault; - e = [[[session configuration] protocolClasses] objectEnumerator]; - while (nil != (protocolClass = [e nextObject])) - { - if ([protocolClass canInitWithTask: self]) - { - _protocolClass = protocolClass; - break; - } - } - NSAssert(nil != _protocolClass, @"Unsupported protocol for request: %@", request); - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_originalRequest); - DESTROY(_currentRequest); - DESTROY(_response); - DESTROY(_taskDescription); - DESTROY(_error); - DESTROY(_protocolLock); - DESTROY(_protocol); - DESTROY(_protocolBag); - DESTROY(_dataCompletionHandler); - DESTROY(_knownBody); dispatch_release(_workQueue); + [super dealloc]; } -- (NSURLSessionTaskState) updateTaskState +@end + +@implementation +NSURLSession (NSURLSessionAsynchronousConvenience) + +- (NSURLSessionDataTask *) + dataTaskWithRequest:(NSURLRequest *)request + completionHandler:(GSNSURLSessionDataCompletionHandler)completionHandler { - if (0 == _suspendCount) - { - _state = NSURLSessionTaskStateRunning; - } - else - { - _state = NSURLSessionTaskStateSuspended; - } - - return _state; + NSURLSessionDataTask *task; + NSInteger identifier; + + identifier = [self _nextTaskIdentifier]; + task = [[NSURLSessionDataTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; + [task setDelegate:(id) _delegate]; + [task _setCompletionHandler:completionHandler]; + [task _enableAutomaticRedirects:YES]; + [task _setProperties:GSURLSessionStoresDataInMemory | + GSURLSessionHasCompletionHandler]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); } -- (NSUInteger) taskIdentifier +- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url + completionHandler: + (GSNSURLSessionDataCompletionHandler)completionHandler { - return _taskIdentifier; + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + + return [self dataTaskWithRequest:request completionHandler:completionHandler]; } -- (NSURLRequest*) originalRequest +- (NSURLSessionUploadTask *) + uploadTaskWithRequest:(NSURLRequest *)request + fromFile:(NSURL *)fileURL + completionHandler:(GSNSURLSessionDataCompletionHandler)completionHandler { - return _originalRequest; + NSURLSessionUploadTask *task; + NSInputStream *stream; + NSInteger identifier; + + identifier = [self _nextTaskIdentifier]; + stream = [NSInputStream inputStreamWithURL:fileURL]; + task = [[NSURLSessionUploadTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; + [task setDelegate:(id) _delegate]; + + [task _setProperties:GSURLSessionStoresDataInMemory + | GSURLSessionHasInputStream | + GSURLSessionHasCompletionHandler]; + [task _setCompletionHandler:completionHandler]; + [task _enableAutomaticRedirects:YES]; + [task _setBodyStream:stream]; + [task _enableUploadWithSize:0]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); } -- (NSURLRequest*) currentRequest +- (NSURLSessionUploadTask *) + uploadTaskWithRequest:(NSURLRequest *)request + fromData:(NSData *)bodyData + completionHandler:(GSNSURLSessionDataCompletionHandler)completionHandler { - return _currentRequest; + NSURLSessionUploadTask *task; + NSInteger identifier; + + identifier = [self _nextTaskIdentifier]; + task = [[NSURLSessionUploadTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; + [task setDelegate:(id) _delegate]; + + [task _setProperties:GSURLSessionStoresDataInMemory | + GSURLSessionHasCompletionHandler]; + [task _setCompletionHandler:completionHandler]; + [task _enableAutomaticRedirects:YES]; + [task _enableUploadWithData:bodyData]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); } -- (NSURLResponse*) response +- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request + completionHandler: + (GSNSURLSessionDownloadCompletionHandler) + completionHandler { - return _response; + NSURLSessionDownloadTask *task; + NSInteger identifier; + + identifier = [self _nextTaskIdentifier]; + task = [[NSURLSessionDownloadTask alloc] initWithSession:self + request:request + taskIdentifier:identifier]; + + [task setDelegate:(id) _delegate]; + + [task _setProperties:GSURLSessionWritesDataToFile | + GSURLSessionHasCompletionHandler]; + [task _enableAutomaticRedirects:YES]; + [task _setCompletionHandler:completionHandler]; + + [self _didCreateTask:task]; + + return AUTORELEASE(task); } -- (void) setResponse: (NSURLResponse*)response +- (NSURLSessionDownloadTask *) + downloadTaskWithURL:(NSURL *)url + completionHandler:(GSNSURLSessionDownloadCompletionHandler)completionHandler { - ASSIGN(_response, response); + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + + return [self downloadTaskWithRequest:request + completionHandler:completionHandler]; } -- (int64_t) countOfBytesReceived +- (NSURLSessionDownloadTask *) + downloadTaskWithResumeData:(NSData *)resumeData + completionHandler: + (GSNSURLSessionDownloadCompletionHandler)completionHandler { - return _countOfBytesReceived; -} - -- (int64_t) countOfBytesSent -{ - return _countOfBytesSent; -} - -- (int64_t) countOfBytesExpectedToSend -{ - return _countOfBytesExpectedToSend; -} - -- (int64_t) countOfBytesExpectedToReceive -{ - return _countOfBytesExpectedToReceive; -} - -- (NSString*) taskDescription -{ - return _taskDescription; -} - -- (void) setTaskDescription: (NSString*)taskDescription -{ - ASSIGN(_taskDescription, taskDescription); -} - -- (NSURLSessionTaskState) state -{ - return _state; -} - -- (NSError*) error -{ - return _error; -} - -- (BOOL) isSuspendedAfterResume -{ - return _hasTriggeredResume && (_state == NSURLSessionTaskStateSuspended); -} - -- (void) cancel -{ - dispatch_sync(_workQueue, - ^{ - if (!(NSURLSessionTaskStateRunning == _state - || NSURLSessionTaskStateSuspended == _state)) - { - return; - } - - _state = NSURLSessionTaskStateCanceling; - - [self getProtocolWithCompletion: - ^(NSURLProtocol *protocol) { - dispatch_async(_workQueue, - ^{ - _error = [[NSError alloc] initWithDomain: NSURLErrorDomain - code: NSURLErrorCancelled - userInfo: nil]; - if (nil != protocol) - { - id client; - - [protocol stopLoading]; - if (nil != (client = [protocol client])) - { - [client URLProtocol: protocol didFailWithError: _error]; - } - } - }); - }]; - - }); -} - -- (void) suspend -{ - dispatch_sync(_workQueue, - ^{ - if (NSURLSessionTaskStateCanceling == _state - || NSURLSessionTaskStateCompleted == _state) - { - return; - } - - _suspendCount++; - - [self updateTaskState]; - - if (1 == _suspendCount) - { - [self getProtocolWithCompletion: - ^(NSURLProtocol *protocol){ - dispatch_async(_workQueue, - ^{ - if (nil != protocol) - { - [protocol stopLoading]; - } - }); - }]; - } - }); -} - -- (void) resume -{ - /* - * Properly retain the session to keep a reference - * to the task. This ensures correct API behaviour. - */ - RETAIN(_session); - - dispatch_sync(_workQueue, - ^{ - if (NSURLSessionTaskStateCanceling == _state - || NSURLSessionTaskStateCompleted == _state) - { - return; - } - - if (_suspendCount > 0) - { - _suspendCount--; - } - - [self updateTaskState]; - - if (0 == _suspendCount) - { - _hasTriggeredResume = YES; - [self getProtocolWithCompletion: - ^(NSURLProtocol* protocol) { - dispatch_async(_workQueue, - ^{ - if (_suspendCount != 0 - || NSURLSessionTaskStateCanceling == _state - || NSURLSessionTaskStateCompleted == _state) - { - return; - } - - if (nil != protocol) - { - [protocol startLoading]; - } - else if (nil == _error) - { - NSDictionary *userInfo; - _NSURLProtocolClient *client; - - userInfo = [NSDictionary dictionaryWithObjectsAndKeys: - [_originalRequest URL], NSURLErrorFailingURLErrorKey, - [[_originalRequest URL] absoluteString], NSURLErrorFailingURLStringErrorKey, - nil]; - _error = [[NSError alloc] initWithDomain: NSURLErrorDomain - code: NSURLErrorUnsupportedURL - userInfo: userInfo]; - client = AUTORELEASE([[_NSURLProtocolClient alloc] init]); - [client task: self didFailWithError: _error]; - } - }); - }]; - } - }); -} - -- (float) priority -{ - return _priority;; -} - -- (void) setPriority: (float)priority -{ - _priority = priority; -} - -- (id) copyWithZone: (NSZone*)zone -{ - NSURLSessionTask *copy = [[[self class] alloc] init]; - - if (copy) - { - copy->_taskIdentifier = _taskIdentifier; - copy->_originalRequest = [_originalRequest copyWithZone: zone]; - copy->_currentRequest = [_currentRequest copyWithZone: zone]; - copy->_response = [_response copyWithZone: zone]; - copy->_countOfBytesReceived = _countOfBytesReceived; - copy->_countOfBytesSent = _countOfBytesSent; - copy->_countOfBytesExpectedToSend = _countOfBytesExpectedToSend; - copy->_countOfBytesExpectedToReceive = _countOfBytesExpectedToReceive; - copy->_taskDescription = [_taskDescription copyWithZone: zone]; - copy->_state = _state; - copy->_error = [_error copyWithZone: zone]; - copy->_session = _session; - dispatch_retain(_workQueue); - copy->_workQueue = _workQueue; - copy->_suspendCount = _suspendCount; - copy->_protocolLock = [_protocolLock copy]; - } - - return copy; -} - -- (void) getProtocolWithCompletion: (void (^)(NSURLProtocol* protocol))completion -{ - [_protocolLock lock]; - switch (_protocolState) - { - case NSURLSessionTaskProtocolStateToBeCreated: - { - NSURLCache *cache; - NSURLRequestCachePolicy cachePolicy; - NSCachedURLResponse *cachedResponse; - - cache = [[_session configuration] URLCache]; - cachePolicy = [[self currentRequest] cachePolicy]; - - if (nil != cache - && [self isUsingLocalCacheWithPolicy: cachePolicy] - && [self isKindOfClass: [NSURLSessionDataTask class]]) - { - ASSIGN(_protocolBag, [NSMutableArray array]); - [_protocolBag addObject: completion]; - - _protocolState = NSURLSessionTaskProtocolStateAwaitingCacheReply; - - [_protocolLock unlock]; - - cachedResponse = - [cache cachedResponseForDataTask: (NSURLSessionDataTask*)self]; - - _protocol = [[_protocolClass alloc] initWithTask: self - cachedResponse: cachedResponse - client: nil]; - [self satisfyProtocolRequest]; - } - else - { - _protocol = [[_protocolClass alloc] initWithTask: self - cachedResponse: nil - client: nil]; - _protocolState = NSURLSessionTaskProtocolStateExisting; - [_protocolLock unlock]; - completion(_protocol); - } - break; - } - case NSURLSessionTaskProtocolStateAwaitingCacheReply: - { - [_protocolBag addObject: completion]; - [_protocolLock unlock]; - break; - } - case NSURLSessionTaskProtocolStateExisting: - { - [_protocolLock unlock]; - completion(_protocol); - break; - } - case NSURLSessionTaskProtocolStateInvalidated: - { - [_protocolLock unlock]; - completion(nil); - break; - } - } -} - -- (void) satisfyProtocolRequest -{ - [_protocolLock lock]; - switch (_protocolState) - { - case NSURLSessionTaskProtocolStateToBeCreated: - { - _protocolState = NSURLSessionTaskProtocolStateExisting; - [_protocolLock unlock]; - break; - } - case NSURLSessionTaskProtocolStateAwaitingCacheReply: - { - _protocolState = NSURLSessionTaskProtocolStateExisting; - [_protocolLock unlock]; - void (^callback)(NSURLProtocol*); - for (int i = 0; i < [_protocolBag count]; i++) - { - callback = [_protocolBag objectAtIndex: i]; - callback(_protocol); - } - DESTROY(_protocolBag); - break; - } - case NSURLSessionTaskProtocolStateExisting: - case NSURLSessionTaskProtocolStateInvalidated: - { - [_protocolLock unlock]; - break; - } - } -} - -- (BOOL) isUsingLocalCacheWithPolicy: (NSURLRequestCachePolicy)policy -{ - switch (policy) - { - case NSURLRequestUseProtocolCachePolicy: - return true; - case NSURLRequestReloadIgnoringLocalCacheData: - return false; - case NSURLRequestReturnCacheDataElseLoad: - return true; - case NSURLRequestReturnCacheDataDontLoad: - return true; - case NSURLRequestReloadIgnoringLocalAndRemoteCacheData: - case NSURLRequestReloadRevalidatingCacheData: - return false; - default: - return false; - } -} - -- (NSURLSession*) session -{ - return _session; -} - -- (void) setState: (NSURLSessionTaskState)state -{ - _state = state; -} - -- (void) invalidateProtocol -{ - [_protocolLock lock]; - _protocolState = NSURLSessionTaskProtocolStateInvalidated; - DESTROY(_protocol); - /* - * Release session at the end of the task - * and not when -dealloc is called. - */ - DESTROY(_session); - [_protocolLock unlock]; -} - -- (void (^)(NSData *data, NSURLResponse *response, NSError *error)) dataCompletionHandler -{ - return _dataCompletionHandler; -} - -- (void) setDataCompletionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler -{ - ASSIGN(_dataCompletionHandler, completionHandler); -} - -- (void (^)(NSURL *location, NSURLResponse *response, NSError *error)) downloadCompletionHandler -{ - return _downloadCompletionHandler; -} - -- (void) setDownloadCompletionHandler: (void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler -{ - ASSIGN(_downloadCompletionHandler, completionHandler); -} - -@end - -@implementation NSURLSessionDataTask - -@end - -@implementation NSURLSessionUploadTask - -@end - -@implementation NSURLSessionDownloadTask - -@end - -@implementation NSURLSessionConfiguration - -static NSURLSessionConfiguration *def = nil; - -+ (void) initialize -{ - if (nil == def) - { - def = [NSURLSessionConfiguration new]; - } -} - -+ (NSURLSessionConfiguration*) defaultSessionConfiguration -{ - return AUTORELEASE([def copy]); -} - -+ (NSURLSessionConfiguration*) ephemeralSessionConfiguration -{ - // return default session since we don't store any data on disk anyway - return AUTORELEASE([def copy]); -} - -+ (NSURLSessionConfiguration*) backgroundSessionConfigurationWithIdentifier:(NSString*)identifier -{ - NSURLSessionConfiguration *configuration = [def copy]; - configuration->_identifier = [identifier copy]; - return AUTORELEASE(configuration); -} - -- (instancetype) init -{ - if (nil != (self = [super init])) - { - _protocolClasses = [[NSArray alloc] initWithObjects: - [GSHTTPURLProtocol class], nil]; - _HTTPMaximumConnectionsPerHost = 1; - _HTTPShouldUsePipelining = YES; - _HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever; - _HTTPCookieStorage = nil; - _HTTPShouldSetCookies = NO; - _HTTPAdditionalHeaders = nil; - _HTTPMaximumConnectionLifetime = 0; // Zero or less means default - } - - return self; -} - -- (void) dealloc -{ - DESTROY(_identifier); - DESTROY(_HTTPAdditionalHeaders); - DESTROY(_HTTPCookieStorage); - DESTROY(_protocolClasses); - DESTROY(_URLCache); - DESTROY(_URLCredentialStorage); - [super dealloc]; -} - -- (NSString*) identifier -{ - return _identifier; -} - -- (NSURLCache*) URLCache -{ - return _URLCache; -} - -- (void) setURLCache: (NSURLCache*)cache -{ - ASSIGN(_URLCache, cache); -} - -- (void) setURLCredentialStorage: (NSURLCredentialStorage*)storage -{ - ASSIGN(_URLCredentialStorage, storage); -} - -- (NSURLRequestCachePolicy) requestCachePolicy -{ - return _requestCachePolicy; -} - -- (void) setRequestCachePolicy: (NSURLRequestCachePolicy)policy -{ - _requestCachePolicy = policy; -} - -- (NSArray*) protocolClasses -{ - return _protocolClasses; -} - -- (NSInteger) HTTPMaximumConnectionsPerHost -{ - return _HTTPMaximumConnectionsPerHost; -} - -- (void) setHTTPMaximumConnectionsPerHost: (NSInteger)n -{ - _HTTPMaximumConnectionsPerHost = n; -} - -- (NSInteger) HTTPMaximumConnectionLifetime -{ - return _HTTPMaximumConnectionLifetime; -} - -- (void) setHTTPMaximumConnectionLifetime: (NSInteger)n -{ - _HTTPMaximumConnectionLifetime = n; -} - -- (BOOL) HTTPShouldUsePipelining -{ - return _HTTPShouldUsePipelining; -} - -- (void) setHTTPShouldUsePipelining: (BOOL)flag -{ - _HTTPShouldUsePipelining = flag; -} - -- (NSHTTPCookieAcceptPolicy) HTTPCookieAcceptPolicy -{ - return _HTTPCookieAcceptPolicy; -} - -- (void) setHTTPCookieAcceptPolicy: (NSHTTPCookieAcceptPolicy)policy -{ - _HTTPCookieAcceptPolicy = policy; -} - -- (NSHTTPCookieStorage*) HTTPCookieStorage -{ - return _HTTPCookieStorage; -} - -- (void) setHTTPCookieStorage: (NSHTTPCookieStorage*)storage -{ - ASSIGN(_HTTPCookieStorage, storage); -} - -- (BOOL) HTTPShouldSetCookies -{ - return _HTTPShouldSetCookies; -} - -- (void) setHTTPShouldSetCookies: (BOOL)flag -{ - _HTTPShouldSetCookies = flag; -} - -- (NSDictionary*) HTTPAdditionalHeaders -{ - return _HTTPAdditionalHeaders; -} - -- (void) setHTTPAdditionalHeaders: (NSDictionary*)headers -{ - ASSIGN(_HTTPAdditionalHeaders, headers); -} - -- (NSURLRequest*) configureRequest: (NSURLRequest*)request -{ - return [self setCookiesOnRequest: request]; -} - -- (NSURLRequest*) setCookiesOnRequest: (NSURLRequest*)request -{ - NSMutableURLRequest *r = AUTORELEASE([request mutableCopy]); - - if (_HTTPShouldSetCookies) - { - if (nil != _HTTPCookieStorage && nil != [request URL]) - { - NSArray *cookies = [_HTTPCookieStorage cookiesForURL: [request URL]]; - if (nil != cookies && [cookies count] > 0) - { - NSDictionary *cookiesHeaderFields; - NSString *cookieValue; - - cookiesHeaderFields - = [NSHTTPCookie requestHeaderFieldsWithCookies: cookies]; - cookieValue = [cookiesHeaderFields objectForKey: @"Cookie"]; - if (nil != cookieValue && [cookieValue length] > 0) - { - [r setValue: cookieValue forHTTPHeaderField: @"Cookie"]; - } - } - } - } - - return AUTORELEASE([r copy]); -} - -- (NSURLCredentialStorage*) URLCredentialStorage -{ - return _URLCredentialStorage; -} - -- (id) copyWithZone: (NSZone*)zone -{ - NSURLSessionConfiguration *copy = [[[self class] alloc] init]; - - if (copy) - { - copy->_identifier = [_identifier copy]; - copy->_URLCache = [_URLCache copy]; - copy->_URLCredentialStorage = [_URLCredentialStorage copy]; - copy->_protocolClasses = [_protocolClasses copyWithZone: zone]; - copy->_HTTPMaximumConnectionsPerHost = _HTTPMaximumConnectionsPerHost; - copy->_HTTPShouldUsePipelining = _HTTPShouldUsePipelining; - copy->_HTTPCookieAcceptPolicy = _HTTPCookieAcceptPolicy; - copy->_HTTPCookieStorage = [_HTTPCookieStorage copy]; - copy->_HTTPShouldSetCookies = _HTTPShouldSetCookies; - copy->_HTTPAdditionalHeaders - = [_HTTPAdditionalHeaders copyWithZone: zone]; - } - - return copy; + return [self notImplemented:_cmd]; } @end diff --git a/Source/NSURLSessionConfiguration.m b/Source/NSURLSessionConfiguration.m new file mode 100644 index 000000000..49068389b --- /dev/null +++ b/Source/NSURLSessionConfiguration.m @@ -0,0 +1,283 @@ +/** + NSURLSessionConfiguration.m + + Copyright (C) 2017-2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: May 2024 + Author: Hugo Melder + + 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 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 "Foundation/NSURLSession.h" +#import "Foundation/NSHTTPCookie.h" + +// TODO: This is the old implementation. It requires a rewrite! + +@implementation NSURLSessionConfiguration + +static NSURLSessionConfiguration *def = nil; + ++ (void)initialize +{ + if (nil == def) + { + def = [NSURLSessionConfiguration new]; + } +} + ++ (NSURLSessionConfiguration *)defaultSessionConfiguration +{ + return AUTORELEASE([def copy]); +} + ++ (NSURLSessionConfiguration *)ephemeralSessionConfiguration +{ + // return default session since we don't store any data on disk anyway + return AUTORELEASE([def copy]); +} + ++ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier: + (NSString *)identifier +{ + NSURLSessionConfiguration *configuration = [def copy]; + configuration->_identifier = [identifier copy]; + return AUTORELEASE(configuration); +} + +- (instancetype)init +{ + if (nil != (self = [super init])) + { + _protocolClasses = nil; + _HTTPMaximumConnectionsPerHost = 6; + _HTTPShouldUsePipelining = YES; + _HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever; + _HTTPCookieStorage = nil; + _HTTPShouldSetCookies = NO; + _HTTPAdditionalHeaders = nil; + _HTTPMaximumConnectionLifetime = 0; // Zero or less means default + _timeoutIntervalForResource = 604800; // 7 days in seconds + _timeoutIntervalForRequest = 60; // 60 seconds + } + + return self; +} + +- (void)dealloc +{ + DESTROY(_identifier); + DESTROY(_HTTPAdditionalHeaders); + DESTROY(_HTTPCookieStorage); + DESTROY(_protocolClasses); + DESTROY(_URLCache); + DESTROY(_URLCredentialStorage); + [super dealloc]; +} + +- (NSString *)identifier +{ + return _identifier; +} + +- (NSURLCache *)URLCache +{ + return _URLCache; +} + +- (void)setURLCache:(NSURLCache *)cache +{ + ASSIGN(_URLCache, cache); +} + +- (void)setURLCredentialStorage:(NSURLCredentialStorage *)storage +{ + ASSIGN(_URLCredentialStorage, storage); +} + +- (NSURLRequestCachePolicy)requestCachePolicy +{ + return _requestCachePolicy; +} + +- (void)setRequestCachePolicy:(NSURLRequestCachePolicy)policy +{ + _requestCachePolicy = policy; +} + +- (NSArray *)protocolClasses +{ + return _protocolClasses; +} + +- (NSTimeInterval)timeoutIntervalForRequest +{ + return _timeoutIntervalForRequest; +} + +- (void)setTimeoutIntervalForRequest:(NSTimeInterval)interval +{ + _timeoutIntervalForRequest = interval; +} + +- (NSTimeInterval)timeoutIntervalForResource +{ + return _timeoutIntervalForResource; +} + +- (void)setTimeoutIntervalForResource:(NSTimeInterval)interval +{ + _timeoutIntervalForResource = interval; +} + +- (NSInteger)HTTPMaximumConnectionsPerHost +{ + return _HTTPMaximumConnectionsPerHost; +} + +- (void)setHTTPMaximumConnectionsPerHost:(NSInteger)n +{ + _HTTPMaximumConnectionsPerHost = n; +} + +- (NSInteger)HTTPMaximumConnectionLifetime +{ + return _HTTPMaximumConnectionLifetime; +} + +- (void)setHTTPMaximumConnectionLifetime:(NSInteger)n +{ + _HTTPMaximumConnectionLifetime = n; +} + +- (BOOL)HTTPShouldUsePipelining +{ + return _HTTPShouldUsePipelining; +} + +- (void)setHTTPShouldUsePipelining:(BOOL)flag +{ + _HTTPShouldUsePipelining = flag; +} + +- (NSHTTPCookieAcceptPolicy)HTTPCookieAcceptPolicy +{ + return _HTTPCookieAcceptPolicy; +} + +- (void)setHTTPCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)policy +{ + _HTTPCookieAcceptPolicy = policy; +} + +- (NSHTTPCookieStorage *)HTTPCookieStorage +{ + return _HTTPCookieStorage; +} + +- (void)setHTTPCookieStorage:(NSHTTPCookieStorage *)storage +{ + ASSIGN(_HTTPCookieStorage, storage); +} + +- (BOOL)HTTPShouldSetCookies +{ + return _HTTPShouldSetCookies; +} + +- (void)setHTTPShouldSetCookies:(BOOL)flag +{ + _HTTPShouldSetCookies = flag; +} + +- (NSDictionary *)HTTPAdditionalHeaders +{ + return _HTTPAdditionalHeaders; +} + +- (void)setHTTPAdditionalHeaders:(NSDictionary *)headers +{ + ASSIGN(_HTTPAdditionalHeaders, headers); +} + +- (NSURLRequest *)configureRequest:(NSURLRequest *)request +{ + return [self setCookiesOnRequest:request]; +} + +- (NSURLRequest *)setCookiesOnRequest:(NSURLRequest *)request +{ + NSMutableURLRequest *r = AUTORELEASE([request mutableCopy]); + + if (_HTTPShouldSetCookies) + { + if (nil != _HTTPCookieStorage && nil != [request URL]) + { + NSArray *cookies = [_HTTPCookieStorage cookiesForURL:[request URL]]; + if (nil != cookies && [cookies count] > 0) + { + NSDictionary *cookiesHeaderFields; + NSString *cookieValue; + + cookiesHeaderFields = + [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + cookieValue = [cookiesHeaderFields objectForKey:@"Cookie"]; + if (nil != cookieValue && [cookieValue length] > 0) + { + [r setValue:cookieValue forHTTPHeaderField:@"Cookie"]; + } + } + } + } + + return AUTORELEASE([r copy]); +} + +- (NSURLCredentialStorage *)URLCredentialStorage +{ + return _URLCredentialStorage; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NSURLSessionConfiguration *copy = [[[self class] alloc] init]; + + if (copy) + { + copy->_identifier = [_identifier copy]; + copy->_URLCache = [_URLCache copy]; + copy->_URLCredentialStorage = [_URLCredentialStorage copy]; + copy->_protocolClasses = [_protocolClasses copyWithZone:zone]; + copy->_HTTPMaximumConnectionsPerHost = _HTTPMaximumConnectionsPerHost; + copy->_HTTPShouldUsePipelining = _HTTPShouldUsePipelining; + copy->_HTTPCookieAcceptPolicy = _HTTPCookieAcceptPolicy; + copy->_HTTPCookieStorage = [_HTTPCookieStorage retain]; + copy->_HTTPShouldSetCookies = _HTTPShouldSetCookies; + copy->_HTTPAdditionalHeaders = [_HTTPAdditionalHeaders copyWithZone:zone]; + copy->_timeoutIntervalForRequest = _timeoutIntervalForRequest; + copy->_timeoutIntervalForResource = _timeoutIntervalForResource; + } + + return copy; +} + +@end \ No newline at end of file diff --git a/Source/NSURLSessionPrivate.h b/Source/NSURLSessionPrivate.h new file mode 100644 index 000000000..b4beb684a --- /dev/null +++ b/Source/NSURLSessionPrivate.h @@ -0,0 +1,90 @@ +/** + NSURLSessionPrivate.h + + Copyright (C) 2017-2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: May 2024 + Author: Hugo Melder + + 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 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 "common.h" + +#import "Foundation/NSURLSession.h" +#import "Foundation/NSDictionary.h" +#import +#import + +extern NSString *GS_NSURLSESSION_DEBUG_KEY; + +/* libcurl may request a full-duplex socket configuration with + * CURL_POLL_INOUT, but libdispatch distinguishes between a read and write + * socket source. + * + * We thus need to keep track of two dispatch sources. One may be set to NULL + * if not used. + */ +struct SourceInfo +{ + dispatch_source_t readSocket; + dispatch_source_t writeSocket; +}; + +typedef NS_ENUM(NSInteger, GSURLSessionProperties) { + GSURLSessionStoresDataInMemory = (1 << 0), + GSURLSessionWritesDataToFile = (1 << 1), + GSURLSessionUpdatesDelegate = (1 << 2), + GSURLSessionHasCompletionHandler = (1 << 3), + GSURLSessionHasInputStream = (1 << 4) +}; + +@interface +NSURLSession (Private) + +- (dispatch_queue_t)_workQueue; + +- (NSData *)_certificateBlob; +- (NSString *)_certificatePath; + +/* Adds the internal easy handle to the multi handle. + * Modifications are performed on the workQueue. + */ +- (void)_resumeTask:(NSURLSessionTask *)task; + +/* The following methods must only be called from within callbacks dispatched on + * the workQueue.*/ +- (void)_setTimer:(NSInteger)timeout; +- (void)_suspendTimer; + +/* Required for manual redirects. + */ +- (void)_addHandle:(CURL *)easy; +- (void)_removeHandle:(CURL *)easy; + +- (void)_removeSocket:(struct SourceInfo *)sources; +- (int)_addSocket:(curl_socket_t)socket easyHandle:(CURL *)easy what:(int)what; +- (int)_setSocket:(curl_socket_t)socket + sources:(struct SourceInfo *)sources + what:(int)what; + +@end diff --git a/Source/NSURLSessionTask.m b/Source/NSURLSessionTask.m new file mode 100644 index 000000000..0c5208b7c --- /dev/null +++ b/Source/NSURLSessionTask.m @@ -0,0 +1,1629 @@ +/** + NSURLSessionTask.m + + Copyright (C) 2017-2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + 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 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 +#include +#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 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 *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 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) _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)delegate +{ + return _delegate; +} + +- (void)setDelegate:(id)delegate +{ + id 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 diff --git a/Source/NSURLSessionTaskPrivate.h b/Source/NSURLSessionTaskPrivate.h new file mode 100644 index 000000000..830e3c8a7 --- /dev/null +++ b/Source/NSURLSessionTaskPrivate.h @@ -0,0 +1,123 @@ +/** + NSURLSessionTaskPrivate.h + + Copyright (C) 2017-2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: May 2024 + Author: Hugo Melder + + 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 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 "Foundation/NSDictionary.h" +#import "Foundation/NSFileHandle.h" +#import "Foundation/NSURLSession.h" +#import + +@interface +NSURLSessionTask (Private) + +- (instancetype)initWithSession:(NSURLSession *)session + request:(NSURLRequest *)request + taskIdentifier:(NSUInteger)identifier; + +- (CURL *)_easyHandle; + +/* Enable or disable libcurl verbose output. Disabled by default. */ +- (void)_setVerbose:(BOOL)flag; + +/* This method is called by -[NSURLSession _checkForCompletion] + * + * We release the session (previously retained in -[NSURLSessionTask resume]) + * here and inform the delegate about the transfer state. + */ +- (void)_transferFinishedWithCode:(CURLcode)code; + +/* Explicitly enable data upload with an optional estimated size. Set to 0 if + * not available. + * + * This may be used when a body stream is passed at a later stage + * (see URLSession:task:needNewBodyStream:). + */ +- (void)_enableUploadWithSize:(NSInteger)size; +- (void)_setBodyStream:(NSInputStream *)stream; + +- (void)_enableUploadWithData:(NSData *)data; +- (void)_enableAutomaticRedirects:(BOOL)flag; + +/* Assign with copying */ +- (void)_setOriginalRequest:(NSURLRequest *)request; +- (void)_setCurrentRequest:(NSURLRequest *)request; + +- (void)_setResponse:(NSURLResponse *)response; +- (void)_setCookiesFromHeaders:(NSDictionary *)headers; + +- (void)_setCountOfBytesSent:(int64_t)count; +- (void)_setCountOfBytesReceived:(int64_t)count; +- (void)_setCountOfBytesExpectedToSend:(int64_t)count; +- (void)_setCountOfBytesExpectedToReceive:(int64_t)count; + +- (NSMutableDictionary *)_taskData; + +- (NSURLSession *)_session; + +/* Task specific properties. + * + * See GSURLSessionProperties in NSURLSessionPrivate.h. + */ +- (NSInteger)_properties; +- (void)_setProperties:(NSInteger)properties; + +/* This value is periodically checked in progress_callback. + * We then abort the transfer in the progress_callback if this flag is set. + */ +- (BOOL)_shouldStopTransfer; +- (void)_setShouldStopTransfer:(BOOL)flag; + +- (NSInteger)_numberOfRedirects; +- (void)_setNumberOfRedirects:(NSInteger)redirects; + +- (NSInteger)_headerCallbackCount; +- (void)_setHeaderCallbackCount:(NSInteger)count; + +- (NSFileHandle *)_createTemporaryFileHandleWithError:(NSError **)error; + +@end + +@interface +NSURLSessionDataTask (Private) + +- (GSNSURLSessionDataCompletionHandler)_completionHandler; +- (void)_setCompletionHandler:(GSNSURLSessionDataCompletionHandler)handler; + +@end + +@interface +NSURLSessionDownloadTask (Private) + +- (GSNSURLSessionDownloadCompletionHandler)_completionHandler; + +- (int64_t)_countOfBytesWritten; +- (void)_updateCountOfBytesWritten:(int64_t)count; +- (void)_setCompletionHandler:(GSNSURLSessionDownloadCompletionHandler)handler; + +@end \ No newline at end of file diff --git a/Source/externs.m b/Source/externs.m index 63bc85c54..1f123f3db 100644 --- a/Source/externs.m +++ b/Source/externs.m @@ -298,6 +298,7 @@ GS_DECLARE NSString* const NSFormalName = @"NSFormalName"; /* For GNUstep */ GS_DECLARE NSString* const GSLocale = @"GSLocale"; +GS_DECLARE NSString *const GSCACertificateFilePath = @"GSCACertificateFilePath"; /* diff --git a/Tests/base/NSURLSession/.gitignore b/Tests/base/NSURLSession/.gitignore new file mode 100644 index 000000000..f69cff4fd --- /dev/null +++ b/Tests/base/NSURLSession/.gitignore @@ -0,0 +1 @@ +Helpers/HTTPServer.bundle \ No newline at end of file diff --git a/Tests/base/NSURLSession/GNUmakefile.preamble b/Tests/base/NSURLSession/GNUmakefile.preamble index 84e483528..899dc2db5 100644 --- a/Tests/base/NSURLSession/GNUmakefile.preamble +++ b/Tests/base/NSURLSession/GNUmakefile.preamble @@ -1,5 +1,8 @@ -SUBPROJECTS = ../NSURLConnection/Helpers +include $(GNUSTEP_MAKEFILES)/common.make + +ifeq ($(the_library_combo), ng-gnu-gnu) +SUBPROJECTS = Helpers include $(GNUSTEP_MAKEFILES)/aggregate.make - +endif diff --git a/Tests/base/NSURLSession/Helpers/GNUmakefile b/Tests/base/NSURLSession/Helpers/GNUmakefile new file mode 100644 index 000000000..6c3a7beac --- /dev/null +++ b/Tests/base/NSURLSession/Helpers/GNUmakefile @@ -0,0 +1,20 @@ +include $(GNUSTEP_MAKEFILES)/common.make + +BUNDLE_NAME = HTTPServer + +NEEDS_GUI=NO + +HTTPServer_OBJC_FILES = HTTPServer.m +HTTPServer_OBJC_LIBS += -ldispatch + +ifeq ($(GNUSTEP_TARGET_OS), windows) +HTTPServer_OBJC_LIBS += -lws2_32 +endif + +HTTPServer_OBJCFLAGS += -fobjc-arc +#HTTPServer_RESOURCE_FILES += key.pem certificate.pem +HTTPServer_PRINCIPAL_CLASS = HTTPServer + +-include GNUmakefile.preamble +include $(GNUSTEP_MAKEFILES)/bundle.make +-include GNUmakefile.postamble diff --git a/Tests/base/NSURLSession/Helpers/HTTPServer.h b/Tests/base/NSURLSession/Helpers/HTTPServer.h new file mode 100644 index 000000000..d0916575f --- /dev/null +++ b/Tests/base/NSURLSession/Helpers/HTTPServer.h @@ -0,0 +1,30 @@ +#import +#import +#import +#import + +typedef NSData * (^RequestHandlerBlock)(NSURLRequest *); + +@interface Route : NSObject + ++ (instancetype)routeWithURL:(NSURL *)url + method:(NSString *)method + handler:(RequestHandlerBlock)block; + +- (NSString *)method; +- (NSURL *)url; +- (RequestHandlerBlock)block; + +- (BOOL)acceptsURL:(NSURL *)url method:(NSString *)method; + +@end + +@interface HTTPServer : NSObject +- initWithPort:(NSInteger)port routes:(NSArray *)routes; + +- (NSInteger)port; +- (void)resume; +- (void)suspend; + +- (void)setRoutes:(NSArray *)routes; +@end diff --git a/Tests/base/NSURLSession/Helpers/HTTPServer.m b/Tests/base/NSURLSession/Helpers/HTTPServer.m new file mode 100644 index 000000000..cbc963bee --- /dev/null +++ b/Tests/base/NSURLSession/Helpers/HTTPServer.m @@ -0,0 +1,414 @@ +#import + +#import + +#ifdef _WIN32 +#import + +#define close(x) closesocket(x) +#else + +#import +#import + +#endif + +#import "HTTPServer.h" + +@interface +NSString (ServerAdditions) +- (void)enumerateLinesUsingBlock2: + (void (^)(NSString *line, NSUInteger lineEndIndex, BOOL *stop))block; +@end + +@implementation +NSString (ServerAdditions) + +- (void)enumerateLinesUsingBlock2: + (void (^)(NSString *line, NSUInteger lineEndIndex, BOOL *stop))block +{ + NSUInteger length; + NSUInteger lineStart, lineEnd, contentsEnd; + NSRange currentLocationRange; + BOOL stop; + + length = [self length]; + lineStart = lineEnd = contentsEnd = 0; + stop = NO; + + // Enumerate through the string line by line + while (lineStart < length && !stop) + { + NSString *line; + NSRange lineRange; + + currentLocationRange = NSMakeRange(lineStart, 0); + [self getLineStart:&lineStart + end:&lineEnd + contentsEnd:&contentsEnd + forRange:currentLocationRange]; + + lineRange = NSMakeRange(lineStart, contentsEnd - lineStart); + line = [self substringWithRange:lineRange]; + + // Execute the block + block(line, lineEnd, &stop); + + // Move to the next line + lineStart = lineEnd; + } +} +@end + +/* We don't need this once toll-free bridging works */ +NSData * +copyDispatchDataToNSData(dispatch_data_t dispatchData) +{ + NSMutableData *mutableData = + [NSMutableData dataWithCapacity:dispatch_data_get_size(dispatchData)]; + + dispatch_data_apply(dispatchData, ^bool(dispatch_data_t region, size_t offset, + const void *buffer, size_t size) { + [mutableData appendBytes:buffer length:size]; + return true; // Continue iterating + }); + + return [mutableData copy]; +} + +@implementation Route +{ + NSString *_method; + NSURL *_url; + RequestHandlerBlock _block; +} ++ (instancetype)routeWithURL:(NSURL *)url + method:(NSString *)method + handler:(RequestHandlerBlock)block +{ + return [[Route alloc] initWithURL:url method:method handler:block]; +} + +- (instancetype)initWithURL:(NSURL *)url + method:(NSString *)method + handler:(RequestHandlerBlock)block +{ + self = [super init]; + + if (self) + { + _url = url; + _method = method; + _block = block; + } + + return self; +} + +- (NSString *)method +{ + return _method; +} +- (NSURL *)url +{ + return _url; +} +- (RequestHandlerBlock)block +{ + return _block; +} + +- (BOOL)acceptsURL:(NSURL *)url method:(NSString *)method +{ + return [[_url path] isEqualTo:[url path]]; +} + +@end /* Route */ + +@implementation HTTPServer +{ + _Atomic(BOOL) _stop; + int _socket; + NSInteger _port; + NSArray *_routes; + dispatch_queue_t _queue; + dispatch_queue_t _acceptQueue; +} + +- initWithPort:(NSInteger)port routes:(NSArray *)routes +{ + self = [super init]; + if (!self) + { + return nil; + } + +#ifdef _WIN32 + WSADATA wsaData; + + // Initialise WinSock2 API + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) + { + NSLog(@"Error Creating Socket: %d", WSAGetLastError()); + return nil; + } +#endif + + _stop = YES; + _socket = socket(AF_INET, SOCK_STREAM, 0); + if (_socket == -1) + { + NSLog(@"Error creating socket %s", strerror(errno)); + return nil; + } + + _routes = [routes copy]; + + struct sockaddr_in serverAddr; + NSUInteger addrLen = sizeof(struct sockaddr_in); + serverAddr.sin_family = AF_INET; + serverAddr.sin_port = NSSwapHostShortToBig(port); + serverAddr.sin_addr.s_addr = INADDR_ANY; + + int rc; + int yes = 1; + rc = setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)); + if (rc == -1) + { + NSLog(@"Error setting socket options %s", strerror(errno)); + return nil; + } + + rc = bind(_socket, (struct sockaddr *) &serverAddr, sizeof(struct sockaddr)); + if (rc < 0) + { + NSLog(@"Error binding to socket %s", strerror(errno)); + return nil; + } + + // Get Port Number + if (getsockname(_socket, (struct sockaddr *) &serverAddr, &addrLen) == -1) + { + NSLog(@"Error getting socket name %s", strerror(errno)); + return nil; + } + _port = NSSwapBigShortToHost(serverAddr.sin_port); + + rc = listen(_socket, 20); + if (rc < 0) + { + NSLog(@"Error listening on socket %s", strerror(errno)); + return nil; + } + + _queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + _acceptQueue = dispatch_queue_create("org.gnustep.HTTPServer.AcceptQueue", + DISPATCH_QUEUE_CONCURRENT); + + return self; +} + +- (void)acceptConnection +{ + struct sockaddr_in clientAddr; + dispatch_source_t clientSource; + NSUInteger sin_size; + int clientSocket; + + sin_size = sizeof(struct sockaddr_in); + clientSocket = accept(_socket, (struct sockaddr *) &clientAddr, &sin_size); + if (clientSocket < 0) + { + NSLog(@"Error accepting connection %s", strerror(errno)); + return; + } + + clientSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + clientSocket, 0, _queue); + + dispatch_source_set_cancel_handler(clientSource, ^{ + close(clientSocket); + }); + + dispatch_source_set_event_handler(clientSource, ^{ + while (1) + { + char buffer[4096]; + NSInteger bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0); + if (bytesRead < 0) + { + #ifdef _WIN32 + int error = WSAGetLastError(); + if (error == WSAEWOULDBLOCK) + { + break; + } + #else + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + break; + } + #endif + else + { + NSLog(@"Error reading data %s", strerror(errno)); + dispatch_source_cancel(clientSource); + return; + } + } + else if (bytesRead == 0) + { + dispatch_source_cancel(clientSource); + return; + } + else + { + NSData *data = [NSData dataWithBytes:buffer length:bytesRead]; + [self handleConnectionData:data forSocket:clientSocket]; + } + } + }); + + dispatch_resume(clientSource); +} + +- (void)handleConnectionData:(NSData *)reqData forSocket:(int)sock +{ + NSString *reqString; + NSRange bodyRange; + NSString *method, *url, *version; + NSURL *requestURL; + NSScanner *scanner; + Route *selectedRoute = nil; + + __block NSString *firstLine = nil; + __block NSMutableURLRequest *request = [NSMutableURLRequest new]; + __block NSUInteger headerEndIndex = 1; + + reqString = [[NSString alloc] initWithData:reqData + encoding:NSUTF8StringEncoding]; + + /* + * generic-message = Request-Line + * *(message-header CRLF) + * CRLF + * [ message-body ] + * Request-Line = Method SP Request-URI SP HTTP-Version CRLF + */ + [reqString enumerateLinesUsingBlock2:^(NSString *line, + NSUInteger lineEndIndex, BOOL *stop) { + NSRange range; + NSString *key, *value; + NSCharacterSet *set; + + set = [NSCharacterSet whitespaceCharacterSet]; + + /* Parse Request Line */ + if (nil == firstLine) + { + firstLine = [line stringByTrimmingCharactersInSet:set]; + return; + } + + /* Reached end of message header. Stop. */ + if ([line length] == 0) + { + *stop = YES; + headerEndIndex = lineEndIndex; + } + + range = [line rangeOfString:@":"]; + /* Ignore this line */ + if (NSNotFound == range.location) + { + return; + } + + key = [[line substringToIndex:range.location] + stringByTrimmingCharactersInSet:set]; + value = [[line substringFromIndex:range.location + 1] + stringByTrimmingCharactersInSet:set]; + + [request addValue:value forHTTPHeaderField:key]; + }]; + + /* Calculate remaining body range */ + bodyRange = NSMakeRange(headerEndIndex, [reqData length] - headerEndIndex); + reqData = [reqData subdataWithRange:bodyRange]; + + /* Parse Request Line */ + scanner = [NSScanner scannerWithString:firstLine]; + [scanner scanUpToString:@" " intoString:&method]; + [scanner scanUpToString:@" " intoString:&url]; + [scanner scanUpToString:@" " intoString:&version]; + + requestURL = [NSURL URLWithString:url]; + + [request setURL:requestURL]; + [request setHTTPMethod:method]; + [request setHTTPBody:reqData]; + + for (Route *r in _routes) + { + if ([r acceptsURL:requestURL method:method]) + { + selectedRoute = r; + break; + } + } + + NSData *responseData; + if (selectedRoute) + { + responseData = [selectedRoute block]([request copy]); + } + else + { + responseData = [@"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n" + dataUsingEncoding:NSASCIIStringEncoding]; + } + + send(sock, [responseData bytes], [responseData length], 0); +} + +- (void)setRoutes:(NSArray *)routes +{ + _routes = [routes copy]; +} + +- (NSInteger)port +{ + return _port; +} + +- (void)resume +{ + if (_stop) + { + _stop = NO; + dispatch_async(_acceptQueue, ^{ + while (!_stop) + { + [self acceptConnection]; + } + }); + } +} +- (void)suspend +{ + _stop = YES; +} + +- (void)dealloc +{ +#ifndef __APPLE__ + dispatch_release(_acceptQueue); +#endif + + close(_socket); +#ifdef _WIN32 + WSACleanup(); +#endif +} + +@end diff --git a/Tests/base/NSURLSession/NSRunLoop+TimeOutAdditions.h b/Tests/base/NSURLSession/NSRunLoop+TimeOutAdditions.h new file mode 100644 index 000000000..75e5fe1ff --- /dev/null +++ b/Tests/base/NSURLSession/NSRunLoop+TimeOutAdditions.h @@ -0,0 +1,26 @@ +#import +#import + +@interface +NSRunLoop (TimeOutAdditions) +- (void)runForSeconds:(NSTimeInterval)seconds conditionBlock:(BOOL (^)())block; +@end + +@implementation +NSRunLoop (TimeOutAdditions) +- (void)runForSeconds:(NSTimeInterval)seconds conditionBlock:(BOOL (^)())block +{ + NSDate *startDate = [NSDate date]; + NSTimeInterval endTime = [startDate timeIntervalSince1970] + seconds; + NSTimeInterval interval = 0.1; // Interval to check the condition + + while (block() && [[NSDate date] timeIntervalSince1970] < endTime) + { + @autoreleasepool + { + [[NSRunLoop currentRunLoop] + runUntilDate:[NSDate dateWithTimeIntervalSinceNow:interval]]; + } + } +} +@end \ No newline at end of file diff --git a/Tests/base/NSURLSession/Resources/largeBody.txt b/Tests/base/NSURLSession/Resources/largeBody.txt new file mode 100644 index 000000000..008fbf091 --- /dev/null +++ b/Tests/base/NSURLSession/Resources/largeBody.txt @@ -0,0 +1,54 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas volutpat justo +in aliquet venenatis. Aliquam eu odio volutpat, euismod turpis a, accumsan leo. +Aliquam mauris urna, dictum a ex vel, fermentum faucibus enim. Donec eu massa +ipsum. Vestibulum diam metus, dictum mattis urna vel, dignissim pretium magna. +Phasellus quis mattis felis. Morbi non dapibus nunc. Fusce id ligula fringilla, +varius tortor et, vulputate arcu. Quisque justo dolor, bibendum et imperdiet +ultricies, posuere vel elit. Aliquam vel risus et sapien malesuada tempor. +Aenean lorem metus, rutrum ut massa non, ultrices tincidunt neque. Nullam +viverra rhoncus sem et convallis. + +Sed id ligula non mauris lacinia mattis. Donec commodo tortor sed aliquet +commodo. Curabitur auctor, nibh eu dignissim gravida, lacus eros tincidunt +metus, ac fermentum mauris ex vitae magna. Vestibulum sed egestas elit, eu +placerat enim. Vivamus ullamcorper sollicitudin ullamcorper. Mauris consequat +quam et purus malesuada, quis euismod nisl scelerisque. Sed vestibulum dictum +nisi a viverra. Ut rutrum aliquam porta. Pellentesque est augue, ullamcorper vel +ultricies quis, posuere non turpis. Aenean suscipit nulla massa, vel venenatis +felis molestie ut. Morbi in lorem quam. Quisque sed facilisis neque, et mattis +ligula. Nam non placerat odio, ac sollicitudin dolor. Donec elit neque, rhoncus +at risus fermentum, ultrices hendrerit massa. Nulla volutpat, urna id molestie +scelerisque, ligula est laoreet mauris, eget maximus leo purus id mi. + +Aenean arcu metus, ornare eget est sit amet, vehicula accumsan enim. Morbi ante +diam, faucibus nec tempus molestie, vulputate auctor massa. Curabitur posuere +iaculis pulvinar. Fusce id ligula at justo iaculis accumsan eu sed velit. +Quisque condimentum augue non lobortis finibus. In quis interdum ligula, a +varius ex. Vestibulum tristique orci eget velit lacinia, at lacinia lorem +pretium. Sed ut varius mi. Vivamus elementum luctus arcu, vitae porta felis +cursus nec. + +Etiam sed nisl eu nulla scelerisque fermentum. Praesent blandit eros at lacus +blandit auctor. Sed in eros sapien. Quisque ac laoreet nisi, a fringilla nunc. +Sed auctor eros non mauris elementum vestibulum. Aenean vel urna orci. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia +curae; In sed blandit nibh. Aenean condimentum, lorem a tincidunt consequat, +nulla nulla ornare lorem, eu cursus sapien mi aliquam arcu. Duis porttitor mi et +ultricies porta. Donec tempus posuere lorem sit amet tincidunt. Nam nec purus +sit amet sem accumsan consectetur. Ut a nisl volutpat, egestas diam non, feugiat +mi. + +Cras sed risus eget ligula tincidunt luctus. Vestibulum ut sapien dictum, +sagittis elit vel, semper diam. Curabitur tristique ullamcorper lacus, quis +finibus ex rutrum vel. Phasellus non blandit nisl, id tincidunt augue. Integer +commodo metus at augue fringilla, sit amet posuere lectus iaculis. In nunc +lacus, pulvinar scelerisque faucibus ut, fermentum ac ante. Duis in ante +faucibus, ullamcorper est vel, convallis lacus. Nulla fermentum, nisi eget +sollicitudin sodales, augue felis gravida est, id luctus sem augue non turpis. +Sed a lorem vel diam maximus malesuada ut ut leo. Fusce venenatis, tellus a +scelerisque dictum, metus elit vehicula purus, id gravida libero libero vitae +arcu. Curabitur a cursus metus. Aenean efficitur rhoncus ante vitae imperdiet. +Etiam ante lacus, aliquam id sem non, ullamcorper interdum libero. Sed fringilla +dui sed est vehicula, vitae sollicitudin mauris tristique. Praesent ut arcu ut +neque sollicitudin finibus. Vestibulum lacus risus, egestas vel lorem eget, +pellentesque scelerisque lectus. diff --git a/Tests/base/NSURLSession/URLManager.h b/Tests/base/NSURLSession/URLManager.h new file mode 100644 index 000000000..18449bfda --- /dev/null +++ b/Tests/base/NSURLSession/URLManager.h @@ -0,0 +1,224 @@ +#import +#import + +@class URLManager; + +typedef void (^URLManagerCheckBlock)(URLManager *); + +@interface URLManager + : NSObject +{ + URLManagerCheckBlock _checkBlock; + +@public + NSURLSession *currentSession; + NSURLSessionResponseDisposition responseAnswer; + + NSInteger numberOfExpectedTasksBeforeCheck; + + NSInteger didCreateTaskCount; + NSURLSessionTask *didCreateTask; + + NSInteger didBecomeInvalidCount; + NSError *didBecomeInvalidError; + + NSInteger httpRedirectionCount; + NSURLSessionTask *httpRedirectionTask; + NSHTTPURLResponse *httpRedirectionResponse; + NSURLRequest *httpRedirectionRequest; + + NSInteger didCompleteCount; + NSURLSessionTask *didCompleteTask; + NSError *didCompleteError; + + NSInteger didWriteDataCount; + NSURLSessionDownloadTask *didWriteDataTask; + int64_t downloadBytesWritten; + int64_t downloadTotalBytesWritten; + int64_t downloadTotalBytesExpectedToWrite; + + NSInteger didFinishDownloadingCount; + NSURLSessionDownloadTask *didFinishDownloadingTask; + NSURL *didFinishDownloadingURL; + + NSInteger didReceiveResponseCount; + NSURLSessionDataTask *didReceiveResponseTask; + NSURLResponse *didReceiveResponse; + + NSInteger didReceiveDataCount; + NSURLSessionDataTask *didReceiveDataTask; + NSMutableData *accumulatedData; + + BOOL cancelRedirect; +} + +- (void)setCheckBlock:(URLManagerCheckBlock)block; + +@end + +@implementation URLManager + +- (instancetype)init +{ + self = [super init]; + if (self) + { + responseAnswer = NSURLSessionResponseAllow; + accumulatedData = [[NSMutableData alloc] init]; + } + + return self; +} + +- (void)setCheckBlock:(URLManagerCheckBlock)block +{ + _checkBlock = _Block_copy(block); +} + +#pragma mark - Session Lifecycle + +- (void)URLSession:(NSURLSession *)session + didCreateTask:(NSURLSessionTask *)task +{ + ASSIGN(currentSession, session); + + didCreateTaskCount += 1; + ASSIGN(didCreateTask, task); +} +- (void)URLSession:(NSURLSession *)session + didBecomeInvalidWithError:(NSError *)error +{ + ASSIGN(currentSession, session); + ASSIGN(didBecomeInvalidError, error); + + didBecomeInvalidCount += 1; +} + +#pragma mark - Task Updates + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLRequest *))completionHandler +{ + ASSIGN(currentSession, session); + ASSIGN(httpRedirectionTask, task); + ASSIGN(httpRedirectionResponse, response); + ASSIGN(httpRedirectionRequest, request); + + if (cancelRedirect) + { + completionHandler(NULL); + } + else + { + completionHandler(request); + } + + httpRedirectionCount += 1; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didCompleteWithError:(nullable NSError *)error +{ + ASSIGN(currentSession, session); + ASSIGN(didCompleteTask, task); + ASSIGN(didCompleteError, error); + + didCompleteCount += 1; + if (didCompleteCount == numberOfExpectedTasksBeforeCheck + && _checkBlock != NULL) + { + _checkBlock(self); + } +} + +#pragma mark - Data Updates + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveResponse:(NSURLResponse *)response + completionHandler: + (void (^)(NSURLSessionResponseDisposition))completionHandler +{ + ASSIGN(currentSession, session); + ASSIGN(didReceiveResponseTask, dataTask); + ASSIGN(didReceiveResponse, response); + + didReceiveResponseCount += 1; + + completionHandler(responseAnswer); +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data +{ + ASSIGN(currentSession, session); + ASSIGN(didReceiveResponseTask, dataTask); + + didReceiveDataCount += 1; + + [accumulatedData appendData:data]; +} + +#pragma mark - Download Updates + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didWriteData:(int64_t)bytesWritten + totalBytesWritten:(int64_t)totalBytesWritten + totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite +{ + ASSIGN(currentSession, session); + + didWriteDataCount += 1; + downloadBytesWritten = bytesWritten; + downloadTotalBytesExpectedToWrite = totalBytesExpectedToWrite; + downloadTotalBytesExpectedToWrite = totalBytesExpectedToWrite; +} + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didFinishDownloadingToURL:(NSURL *)location +{ + ASSIGN(currentSession, session); + ASSIGN(didFinishDownloadingTask, downloadTask); + ASSIGN(didFinishDownloadingURL, location); + + didFinishDownloadingCount += 1; +} + +- (void)dealloc +{ + RELEASE(currentSession); + + RELEASE(didCreateTask); + RELEASE(didBecomeInvalidError); + + RELEASE(httpRedirectionTask); + RELEASE(httpRedirectionResponse); + RELEASE(httpRedirectionRequest); + + RELEASE(didCompleteTask); + RELEASE(didCompleteError); + + RELEASE(didWriteDataTask); + + RELEASE(didFinishDownloadingTask); + RELEASE(didFinishDownloadingURL); + + RELEASE(didReceiveResponseTask); + RELEASE(didReceiveResponse); + + RELEASE(didReceiveDataTask); + RELEASE(accumulatedData); + + _Block_release(_checkBlock); + [super dealloc]; +} + +@end diff --git a/Tests/base/NSURLSession/delegate.g b/Tests/base/NSURLSession/delegate.g deleted file mode 100644 index eba1e2201..000000000 --- a/Tests/base/NSURLSession/delegate.g +++ /dev/null @@ -1,131 +0,0 @@ -#import -#import "Testing.h" -#import "ObjectTesting.h" - -@interface MyDelegate : NSObject -{ - BOOL _finished; - NSMutableArray *_order; - NSURLResponse *_response; - NSData *_taskData; - NSString *_taskText; - NSError *_taskError; - NSURL *_taskLocation; -} -- (BOOL) finished; -- (void) reset; -@end - -@implementation MyDelegate -- (void) dealloc -{ - [self reset]; - [super dealloc]; -} - -- (BOOL) finished -{ - return _finished; -} - -- (id) init -{ - if (nil != (self = [super init])) - { - _order = [NSMutableArray new]; - } - return self; -} - -- (NSMutableArray*) order -{ - return _order; -} - -- (void) reset -{ - DESTROY(_order); - DESTROY(_response); - DESTROY(_taskData); - DESTROY(_taskError); - DESTROY(_taskText); - DESTROY(_taskLocation); - _finished = NO; -} - -- (NSURLResponse*) response -{ - return _response; -} -- (NSData*) taskData -{ - return _taskData; -} -- (NSError*) taskError -{ - return _taskError; -} -- (NSString*) taskText -{ - return _taskText; -} - -- (NSURL*) taskLocation -{ - return _taskLocation; -} - -- (void) URLSession: (NSURLSession*)session - dataTask: (NSURLSessionDataTask*)dataTask - didReceiveResponse: (NSURLResponse*)response - completionHandler: (void (^)(NSURLSessionResponseDisposition disposition))completionHandler -{ - [_order addObject: NSStringFromSelector(_cmd)]; - ASSIGN(_response, response); - completionHandler(NSURLSessionResponseAllow); -} - -- (void) URLSession: (NSURLSession*)session - dataTask: (NSURLSessionDataTask*)dataTask - didReceiveData: (NSData*)data -{ - [_order addObject: NSStringFromSelector(_cmd)]; - - NSString *text; - - ASSIGN(_taskData, data); - text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; - if (nil != text) - { - ASSIGN(_taskText, text); - } - RELEASE(text); -} - -/* NSURLSessionDownloadDelegate */ -- (void) URLSession: (NSURLSession *)session - downloadTask: (NSURLSessionDownloadTask *)downloadTask - didFinishDownloadingToURL: (NSURL *)location -{ - ASSIGN(_taskLocation, location); -} - -- (void) URLSession: (NSURLSession*)session - task: (NSURLSessionTask*)task -didCompleteWithError: (NSError*)error -{ - [_order addObject: NSStringFromSelector(_cmd)]; - _finished = YES; - - if (error == nil) - { - NSLog(@"Download is Successful"); - } - else - { - NSLog(@"Error %@", [error userInfo]); - } - ASSIGN(_taskError, error); -} -@end - diff --git a/Tests/base/NSURLSession/gdbinit b/Tests/base/NSURLSession/gdbinit deleted file mode 100644 index e125a1993..000000000 --- a/Tests/base/NSURLSession/gdbinit +++ /dev/null @@ -1,79 +0,0 @@ -def print_ivar - set $adr = malloc(1000) - call strcpy(($adr),($arg1)) - call GSObjCPrint(($arg0),($adr)) - call free($adr) -end - -def print_ivars - if ((($arg0)->isa->info & 0x01) || (($arg0)->isa->info == 2816)) - # arg0 is a pointer to an object - set $cls = ($arg0)->isa - else - if (($arg0)->isa->info & 0x02) - # arg0 is a pointer to a class - set $cls = ($arg0) - else - # arg0 is something else - set $cls = 0 - end - end - while (($cls) != 0) - set $ivars = ($cls)->ivars - if (($ivars) != 0) - set $i = 0 - while ($i < ($ivars)->count) - output ($ivars)->ivar_list[$i] - echo \n - set $i = $i + 1 - end - end - set $cls = ($cls)->super_class - end -end - -def pivi - print *(int *)((char *)($arg0) + ($arg1))) -end - -def pivl - print *(long *)((char *)($arg0) + ($arg1))) -end - -def pivp - print *(void *)((char *)($arg0) + ($arg1))) -end - -def pivo - po *((id *)((char *)($arg0) + ($arg1))) -end - - -document print_ivars -Recursively print the instance varibles of the object or a class given -as first (and only) argument. -end - -document pivi -Print the value of the an instance variable as an int. -The first argument is the pointer to the object and the second the -offset of the instance variable. -end - -document pivl -Print the value of the an instance variable as a long. -The first argument is the pointer to the object and the second the -offset of the instance variable. -end - -document pivp -Print the value of the an instance variable as a pointer (to void). -The first argument is the pointer to the object and the second the -offset of the instance variable. -end - -document pivo -Ask an instance variable to print itself (using po). -The first argument is the pointer to the object and the second the -offset of the instance variable. -end diff --git a/Tests/base/NSURLSession/simpleTaskTests.m b/Tests/base/NSURLSession/simpleTaskTests.m new file mode 100644 index 000000000..6705fbcbd --- /dev/null +++ b/Tests/base/NSURLSession/simpleTaskTests.m @@ -0,0 +1,886 @@ +#import + +#if GS_HAVE_NSURLSESSION + +#import "Helpers/HTTPServer.h" +#import "NSRunLoop+TimeOutAdditions.h" +#import "URLManager.h" +#import "Testing.h" + +/* Timeout in Seconds */ +static NSInteger testTimeOut = 60; +static NSTimeInterval expectedCountOfTasksToComplete = 0; + +/* Accessed in delegate on different thread. + */ +static _Atomic(NSInteger) currentCountOfCompletedTasks = 0; +static NSLock *countLock; + +static NSDictionary *requestCookieProperties; + +static NSArray * +createRoutes(Class routeClass, NSURL *baseURL) +{ + Route *routeOKWithContent; + Route *routeTmpRedirectToOK; + Route *routeTmpRelativeRedirectToOK; + Route *routeSetCookiesOK; + Route *routeFoldedHeaders; + Route *routeIncorrectlyFoldedHeaders; + NSURL *routeOKWithContentURL; + NSURL *routeTmpRedirectToOKURL; + NSURL *routeTmpRelativeRedirectToOKURL; + NSURL *routeSetCookiesOKURL; + NSURL *routeFoldedHeadersURL; + NSURL *routeIncorrectlyFoldedHeadersURL; + + routeOKWithContentURL = [NSURL URLWithString:@"/contentOK"]; + routeTmpRedirectToOKURL = [NSURL URLWithString:@"/tmpRedirectToOK"]; + routeTmpRelativeRedirectToOKURL = + [NSURL URLWithString:@"/tmpRelativeRedirectToOK"]; + routeSetCookiesOKURL = [NSURL URLWithString:@"/setCookiesOK"]; + routeFoldedHeadersURL = [NSURL URLWithString:@"/foldedHeaders"]; + routeIncorrectlyFoldedHeadersURL = + [NSURL URLWithString:@"/incorrectFoldedHeaders"]; + + routeOKWithContent = [routeClass + routeWithURL:routeOKWithContentURL + method:@"GET" + handler:^NSData *(NSURLRequest *req) { + NSData *response; + + response = + [@"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!" + dataUsingEncoding:NSASCIIStringEncoding]; + return response; + }]; + + routeTmpRedirectToOK = [routeClass + routeWithURL:routeTmpRedirectToOKURL + method:@"GET" + handler:^NSData *(NSURLRequest *req) { + NSData *response; + NSString *responseString; + + responseString = [NSString + stringWithFormat:@"HTTP/1.1 307 Temporary Redirect\r\nLocation: " + @"%@\r\nContent-Length: 0\r\n\r\n", + [baseURL + URLByAppendingPathComponent:@"contentOK"]]; + + response = [responseString dataUsingEncoding:NSASCIIStringEncoding]; + return response; + }]; + + routeTmpRelativeRedirectToOK = [routeClass + routeWithURL:routeTmpRelativeRedirectToOKURL + method:@"GET" + handler:^NSData *(NSURLRequest *req) { + NSData *response; + NSString *responseString; + + responseString = [NSString + stringWithFormat:@"HTTP/1.1 307 Temporary Redirect\r\nLocation: " + @"contentOK\r\nContent-Length: 0\r\n\r\n"]; + + response = [responseString dataUsingEncoding:NSASCIIStringEncoding]; + return response; + }]; + + routeSetCookiesOK = [routeClass + routeWithURL:routeSetCookiesOKURL + method:@"GET" + handler:^NSData *(NSURLRequest *req) { + NSData *response; + NSString *httpResponse; + + httpResponse = @"HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Set-Cookie: sessionId=abc123; Expires=Wed, 09 Jun " + "2100 10:18:14 GMT; Path=/\r\n" + "Content-Length: 13\r\n" + "\r\n" + "Hello, world!"; + + NSString *cookie = [req allHTTPHeaderFields][@"Cookie"]; + PASS(cookie != nil, "Cookie field is not nil"); + PASS([cookie containsString:@"RequestCookie=1234"], + "cookie contains request cookie"); + + response = [httpResponse dataUsingEncoding:NSASCIIStringEncoding]; + return response; + }]; + + routeFoldedHeaders = [routeClass + routeWithURL:routeFoldedHeadersURL + method:@"GET" + handler:^NSData *(NSURLRequest *req) { + NSData *response; + + response = + [@"HTTP/1.1 200 OK\r\nContent-Length: 12\r\nFolded-Header-SP: " + @"Test\r\n ing\r\nFolded-Header-TAB: Test\r\n\ting\r\n\r\nHello " + @"World!" dataUsingEncoding:NSASCIIStringEncoding]; + return response; + }]; + + routeIncorrectlyFoldedHeaders = [routeClass + routeWithURL:routeIncorrectlyFoldedHeadersURL + method:@"GET" + handler:^NSData *(NSURLRequest *req) { + NSData *response; + + response = [@"HTTP/1.1 200 OK\r\n" + @" ing\r\nFolded-Header-TAB: Test\r\n\ting\r\n\r\nHello " + @"World!" dataUsingEncoding:NSASCIIStringEncoding]; + return response; + }]; + + return @[ + routeOKWithContent, routeTmpRedirectToOK, routeTmpRelativeRedirectToOK, + routeSetCookiesOK, routeFoldedHeaders, routeIncorrectlyFoldedHeaders + ]; +} + +/* Block needs to be released */ +static URLManagerCheckBlock +downloadCheckBlock(const char *prefix, NSURLSession *session, + NSURLSessionTask *task) +{ + return _Block_copy(^(URLManager *mgr) { + NSURL *location; + NSData *data; + NSString *string; + NSFileManager *fm; + + location = mgr->didFinishDownloadingURL; + fm = [NSFileManager defaultManager]; + + PASS_EQUAL(mgr->currentSession, session, + "%s URLManager Session is equal to session", prefix); + + /* Check URLSession:didCreateTask: callback */ + PASS(mgr->didCreateTaskCount == 1, "%s didCreateTask: Count is correct", + prefix); + PASS_EQUAL(mgr->didCreateTask, task, + "%s didCreateTask: task is equal to returned task", prefix); + + /* Check URLSession:task:didCompleteWithError: */ + PASS(nil == mgr->didCompleteError, + "%s didCompleteWithError: No error occurred", prefix) + PASS(mgr->didCompleteCount == 1, + "%s didCompleteWithError: Count is correct", prefix); + PASS_EQUAL(mgr->didCompleteTask, task, + "%s didCompleteWithError: task is equal to returned task", + prefix); + + /* Check Progress Reporting */ + PASS(mgr->didWriteDataCount == 1, "%s didWriteData: count is correct", + prefix); + PASS(mgr->downloadTotalBytesWritten + == mgr->downloadTotalBytesExpectedToWrite, + "%s didWriteData: Downloaded all expected data", prefix); + PASS(nil != mgr->didFinishDownloadingURL, + "%s didWriteData: Download location is not nil", prefix); + PASS([location isFileURL], "%s location is a fileURL", prefix); + + data = [NSData dataWithContentsOfURL:location]; + PASS(nil != data, "%s dataWithContentsOfURL is not nil", prefix) + + string = [[NSString alloc] initWithData:data + encoding:NSASCIIStringEncoding]; + PASS(nil != string, "%s string from data is not nil", prefix); + PASS_EQUAL(string, @"Hello World!", "%s data is correct", prefix); + + [string release]; + + /* Remove Downloaded Item */ + if (location) + { + [fm removeItemAtURL:location error:NULL]; + } + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + }); +} + +static URLManagerCheckBlock +dataCheckBlock(const char *prefix, NSURLSession *session, + NSURLSessionTask *task) +{ + return _Block_copy(^(URLManager *mgr) { + PASS_EQUAL(mgr->currentSession, session, + "%s URLManager Session is equal to session", prefix); + + /* Check URLSession:didCreateTask: callback */ + PASS(mgr->didCreateTaskCount == 1, "%s didCreateTask: Count is correct", + prefix); + PASS_EQUAL(mgr->didCreateTask, task, + "%s didCreateTask: task is equal to returned task", prefix); + + /* Check URLSession:task:didCompleteWithError: */ + PASS(nil == mgr->didCompleteError, + "%s didCompleteWithError: No error occurred", prefix) + PASS(mgr->didCompleteCount == 1, + "%s didCompleteWithError: Count is correct", prefix); + PASS_EQUAL(mgr->didCompleteTask, task, + "%s didCompleteWithError: task is equal to returned task", + prefix); + + NSData *data = mgr->accumulatedData; + PASS(mgr->didReceiveDataCount == 1, "%s didReceiveData: Count is correct", + prefix); + PASS(nil != data, "%s data in didReceiveData is not nil", prefix); + + NSString *string = [[NSString alloc] initWithData:data + encoding:NSASCIIStringEncoding]; + PASS(nil != string, "%s string from data is not nil", prefix); + PASS_EQUAL(string, @"Hello World!", "%s data is correct", prefix); + + [string release]; + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + }); +} + +/* Block needs to be released */ +static URLManagerCheckBlock +dataCheckBlockFailedRequest(const char *prefix, NSURLSession *session, + NSURLSessionTask *task) +{ + return _Block_copy(^(URLManager *mgr) { + PASS_EQUAL(mgr->currentSession, session, + "%s URLManager Session is equal to session", prefix); + + /* Check URLSession:didCreateTask: callback */ + PASS(mgr->didCreateTaskCount == 1, "%s didCreateTask: Count is correct", + prefix); + PASS_EQUAL(mgr->didCreateTask, task, + "%s didCreateTask: task is equal to returned task", prefix); + + /* Check URLSession:task:didCompleteWithError: */ + PASS(nil != mgr->didCompleteError, + "%s didCompleteWithError: An error occurred", prefix) + PASS(mgr->didCompleteCount == 1, + "%s didCompleteWithError: Count is correct", prefix); + PASS_EQUAL(mgr->didCompleteTask, task, + "%s didCompleteWithError: task is equal to returned task", + prefix); + + /* Check didReceiveResponse if not a canceled redirect */ + if (!mgr->cancelRedirect) + { + PASS(mgr->didReceiveResponseCount == 1, + "%s didReceiveResponse: Count is correct", prefix); + PASS(nil != mgr->didReceiveResponse, "%s didReceiveResponse is not nil", + prefix); + PASS_EQUAL(mgr->didReceiveResponseTask, task, + "%s didReceiveResponse: task is equal to returned task", + prefix); + } + else + { + PASS_EQUAL([mgr->didCompleteError code], NSURLErrorCancelled, + "%s didCompleteError is NSURLErrorCancelled", prefix); + } + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + }); +} + +/* Creates a downloadTaskWithURL: with the /contentOK route. + * + * Delegate callbacks are checked via the URLManager checkBlock. + */ +static URLManager * +testSimpleDownloadTransfer(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionConfiguration *configuration; + NSURLSessionDownloadTask *task; + URLManager *mgr; + URLManagerCheckBlock block; + const char *prefix = ""; + + NSURL *contentOKURL; + + /* URL Delegate Setup */ + mgr = [URLManager new]; + mgr->numberOfExpectedTasksBeforeCheck = 1; + expectedCountOfTasksToComplete += 1; + + /* URL Setup */ + contentOKURL = [baseURL URLByAppendingPathComponent:@"contentOK"]; + + configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + session = [NSURLSession sessionWithConfiguration:configuration + delegate:mgr + delegateQueue:nil]; + + task = [session downloadTaskWithURL:contentOKURL]; + PASS(nil != task, "%s Session created a valid download task", prefix); + + /* Setup Check Block */ + block = downloadCheckBlock(prefix, session, task); + [mgr setCheckBlock:block]; + _Block_release(block); + + [task resume]; + + return mgr; +} + +static URLManager * +testDownloadTransferWithRedirect(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionConfiguration *configuration; + NSURLSessionDownloadTask *task; + URLManager *mgr; + URLManagerCheckBlock block; + const char *prefix = ""; + + NSURL *contentOKURL; + + /* URL Delegate Setup */ + mgr = [URLManager new]; + mgr->numberOfExpectedTasksBeforeCheck = 1; + expectedCountOfTasksToComplete += 1; + + /* URL Setup */ + contentOKURL = [baseURL URLByAppendingPathComponent:@"tmpRedirectToOK"]; + + configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + session = [NSURLSession sessionWithConfiguration:configuration + delegate:mgr + delegateQueue:nil]; + + task = [session downloadTaskWithURL:contentOKURL]; + PASS(nil != task, "%s Session created a valid download task", prefix); + + /* Setup Check Block */ + block = downloadCheckBlock(prefix, session, task); + [mgr setCheckBlock:block]; + _Block_release(block); + + [task resume]; + + return mgr; +} + +/* This should use the build in redirection system from libcurl */ +static void +testDataTransferWithRedirectAndBlock(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionDataTask *task; + NSURL *url; + + expectedCountOfTasksToComplete += 1; + + session = [NSURLSession sharedSession]; + url = [baseURL URLByAppendingPathComponent:@"tmpRedirectToOK"]; + task = [session + dataTaskWithURL:url + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSString *string; + const char *prefix = ""; + + PASS(nil != data, "%s data in completion handler is not nil", prefix); + PASS(nil != response, "%s response is not nil", prefix); + PASS([response isKindOfClass:[NSHTTPURLResponse class]], + "%s response is an NSHTTPURLResponse", prefix); + PASS(nil == error, "%s error is nil", prefix); + + string = [[NSString alloc] initWithData:data + encoding:NSASCIIStringEncoding]; + PASS_EQUAL(string, @"Hello World!", "%s received data is correct", + prefix); + + [string release]; + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + }]; + + [task resume]; +} + +static void +testDataTransferWithCanceledRedirect(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionConfiguration *configuration; + NSURLSessionDataTask *task; + URLManager *mgr; + URLManagerCheckBlock block; + const char *prefix = ""; + + NSURL *contentOKURL; + + /* URL Delegate Setup */ + mgr = [URLManager new]; + mgr->numberOfExpectedTasksBeforeCheck = 1; + mgr->cancelRedirect = YES; + expectedCountOfTasksToComplete += 1; + + /* URL Setup */ + contentOKURL = [baseURL URLByAppendingPathComponent:@"tmpRedirectToOK"]; + configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + session = [NSURLSession sessionWithConfiguration:configuration + delegate:mgr + delegateQueue:nil]; + + task = [session dataTaskWithURL:contentOKURL]; + PASS(nil != task, "%s Session created a valid download task", prefix); + + /* Setup Check Block */ + block = dataCheckBlockFailedRequest(prefix, session, task); + [mgr setCheckBlock:block]; + _Block_release(block); + + [task resume]; +} + +static void +testDataTransferWithRelativeRedirect(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionConfiguration *configuration; + NSURLSessionDataTask *task; + NSURL *url; + URLManager *mgr; + URLManagerCheckBlock block; + const char *prefix = ""; + + /* URL Delegate Setup */ + mgr = [URLManager new]; + mgr->numberOfExpectedTasksBeforeCheck = 1; + expectedCountOfTasksToComplete += 1; + + session = [NSURLSession sharedSession]; + url = [baseURL URLByAppendingPathComponent:@"tmpRelativeRedirectToOK"]; + configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + session = [NSURLSession sessionWithConfiguration:configuration + delegate:mgr + delegateQueue:nil]; + + task = [session dataTaskWithURL:url]; + PASS(nil != task, "%s Session created a valid download task", prefix); + + /* Setup Check Block */ + block = dataCheckBlock(prefix, session, task); + [mgr setCheckBlock:block]; + _Block_release(block); + + [task resume]; +} + +static void +testDownloadTransferWithBlock(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionDownloadTask *task; + NSURL *url; + + expectedCountOfTasksToComplete += 1; + + session = [NSURLSession sharedSession]; + url = [baseURL URLByAppendingPathComponent:@"contentOK"]; + task = [session + downloadTaskWithURL:url + completionHandler:^(NSURL *location, NSURLResponse *response, + NSError *error) { + NSFileManager *fm; + NSData *data; + NSString *string; + + const char *prefix; + + prefix = ""; + fm = [NSFileManager defaultManager]; + + PASS(nil != location, "%s location is not nil", prefix); + PASS(nil != response, "%s response is not nil", prefix); + PASS(nil == error, "%s error is nil", prefix); + + data = [NSData dataWithContentsOfURL:location]; + PASS(nil != data, "%s data is not nil", prefix); + + string = [[NSString alloc] initWithData:data + encoding:NSASCIIStringEncoding]; + PASS_EQUAL(string, @"Hello World!", "%s content is correct", prefix); + + [fm removeItemAtURL:location error:NULL]; + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + + [string release]; + }]; + + [task resume]; +} + +static URLManager * +testParallelDataTransfer(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionConfiguration *configuration; + URLManager *mgr; + NSURL *url; + const char *prefix = ""; + + NSInteger numberOfParallelTasks = 10; + + /* URL Delegate Setup */ + mgr = [URLManager new]; + mgr->numberOfExpectedTasksBeforeCheck = numberOfParallelTasks; + expectedCountOfTasksToComplete += numberOfParallelTasks; + + url = [baseURL URLByAppendingPathComponent:@"tmpRedirectToOK"]; + configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + [configuration setHTTPMaximumConnectionsPerHost:0]; // Unlimited + session = [NSURLSession sessionWithConfiguration:configuration + delegate:mgr + delegateQueue:nil]; + + /* Setup Check Block */ + [mgr setCheckBlock:^(URLManager *mgr) { + PASS_EQUAL(mgr->currentSession, session, + "%s URLManager Session is equal to session", prefix); + + /* Check URLSession:didCreateTask: callback */ + PASS(mgr->didCreateTaskCount == numberOfParallelTasks, + "%s didCreateTask: Count is correct", prefix); + + /* Check URLSession:task:didCompleteWithError: */ + PASS(nil == mgr->didCompleteError, + "%s didCompleteWithError: No error occurred", prefix) + PASS(mgr->didCompleteCount == numberOfParallelTasks, + "%s didCompleteWithError: Count is correct", prefix); + + [countLock lock]; + currentCountOfCompletedTasks += numberOfParallelTasks; + [countLock unlock]; + }]; + + for (NSInteger i = 0; i < numberOfParallelTasks; i++) + { + NSURLSessionDataTask *task; + + task = [session dataTaskWithURL:url]; + [task resume]; + } + + return mgr; +} + +static void +testDataTaskWithBlock(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionDataTask *task; + NSURL *url; + + expectedCountOfTasksToComplete += 1; + + url = [baseURL URLByAppendingPathComponent:@"contentOK"]; + session = [NSURLSession sharedSession]; + task = [session + dataTaskWithURL:url + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSString *string; + const char *prefix = ""; + + PASS(nil != data, "%s data in completion handler is not nil", prefix); + PASS(nil != response, "%s response is not nil", prefix); + PASS([response isKindOfClass:[NSHTTPURLResponse class]], + "%s response is an NSHTTPURLResponse", prefix); + PASS(nil == error, "%s error is nil", prefix); + + string = [[NSString alloc] initWithData:data + encoding:NSASCIIStringEncoding]; + PASS_EQUAL(string, @"Hello World!", "%s received data is correct", + prefix); + + [string release]; + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + }]; + + [task resume]; +} + +static void +testDataTaskWithCookies(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionConfiguration *config; + NSURLSessionDataTask *task; + NSURL *url; + NSHTTPCookieStorage *cookies; + NSHTTPCookie *requestCookie; + + expectedCountOfTasksToComplete += 1; + + url = [baseURL URLByAppendingPathComponent:@"setCookiesOK"]; + requestCookie = [NSHTTPCookie cookieWithProperties:requestCookieProperties]; + + cookies = [NSHTTPCookieStorage new]; + [cookies setCookie:requestCookie]; + + config = [NSURLSessionConfiguration new]; + [config setHTTPCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; + [config setHTTPCookieStorage:cookies]; + [config setHTTPShouldSetCookies:YES]; + + session = [NSURLSession sessionWithConfiguration:config]; + task = [session + dataTaskWithURL:url + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSString *string; + NSArray *cookieArray; + NSDate *date; + const char *prefix = ""; + + PASS(nil != data, "%s data in completion handler is not nil", prefix); + PASS(nil != response, "%s response is not nil", prefix); + PASS([response isKindOfClass:[NSHTTPURLResponse class]], + "%s response is an NSHTTPURLResponse", prefix); + PASS(nil == error, "%s error is nil", prefix); + + string = [[NSString alloc] initWithData:data + encoding:NSASCIIStringEncoding]; + PASS_EQUAL(string, @"Hello, world!", "%s received data is correct", + prefix); + + cookieArray = [cookies cookiesForURL:url]; + + NSInteger count = 0; + for (NSHTTPCookie *ck in cookieArray) + { + if ([[ck name] isEqualToString:@"RequestCookie"]) + { + PASS_EQUAL(ck, requestCookie, "RequestCookie is correct"); + count += 1; + } + else if ([[ck name] isEqualToString:@"sessionId"]) + { + date = [NSDate dateWithString:@"2100-06-09 10:18:14 +0000"]; + PASS_EQUAL([ck name], @"sessionId", "Cookie name is correct"); + PASS_EQUAL([ck value], @"abc123", "Cookie value is correct"); + PASS([ck version] == 0, "Correct cookie version"); + PASS([date isEqual:[ck expiresDate]], + "Cookie expiresDate is correct"); + count += 1; + } + } + PASS(count == 2, "Found both cookies"); + + [string release]; + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + }]; + + [task resume]; +} + +/* Check if NSURLSessionTask correctly unfolds folded header lines */ +static void +foldedHeaderDataTaskTest(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionDataTask *task; + NSURL *url; + + expectedCountOfTasksToComplete += 1; + + url = [baseURL URLByAppendingPathComponent:@"foldedHeaders"]; + session = [NSURLSession sharedSession]; + task = [session + dataTaskWithURL:url + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *urlResponse = (NSHTTPURLResponse *) response; + NSString *string; + NSDictionary *headerDict; + const char *prefix = ""; + + headerDict = [urlResponse allHeaderFields]; + + PASS(nil != data, "%s data in completion handler is not nil", prefix); + PASS(nil != response, "%s response is not nil", prefix); + PASS([response isKindOfClass:[NSHTTPURLResponse class]], + "%s response is an NSHTTPURLResponse", prefix); + PASS(nil == error, "%s error is nil", prefix); + + string = [[NSString alloc] initWithData:data + encoding:NSASCIIStringEncoding]; + PASS_EQUAL(string, @"Hello World!", "%s received data is correct", + prefix); + + PASS_EQUAL([headerDict objectForKey:@"Folded-Header-SP"], @"Testing", + "Folded header with continuation space is parsed correctly"); + PASS_EQUAL([headerDict objectForKey:@"Folded-Header-TAB"], @"Testing", + "Folded header with continuation tab is parsed correctly"); + + [string release]; + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + }]; + + [task resume]; +} + +/* The disposition handler triggers transfer cancelation */ +static void +testAbortAfterDidReceiveResponse(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionConfiguration *configuration; + NSURLSessionDataTask *task; + URLManager *mgr; + URLManagerCheckBlock block; + const char *prefix = ""; + + NSURL *contentOKURL; + + /* URL Delegate Setup */ + mgr = [URLManager new]; + mgr->numberOfExpectedTasksBeforeCheck = 1; + mgr->responseAnswer = NSURLSessionResponseCancel; + expectedCountOfTasksToComplete += 1; + + /* URL Setup */ + contentOKURL = [baseURL URLByAppendingPathComponent:@"contentOK"]; + + configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + session = [NSURLSession sessionWithConfiguration:configuration + delegate:mgr + delegateQueue:nil]; + + task = [session dataTaskWithURL:contentOKURL]; + PASS(nil != task, "%s Session created a valid download task", prefix); + + /* Setup Check Block */ + block = dataCheckBlockFailedRequest(prefix, session, task); + [mgr setCheckBlock:block]; + _Block_release(block); + + [task resume]; +} + +int +main(int argc, char *argv[]) +{ + @autoreleasepool + { + NSBundle *bundle; + NSString *helperPath; + NSURL *baseURL; + NSFileManager *fm; + HTTPServer *server; + + Class httpServerClass; + Class routeClass; + + requestCookieProperties = @{ + NSHTTPCookieName : @"RequestCookie", + NSHTTPCookieValue : @"1234", + NSHTTPCookieDomain : @"127.0.0.1", + NSHTTPCookiePath : @"/", + NSHTTPCookieExpires : + [NSDate dateWithString:@"2100-06-09 12:18:14 +0000"], + NSHTTPCookieSecure : @NO, + }; + + fm = [NSFileManager defaultManager]; + helperPath = [[fm currentDirectoryPath] + stringByAppendingString:@"/Helpers/HTTPServer.bundle"]; + countLock = [[NSLock alloc] init]; + + bundle = [NSBundle bundleWithPath:helperPath]; + if (![bundle load]) + { + [NSException raise:NSInternalInconsistencyException + format:@"failed to load HTTPServer.bundle"]; + } + + httpServerClass = [bundle principalClass]; + routeClass = [bundle classNamed:@"Route"]; + + /* Bind to dynamic port. Set routes after initialisation. */ + server = [[httpServerClass alloc] initWithPort:0 routes:nil]; + if (!server) + { + [NSException raise:NSInternalInconsistencyException + format:@"failed to initialise HTTPServer"]; + } + + baseURL = + [NSURL URLWithString:[NSString stringWithFormat:@"http://127.0.0.1:%ld", + [server port]]]; + + NSLog(@"Test Server: baseURL=%@", baseURL); + + [server setRoutes:createRoutes(routeClass, baseURL)]; + [server resume]; + + // Call Test Functions here + testSimpleDownloadTransfer(baseURL); + testDownloadTransferWithBlock(baseURL); + + testParallelDataTransfer(baseURL); + testDataTaskWithBlock(baseURL); + testDataTaskWithCookies(baseURL); + + // Testing Header Line Unfolding + foldedHeaderDataTaskTest(baseURL); + + // Redirects + testDownloadTransferWithRedirect(baseURL); + testDataTransferWithRedirectAndBlock(baseURL); + testDataTransferWithCanceledRedirect(baseURL); + testDataTransferWithRelativeRedirect(baseURL); + + /* Abort in Delegate */ + testAbortAfterDidReceiveResponse(baseURL); + + [[NSRunLoop currentRunLoop] + runForSeconds:testTimeOut + conditionBlock:^BOOL(void) { + return expectedCountOfTasksToComplete != currentCountOfCompletedTasks; + }]; + + [server suspend]; + PASS(expectedCountOfTasksToComplete == currentCountOfCompletedTasks, + "All transfers were completed before a timeout occurred"); + + [server release]; + [countLock release]; + } +} + +#else + +int +main(int argc, char *argv[]) +{ + return 0; +} + +#endif /* GS_HAVE_NSURLSESSION */ \ No newline at end of file diff --git a/Tests/base/NSURLSession/test01.m b/Tests/base/NSURLSession/test01.m deleted file mode 100644 index 8a2e9fe0a..000000000 --- a/Tests/base/NSURLSession/test01.m +++ /dev/null @@ -1,141 +0,0 @@ -#import -#import "Testing.h" -#import "ObjectTesting.h" - -#if GS_HAVE_NSURLSESSION -@interface MyDelegate : NSObject -{ -@public - BOOL responseCompletion; - BOOL didComplete; - NSString *taskText; - NSError *taskError; -} -@end - -@implementation MyDelegate -- (void) dealloc -{ - RELEASE(taskText); - RELEASE(taskError); - [super dealloc]; -} - -- (void) URLSession: (NSURLSession*)session - dataTask: (NSURLSessionDataTask*)dataTask - didReceiveResponse: (NSURLResponse*)response - completionHandler: (void (^)(NSURLSessionResponseDisposition disposition))completionHandler -{ - responseCompletion = YES; - if (NO == didComplete) - { - NSLog(@"### handler 1 before didComplete..."); - } - else - { - NSLog(@"### handler 1 after didComplete..."); - } - completionHandler(NSURLSessionResponseAllow); -} - -- (void) URLSession: (NSURLSession*)session - dataTask: (NSURLSessionDataTask*)dataTask - didReceiveData: (NSData*)data -{ - NSString *text; - - text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; - if (nil == text) - { - NSLog(@"Received non-utf8 %@", data); - } - else - { - ASSIGN(taskText, text); - NSLog(@"Received String %@", text); - } - RELEASE(text); -} -- (void) URLSession: (NSURLSession*)session - task: (NSURLSessionTask*)task -didCompleteWithError: (NSError*)error -{ - if (error == nil) - { - NSLog(@"Download is Succesfull"); - } - else - { - NSLog(@"Error %@", [error userInfo]); - } - didComplete = YES; - ASSIGN(taskError, error); -} -@end -#endif - -int main() -{ - START_SET("NSURLSession test01") - -#if !GS_HAVE_NSURLSESSION - SKIP("library built without NSURLSession support") -#else - NSURLSessionConfiguration *defaultConfigObject; - NSURLSession *defaultSession; - NSURLSessionDataTask *dataTask; - NSMutableURLRequest *urlRequest; - NSURL *url; - NSOperationQueue *mainQueue; - NSString *params; - MyDelegate *object; - -#if defined(_WIN32) - NSLog(@"Marking nonexistant host test as hopeful on Windows as it seems to be broken"); - testHopeful = YES; -#endif - - object = AUTORELEASE([MyDelegate new]); - mainQueue = [NSOperationQueue mainQueue]; - defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration]; - defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject - delegate: object - delegateQueue: mainQueue]; - url = [NSURL URLWithString: - @"http://localhost:12345/not-here"]; - urlRequest = [NSMutableURLRequest requestWithURL: url]; - [urlRequest setHTTPMethod: @"POST"]; - params = @"name=Ravi&loc=India&age=31&submit=true"; - [urlRequest setHTTPBody: [params dataUsingEncoding: NSUTF8StringEncoding]]; - if ([urlRequest respondsToSelector: @selector(setDebug:)]) - { - [urlRequest setDebug: YES]; - } - - dataTask = [defaultSession dataTaskWithRequest: urlRequest]; - [dataTask resume]; - - NSDate *limit = [NSDate dateWithTimeIntervalSinceNow: 60.0]; - while (object->didComplete == NO - && [limit timeIntervalSinceNow] > 0.0) - { - ENTER_POOL - NSDate *when = [NSDate dateWithTimeIntervalSinceNow: 0.1]; - - [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode - beforeDate: when]; - LEAVE_POOL - } - - PASS(YES == object->didComplete, "request completed") - PASS([object->taskError code] == NSURLErrorCannotConnectToHost, - "unable to connect to host") - -#if defined(_WIN32) - testHopeful = NO; -#endif - -#endif - END_SET("NSURLSession test01") - return 0; -} diff --git a/Tests/base/NSURLSession/test02.m b/Tests/base/NSURLSession/test02.m deleted file mode 100644 index 223704b46..000000000 --- a/Tests/base/NSURLSession/test02.m +++ /dev/null @@ -1,100 +0,0 @@ -#import -#import "Testing.h" -#import "ObjectTesting.h" -#import "../NSURLConnection/Helpers/TestWebServer.h" - -#if GS_HAVE_NSURLSESSION -#import "delegate.g" -#endif - -int main() -{ - START_SET("NSURLSession http") - -#if !GS_HAVE_NSURLSESSION - SKIP("library built without NSURLSession support") -#else - NSFileManager *fm; - NSBundle *bundle; - NSString *helperPath; - - // load the test suite's classes - fm = [NSFileManager defaultManager]; - helperPath = [[fm currentDirectoryPath] stringByAppendingPathComponent: - @"../NSURLConnection/Helpers/TestConnection.bundle"]; - bundle = [NSBundle bundleWithPath: helperPath]; - NSCAssert([bundle load], NSInternalInconsistencyException); - - TestWebServer *server; - Class c; - BOOL debug = YES; - - // create a shared TestWebServer instance for performance - c = [bundle principalClass]; - server = [[c testWebServerClass] new]; - NSCAssert(server != nil, NSInternalInconsistencyException); - [server setDebug: debug]; - [server start: nil]; // localhost:1234 HTTP - - - NSURLSessionConfiguration *configuration; - NSURLSession *defaultSession; - NSURLSessionDataTask *dataTask; - NSMutableURLRequest *urlRequest; - NSURL *url; - NSOperationQueue *mainQueue; - NSString *params; - MyDelegate *object; - - configuration = [[NSURLSessionConfiguration alloc] init]; - [configuration setHTTPShouldUsePipelining: YES]; - - testHopeful=YES; - PASS_RUNS([configuration setHTTPMaximumConnectionLifetime: 42];, - "-setHTTPMaximumConnectionLifetime: support available in CURL") - testHopeful=NO; - - [configuration setHTTPMaximumConnectionsPerHost: 1]; - [configuration setRequestCachePolicy: NSURLCacheStorageNotAllowed]; - - object = AUTORELEASE([MyDelegate new]); - mainQueue = [NSOperationQueue mainQueue]; - defaultSession = [NSURLSession sessionWithConfiguration: configuration - delegate: object - delegateQueue: mainQueue]; - RELEASE(configuration); - url = [NSURL URLWithString: @"http://localhost:1234/xxx"]; - params = @"dummy=true"; - urlRequest = [NSMutableURLRequest requestWithURL: url]; - [urlRequest setHTTPMethod: @"POST"]; - [urlRequest setHTTPBody: [params dataUsingEncoding: NSUTF8StringEncoding]]; - if ([urlRequest respondsToSelector: @selector(setDebug:)]) - { - [urlRequest setDebug: YES]; - } - - dataTask = [defaultSession dataTaskWithRequest: urlRequest]; - [dataTask resume]; - - NSDate *limit = [NSDate dateWithTimeIntervalSinceNow: 60.0]; - while ([object finished] == NO - && [limit timeIntervalSinceNow] > 0.0) - { - ENTER_POOL - NSDate *when = [NSDate dateWithTimeIntervalSinceNow: 0.1]; - - [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode - beforeDate: when]; - LEAVE_POOL - } - - PASS(YES == [object finished], "request completed") - PASS_EQUAL([object taskError], nil, "request did not error") - - NSString *expect = @"Please give login and password"; - PASS_EQUAL([object taskText], expect, "request returned text") - -#endif - END_SET("NSURLSession http") - return 0; -} diff --git a/Tests/base/NSURLSession/test03.m b/Tests/base/NSURLSession/test03.m deleted file mode 100644 index 04bffe02d..000000000 --- a/Tests/base/NSURLSession/test03.m +++ /dev/null @@ -1,94 +0,0 @@ -#import -#import "Testing.h" -#import "ObjectTesting.h" -#import "../NSURLConnection/Helpers/TestWebServer.h" - -#if GS_HAVE_NSURLSESSION -#import "delegate.g" -#endif - -int main() -{ - START_SET("NSURLSession test03") - -#if !GS_HAVE_NSURLSESSION - SKIP("library built without NSURLSession support") -#else - NSFileManager *fm; - NSBundle *bundle; - NSString *helperPath; - - // load the test suite's classes - fm = [NSFileManager defaultManager]; - helperPath = [[fm currentDirectoryPath] stringByAppendingPathComponent: - @"../NSURLConnection/Helpers/TestConnection.bundle"]; - bundle = [NSBundle bundleWithPath: helperPath]; - NSCAssert([bundle load], NSInternalInconsistencyException); - - TestWebServer *server; - Class c; - BOOL debug = YES; - - // create a shared TestWebServer instance for performance - c = [bundle principalClass]; - server = [[c testWebServerClass] new]; - NSCAssert(server != nil, NSInternalInconsistencyException); - [server setDebug: debug]; - [server start: nil]; // localhost:1234 HTTP - - - NSURLSessionConfiguration *configuration; - NSURLSession *defaultSession; - NSURLSessionDownloadTask *downloadTask; - NSMutableURLRequest *urlRequest; - NSURL *url; - NSOperationQueue *mainQueue; - MyDelegate *object; - NSString *content; - - configuration = [[NSURLSessionConfiguration alloc] init]; - - object = AUTORELEASE([MyDelegate new]); - mainQueue = [NSOperationQueue mainQueue]; - defaultSession = [NSURLSession sessionWithConfiguration: configuration - delegate: object - delegateQueue: mainQueue]; - RELEASE(configuration); - url = [NSURL URLWithString: @"http://localhost:1234/index"]; - urlRequest = [NSMutableURLRequest requestWithURL: url]; - if ([urlRequest respondsToSelector: @selector(setDebug:)]) - { - [urlRequest setDebug: YES]; - } - - downloadTask = [defaultSession downloadTaskWithRequest: urlRequest]; - [downloadTask resume]; - - NSDate *limit = [NSDate dateWithTimeIntervalSinceNow: 60.0]; - while ([object finished] == NO - && [limit timeIntervalSinceNow] > 0.0) - { - ENTER_POOL - NSDate *when = [NSDate dateWithTimeIntervalSinceNow: 0.1]; - - [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode - beforeDate: when]; - LEAVE_POOL - } - - PASS(YES == [object finished], "request completed") - PASS_EQUAL([object taskError], nil, "request did not error") - - /* Get content from file */ - content = [NSString stringWithContentsOfFile: [[object taskLocation] path] - encoding: NSUTF8StringEncoding - error:nil]; - - - NSString *expect = @"Please give login and password"; - PASS_EQUAL(content, expect, "request returned text") - -#endif - END_SET("NSURLSession test03") - return 0; -} diff --git a/Tests/base/NSURLSession/uploadTaskTests.m b/Tests/base/NSURLSession/uploadTaskTests.m new file mode 100644 index 000000000..da95691d4 --- /dev/null +++ b/Tests/base/NSURLSession/uploadTaskTests.m @@ -0,0 +1,204 @@ +#include "Foundation/NSDate.h" +#import +#include +#include + +#if GS_HAVE_NSURLSESSION + +#import "Helpers/HTTPServer.h" +#import "NSRunLoop+TimeOutAdditions.h" +#import "URLManager.h" +#import "Testing.h" + +typedef void (^dataCompletionHandler)(NSData *data, NSURLResponse *response, + NSError *error); + +/* Timeout in Seconds */ +static NSInteger testTimeOut = 60; +static NSTimeInterval expectedCountOfTasksToComplete = 0; + +/* Accessed in delegate on different thread. + */ +static _Atomic(NSInteger) currentCountOfCompletedTasks = 0; +static NSLock *countLock; + +/* Expected Content */ +static NSString *largeBodyPath; +static NSData *largeBodyContent; + +static NSArray * +createRoutes(Class routeClass) +{ + Route *routeOKWithContent; + Route *routeLargeUpload; + NSURL *routeOKWithContentURL; + NSURL *routeLargeUploadURL; + + routeOKWithContentURL = [NSURL URLWithString:@"/smallUploadOK"]; + routeLargeUploadURL = [NSURL URLWithString:@"/largeUploadOK"]; + + routeOKWithContent = [routeClass + routeWithURL:routeOKWithContentURL + method:@"POST" + handler:^NSData *(NSURLRequest *req) { + NSData *response; + + response = + [@"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nHeader-Key: " + @"Header-Value\r\n\r\n" dataUsingEncoding:NSASCIIStringEncoding]; + return response; + }]; + + routeLargeUpload = [routeClass + routeWithURL:routeLargeUploadURL + method:@"POST" + handler:^NSData *(NSURLRequest *req) { + NSData *response; + + PASS_EQUAL([req valueForHTTPHeaderField:@"Request-Key"], + @"Request-Value", + "Request contains user-specific header line"); + PASS_EQUAL([req valueForHTTPHeaderField:@"Content-Type"], + @"text/plain", + "Request contains the correct Content-Type"); + PASS_EQUAL([req HTTPBody], largeBodyContent, "HTTPBody is correct"); + + response = + [@"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nHeader-Key: " + @"Header-Value\r\n\r\n" dataUsingEncoding:NSASCIIStringEncoding]; + + return response; + }]; + + return @[ routeOKWithContent, routeLargeUpload ]; +} + +static void +testLargeUploadWithBlock(NSURL *baseURL) +{ + NSURLSession *session; + NSURLSessionDataTask *dataTask; + NSURLSessionUploadTask *uploadTask; + NSMutableURLRequest *request; + NSURL *url; + dataCompletionHandler handler; + + expectedCountOfTasksToComplete += 2; + + url = [baseURL URLByAppendingPathComponent:@"largeUploadOK"]; + session = [NSURLSession sharedSession]; + request = [NSMutableURLRequest requestWithURL:url]; + + [request setHTTPBody:largeBodyContent]; + [request setHTTPMethod:@"POST"]; + [request setValue:@"Request-Value" forHTTPHeaderField:@"Request-Key"]; + [request setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"]; + + /* The completion handler for the two requests */ + handler = ^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = response; + + PASS([data length] == 0, "Received empty data object"); + PASS(nil != response, "Response is not nil"); + PASS([response isKindOfClass:[NSHTTPURLResponse class]], + "Response is a NSHTTPURLResponse"); + PASS(nil == error, "Error is nil"); + + PASS_EQUAL([[httpResponse allHeaderFields] objectForKey:@"Header-Key"], + @"Header-Value", "Response contains custom header line"); + + [countLock lock]; + currentCountOfCompletedTasks += 1; + [countLock unlock]; + }; + + dataTask = [session dataTaskWithRequest:request completionHandler:handler]; + uploadTask = [session uploadTaskWithRequest:request + fromData:largeBodyContent + completionHandler:handler]; + + [dataTask resume]; + [uploadTask resume]; +} + +int +main(int argc, char *argv[]) +{ + @autoreleasepool + { + NSBundle *bundle; + NSString *helperPath; + NSString *currentDirectory; + NSURL *baseURL; + NSFileManager *fm; + NSArray *routes; + HTTPServer *server; + + Class httpServerClass; + Class routeClass; + + fm = [NSFileManager defaultManager]; + currentDirectory = [fm currentDirectoryPath]; + helperPath = + [currentDirectory stringByAppendingString:@"/Helpers/HTTPServer.bundle"]; + countLock = [[NSLock alloc] init]; + + bundle = [NSBundle bundleWithPath:helperPath]; + if (![bundle load]) + { + [NSException raise:NSInternalInconsistencyException + format:@"failed to load HTTPServer.bundle"]; + } + + /* Load Test Data */ + largeBodyPath = + [currentDirectory stringByAppendingString:@"/Resources/largeBody.txt"]; + largeBodyContent = [NSData dataWithContentsOfFile:largeBodyPath]; + PASS(nil != largeBodyContent, "can load %s", [largeBodyPath UTF8String]); + + httpServerClass = [bundle principalClass]; + routeClass = [bundle classNamed:@"Route"]; + routes = createRoutes(routeClass); + server = [[httpServerClass alloc] initWithPort:0 routes:routes]; + if (!server) + { + [NSException raise:NSInternalInconsistencyException + format:@"failed to create HTTPServer"]; + } + + baseURL = + [NSURL URLWithString:[NSString stringWithFormat:@"http://127.0.0.1:%ld", + [server port]]]; + + NSLog(@"Server started with baseURL: %@", baseURL); + + [server resume]; + + /* Call Test Functions here! */ + testLargeUploadWithBlock(baseURL); + + [[NSRunLoop currentRunLoop] + runForSeconds:testTimeOut + conditionBlock:^BOOL(void) { + return expectedCountOfTasksToComplete != currentCountOfCompletedTasks; + }]; + + [server suspend]; + PASS(expectedCountOfTasksToComplete == currentCountOfCompletedTasks, + "All transfers were completed before a timeout occurred"); + + [server release]; + [countLock release]; + } + return 0; +} + +#else + +int +main(int argc, char *argv[]) +{ + return 0; +} + +#endif \ No newline at end of file diff --git a/configure b/configure index 091775a7f..4c1cf2c31 100755 --- a/configure +++ b/configure @@ -14510,6 +14510,18 @@ fi if test "$ac_cv_func__dispatch_main_queue_callback_4CF" = "yes" && test "$ac_cv_func__dispatch_get_main_queue_handle_4CF" = "yes"; then HAVE_LIBDISPATCH_RUNLOOP=1 fi + # Check for availability of functions in more recent libdispatch versions. + for ac_func in dispatch_cancel +do : + ac_fn_c_check_func "$LINENO" "dispatch_cancel" "ac_cv_func_dispatch_cancel" +if test "x$ac_cv_func_dispatch_cancel" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_DISPATCH_CANCEL 1 +_ACEOF + +fi +done + fi diff --git a/configure.ac b/configure.ac index 588f9899b..508caa4a8 100644 --- a/configure.ac +++ b/configure.ac @@ -3681,6 +3681,8 @@ if test $HAVE_LIBDISPATCH = 1; then if test "$ac_cv_func__dispatch_main_queue_callback_4CF" = "yes" && test "$ac_cv_func__dispatch_get_main_queue_handle_4CF" = "yes"; then HAVE_LIBDISPATCH_RUNLOOP=1 fi + # Check for availability of functions in more recent libdispatch versions. + AC_CHECK_FUNCS(dispatch_cancel) fi AC_SUBST(HAVE_LIBDISPATCH_RUNLOOP)