diff --git a/.travis.yml b/.travis.yml index 457c90c95..c69e591e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ --- language: cpp -dist: xenial +dist: focal jobs: include: - name: "Linux GCC" @@ -44,7 +44,7 @@ before_install: | if [ $CC = 'gcc' ]; then sudo apt-get install -y gobjc fi - sudo apt-get install -y libobjc-4.8-dev libblocksruntime-dev + sudo apt-get install -y libobjc-9-dev libblocksruntime-dev ;; ng-gnu-gnu) curl -s -o - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add - @@ -115,7 +115,7 @@ before_script: | export LD_LIBRARY_PATH=$DEP_ROOT/lib:$DEP_ROOT/lib64:$LD_LIBRARY_PATH case $LIBRARY_COMBO in gnu-gnu-gnu) - export CPATH=/usr/lib/gcc/x86_64-linux-gnu/4.8/include + export CPATH=/usr/lib/gcc/x86_64-linux-gnu/9/include ;; ng-gnu-gnu) export CPATH=$DEP_ROOT/include diff --git a/Headers/Foundation/NSDebug.h b/Headers/Foundation/NSDebug.h index b282d5cb1..fc5a1afa8 100644 --- a/Headers/Foundation/NSDebug.h +++ b/Headers/Foundation/NSDebug.h @@ -1,5 +1,5 @@ /* Interface to debugging utilities for GNUStep and OpenStep - Copyright (C) 1997,1999 Free Software Foundation, Inc. + Copyright (C) 1997-2020 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald Date: August 1997 @@ -44,6 +44,35 @@ extern "C" { #endif +/** Protocol for a delegate, set as an extension in some classes, to handle + * debug logging of low level I/O. The rationale for this protocol is that + * on occasion debug logging may be required, but the data being logged may + * contain sensitive information which should not be writtent to file. In + * that situation, the delegate may filter/mask the sensitive information + * from the logs by taking over the simpel writing to stderr that the inbuilt + * debug logging provides. + */ +@protocol GSLogDelegate +/** Method sent to the delegate to ask it to log a chunk of data that + * has been read. The delegate should return YES if it has handled + * the logging, NO if it wants the default mechanism to be used.
+ * The handle is the object which is performing the read operation. + */ +- (BOOL) getBytes: (const uint8_t*)bytes + ofLength: (NSUInteger)length + byHandle: (NSObject*)handle; + +/** Method sent to the delegate to ask it to log a chunk of data that + * has been written (or is immediately going to be written). + * The delegate should return YES if it has handled the logging, + * NO if it wants the default logging mechanism to be used.
+ * The handle is the object which is performing the write operation. + */ +- (BOOL) putBytes: (const uint8_t*)bytes + ofLength: (NSUInteger)length + byHandle: (NSObject*)handle; +@end + /* * Functions for debugging object allocation/deallocation * diff --git a/Headers/Foundation/NSURLCache.h b/Headers/Foundation/NSURLCache.h index 0b2ee15da..97a90eee2 100644 --- a/Headers/Foundation/NSURLCache.h +++ b/Headers/Foundation/NSURLCache.h @@ -196,6 +196,19 @@ typedef enum @end +@class NSURLSessionDataTask; + +@interface NSURLCache (NSURLSessionTaskAdditions) + +- (void) storeCachedResponse: (NSCachedURLResponse*)cachedResponse + forDataTask: (NSURLSessionDataTask*)dataTask; + +- (NSCachedURLResponse*) cachedResponseForDataTask: (NSURLSessionDataTask*)dataTask; + +- (void) removeCachedResponseForDataTask: (NSURLSessionDataTask*)dataTask; + +@end + #if defined(__cplusplus) } #endif diff --git a/Headers/Foundation/NSURLError.h b/Headers/Foundation/NSURLError.h index d6ec72941..6b713e17f 100644 --- a/Headers/Foundation/NSURLError.h +++ b/Headers/Foundation/NSURLError.h @@ -71,6 +71,7 @@ enum NSURLErrorFileDoesNotExist = -1100, NSURLErrorFileIsDirectory = -1101, NSURLErrorNoPermissionsToReadFile = -1102, + NSURLErrorDataLengthExceedsMaximum = -1103, NSURLErrorSecureConnectionFailed = -1200, NSURLErrorServerCertificateHasBadDate = -1201, NSURLErrorServerCertificateUntrusted = -1202, diff --git a/Headers/Foundation/NSURLProtocol.h b/Headers/Foundation/NSURLProtocol.h index 49045c46e..1cd11408f 100644 --- a/Headers/Foundation/NSURLProtocol.h +++ b/Headers/Foundation/NSURLProtocol.h @@ -44,6 +44,7 @@ extern "C" { @class NSURLProtocol; @class NSURLRequest; @class NSURLResponse; +@class NSURLSessionTask; /** @@ -178,11 +179,20 @@ extern "C" { cachedResponse: (NSCachedURLResponse *)cachedResponse client: (id )client; +- (instancetype) initWithTask: (NSURLSessionTask*)task + cachedResponse: (NSCachedURLResponse*)cachedResponse + client: (id)client; + /** * Returns the request handled by the receiver. */ - (NSURLRequest *) request; +/** + * Returns the task handled by the receiver. + */ +- (NSURLSessionTask *) task; + @end /** diff --git a/Headers/Foundation/NSURLRequest.h b/Headers/Foundation/NSURLRequest.h index 5f4db1beb..91a525100 100644 --- a/Headers/Foundation/NSURLRequest.h +++ b/Headers/Foundation/NSURLRequest.h @@ -283,6 +283,25 @@ typedef NSUInteger NSURLRequestCachePolicy; @end +@protocol GSLogDelegate; +@interface NSMutableURLRequest (GNUstep) + +/** Sets a flag to turn on low level debug logging for this request and the + * corresponding response. The previous vaue of the setting is returned. + */ +- (int) setDebug: (int)d; + +/** Sets a delegate object to override logging of low level I/O of the + * request as it is sent and the corresponding response as it arrives.
+ * The delegate object is not retained, so it is the responsibility of the + * caller to ensure that it persists until all I/O has completed.
+ * This has no effect unless debug is turned on, but if debug is turned on + * it permits the delegate to override the default behavior of writing the + * data to stderr. + */ +- (id) setDebugLogDelegate: (id)d; +@end + #if defined(__cplusplus) } #endif diff --git a/Headers/Foundation/NSURLSession.h b/Headers/Foundation/NSURLSession.h index 85db480a1..40af03ac5 100644 --- a/Headers/Foundation/NSURLSession.h +++ b/Headers/Foundation/NSURLSession.h @@ -2,18 +2,240 @@ #define __NSURLSession_h_GNUSTEP_BASE_INCLUDE #import +#import +#import +#if GS_HAVE_NSURLSESSION #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; +@class NSURL; +@class NSURLAuthenticationChallenge; +@class NSURLCache; +@class NSURLCredential; +@class NSURLCredentialStorage; +@class NSURLRequest; +@class NSURLResponse; +@class NSURLSessionConfiguration; +@class NSURLSessionDataTask; + + +/** + * NSURLSession is a replacement API for NSURLConnection. It provides + * options that affect the policy of, and various aspects of the + * mechanism by which NSURLRequest objects are retrieved from the + * network.
+ * + * 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 + * by the consumer. + */ @interface NSURLSession : NSObject +{ + NSOperationQueue *_delegateQueue; + id _delegate; + NSURLSessionConfiguration *_configuration; + NSString *_sessionDescription; + GSMultiHandle *_multiHandle; +} + +/* + * 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; + +- (NSOperationQueue*) delegateQueue; + +- (id ) delegate; + +- (NSURLSessionConfiguration*) configuration; + +- (NSString*) sessionDescription; + +- (void) setSessionDescription: (NSString*)sessionDescription; + +/* -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. + * + * 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; + +/* -invalidateAndCancel acts as -finishTasksAndInvalidate, but issues + * -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. + */ +- (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. + * The request may have a body stream. */ +- (NSURLSessionDataTask*) dataTaskWithRequest: (NSURLRequest*)request; + +/* Creates a data task to retrieve the contents of the given URL. */ +- (NSURLSessionDataTask*) dataTaskWithURL: (NSURL*)url; + @end -@interface NSURLSessionConfiguration : NSObject -@end +typedef NS_ENUM(NSUInteger, NSURLSessionTaskState) { + /* The task is currently being serviced by the session */ + NSURLSessionTaskStateRunning = 0, + NSURLSessionTaskStateSuspended = 1, + /* 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 + * delegate notifications */ + NSURLSessionTaskStateCompleted = 3, +}; +/* + * NSURLSessionTask - a cancelable object that refers to the lifetime + * of processing a given request. + */ @interface NSURLSessionTask : NSObject +{ + /** An identifier for this task, assigned by and unique + * to the owning session + */ + NSUInteger _taskIdentifier; + + /** The request this task was created to handle. + */ + NSURLRequest *_originalRequest; + + /** The request this task is currently handling. This may differ from + * originalRequest due to http server redirection + */ + NSURLRequest *_currentRequest; + + /** 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; +} + +- (NSUInteger) taskIdentifier; + +- (NSURLRequest*) originalRequest; + +- (NSURLRequest*) currentRequest; + +- (NSURLResponse*) response; +- (void) setResponse: (NSURLResponse*)response; + +- (int64_t) countOfBytesReceived; + +- (int64_t) countOfBytesSent; + +- (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. + */ +- (void) cancel; + +/* + * 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. + */ +- (void) suspend; +- (void) resume; + @end @interface NSURLSessionDataTask : NSURLSessionTask @@ -30,11 +252,177 @@ @end #endif +/* + * Configuration options for an NSURLSession. When a session is + * created, a copy of the configuration object is made - you cannot + * modify the configuration of a session after it has been created. + */ +@interface NSURLSessionConfiguration : NSObject +{ + NSURLCache *_URLCache; + NSURLRequestCachePolicy _requestCachePolicy; + NSArray *_protocolClasses; + NSInteger _HTTPMaximumConnectionLifetime; + NSInteger _HTTPMaximumConnectionsPerHost; + BOOL _HTTPShouldUsePipelining; + NSHTTPCookieAcceptPolicy _HTTPCookieAcceptPolicy; + NSHTTPCookieStorage *_HTTPCookieStorage; + NSURLCredentialStorage *_URLCredentialStorage; + BOOL _HTTPShouldSetCookies; + NSDictionary *_HTTPAdditionalHeaders; +} + +- (NSURLRequest*) configureRequest: (NSURLRequest*)request; + +@property (class, readonly, strong) + NSURLSessionConfiguration *defaultSessionConfiguration; + +- (NSDictionary*) HTTPAdditionalHeaders; + +- (NSHTTPCookieAcceptPolicy) HTTPCookieAcceptPolicy; + +- (NSHTTPCookieStorage*) HTTPCookieStorage; + +#if !NO_GNUSTEP +- (NSInteger) HTTPMaximumConnectionLifetime; +#endif + +- (NSInteger) HTTPMaximumConnectionsPerHost; + +- (BOOL) HTTPShouldSetCookies; + +- (BOOL) HTTPShouldUsePipelining; + +- (NSArray*) protocolClasses; + +- (NSURLRequestCachePolicy) requestCachePolicy; + +- (void) setHTTPAdditionalHeaders: (NSDictionary*)headers; + +- (void) setHTTPCookieAcceptPolicy: (NSHTTPCookieAcceptPolicy)policy; + +- (void) setHTTPCookieStorage: (NSHTTPCookieStorage*)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. + */ +- (void) setHTTPMaximumConnectionLifetime: (NSInteger)n; +#endif + +- (void) setHTTPMaximumConnectionsPerHost: (NSInteger)n; + +- (void) setHTTPShouldSetCookies: (BOOL)flag; + +- (void) setHTTPShouldUsePipelining: (BOOL)flag; + +- (void) setRequestCachePolicy: (NSURLRequestCachePolicy)policy; + +- (void) setURLCache: (NSURLCache*)cache; + +- (void) setURLCredentialStorage: (NSURLCredentialStorage*)storage; + +- (NSURLCache*) URLCache; + +- (NSURLCredentialStorage*) URLCredentialStorage; + +@end + +typedef NS_ENUM(NSInteger, NSURLSessionAuthChallengeDisposition) { + NSURLSessionAuthChallengeUseCredential = 0, + NSURLSessionAuthChallengePerformDefaultHandling = 1, + NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, + NSURLSessionAuthChallengeRejectProtectionSpace = 3 +}; + +typedef NS_ENUM(NSInteger, NSURLSessionResponseDisposition) { + NSURLSessionResponseCancel = 0, + NSURLSessionResponseAllow = 1, + NSURLSessionResponseBecomeDownload = 2, + NSURLSessionResponseBecomeStream = 3 +}; + @protocol NSURLSessionDelegate +@optional +/* The last message a session receives. A session will only become + * 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; + +/* 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; + @end @protocol NSURLSessionTaskDelegate +@optional +/* 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. + */ +- (void ) URLSession: (NSURLSession*)session + task: (NSURLSessionTask*)task + didCompleteWithError: (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; + +/* 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; + +/* 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 + * response to be delivered as the payload of this request. The default + * is to follow redirections. + * + */ +- (void) URLSession: (NSURLSession*)session + task: (NSURLSessionTask*)task + willPerformHTTPRedirection: (NSHTTPURLResponse*)response + newRequest: (NSURLRequest*)request + completionHandler: (void (^)(NSURLRequest*))completionHandler; + +@end + +@protocol NSURLSessionDataDelegate +@optional +/* Sent when data is available for the delegate to consume. + */ +- (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; + @end #endif #endif +#endif diff --git a/Headers/GNUstepBase/GSConfig.h.in b/Headers/GNUstepBase/GSConfig.h.in index 9ee38c4af..89d927847 100644 --- a/Headers/GNUstepBase/GSConfig.h.in +++ b/Headers/GNUstepBase/GSConfig.h.in @@ -267,8 +267,10 @@ typedef struct { #define GS_USE_AVAHI @HAVE_AVAHI@ #define GS_USE_MDNS @HAVE_MDNS@ #define GS_USE_ICU @HAVE_ICU@ +#define GS_USE_LIBCURL @HAVE_LIBCURL@ #define GS_USE_LIBDISPATCH @HAVE_LIBDISPATCH@ #define GS_USE_LIBDISPATCH_RUNLOOP @HAVE_LIBDISPATCH_RUNLOOP@ +#define GS_HAVE_NSURLSESSION @GS_HAVE_NSURLSESSION@ #define GS_HAVE_OBJC_ROOT_CLASS_ATTR @GS_HAVE_OBJC_ROOT_CLASS_ATTR@ #ifndef __has_include diff --git a/Headers/GNUstepBase/config.h.in b/Headers/GNUstepBase/config.h.in index 34e545bc6..52583b71d 100644 --- a/Headers/GNUstepBase/config.h.in +++ b/Headers/GNUstepBase/config.h.in @@ -189,6 +189,12 @@ /* Define to 1 if you have the `ctime' function. */ #undef HAVE_CTIME +/* Define to 1 if you have the header file. */ +#undef HAVE_CURL_CURL_H + +/* Define to 1 if you have the `curl_global_sslset' function. */ +#undef HAVE_CURL_GLOBAL_SSLSET + /* Define if you have currency_symbol field in struct lconv */ #undef HAVE_CURRENCY_SYMBOL_IN_LCONV @@ -219,6 +225,10 @@ /* Define to 1 if you have the header file. */ #undef HAVE_DISPATCH_PRIVATE_H +/* Define to 1 if you have the `dispatch_queue_create_with_target' function. + */ +#undef HAVE_DISPATCH_QUEUE_CREATE_WITH_TARGET + /* Define to 1 if you have the `dladdr' function. */ #undef HAVE_DLADDR diff --git a/Source/Additions/GSObjCRuntime.m b/Source/Additions/GSObjCRuntime.m index 88ebaddee..6141e0c67 100644 --- a/Source/Additions/GSObjCRuntime.m +++ b/Source/Additions/GSObjCRuntime.m @@ -2091,3 +2091,253 @@ GSClassSwizzle(id instance, Class newClass) } } +void +GSObjCPrint(void *base, void *item) +{ + FILE *fptr = stdout; + Class c; + id o; + + if (NULL == base) + { + fprintf(fptr, "null\n"); + return; + } + if (GSObjCIsClass((Class)base)) + { + o = nil; + c = (Class)base; + if (NULL == item) + { + fprintf(fptr, "%p is class %s {\n", base, GSNameFromClass(c)); + } + } + else if (GSObjCIsInstance((id)base)) + { + o = (id)base; + c = GSObjCClass(o); + if (NULL == item) + { + fprintf(fptr, "%p is instance of class %s (%p) {\n", + base, GSNameFromClass(c), c); + } + } + else + { + fprintf(fptr, "%p is not a class or instance\n", base); + return; + } + + while (c != Nil) + { + unsigned count; + Ivar *ivars = class_copyIvarList(c, &count); + + while (count-- > 0) + { + Ivar ivar = ivars[count]; + const char *name = ivar_getName(ivar); + const char *type = ivar_getTypeEncoding(ivar); + ptrdiff_t offset = ivar_getOffset(ivar); + const char *t; + + if (NULL == item) + { + fprintf(fptr, " (%ld) %s", (long)offset, name); + } + else if (strcmp(item, name) == 0) + { + continue; // not a match + } + else + { + fprintf(fptr, "(%ld) %s", (long)offset, name); + } + + if (nil == o) + { + /* We have no instance ... display offset to ivar + */ + fprintf(fptr, "\n"); + continue; + } + + fprintf(fptr, " = "); + + t = GSSkipTypeQualifierAndLayoutInfo(type); + switch (*t) + { + case _C_ID: + { + id v = *(id *)((char *)o + offset); + + if (nil == v) + { + fprintf(fptr, "nil\n"); + } + else + { + fprintf(fptr, "%s instance %p\n", + GSNameFromClass(GSObjCClass(v)), v); + } + } + break; + + case _C_CLASS: + { + Class v = *(Class *)((char *)o + offset); + + if (Nil == v) + { + fprintf(fptr, "Nil\n"); + } + else + { + fprintf(fptr, "%s class %p\n", GSNameFromClass(v), v); + } + } + break; + + case _C_CHR: + { + signed char v = *(char *)((char *)o + offset); + + fprintf(fptr, "%c %d\n", v, (int)v); + } + break; + + case _C_UCHR: + { + unsigned char v = *(unsigned char *)((char *)o + offset); + + fprintf(fptr, "%c %u\n", v, (unsigned)v); + } + break; + +#if __GNUC__ > 2 && defined(_C_BOOL) + case _C_BOOL: + { + _Bool v = *(_Bool *)((char *)o + offset); + + fprintf(fptr, "%s %u\n", (v ? "YES" : "NO"), (unsigned)v); + } + break; +#endif + + case _C_SHT: + { + short v = *(short *)((char *)o + offset); + + fprintf(fptr, "%hd\n", v); + } + break; + + case _C_USHT: + { + unsigned short v; + + v = *(unsigned short *)((char *)o + offset); + fprintf(fptr, "%hu\n", v); + } + break; + + case _C_INT: + { + int v = *(int *)((char *)o + offset); + + fprintf(fptr, "%d\n", v); + } + break; + + case _C_UINT: + { + unsigned int v = *(unsigned int *)((char *)o + offset); + + fprintf(fptr, "%u\n", v); + } + break; + + case _C_LNG: + { + long v = *(long *)((char *)o + offset); + + fprintf(fptr, "%ld\n", v); + } + break; + + case _C_ULNG: + { + unsigned long v = *(unsigned long *)((char *)o + offset); + + fprintf(fptr, "%lu\n", v); + } + break; + +#ifdef _C_LNG_LNG + case _C_LNG_LNG: + { + long long v = *(long long *)((char *)o + offset); + + fprintf(fptr, "%lld\n", v); + } + break; +#endif + +#ifdef _C_ULNG_LNG + case _C_ULNG_LNG: + { + unsigned long long v; + + v = *(unsigned long long *)((char *)o + offset); + fprintf(fptr, "%llu\n", v); + } + break; +#endif + + case _C_FLT: + { + float v = *(float *)((char *)o + offset); + + fprintf(fptr, "%g\n", v); + } + break; + + case _C_DBL: + { + double v = *(double *)((char *)o + offset); + + fprintf(fptr, "%g\n", v); + } + break; + + case _C_VOID: + { + fprintf(fptr, "void ???\n"); + } + break; + + case _C_STRUCT_B: + { + fprintf(fptr, "struct not supported\n"); + } + break; + + default: + { + fprintf(fptr, "type %s not supported\n", type); + } + break; + } + } + if (ivars != NULL) + { + free(ivars); + } + c = class_getSuperclass(c); + } + if (NULL == item) + { + fprintf(fptr, "}\n"); + } +} + diff --git a/Source/Additions/Makefile.preamble b/Source/Additions/Makefile.preamble index de2bafb8a..b240868be 100644 --- a/Source/Additions/Makefile.preamble +++ b/Source/Additions/Makefile.preamble @@ -67,4 +67,4 @@ ifeq ($(FOUNDATION_LIB),gnu) endif # Additional LDFLAGS to pass to the linker -ADDITIONAL_LDFLAGS = +ADDITIONAL_LDFLAGS = diff --git a/Source/GNUmakefile b/Source/GNUmakefile index 066e60253..832c85591 100644 --- a/Source/GNUmakefile +++ b/Source/GNUmakefile @@ -331,7 +331,6 @@ NSUbiquitousKeyValueStore.m \ NSUnarchiver.m \ NSUndoManager.m \ NSURL.m \ -NSURLSession.m \ NSURLAuthenticationChallenge.m \ NSURLCache.m \ NSURLCredential.m \ @@ -362,33 +361,53 @@ objc-load.m ifneq ($(GNUSTEP_TARGET_OS), mingw32) ifneq ($(GNUSTEP_TARGET_OS), mingw64) -BASE_MFILES += \ -GSFileHandle.m \ -NSMessagePort.m \ -NSMessagePortNameServer.m + BASE_MFILES += \ + GSFileHandle.m \ + NSMessagePort.m \ + NSMessagePortNameServer.m endif endif +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 +endif +endif +endif + ifeq ($(GNUSTEP_BASE_HAVE_MDNS), 1) - BASE_MFILES += NSNetServices.m \ - GSMDNSNetServices.m + BASE_MFILES += \ + GSMDNSNetServices.m \ + NSNetServices.m endif ifeq ($(GNUSTEP_BASE_HAVE_AVAHI), 1) - BASE_MFILES += NSNetServices.m \ - GSAvahiNetService.m \ - GSAvahiNetServiceBrowser.m \ - GSAvahiClient.m \ - GSAvahiRunLoopIntegration.m + BASE_MFILES += \ + GSAvahiNetService.m \ + GSAvahiNetServiceBrowser.m \ + GSAvahiClient.m \ + GSAvahiRunLoopIntegration.m \ + NSNetServices.m endif ifeq ($(WITH_FFI),libffi) -GNU_MFILES += cifframe.m -BASE_MFILES += GSFFIInvocation.m + GNU_MFILES += cifframe.m + BASE_MFILES += GSFFIInvocation.m endif ifeq ($(WITH_FFI),ffcall) -GNU_MFILES += callframe.m -BASE_MFILES += GSFFCallInvocation.m + GNU_MFILES += callframe.m + BASE_MFILES += GSFFCallInvocation.m endif BASE_OTHER_SRCFILES = \ diff --git a/Source/GSEasyHandle.h b/Source/GSEasyHandle.h new file mode 100644 index 000000000..ffe0de589 --- /dev/null +++ b/Source/GSEasyHandle.h @@ -0,0 +1,236 @@ +#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 new file mode 100644 index 000000000..3df174162 --- /dev/null +++ b/Source/GSEasyHandle.m @@ -0,0 +1,756 @@ +#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) +{ + if (!userdata) + { + return 0; + } + + GSEasyHandle *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) +{ + if (!userdata) + { + return 0; + } + + GSEasyHandle *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) +{ + if (!userdata) + { + return 0; + } + + GSEasyHandle *handle = (GSEasyHandle*)userdata; + double length; + + [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) +{ + if (!userdata) + { + return CURL_SEEKFUNC_FAIL; + } + + GSEasyHandle *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) +{ + if (!userptr) + { + return 0; + } + + if (CURLINFO_SSL_DATA_OUT == type || CURLINFO_SSL_DATA_IN == type) + { + return 0; // Don't log encrypted data here + } + + NSURLSessionTask *task = (NSURLSessionTask*)userptr; + NSString *text = @""; + NSURLRequest *o = [task originalRequest]; + NSURLRequest *r = [task currentRequest]; + id 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, [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])) + { + _rawHandle = curl_easy_init(); + _delegate = delegate; + + char *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); + DESTROY(_timeoutTimer); + DESTROY(_URL); + [super dealloc]; +} + +- (CURL*) rawHandle +{ + return _rawHandle; +} + +- (char*) errorBuffer +{ + return _errorBuffer; +} + +- (GSTimeoutSource*) timeoutTimer +{ + return _timeoutTimer; +} + +- (void) setTimeoutTimer: (GSTimeoutSource*)timer +{ + ASSIGN(_timeoutTimer, timer); +} + +- (NSURL*) URL +{ + return _URL; +} + +- (void) transferCompletedWithError: (NSError*)error +{ + [_delegate transferCompletedWithError: error]; +} + +- (void) resetTimer +{ + // simply create a new timer with the same queue, timeout and handler + // this must cancel the old handler and reset the timer + DESTROY(_timeoutTimer); + _timeoutTimer = [[GSTimeoutSource alloc] initWithQueue: [_timeoutTimer queue] + milliseconds: [_timeoutTimer milliseconds] + handler: [_timeoutTimer handler]]; +} + +- (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_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, self)); + 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 = nil; + + if (0 == port) + { + value = [NSString stringWithFormat: @"%@::%@", originHost, host]; + } + else + { + value = [NSString stringWithFormat: @"%@:%lu:%@", + originHost, port, host]; + } + + struct curl_slist *connect_to = NULL; + 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/GSHTTPURLHandle.m b/Source/GSHTTPURLHandle.m index b1d59b639..1097e5e78 100644 --- a/Source/GSHTTPURLHandle.m +++ b/Source/GSHTTPURLHandle.m @@ -108,6 +108,7 @@ static NSString *httpVersion = @"1.1"; BOOL debug; BOOL keepalive; BOOL returnAll; + id ioDelegate; unsigned char challenged; NSFileHandle *sock; NSTimeInterval cacheAge; @@ -135,6 +136,7 @@ static NSString *httpVersion = @"1.1"; } + (void) setMaxCached: (NSUInteger)limit; - (void) _tryLoadInBackground: (NSURL*)fromURL; +- (id) setDebugLogDelegate: (id)d; @end /** @@ -601,7 +603,15 @@ debugWrite(GSHTTPURLHandle *handle, NSData *data) /* * Send request to server. */ - if (YES == debug) debugWrite(self, buf); + if (debug) + { + if (NO == [ioDelegate putBytes: [buf bytes] + ofLength: [buf length] + byHandle: self]) + { + debugWrite(self, buf); + } + } [sock writeInBackgroundAndNotify: buf]; RELEASE(buf); RELEASE(s); @@ -621,8 +631,16 @@ debugWrite(GSHTTPURLHandle *handle, NSData *data) if (debug) NSLog(@"%@ %p %s", NSStringFromSelector(_cmd), self, keepalive?"K":""); d = [dict objectForKey: NSFileHandleNotificationDataItem]; - if (YES == debug) debugRead(self, d); readCount = [d length]; + if (debug) + { + if (NO == [ioDelegate getBytes: [d bytes] + ofLength: readCount + byHandle: self]) + { + debugRead(self, d); + } + } if (connectionState == idle) { @@ -631,11 +649,32 @@ debugWrite(GSHTTPURLHandle *handle, NSData *data) * it should just be the connection being closed by the other * end because of a timeout etc. */ - if (YES == debug && [d length] != 0) + if (debug) { - NSLog(@"%@ %p %s Unexpected data (%*.*s) from remote!", - NSStringFromSelector(_cmd), self, keepalive?"K":"", - (int)[d length], (int)[d length], [d bytes]); + NSUInteger length = [d length]; + + if (length > 0) + { + if (nil == ioDelegate) + { + NSLog(@"%@ %p %s Unexpected data (%*.*s) from remote!", + NSStringFromSelector(_cmd), self, keepalive?"K":"", + (int)[d length], (int)[d length], (char*)[d bytes]); + } + else + { + NSLog(@"%@ %p %s Unexpected data from remote!", + NSStringFromSelector(_cmd), self, keepalive?"K":""); + if (NO == [ioDelegate getBytes: [d bytes] + ofLength: length + byHandle: self]) + { + NSLog(@"%@ %p %s (%*.*s)", + NSStringFromSelector(_cmd), self, keepalive?"K":"", + (int)[d length], (int)[d length], (char*)[d bytes]); + } + } + } } [nc removeObserver: self name: nil object: sock]; [sock closeFile]; @@ -643,7 +682,7 @@ debugWrite(GSHTTPURLHandle *handle, NSData *data) } else if ([parser parse: d] == NO && [parser isComplete] == NO) { - if (YES == debug) + if (debug) { NSLog(@"HTTP parse failure - %@", parser); } @@ -874,7 +913,7 @@ debugWrite(GSHTTPURLHandle *handle, NSData *data) * lost in the network or the remote end received it and * the response was lost. */ - if (YES == debug) + if (debug) { NSLog(@"HTTP response not received - %@", parser); } @@ -906,7 +945,15 @@ debugWrite(GSHTTPURLHandle *handle, NSData *data) NSLog(@"%@ %p %s", NSStringFromSelector(_cmd), self, keepalive?"K":""); } d = [dict objectForKey: NSFileHandleNotificationDataItem]; - if (YES == debug) debugRead(self, d); + if (debug) + { + if (NO == [ioDelegate getBytes: [d bytes] + ofLength: [d length] + byHandle: self]) + { + debugRead(self, d); + } + } if ([d length] > 0) { @@ -1071,7 +1118,15 @@ debugWrite(GSHTTPURLHandle *handle, NSData *data) object: sock]; buf = [cmd dataUsingEncoding: NSASCIIStringEncoding]; - if (YES == debug) debugWrite(self, buf); + if (debug) + { + if (NO == [ioDelegate putBytes: [buf bytes] + ofLength: [buf length] + byHandle: self]) + { + debugWrite(self, buf); + } + } [sock writeInBackgroundAndNotify: buf]; when = [NSDate alloc]; @@ -1365,6 +1420,16 @@ debugWrite(GSHTTPURLHandle *handle, NSData *data) return old; } +- (id) setDebugLogDelegate: (id)d +{ + id old = ioDelegate; + + NSAssert(nil == d || [d conformsToProtocol: @protocol(GSLogDelegate)], + NSInvalidArgumentException); + ioDelegate = d; + return old; +} + - (void) setReturnAll: (BOOL)flag { returnAll = flag; diff --git a/Source/GSHTTPURLProtocol.h b/Source/GSHTTPURLProtocol.h new file mode 100644 index 000000000..78f8ff2da --- /dev/null +++ b/Source/GSHTTPURLProtocol.h @@ -0,0 +1,9 @@ +#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 new file mode 100644 index 000000000..9ca820d07 --- /dev/null +++ b/Source/GSHTTPURLProtocol.m @@ -0,0 +1,1064 @@ +#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/NSURLSession.h" +#import "Foundation/NSValue.h" + + +@interface NSURLSessionTask (Internal) + +- (void) setCountOfBytesExpectedToReceive: (int64_t)count; + +- (void) setCountOfBytesExpectedToSend: (int64_t)count; + +- (dispatch_queue_t) workQueue; + +@end + +@implementation NSURLSessionTask (Internal) + +- (void) setCountOfBytesExpectedToReceive: (int64_t)count +{ + _countOfBytesExpectedToReceive = count; +} + +- (void) setCountOfBytesExpectedToSend: (int64_t)count +{ + _countOfBytesExpectedToSend = count; +} + +- (GSURLSessionTaskBody*) knownBody +{ + return _knownBody; +} + +- (dispatch_queue_t) workQueue +{ + return _workQueue; +} + +@end + +@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) canInitWithRequest: (NSURLRequest*)request +{ + 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]; + + 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; + } + } + + BOOL 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 = [NSMutableDictionary dictionary]; + 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]]; + + NSArray *curlHeaders = [self curlHeadersForHTTPHeaders: hh]; + if ([[request HTTPMethod] isEqualToString:@"POST"] + && [[request HTTPBody] length] > 0 + && [request valueForHTTPHeaderField: @"Content-Type"] == nil) + { + NSMutableArray *temp = [curlHeaders mutableCopy]; + [temp addObject: @"Content-Type:application/x-www-form-urlencoded"]; + curlHeaders = temp; + } + [_easyHandle setCustomHeaders: curlHeaders]; + RELEASE(curlHeaders); + + NSInteger timeoutInterval = [request timeoutInterval] * 1000; + GSTimeoutSource *timeoutTimer; + + timeoutTimer = [[GSTimeoutSource alloc] initWithQueue: [task workQueue] + milliseconds: timeoutInterval + 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]; + } + }]; + [_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; + + 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; + } + + NSMutableURLRequest *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 +- (NSArray*) 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 AUTORELEASE([result copy]); +} + +// 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 new file mode 100644 index 000000000..b370075dc --- /dev/null +++ b/Source/GSMultiHandle.h @@ -0,0 +1,91 @@ +#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; +- (void) createReadSourceWithSocket: (curl_socket_t)socket + queue: (dispatch_queue_t)queue + handler: (dispatch_block_t)handler; +- (void) createWriteSourceWithSocket: (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 new file mode 100644 index 000000000..125451caf --- /dev/null +++ b/Source/GSMultiHandle.m @@ -0,0 +1,514 @@ +#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" + +@interface GSMultiHandle () +- (void) performActionForSocket: (int)socket; +- (void) readAndWriteAvailableDataOnSocket: (int)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; + + DESTROY(_timeoutSource); + + 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) + { + DESTROY(_timeoutSource); + } + else if (0 == value) + { + DESTROY(_timeoutSource); + dispatch_async(_queue, + ^{ + [self timeoutTimerFired]; + }); + } + else + { + if (nil == _timeoutSource || value != [_timeoutSource milliseconds]) + { + DESTROY(_timeoutSource); + _timeoutSource = [[GSTimeoutSource alloc] initWithQueue: _queue + milliseconds: value + handler: ^{ + [self timeoutTimerFired]; + }]; + } + } +} + +- (void) performActionForSocket: (int)socket +{ + [self readAndWriteAvailableDataOnSocket: socket]; +} + +- (void)timeoutTimerFired +{ + [self readAndWriteAvailableDataOnSocket: CURL_SOCKET_TIMEOUT]; +} + +- (void) readAndWriteAvailableDataOnSocket: (int)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}]; + 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); + } + + if (nil != socketSources) + { + [socketSources createSourcesWithAction: action + socket: socket + queue: _queue + handler: ^{ + [self performActionForSocket: 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 GSSocketRegisterActionTypeNone: + return false; + case GSSocketRegisterActionTypeRegisterRead: + return true; + case GSSocketRegisterActionTypeRegisterWrite: + return false; + case GSSocketRegisterActionTypeRegisterReadAndWrite: + return true; + case GSSocketRegisterActionTypeUnregister: + return false; + } +} + +- (BOOL) needsWriteSource +{ + switch (self.type) + { + case GSSocketRegisterActionTypeNone: + return false; + case GSSocketRegisterActionTypeRegisterRead: + return false; + case GSSocketRegisterActionTypeRegisterWrite: + return true; + case GSSocketRegisterActionTypeRegisterReadAndWrite: + return true; + case GSSocketRegisterActionTypeUnregister: + return false; + } +} + +- (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 ([action needsReadSource]) + { + [self createReadSourceWithSocket: socket queue: queue handler: handler]; + } + + if ([action needsWriteSource]) + { + [self createWriteSourceWithSocket: socket queue: queue handler: handler]; + } +} + +- (void) createReadSourceWithSocket: (curl_socket_t)socket + queue: (dispatch_queue_t)queue + handler: (dispatch_block_t)handler +{ + dispatch_source_t s; + + if (_readSource) + { + return; + } + + s = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket, 0, queue); + dispatch_source_set_event_handler(s, handler); + _readSource = s; + dispatch_resume(s); +} + +- (void) createWriteSourceWithSocket: (curl_socket_t)socket + queue: (dispatch_queue_t)queue + handler: (dispatch_block_t)handler +{ + dispatch_source_t s; + + if (_writeSource) + { + return; + } + + s = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket, 0, queue); + dispatch_source_set_event_handler(s, handler); + _writeSource = s; + dispatch_resume(s); +} + ++ (instancetype) from: (void*)socketSourcePtr +{ + if (!socketSourcePtr) + { + return nil; + } + else + { + return (GSSocketSources*)socketSourcePtr; + } +} + +@end diff --git a/Source/GSNativeProtocol.h b/Source/GSNativeProtocol.h new file mode 100644 index 000000000..487ad902a --- /dev/null +++ b/Source/GSNativeProtocol.h @@ -0,0 +1,91 @@ +#ifndef INCLUDED_GSNATIVEPROTOCOL_H +#define INCLUDED_GSNATIVEPROTOCOL_H + +#import "GSEasyHandle.h" +#import "Foundation/NSURLProtocol.h" + +@class GSTransferState; + +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 new file mode 100644 index 000000000..94be34470 --- /dev/null +++ b/Source/GSNativeProtocol.m @@ -0,0 +1,768 @@ +#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" + + +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 (Internal) + +- (void) removeHandle: (GSEasyHandle*)handle; + +- (void) addHandle: (GSEasyHandle*)handle; + +@end + +@implementation NSURLSession (Internal) + +- (void) removeHandle: (GSEasyHandle*)handle +{ + [_multiHandle removeHandle: handle]; +} + +- (void) addHandle: (GSEasyHandle*)handle +{ + [_multiHandle addHandle: handle]; +} + +@end + +@interface NSURLSessionTask (Internal) + +- (void) setCurrentRequest: (NSURLRequest*)request; + +- (dispatch_queue_t) workQueue; + +- (NSUInteger) suspendCount; + +- (void) getBodyWithCompletion: (void (^)(GSURLSessionTaskBody *body))completion; + +- (void) setKnownBody: (GSURLSessionTaskBody*)body; + +- (void) setError: (NSError*)error; + +@end + +@implementation NSURLSessionTask (Internal) + +- (void) setCurrentRequest: (NSURLRequest*)request +{ + ASSIGN(_currentRequest, request); +} + +- (dispatch_queue_t) workQueue +{ + return _workQueue; +} + +- (NSUInteger) suspendCount +{ + return _suspendCount; +} + +- (void) getBodyWithCompletion: (void (^)(GSURLSessionTaskBody *body))completion +{ + if (nil != _knownBody) + { + completion(_knownBody); + return; + }; + + GSURLSessionTaskBody *body = AUTORELEASE([[GSURLSessionTaskBody alloc] init]); + completion(body); +} + +- (void) setKnownBody: (GSURLSessionTaskBody*)body +{ + ASSIGN(_knownBody, body); +} + +- (void) setError: (NSError*)error +{ + ASSIGN(_error, error); +} + +@end + +@implementation GSCompletionAction + +- (void) dealloc +{ + DESTROY(_redirectRequest); + [super dealloc]; +} + +- (GSCompletionActionType) type +{ + return _type; +} + +- (void) setType: (GSCompletionActionType) type +{ + _type = type; +} + +- (int) errorCode +{ + return _errorCode; +} + +- (void) setErrorCode: (int)code +{ + _errorCode = code; +} + +- (NSURLRequest*) redirectRequest +{ + return _redirectRequest; +} + +- (void) setRedirectRequest: (NSURLRequest*)request +{ + ASSIGN(_redirectRequest, request); +} + +@end + +@implementation GSNativeProtocol + +- (instancetype) initWithTask: (NSURLSessionTask*)_task + cachedResponse: (NSCachedURLResponse*)_cachedResponse + client: (id)_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 delegate need. +- (GSDataDrain*) createTransferBodyDataDrain +{ + NSURLSession *s = [[self task] session]; + GSDataDrain *dd = AUTORELEASE([[GSDataDrain alloc] init]); + + if (nil != [s delegate]) + { + // Data will be forwarded to the delegate as we receive it, we don't + // need to do anything about it. + [dd setType: GSDataDrainTypeIgnore]; + return dd; + } + else + { + [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 +{ + NSURLSessionTask *task; + id delegate; + + task = [self task]; + + NSAssert(nil != task, @"Cannot notify"); + + delegate = [[task session] delegate]; + if (nil != delegate + && [task isKindOfClass: [NSURLSessionDataTask class]] + && [delegate respondsToSelector: @selector(URLSession:dataTask:didReceiveData:)]) + { + id dataDelegate; + NSURLSessionDataTask *dataTask; + NSURLSession *session; + + session = [task session]; + NSAssert(nil != session, @"Missing session"); + dataDelegate = (id)delegate; + dataTask = (NSURLSessionDataTask*)task; + [[session delegateQueue] addOperationWithBlock: + ^{ + [dataDelegate URLSession: session + dataTask: dataTask + didReceiveData: data]; + }]; + } +} + +- (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]; + + // because we deregister the task with the session on internalState being set + // to taskCompleted we need to do the latter after the delegate/handler was + // notified/invoked + bodyDataDrain = [_transferState bodyDataDrain]; + if (GSDataDrainInMemory == [bodyDataDrain type]) + { + NSData *data; + + if (nil != [bodyDataDrain data]) + { + data = [NSData dataWithData: [bodyDataDrain data]]; + } + else + { + data = [NSData data]; + } + + if ([client respondsToSelector: @selector(URLProtocol:didLoadData:)]) + { + [client URLProtocol: self didLoadData: data]; + } + [self setInternalState: GSNativeProtocolInternalStateTaskCompleted]; + } + + 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/GSTLS.m b/Source/GSTLS.m index dbacd7dfe..d4c812afc 100644 --- a/Source/GSTLS.m +++ b/Source/GSTLS.m @@ -2180,11 +2180,6 @@ retrieve_callback(gnutls_session_t session, [str appendFormat: _(@"- Certificate Type: %s\n"), tmp]; } - /* print the compression algorithm (if any) - */ - tmp = gnutls_compression_get_name(gnutls_compression_get(session)); - [str appendFormat: _(@"- Compression: %s\n"), tmp]; - /* print the name of the cipher used. * eg 3DES. */ diff --git a/Source/GSTaskRegistry.h b/Source/GSTaskRegistry.h new file mode 100644 index 000000000..610339a44 --- /dev/null +++ b/Source/GSTaskRegistry.h @@ -0,0 +1,30 @@ +#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 new file mode 100644 index 000000000..07e99f655 --- /dev/null +++ b/Source/GSTaskRegistry.m @@ -0,0 +1,98 @@ +#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 new file mode 100644 index 000000000..6743f1e89 --- /dev/null +++ b/Source/GSTimeoutSource.h @@ -0,0 +1,32 @@ +#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 _rawSource; + NSInteger _milliseconds; + dispatch_queue_t _queue; + dispatch_block_t _handler; +} + +- (NSInteger) milliseconds; + +- (dispatch_queue_t) queue; + +- (dispatch_block_t) handler; + +- (instancetype) initWithQueue: (dispatch_queue_t)queue + milliseconds: (NSInteger)milliseconds + handler: (dispatch_block_t)handler; + +@end + +#endif diff --git a/Source/GSTimeoutSource.m b/Source/GSTimeoutSource.m new file mode 100644 index 000000000..26b05199e --- /dev/null +++ b/Source/GSTimeoutSource.m @@ -0,0 +1,48 @@ +#import "GSTimeoutSource.h" + +@implementation GSTimeoutSource + +- (instancetype) initWithQueue: (dispatch_queue_t)queue + milliseconds: (NSInteger)milliseconds + handler: (dispatch_block_t)handler +{ + if (nil != (self = [super init])) + { + _queue = queue; + _handler = handler; + _milliseconds = milliseconds; + _rawSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue); + + uint64_t delay = MAX(1, milliseconds - 1); + + dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_MSEC); + + dispatch_source_set_timer(_rawSource, start, delay * NSEC_PER_MSEC, _milliseconds == 1 ? 1 * NSEC_PER_USEC : 1 * NSEC_PER_MSEC); + dispatch_source_set_event_handler(_rawSource, _handler); + dispatch_resume(_rawSource); + } + return self; +} + +- (void) dealloc +{ + dispatch_source_cancel(_rawSource); + [super dealloc]; +} + +- (NSInteger) milliseconds +{ + return _milliseconds; +} + +- (dispatch_queue_t) queue +{ + return _queue; +} + +- (dispatch_block_t) handler +{ + return _handler; +} + +@end \ No newline at end of file diff --git a/Source/GSTransferState.h b/Source/GSTransferState.h new file mode 100644 index 000000000..1a89e1c3e --- /dev/null +++ b/Source/GSTransferState.h @@ -0,0 +1,142 @@ +#ifndef INCLUDED_GSTRANSFERTSTATE_H +#define INCLUDED_GSTRANSFERTSTATE_H + +#import "GSURLSessionTaskBodySource.h" + + +@class GSURLSessionTaskBodySource; +@class NSArray; +@class NSData; +@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; + NSData *_data; + NSURL *_fileURL; + NSFileHandle *_fileHandle; +} + +- (GSDataDrainType) type; +- (void) setType: (GSDataDrainType)type; + +- (NSData*) data; +- (void) setData: (NSData*)data; + +- (NSURL*) fileURL; +- (void) setFileURL: (NSURL*)url; + +- (NSFileHandle*) fileHandle; +- (void) setFileHandle: (NSFileHandle*)handle; + +@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 new file mode 100644 index 000000000..bd1ee2471 --- /dev/null +++ b/Source/GSTransferState.m @@ -0,0 +1,487 @@ +#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" + +#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; +} + +- (NSData*) data +{ + return _data; +} + +- (void) setData: (NSData*)data +{ + ASSIGN(_data, data); +} + +- (NSURL*) fileURL +{ + return _fileURL; +} + +- (void) setFileURL: (NSURL*)url +{ + ASSIGN(_fileURL, url); +} + +- (NSFileHandle*) fileHandle +{ + return _fileHandle; +} + +- (void) setFileHandle: (NSFileHandle*)handle +{ + ASSIGN(_fileHandle, handle); +} + +@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: + { + NSMutableData *data; + GSDataDrain *dataDrain; + GSTransferState *ts; + + data = [_bodyDataDrain data] ? + AUTORELEASE([[_bodyDataDrain data] mutableCopy]) + : [NSMutableData data]; + + [data appendData: bodyData]; + dataDrain = AUTORELEASE([[GSDataDrain alloc] init]); + [dataDrain setType: GSDataDrainInMemory]; + [dataDrain setData: data]; + + ts = [[GSTransferState alloc] initWithURL: _url + parsedResponseHeader: _parsedResponseHeader + response: _response + bodySource: _requestBodySource + bodyDataDrain: dataDrain]; + return AUTORELEASE(ts); + } + 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 9738a262e..afb5a1e09 100644 --- a/Source/GSURLPrivate.h +++ b/Source/GSURLPrivate.h @@ -58,6 +58,7 @@ @interface NSURLRequest (Private) - (BOOL) _debug; +- (id) _debugLogDelegate; - (id) _propertyForKey: (NSString*)key; - (void) _setProperty: (id)value forKey: (NSString*)key; @end @@ -72,11 +73,23 @@ @interface NSURLProtocol (Private) -+ (Class) _classToHandleRequest:(NSURLRequest *)request; ++ (Class) _classToHandleRequest: (NSURLRequest *)request; ++ (id) _ProtocolClient; @end -/* - * Internal class for handling HTTP authentication + +/* Internal class for handling HTTP protocol client messages + */ +@interface _NSURLProtocolClient : NSObject +{ + NSURLRequestCachePolicy _cachePolicy; + NSMutableArray *_cacheableData; + NSURLResponse *_cacheableResponse; +} +@end + + +/* Internal class for handling HTTP authentication */ @class NSLock; @interface GSHTTPAuthentication : NSObject diff --git a/Source/GSURLSessionTaskBody.h b/Source/GSURLSessionTaskBody.h new file mode 100644 index 000000000..681fa5819 --- /dev/null +++ b/Source/GSURLSessionTaskBody.h @@ -0,0 +1,46 @@ +#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 new file mode 100644 index 000000000..34c5e32c9 --- /dev/null +++ b/Source/GSURLSessionTaskBody.m @@ -0,0 +1,111 @@ +#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 new file mode 100644 index 000000000..142dc31db --- /dev/null +++ b/Source/GSURLSessionTaskBodySource.h @@ -0,0 +1,52 @@ +#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 new file mode 100644 index 000000000..8d4ddaedc --- /dev/null +++ b/Source/GSURLSessionTaskBodySource.m @@ -0,0 +1,152 @@ +#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/NSConcretePointerFunctions.m b/Source/NSConcretePointerFunctions.m index 58ab5ced9..b2c28d930 100644 --- a/Source/NSConcretePointerFunctions.m +++ b/Source/NSConcretePointerFunctions.m @@ -62,7 +62,7 @@ acquireExistingMemory(const void *item, static NSString* describeString(const void *item) { - return [NSString stringWithFormat: @"%s", item]; + return [NSString stringWithFormat: @"%s", (char*)item]; } static NSString* diff --git a/Source/NSException.m b/Source/NSException.m index 35503b89b..c5bc28f5a 100644 --- a/Source/NSException.m +++ b/Source/NSException.m @@ -687,6 +687,14 @@ recover(int sig) siglongjmp(jbuf()->buf, 1); } +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wframe-address" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wframe-address" +#endif + void * NSFrameAddress(NSUInteger offset) { @@ -873,6 +881,12 @@ NSReturnAddress(NSUInteger offset) return env->addr; } +#ifdef __clang__ +#pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif + unsigned GSPrivateReturnAddresses(NSUInteger **returns) { diff --git a/Source/NSMessagePort.m b/Source/NSMessagePort.m index 34c7df66d..b95d524ae 100644 --- a/Source/NSMessagePort.m +++ b/Source/NSMessagePort.m @@ -1412,7 +1412,7 @@ typedef struct { desc = [NSString stringWithFormat: @"<%s %p file name %s>", - GSClassNameFromObject(self), self, [name bytes]]; + GSClassNameFromObject(self), self, (char*)[name bytes]]; return desc; } diff --git a/Source/NSSocketPort.m b/Source/NSSocketPort.m index 19eef1f3e..a3c89d5b3 100644 --- a/Source/NSSocketPort.m +++ b/Source/NSSocketPort.m @@ -1469,7 +1469,7 @@ static Class tcpPortClass; * Look up an existing NSSocketPort given a host and number */ + (NSSocketPort*) existingPortWithNumber: (uint16_t)number - onHost: (NSHost*)aHost + onHost: (NSHost*)aHost { NSSocketPort *port = nil; NSMapTable *thePorts; @@ -1763,6 +1763,9 @@ static Class tcpPortClass; { NSDebugMLLog(@"NSPort", @"NSSocketPort 0x%"PRIxPTR" finalized", (NSUInteger)self); + M_LOCK(tcpPortLock); + NSMapRemove(tcpPortMap, (void*)(uintptr_t)portNum); + M_UNLOCK(tcpPortLock); [self invalidate]; if (handles != 0) { diff --git a/Source/NSURLCache.m b/Source/NSURLCache.m index 056afc560..aaccdcc67 100644 --- a/Source/NSURLCache.m +++ b/Source/NSURLCache.m @@ -24,6 +24,10 @@ #import "common.h" +#if GS_HAVE_NSURLSESSION +#import +#endif + #define EXPOSE_NSURLCache_IVARS 1 #import "GSURLPrivate.h" @@ -215,3 +219,27 @@ static NSURLCache *shared = nil; @end +#if GS_HAVE_NSURLSESSION +@implementation NSURLCache (NSURLSessionTaskAdditions) + +- (void) storeCachedResponse: (NSCachedURLResponse*)cachedResponse + forDataTask: (NSURLSessionDataTask*)dataTask +{ + [self storeCachedResponse: cachedResponse + forRequest: [dataTask currentRequest]]; +} + +- (NSCachedURLResponse*) cachedResponseForDataTask: + (NSURLSessionDataTask*)dataTask +{ + return [self cachedResponseForRequest: [dataTask currentRequest]]; +} + +- (void) removeCachedResponseForDataTask: (NSURLSessionDataTask*)dataTask +{ + [self removeCachedResponseForRequest: [dataTask currentRequest]]; +} + +@end +#endif + diff --git a/Source/NSURLConnection.m b/Source/NSURLConnection.m index c3ba2e1e1..28f60def1 100644 --- a/Source/NSURLConnection.m +++ b/Source/NSURLConnection.m @@ -169,6 +169,11 @@ typedef struct DESTROY(this->_delegate); } +- (id) delegate +{ + return this->_delegate; +} + - (void) scheduleInRunLoop: (NSRunLoop *)aRunLoop forMode: (NSRunLoopMode)mode { @@ -209,7 +214,9 @@ typedef struct } } -- (id) initWithRequest: (NSURLRequest *)request delegate: (id)delegate startImmediately: (BOOL)startImmediately +- (id) initWithRequest: (NSURLRequest *)request + delegate: (id)delegate + startImmediately: (BOOL)startImmediately { if ((self = [super init]) != nil) { diff --git a/Source/NSURLProtocol.m b/Source/NSURLProtocol.m index 2eb10d00e..5ce726649 100644 --- a/Source/NSURLProtocol.m +++ b/Source/NSURLProtocol.m @@ -25,13 +25,19 @@ #import "common.h" -#define EXPOSE_NSURLProtocol_IVARS 1 +//#define EXPOSE_NSURLProtocol_IVARS 1 #import "Foundation/NSError.h" #import "Foundation/NSHost.h" #import "Foundation/NSNotification.h" #import "Foundation/NSRunLoop.h" #import "Foundation/NSValue.h" +#if GS_HAVE_NSURLSESSION +#import "Foundation/NSURLSession.h" +#else +@class NSURLSessionTask; +#endif + #import "GSPrivate.h" #import "GSURLPrivate.h" #import "GNUstepBase/GSMime.h" @@ -358,6 +364,7 @@ static NSLock *pairLock = nil; NSURLAuthenticationChallenge *_challenge; NSURLCredential *_credential; NSHTTPURLResponse *_response; + id _logDelegate; } @end @@ -367,27 +374,6 @@ static NSLock *pairLock = nil; @interface _NSDataURLProtocol : NSURLProtocol @end - -// Internal data storage -typedef struct { - NSInputStream *input; - NSOutputStream *output; - NSCachedURLResponse *cachedResponse; - id client; - NSURLRequest *request; - NSString *in; - NSString *out; -#if USE_ZLIB - z_stream z; // context for decompress - BOOL compressing; // are we compressing? - BOOL decompressing; // are we decompressing? - NSData *compressed; // only partially decompressed -#endif -} Internal; - -#define this ((Internal*)(self->_NSURLProtocolInternal)) -#define inst ((Internal*)(o->_NSURLProtocolInternal)) - static NSMutableArray *registered = nil; static NSLock *regLock = nil; static Class abstractClass = nil; @@ -419,8 +405,44 @@ static NSURLProtocol *placeholder = nil; } @end + + @implementation NSURLProtocol +#if GS_NONFRAGILE +{ + @protected +#else +// Internal data storage +typedef struct { +#endif + + NSInputStream *input; + NSOutputStream *output; + NSCachedURLResponse *cachedResponse; + id client; + NSURLRequest *request; + NSURLSessionTask *task; + NSString *in; + NSString *out; +#if USE_ZLIB + z_stream z; // context for decompress + BOOL compressing; // are we compressing? + BOOL decompressing; // are we decompressing? + NSData *compressed; // only partially decompressed +#endif + +#if GS_NONFRAGILE +} +#define this self +#define inst o +#else +} Internal; +#define this ((Internal*)(self->_NSURLProtocolInternal)) +#define inst ((Internal*)(o->_NSURLProtocolInternal)) +#endif + + + (id) allocWithZone: (NSZone*)z { NSURLProtocol *o; @@ -508,7 +530,6 @@ static NSURLProtocol *placeholder = nil; { if (this != 0) { - [self stopLoading]; if (this->input != nil) { [this->input setDelegate: nil]; @@ -526,7 +547,11 @@ static NSURLProtocol *placeholder = nil; } DESTROY(this->cachedResponse); DESTROY(this->request); +#if GS_HAVE_NSURLSESSION + DESTROY(this->task); +#endif DESTROY(this->client); + #if USE_ZLIB if (this->compressing == YES) { @@ -538,8 +563,10 @@ static NSURLProtocol *placeholder = nil; } DESTROY(this->compressed); #endif +#if !GS_NONFRAGILE NSZoneFree([self zone], this); _NSURLProtocolInternal = 0; +#endif } [super dealloc]; } @@ -554,6 +581,7 @@ static NSURLProtocol *placeholder = nil; { if ((self = [super init]) != nil) { +#if !GS_NONFRAGILE Class c = object_getClass(self); if (c != abstractClass && c != placeholderClass) @@ -561,13 +589,14 @@ static NSURLProtocol *placeholder = nil; _NSURLProtocolInternal = NSZoneCalloc([self zone], 1, sizeof(Internal)); } +#endif } return self; } -- (id) initWithRequest: (NSURLRequest *)request - cachedResponse: (NSCachedURLResponse *)cachedResponse - client: (id )client +- (id) initWithRequest: (NSURLRequest*)_request + cachedResponse: (NSCachedURLResponse*)_cachedResponse + client: (id )_client { Class c = object_getClass(self); @@ -582,31 +611,57 @@ static NSURLProtocol *placeholder = nil; { Class proto = [registered objectAtIndex: count]; - if ([proto canInitWithRequest: request] == YES) + if ([proto canInitWithRequest: _request] == YES) { self = [proto alloc]; break; } } [regLock unlock]; - return [self initWithRequest: request - cachedResponse: cachedResponse - client: client]; + return [self initWithRequest: _request + cachedResponse: _cachedResponse + client: _client]; } if ((self = [self init]) != nil) { - this->request = [request copy]; - this->cachedResponse = RETAIN(cachedResponse); - this->client = RETAIN(client); + this->request = [_request copy]; + this->cachedResponse = RETAIN(_cachedResponse); + if (nil == _client) + { + _client = [[self class] _ProtocolClient]; + } + this->client = RETAIN(_client); } return self; } +- (instancetype) initWithTask: (NSURLSessionTask*)_task + cachedResponse: (NSCachedURLResponse*)_cachedResponse + client: (id)_client +{ +#if GS_HAVE_NSURLSESSION + if (nil != (self = [self initWithRequest: [_task currentRequest] + cachedResponse: _cachedResponse + client: _client])) + { + ASSIGN(this->task, _task); + } +#else + DESTROY(self); +#endif + return self; +} + - (NSURLRequest *) request { return this->request; } +- (NSURLSessionTask*) task +{ + return this->task; +} + @end @implementation NSURLProtocol (Debug) @@ -643,6 +698,13 @@ static NSURLProtocol *placeholder = nil; return protoClass; } +/* Internal method to return a client to handle callbacks if the protocol + * is initialised without one. + */ ++ (id) _ProtocolClient +{ + return nil; +} @end @implementation NSURLProtocol (Subclassing) @@ -726,7 +788,14 @@ static NSURLProtocol *placeholder = nil; static NSDictionary *methods = nil; _debug = GSDebugSet(@"NSURLProtocol"); - if (YES == [this->request _debug]) _debug = YES; + if (YES == [this->request _debug]) + { + _debug = YES; + } + if (_debug) + { + _logDelegate = [this->request _debugLogDelegate]; + } if (methods == nil) { @@ -795,12 +864,12 @@ static NSURLProtocol *placeholder = nil; } else { - NSMutableURLRequest *request; + NSMutableURLRequest *r; - request = [[this->request mutableCopy] autorelease]; - [request setURL: url]; + r = [[this->request mutableCopy] autorelease]; + [r setURL: url]; [this->client URLProtocol: self - wasRedirectedToRequest: request + wasRedirectedToRequest: r redirectResponse: nil]; } if (NO == _isLoading) @@ -979,7 +1048,12 @@ static NSURLProtocol *placeholder = nil; } if (_debug) { - debugRead(self, readCount, buffer); + if (NO == [_logDelegate getBytes: buffer + ofLength: readCount + byHandle: self]) + { + debugRead(self, readCount, buffer); + } } if (_parser == nil) @@ -991,7 +1065,7 @@ static NSURLProtocol *placeholder = nil; d = [NSData dataWithBytes: buffer length: readCount]; if ([_parser parse: d] == NO && (_complete = [_parser isComplete]) == NO) { - if (_debug == YES) + if (_debug) { NSLog(@"%@ HTTP parse failure - %@", self, _parser); } @@ -1113,12 +1187,12 @@ static NSURLProtocol *placeholder = nil; } else { - NSMutableURLRequest *request; + NSMutableURLRequest *r; - request = [[this->request mutableCopy] autorelease]; - [request setURL: url]; + r = AUTORELEASE([this->request mutableCopy]); + [r setURL: url]; [this->client URLProtocol: self - wasRedirectedToRequest: request + wasRedirectedToRequest: r redirectResponse: _response]; } } @@ -1335,18 +1409,18 @@ static NSURLProtocol *placeholder = nil; } else { - NSMutableURLRequest *request; + NSMutableURLRequest *r; /* To answer the authentication challenge, * we must retry with a modified request and * with the cached response cleared. */ - request = [this->request mutableCopy]; - [request setValue: auth + r = [this->request mutableCopy]; + [r setValue: auth forHTTPHeaderField: @"Authorization"]; [self stopLoading]; - [this->request release]; - this->request = request; + RELEASE(this->request); + this->request = r; DESTROY(this->cachedResponse); [self startLoading]; return; @@ -1426,7 +1500,7 @@ static NSURLProtocol *placeholder = nil; * lost in the network or the remote end received it and * the response was lost. */ - if (_debug == YES) + if (_debug) { NSLog(@"%@ HTTP response not received - %@", self, _parser); } @@ -1460,7 +1534,7 @@ static NSURLProtocol *placeholder = nil; return; case NSStreamEventOpenCompleted: - if (_debug == YES) + if (_debug) { NSLog(@"%@ HTTP input stream opened", self); } @@ -1483,7 +1557,7 @@ static NSURLProtocol *placeholder = nil; NSURL *u; int l; - if (_debug == YES) + if (_debug) { NSLog(@"%@ HTTP output stream opened", self); } @@ -1624,9 +1698,14 @@ static NSURLProtocol *placeholder = nil; maxLength: len - _writeOffset]; if (written > 0) { - if (_debug == YES) + if (_debug) { - debugWrite(self, written, bytes + _writeOffset); + if (NO == [_logDelegate putBytes: bytes + _writeOffset + ofLength: written + byHandle: self]) + { + debugWrite(self, written, bytes + _writeOffset); + } } _writeOffset += written; if (_writeOffset >= len) @@ -1664,7 +1743,7 @@ static NSURLProtocol *placeholder = nil; len = [_body read: buffer maxLength: sizeof(buffer)]; if (len < 0) { - if (_debug == YES) + if (_debug) { NSLog(@"%@ error reading from HTTPBody stream %@", self, [NSError _last]); @@ -1681,9 +1760,14 @@ static NSURLProtocol *placeholder = nil; written = [this->output write: buffer maxLength: len]; if (written > 0) { - if (_debug == YES) + if (_debug) { - debugWrite(self, written, buffer); + if (NO == [_logDelegate putBytes: buffer + ofLength: written + byHandle: self]) + { + debugWrite(self, written, buffer); + } } len -= written; if (len > 0) diff --git a/Source/NSURLRequest.m b/Source/NSURLRequest.m index 7a4db342b..beed313a7 100644 --- a/Source/NSURLRequest.m +++ b/Source/NSURLRequest.m @@ -39,6 +39,7 @@ typedef struct { NSMutableDictionary *headers; BOOL shouldHandleCookies; BOOL debug; + id ioDelegate; NSURL *URL; NSURL *mainDocumentURL; NSURLRequestCachePolicy cachePolicy; @@ -116,6 +117,7 @@ typedef struct { ASSIGN(inst->method, this->method); inst->shouldHandleCookies = this->shouldHandleCookies; inst->debug = this->debug; + inst->ioDelegate = this->ioDelegate; inst->headers = [this->headers mutableCopy]; } } @@ -284,6 +286,16 @@ typedef struct { return old; } +- (id) setDebugLogDelegate: (id)d +{ + id old = this->ioDelegate; + + NSAssert(nil == d || [d conformsToProtocol: @protocol(GSLogDelegate)], + NSInvalidArgumentException); + this->ioDelegate = d; + return old; +} + - (NSTimeInterval) timeoutInterval { return this->timeoutInterval; @@ -444,6 +456,11 @@ typedef struct { return this->debug; } +- (id) _debugLogDelegate +{ + return this->ioDelegate; +} + - (id) _propertyForKey: (NSString*)key { return [this->properties objectForKey: key]; diff --git a/Source/NSURLResponse.m b/Source/NSURLResponse.m index 623ca68d3..3c15ce3f5 100644 --- a/Source/NSURLResponse.m +++ b/Source/NSURLResponse.m @@ -221,6 +221,11 @@ typedef struct { [super dealloc]; } +- (NSString*) description +{ + return [NSString stringWithFormat: @"%@ { URL: %@ } { Status Code: %d, Headers %@ }", [super description], this->URL, this->statusCode, this->headers]; +} + - (void) encodeWithCoder: (NSCoder*)aCoder { // FIXME diff --git a/Source/NSURLSession.m b/Source/NSURLSession.m index 1a947bed1..4a7001fb4 100644 --- a/Source/NSURLSession.m +++ b/Source/NSURLSession.m @@ -1,4 +1,1296 @@ -#import +#import "GSURLPrivate.h" +#import + +#import "GSDispatch.h" +#import "GSMultiHandle.h" +#import "GSEasyHandle.h" +#import "GSTaskRegistry.h" +#import "GSHTTPURLProtocol.h" +#import "GSURLSessionTaskBody.h" + +#import "Foundation/NSError.h" +#import "Foundation/NSException.h" +#import "Foundation/NSOperation.h" +#import "Foundation/NSURLError.h" +#import "Foundation/NSURLSession.h" +#import "Foundation/NSURLRequest.h" +#import "Foundation/NSValue.h" + + +/* 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. + */ +@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; +@end + +@interface NSURLSessionTask (URLProtocolClient) + +@end + +typedef NS_ENUM(NSUInteger, NSURLSessionTaskProtocolState) { + NSURLSessionTaskProtocolStateToBeCreated = 0, + NSURLSessionTaskProtocolStateAwaitingCacheReply = 1, + NSURLSessionTaskProtocolStateExisting = 2, + NSURLSessionTaskProtocolStateInvalidated = 3, +}; + +static dispatch_queue_t _globalVarSyncQ = NULL; +static int sessionCounter = 0; +static int nextSessionIdentifier() +{ + if (NULL == _globalVarSyncQ) + { + _globalVarSyncQ = dispatch_queue_create("org.gnustep.NSURLSession.GlobalVarSyncQ", DISPATCH_QUEUE_SERIAL); + } + dispatch_sync(_globalVarSyncQ, + ^{ + sessionCounter += 1; + }); + return sessionCounter; +} @implementation NSURLSession +{ + int _identifier; + dispatch_queue_t _workQueue; + NSUInteger _nextTaskIdentifier; + BOOL _invalidated; + GSTaskRegistry *_taskRegistry; +} + ++ (NSURLSession *) sessionWithConfiguration: (NSURLSessionConfiguration*)configuration + delegate: (id )delegate + delegateQueue: (NSOperationQueue*)queue +{ + NSURLSession *session; + + session = [[NSURLSession alloc] initWithConfiguration: configuration + delegate: delegate + delegateQueue: queue]; + + return AUTORELEASE(session); +} + +- (instancetype) initWithConfiguration: (NSURLSessionConfiguration*)configuration + delegate: (id )delegate + delegateQueue: (NSOperationQueue*)queue +{ + if (nil != (self = [super init])) + { + char label[30]; + + _taskRegistry = [[GSTaskRegistry alloc] init]; +#if defined(CURLSSLBACKEND_GNUTLS) + curl_global_sslset(CURLSSLBACKEND_GNUTLS, NULL, NULL)l +#endif + curl_global_init(CURL_GLOBAL_SSL); + _identifier = nextSessionIdentifier(); + sprintf(label, "NSURLSession %d", _identifier); + _workQueue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL); + if (nil != queue) + { + ASSIGN(_delegateQueue, queue); + } + else + { + _delegateQueue = [[NSOperationQueue alloc] init]; + [_delegateQueue setMaxConcurrentOperationCount: 1]; + } + _delegate = delegate; + ASSIGN(_configuration, configuration); + _nextTaskIdentifier = 1; + _invalidated = NO; + _multiHandle = [[GSMultiHandle alloc] initWithConfiguration: configuration + workQueue: _workQueue]; + [NSURLProtocol registerClass: [GSHTTPURLProtocol class]]; + } + + return self; +} + +- (void) dealloc +{ + DESTROY(_taskRegistry); + DESTROY(_configuration); + DESTROY(_delegateQueue); + DESTROY(_multiHandle); + [super dealloc]; +} + +- (dispatch_queue_t) workQueue +{ + return _workQueue; +} + +- (NSOperationQueue*) delegateQueue +{ + return _delegateQueue; +} + +- (id ) delegate +{ + return _delegate; +} + +- (NSURLSessionConfiguration*) configuration +{ + 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]) + { + [_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; + } + + [_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]; +} + +- (void) addTask: (NSURLSessionTask*)task +{ + [_taskRegistry addTask: task]; +} + +- (void) removeTask: (NSURLSessionTask*)task +{ + [_taskRegistry removeTask: task]; +} + +@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; + + session = [task session]; + NSAssert(nil != session, @"Missing session"); + + delegateQueue = [session delegateQueue]; + delegate = [session delegate]; + + 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 (NSURLSessionTaskStateCompleted == [task state]) + { + return; + } + + [task setState: NSURLSessionTaskStateCompleted]; + dispatch_async([session workQueue], + ^{ + [session removeTask: task]; + }); + } + + [task invalidateProtocol]; +} + +- (void) URLProtocol: (NSURLProtocol *)protocol + didLoadData: (NSData *)data +{ + NSURLSessionTask *task = [protocol task]; + NSURLSession *session; + NSOperationQueue *delegateQueue; + id delegate; + + NSAssert(nil != task, @"Missing task"); + + session = [task session]; + delegate = [session delegate]; + delegateQueue = [session delegateQueue]; + + switch (_cachePolicy) + { + case NSURLCacheStorageAllowed: + case NSURLCacheStorageAllowedInMemoryOnly: + { + if (nil != _cacheableData) + { + [_cacheableData addObject: data]; + } + break; + } + case NSURLCacheStorageNotAllowed: + break; + } + + if (nil != delegate + && [task isKindOfClass: [NSURLSessionDataTask class]] + && [delegate respondsToSelector: + @selector(URLSession:dataTask:didReceiveData:)]) + { + [delegateQueue addOperationWithBlock: + ^{ + [(id)delegate URLSession: session + dataTask: (NSURLSessionDataTask*)task + didReceiveData: data]; + }]; + } +} + +- (void) URLProtocol: (NSURLProtocol *)protocol + didReceiveAuthenticationChallenge: (NSURLAuthenticationChallenge *)challenge +{ + //FIXME +} + +- (void) URLProtocol: (NSURLProtocol *)protocol + didReceiveResponse: (NSURLResponse *)response + cacheStoragePolicy: (NSURLCacheStoragePolicy)policy +{ + NSURLSessionTask *task = [protocol task]; + NSURLSession *session; + + NSAssert(nil != task, @"Missing task"); + + [task setResponse: response]; + + session = [task session]; + + if (![task isKindOfClass: [NSURLSessionDataTask class]]) + { + 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; + } + } + + id 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) { + NSLog(@"Ignoring disposition from completion handler."); + }]; + } + }]; + } +} + +- (void) URLProtocol: (NSURLProtocol *)protocol + wasRedirectedToRequest: (NSURLRequest *)request + redirectResponse: (NSURLResponse *)redirectResponse +{ + NSAssert(NO, @"The NSURLSession implementation doesn't currently handle redirects directly."); +} + +- (NSURLProtectionSpace*) _protectionSpaceFrom: (NSHTTPURLResponse*)response +{ + NSURLProtectionSpace *space = nil; + NSString *auth; + + 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; + + 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; +} + +- (void) URLProtocolDidFinishLoading: (NSURLProtocol *)protocol +{ + NSURLSessionTask *task = [protocol task]; + NSURLSession *session; + NSHTTPURLResponse *urlResponse; + NSURLCache *cache; + NSOperationQueue *delegateQueue; + id delegate; + + NSAssert(nil != task, @"Missing task"); + + session = [task session]; + urlResponse = (NSHTTPURLResponse*)[task response]; + + if ([urlResponse statusCode] == 401) + { + NSURLProtectionSpace *space; + + if (nil != (space = [self _protectionSpaceFrom: urlResponse])) + { + } + } + + delegate = [session delegate]; + delegateQueue = [session delegateQueue]; + + 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 != delegate) + { + [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]; +} + +- (void) URLProtocol: (NSURLProtocol *)protocol + didCancelAuthenticationChallenge: (NSURLAuthenticationChallenge *)challenge +{ + //FIXME +} + +@end + +@implementation NSURLSessionTask +{ + NSURLSession *_session; /* not retained */ + NSLock *_protocolLock; + NSURLSessionTaskProtocolState _protocolState; + NSURLProtocol *_protocol; + NSMutableArray *_protocolBag; + Class _protocolClass; + BOOL _hasTriggeredResume; +} + +- (instancetype) initWithSession: (NSURLSession*)session + request: (NSURLRequest*)request + taskIdentifier: (NSUInteger)identifier +{ + NSEnumerator *e; + Class protocolClass; + + if (nil != (self = [super init])) + { + NSData *data; + NSInputStream *stream; + + _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]); +#else + _workQueue = dispatch_queue_create("org.gnustep.NSURLSessionTask.WorkQueue", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(_workQueue, [session workQueue]); +#endif + _state = NSURLSessionTaskStateSuspended; + _suspendCount = 1; + _protocolLock = [[NSLock alloc] init]; + _protocolState = NSURLSessionTaskProtocolStateToBeCreated; + _protocol = nil; + _hasTriggeredResume = NO; + e = [[[session configuration] protocolClasses] objectEnumerator]; + while (nil != (protocolClass = [e nextObject])) + { + if ([protocolClass canInitWithRequest: request]) + { + _protocolClass = protocolClass; + } + } + NSAssert(nil != _protocolClass, @"Unsupported protocol"); + } + + return self; +} + +- (void) dealloc +{ + DESTROY(_originalRequest); + DESTROY(_currentRequest); + DESTROY(_response); + DESTROY(_taskDescription); + DESTROY(_error); + DESTROY(_protocolLock); + DESTROY(_protocol); + DESTROY(_protocolBag); + DESTROY(_knownBody); + [super dealloc]; +} + +- (NSURLSessionTaskState) updateTaskState +{ + if (0 == _suspendCount) + { + _state = NSURLSessionTaskStateRunning; + } + else + { + _state = NSURLSessionTaskStateSuspended; + } + + return _state; +} + +- (NSUInteger) taskIdentifier +{ + return _taskIdentifier; +} + +- (NSURLRequest*) originalRequest +{ + return _originalRequest; +} + +- (NSURLRequest*) currentRequest +{ + return _currentRequest; +} + +- (NSURLResponse*) response +{ + return _response; +} + +- (void) setResponse: (NSURLResponse*)response +{ + ASSIGN(_response, response); +} + +- (int64_t) countOfBytesReceived +{ + 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 +{ + 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]; + } + }); + }]; + } + }); +} + +- (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; + 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); + [_protocolLock unlock]; +} + +@end + +@implementation NSURLSessionDataTask + +@end + +@implementation NSURLSessionUploadTask + +@end + +@implementation NSURLSessionConfiguration + +static NSURLSessionConfiguration *def = nil; + ++ (void) initialize +{ + if (nil == def) + { + def = [NSURLSessionConfiguration new]; + } +} + ++ (NSURLSessionConfiguration*) defaultSessionConfiguration +{ + return AUTORELEASE([def copy]); +} + +- (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(_HTTPAdditionalHeaders); + DESTROY(_HTTPCookieStorage); + DESTROY(_protocolClasses); + DESTROY(_URLCache); + DESTROY(_URLCredentialStorage); + [super dealloc]; +} + +- (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 +{ +#if !defined(CURLOPT_MAXAGE_CONN) + [NSException raise: NSInternalInconsistencyException + format: @"-[%@ %@] not supported by the version of Curl" + @" this library was built with", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; +#endif + _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->_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; +} + @end diff --git a/Source/NSXPCConnection.m b/Source/NSXPCConnection.m index c394efd27..81bb955d3 100644 --- a/Source/NSXPCConnection.m +++ b/Source/NSXPCConnection.m @@ -146,15 +146,15 @@ } - (pid_t) processIdentifier { - return (pid_t)[self notImplemented: _cmd]; + return (pid_t)(uintptr_t)[self notImplemented: _cmd]; } - (uid_t) effectiveUserIdentifier { - return (uid_t)[self notImplemented: _cmd]; + return (uid_t)(uintptr_t)[self notImplemented: _cmd]; } - (gid_t) effectiveGroupIdentifier { - return (gid_t)[self notImplemented: _cmd]; + return (gid_t)(uintptr_t)[self notImplemented: _cmd]; } @end diff --git a/Tests/base/NSURL/Helpers/keepalive.m b/Tests/base/NSURL/Helpers/keepalive.m index 962ec8059..fc8d78642 100644 --- a/Tests/base/NSURL/Helpers/keepalive.m +++ b/Tests/base/NSURL/Helpers/keepalive.m @@ -36,7 +36,7 @@ NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; NSString *file, *lengthHeader; - NSHost *host = [NSHost hostWithAddress: @"127.0.0.1"]; + NSHost *host = [NSHost hostWithName: @"localhost"]; NSStream *serverStream; int port = [[defs stringForKey: @"Port"] intValue]; int lifetime = [[defs stringForKey: @"Lifetime"] intValue]; diff --git a/Tests/base/NSURL/test00.m b/Tests/base/NSURL/test00.m index 670dd1d01..24308af0f 100644 --- a/Tests/base/NSURL/test00.m +++ b/Tests/base/NSURL/test00.m @@ -48,7 +48,7 @@ int main() data = [u resourceDataUsingCache: NO]; // Get status code str = [u propertyForKey: NSHTTPPropertyStatusCodeKey]; - PASS([data isEqual: cont], "NSURL chunked test OK"); + PASS_EQUAL(data, cont, "NSURL chunked test OK"); // Wait for server termination [t terminate]; [t waitUntilExit]; diff --git a/Tests/base/NSURLConnection/Helpers/NSURLConnectionTest.h b/Tests/base/NSURLConnection/Helpers/NSURLConnectionTest.h index eb31d0a50..193035915 100644 --- a/Tests/base/NSURLConnection/Helpers/NSURLConnectionTest.h +++ b/Tests/base/NSURLConnection/Helpers/NSURLConnectionTest.h @@ -13,7 +13,7 @@ * test. * * The test case which the NSURLConnectionTest implements by default is connecting - * to the http://127.0.0.1:1234/ whith the HTTP method 'GET'. You can change variable + * to the http://localhost:1234/ whith the HTTP method 'GET'. You can change variable * parts of process by supplying a custom dictionary to the method -[setUpTest:] * before the test case is started by a call of the -[startTest:]. The use pattern: * -------------------------------------------------------------------------- @@ -57,7 +57,7 @@ * (the instance will be run within a detached thread). * Default: NO * 'Address' - the address of the remote side. - * Default: 127.0.0.1 + * Default: localhost * 'Port' - the port of the remote side. * Default: 1234 * 'AuxPort' - the port of the auxilliary remote side (where the connection diff --git a/Tests/base/NSURLConnection/Helpers/NSURLConnectionTest.m b/Tests/base/NSURLConnection/Helpers/NSURLConnectionTest.m index 822c5e557..c793fd890 100644 --- a/Tests/base/NSURLConnection/Helpers/NSURLConnectionTest.m +++ b/Tests/base/NSURLConnection/Helpers/NSURLConnectionTest.m @@ -590,7 +590,7 @@ didReceiveResponse:(NSURLResponse *)response } if (nil == address) { - address = @"127.0.0.1"; + address = @"localhost"; } if (nil == port) { diff --git a/Tests/base/NSURLConnection/Helpers/TestCase.m b/Tests/base/NSURLConnection/Helpers/TestCase.m index 77b3ff729..712b5f338 100644 --- a/Tests/base/NSURLConnection/Helpers/TestCase.m +++ b/Tests/base/NSURLConnection/Helpers/TestCase.m @@ -6,9 +6,9 @@ @implementation TestCase -- (id)init +- (id) init { - if((self = [super init]) != nil) + if ((self = [super init]) != nil) { [self resetFlags]; [self setReferenceFlags: NORESULTS]; @@ -19,7 +19,7 @@ return self; } -- (void)dealloc +- (void) dealloc { [self tearDownTest: _extra]; @@ -27,47 +27,47 @@ } /* TestProgress */ -- (void)resetFlags +- (void) resetFlags { _flags = NORESULTS; } -- (void)setFlags:(NSUInteger)mask +- (void) setFlags:(NSUInteger)mask { _flags = _flags | mask; } -- (void)unsetFlags:(NSUInteger)mask +- (void) unsetFlags:(NSUInteger)mask { _flags = _flags & ~mask; } -- (BOOL)isFlagSet:(NSUInteger)mask +- (BOOL) isFlagSet:(NSUInteger)mask { return ((_flags & mask) == mask); } -- (void)resetReferenceFlags +- (void) resetReferenceFlags { _refFlags = NORESULTS; } -- (void)setReferenceFlags:(NSUInteger)mask +- (void) setReferenceFlags:(NSUInteger)mask { _refFlags = _refFlags | mask; } -- (void)unsetReferenceFlags:(NSUInteger)mask +- (void) unsetReferenceFlags:(NSUInteger)mask { _refFlags = _refFlags & ~mask; } -- (BOOL)isReferenceFlagSet:(NSUInteger)mask +- (BOOL) isReferenceFlagSet:(NSUInteger)mask { return ((_refFlags & mask) == mask); } -- (BOOL)isSuccess +- (BOOL) isSuccess { if(!_failed && (_flags == _refFlags)) { diff --git a/Tests/base/NSURLConnection/Helpers/TestWebServer.h b/Tests/base/NSURLConnection/Helpers/TestWebServer.h index a7dd90803..ae3a8d50e 100644 --- a/Tests/base/NSURLConnection/Helpers/TestWebServer.h +++ b/Tests/base/NSURLConnection/Helpers/TestWebServer.h @@ -13,7 +13,7 @@ * It is designed to call the delegate's callbacks (if implemented) of * the TestWebServerDelegate protocol for every request made to * the SimpleWebServer's assigned address on the SimpleWebServer's assigned port - * (by default 127.0.0.1 and 1234 respectively). However it currently doesn't + * (by default localhost and 1234 respectively). However it currently doesn't * handle any request on it's own. Instead the class uses a collection of * handlers. Every handler makes a custom response. So the TestWebServer only * has to dispatch a request to a corresponding handler and then to send back @@ -29,7 +29,7 @@ * NSDictionary *extra = [NSDictionary dictionaryWithObjectsAndKeys: * @"https", @"Protocol", * nil]; - * TestWebServer *server = [[TestWebServer alloc] initWithAddress: @"127.0.0.1" + * TestWebServer *server = [[TestWebServer alloc] initWithAddress: @"localhost" * port: @"1234" * mode: NO * extra: extra]; @@ -72,7 +72,7 @@ * 301 "Redirect to " * Returns in the header 'Location' a new which by default * has the same protocol, address but the port is incremented by - * 1 (e.g. http://127.0.0.1:1235/ with other parameters set to + * 1 (e.g. http://localhost:1235/ with other parameters set to * their default values). * 400 "You have issued a request with invalid data" * 401 "Invalid login or password" diff --git a/Tests/base/NSURLConnection/Helpers/TestWebServer.m b/Tests/base/NSURLConnection/Helpers/TestWebServer.m index 34fcbc881..a06c844c2 100644 --- a/Tests/base/NSURLConnection/Helpers/TestWebServer.m +++ b/Tests/base/NSURLConnection/Helpers/TestWebServer.m @@ -9,7 +9,7 @@ /** * Starts the detached thread. It is the entry point of the thread. - */ + */ - (void)_startDetached:(id)extra; /** @@ -23,10 +23,10 @@ - (void)_stopHTTPServer; -@end +@end /* default 'constants' */ -#define DEFAULTADDRESS @"127.0.0.1" +#define DEFAULTADDRESS @"localhost" #define DEFAULTPORT @"1234" #define DEFAULTMODE NO #define DEFAULTLOGIN @"login" @@ -45,7 +45,7 @@ @implementation TestWebServer -- (id)init +- (id) init { return [self initWithAddress: DEFAULTADDRESS port: DEFAULTPORT @@ -68,7 +68,7 @@ _password = nil; _login = nil; _isSecure = NO; - + if ([extra isKindOfClass: [NSDictionary class]]) { NSDictionary *d = extra; @@ -123,7 +123,7 @@ selector: @selector(_startDetached:) object: extra]; } - else + else { _serverThread = nil; } @@ -145,36 +145,40 @@ [super dealloc]; } -- (void)start:(id)extra +- (void) start: (id)extra { - if ([_server port] != nil) - { - if (YES == _debug) + if ([_server port] != nil) + { + if (_debug) { NSWarnMLog(@"SimpleWebServer already started"); } - return; + return; } if (nil != _serverThread) { + if (_debug) + NSLog(@"Waiting for startup"); if (![_serverThread isExecuting]) { NSTimeInterval duration = 0.0; + [_serverThread start]; // wait for the SimpleWebServer is started - while(![_lock tryLockWhenCondition: STARTED] && - duration < MAXDURATION) + while (![_lock tryLockWhenCondition: STARTED] + && duration < MAXDURATION) { [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: TIMING]]; duration += TIMING; } [_lock unlock]; - if (duration >= MAXDURATION && - nil != _delegate && - [_delegate respondsToSelector: @selector(timeoutExceededByHandler:)]) + if (duration >= MAXDURATION + && nil != _delegate + && [_delegate respondsToSelector: + @selector(timeoutExceededByHandler:)]) { [_delegate timeoutExceededByHandler: self]; [self stop]; @@ -191,7 +195,7 @@ } } -- (void)stop +- (void) stop { if ([_server port] == nil) { @@ -283,18 +287,18 @@ _isSecure ? @"https" : @"http", _address, _port]; - + [(HandlerIndex *)handler setURLString: urlString]; } else if ([handler isKindOfClass: [Handler301 class]]) { - // by default http://127.0.0.1:1235/ + // by default http://localhost:1235/ NSString *port = [NSString stringWithFormat: @"%u", [_port intValue] + 1]; // the TestWebServer's port + 1 NSString *urlString = [NSString stringWithFormat: @"%@://%@:%@/", _isSecure ? @"https" : @"http", _address, port]; - + [(Handler301 *)handler setURLString: urlString]; } } @@ -314,7 +318,7 @@ [handler posthandleRequest: request response: response for: self]; - + return ret; } @@ -369,7 +373,7 @@ @implementation TestWebServer (Private) -- (void)_startHTTPServer:(id)extra +- (void) _startHTTPServer: (id)extra { NSString *certPath; NSString *keyPath; @@ -381,8 +385,10 @@ [_server setDelegate: self]; if (_isSecure) { - if ([_address isEqualToString: @"127.0.0.1"] || - [_address isEqualToString: @"localhost"]) + NSHost *h = [NSHost hostWithAddress: _address]; + NSHost *l = [NSHost hostWithName: @"localhost"]; + + if ([h isEqual: l]) { certPath = [[NSBundle bundleForClass: [self class]] pathForResource: @"testCert" @@ -397,22 +403,31 @@ } else { + [NSException raise: NSInternalInconsistencyException + format: @"The server hasn't run. Address %@ is not localhost (%@)", + _address, l]; // NOTE: generate corresponding certificates for any address differing - // from 127.0.0.1 + // from localhost } } - status = [_server setAddress: _address port: _port secure: secure]; // run the server - if (!status) + if (_debug) + { + NSLog(@"Starting web server with address %@, port %@ %@", + _address, _port, secure ? @" with TLS" : @""); + } + status = [_server setAddress: _address port: _port secure: secure]; + if (!status) { [NSException raise: NSInternalInconsistencyException - format: @"The server hasn't run. Perhaps the port %@ is invalid", DEFAULTPORT]; + format: @"The server hasn't run. Perhaps the port %@ is invalid", + DEFAULTPORT]; } } - (void)_stopHTTPServer { - if (nil != _server && [_server port] != nil) + if (nil != _server && [_server port] != nil) { [_server stop]; // shut down the server if (YES == _debug) diff --git a/Tests/base/NSURLConnection/Helpers/testTestWebServer.m b/Tests/base/NSURLConnection/Helpers/testTestWebServer.m index 1820926d4..bda30d54f 100644 --- a/Tests/base/NSURLConnection/Helpers/testTestWebServer.m +++ b/Tests/base/NSURLConnection/Helpers/testTestWebServer.m @@ -3,15 +3,15 @@ * Author: Sergei Golovin * * Runs two TestWebServer instances to check how the class TestWebServer - * behaves. Visit http://127.0.0.1:1234/index to see all supported resources. + * behaves. Visit http://localhost:1234/index to see all supported resources. * * If you visit the main TestWebServer instance with the following command: * - * wget -O - --user=login --password=password http://127.0.0.1:1234/301 2>&1 + * wget -O - --user=login --password=password http://localhost:1234/301 2>&1 * * you should get a session log like this: * - * --2014-08-13 12:08:01-- http://127.0.0.1:1234/301 + * --2014-08-13 12:08:01-- http://localhost:1234/301 * Resolving 127.0.0.1 (localhost)... 127.0.0.1 * Connecting to 127.0.0.1 (localhost)|127.0.0.1|:1234... connected. * HTTP request sent, awaiting response... 401 Unauthorized @@ -64,20 +64,20 @@ int main(int argc, char **argv, char **env) // @"https", @"Protocol", nil]; server1 = [[[testClass testWebServerClass] alloc] - initWithAddress: @"127.0.0.1" + initWithAddress: @"localhost" port: @"1234" mode: NO extra: d]; [server1 setDebug: debug]; - [server1 start: d]; // 127.0.0.1:1234 HTTP + [server1 start: d]; // localhost:1234 HTTP server2 = [[[testClass testWebServerClass] alloc] - initWithAddress: @"127.0.0.1" + initWithAddress: @"localhost" port: @"1235" mode: NO extra: d]; [server2 setDebug: debug]; - [server2 start: d]; // 127.0.0.1:1235 HTTP + [server2 start: d]; // localhost:1235 HTTP while (YES) { diff --git a/Tests/base/NSURLConnection/test01.m b/Tests/base/NSURLConnection/test01.m index e94946618..2be7201ef 100644 --- a/Tests/base/NSURLConnection/test01.m +++ b/Tests/base/NSURLConnection/test01.m @@ -59,7 +59,7 @@ int main(int argc, char **argv, char **env) duration = 0.0; timing = 0.1; - urlString = @"http://127.0.0.1:19750"; + urlString = @"http://localhost:19750"; req = [NSURLRequest requestWithURL: [NSURL URLWithString: urlString]]; del = [[Delegate new] autorelease]; [NSURLConnection connectionWithRequest: req @@ -76,7 +76,7 @@ int main(int argc, char **argv, char **env) [del reset]; duration = 0.0; - urlString = @"https://127.0.0.1:19750"; + urlString = @"https://localhost:19750"; req = [NSURLRequest requestWithURL: [NSURL URLWithString: urlString]]; [NSURLConnection connectionWithRequest: req delegate: del]; diff --git a/Tests/base/NSURLConnection/test02.m b/Tests/base/NSURLConnection/test02.m index 4238a360a..9091dbe44 100644 --- a/Tests/base/NSURLConnection/test02.m +++ b/Tests/base/NSURLConnection/test02.m @@ -35,7 +35,7 @@ int main(int argc, char **argv, char **env) // create a shared TestWebServer instance for performance server = [[testClass testWebServerClass] new]; [server setDebug: debug]; - [server start: nil]; // 127.0.0.1:1234 HTTP + [server start: nil]; // localhost:1234 HTTP /* * Simple GET via HTTP with empty response's body and diff --git a/Tests/base/NSURLConnection/test03.m b/Tests/base/NSURLConnection/test03.m index e81fa746b..b7986aff7 100644 --- a/Tests/base/NSURLConnection/test03.m +++ b/Tests/base/NSURLConnection/test03.m @@ -40,12 +40,12 @@ int main(int argc, char **argv, char **env) nil]; // create a shared TestWebServer instance for performance server = [[[testClass testWebServerClass] alloc] - initWithAddress: @"127.0.0.1" + initWithAddress: @"localhost" port: @"1234" mode: NO extra: d]; [server setDebug: debug]; - [server start: d]; // 127.0.0.1:1234 HTTPS + [server start: d]; // localhost:1234 HTTPS /* * Simple GET via HTTPS with empty response's body and @@ -59,7 +59,7 @@ int main(int argc, char **argv, char **env) nil]; [testCase setUpTest: d]; [testCase startTest: d]; - PASS([testCase isSuccess], "HTTPS... GET https://127.0.0.1:1234/"); + PASS([testCase isSuccess], "HTTPS... GET https://localhost:1234/"); [testCase tearDownTest: d]; DESTROY(testCase); diff --git a/Tests/base/NSURLConnection/test04.m b/Tests/base/NSURLConnection/test04.m index b0c26ae8a..58a3064b7 100644 --- a/Tests/base/NSURLConnection/test04.m +++ b/Tests/base/NSURLConnection/test04.m @@ -35,7 +35,7 @@ int main(int argc, char **argv, char **env) // create a shared TestWebServer instance for performance server = [[testClass testWebServerClass] new]; [server setDebug: debug]; - [server start: nil]; // 127.0.0.1:1234 HTTP + [server start: nil]; // localhost:1234 HTTP /* * Simple GET via HTTP without authorization with empty response's body and diff --git a/Tests/base/NSURLConnection/test05.m b/Tests/base/NSURLConnection/test05.m index 179ffd850..42b7ca614 100644 --- a/Tests/base/NSURLConnection/test05.m +++ b/Tests/base/NSURLConnection/test05.m @@ -44,7 +44,7 @@ int main(int argc, char **argv, char **env) mode: NO extra: d]; [server setDebug: debug]; - [server start: d]; // 127.0.0.1:1234 HTTPS + [server start: d]; // localhost:1234 HTTPS /* Simple GET via HTTPS without authorization with empty response's * body and the response's status code 204 (by default) diff --git a/Tests/base/NSURLConnection/test06.m b/Tests/base/NSURLConnection/test06.m index 5fded6934..2239a97d5 100644 --- a/Tests/base/NSURLConnection/test06.m +++ b/Tests/base/NSURLConnection/test06.m @@ -39,7 +39,7 @@ int main(int argc, char **argv, char **env) // login:password server = [[testClass testWebServerClass] new]; [server setDebug: debug]; - [server start: nil]; // 127.0.0.1:1234 HTTP + [server start: nil]; // localhost:1234 HTTP /* * Simple GET via HTTP with some response's body and diff --git a/Tests/base/NSURLConnection/test07.m b/Tests/base/NSURLConnection/test07.m index c7be025d0..8e7980c5f 100644 --- a/Tests/base/NSURLConnection/test07.m +++ b/Tests/base/NSURLConnection/test07.m @@ -45,7 +45,7 @@ int main(int argc, char **argv, char **env) mode: NO extra: d]; [server setDebug: debug]; - [server start: d]; // 127.0.0.1:1234 HTTPS + [server start: d]; // localhost:1234 HTTPS /* Simple POST via HTTPS with the response's status code 400 and * non-empty response's body diff --git a/Tests/base/NSURLSession/GNUmakefile.preamble b/Tests/base/NSURLSession/GNUmakefile.preamble new file mode 100644 index 000000000..84e483528 --- /dev/null +++ b/Tests/base/NSURLSession/GNUmakefile.preamble @@ -0,0 +1,5 @@ + +SUBPROJECTS = ../NSURLConnection/Helpers + +include $(GNUSTEP_MAKEFILES)/aggregate.make + diff --git a/Tests/base/NSURLSession/TestInfo b/Tests/base/NSURLSession/TestInfo new file mode 100644 index 000000000..e69de29bb diff --git a/Tests/base/NSURLSession/delegate.g b/Tests/base/NSURLSession/delegate.g new file mode 100644 index 000000000..15f5f9e06 --- /dev/null +++ b/Tests/base/NSURLSession/delegate.g @@ -0,0 +1,116 @@ +#import +#import "Testing.h" +#import "ObjectTesting.h" + +@interface MyDelegate : NSObject +{ + BOOL _finished; + NSMutableArray *_order; + NSURLResponse *_response; + NSData *_taskData; + NSString *_taskText; + NSError *_taskError; +} +- (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); + _finished = NO; +} + +- (NSURLResponse*) response +{ + return _response; +} +- (NSData*) taskData +{ + return _taskData; +} +- (NSError*) taskError +{ + return _taskError; +} +- (NSString*) taskText +{ + return _taskText; +} + +- (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); +} + +- (void) URLSession: (NSURLSession*)session + task: (NSURLSessionTask*)task +didCompleteWithError: (NSError*)error +{ + [_order addObject: NSStringFromSelector(_cmd)]; + _finished = YES; + + if (error == nil) + { + NSLog(@"Download is Succesfull"); + } + else + { + NSLog(@"Error %@", [error userInfo]); + } + ASSIGN(_taskError, error); +} +@end + diff --git a/Tests/base/NSURLSession/gdbinit b/Tests/base/NSURLSession/gdbinit new file mode 100644 index 000000000..e125a1993 --- /dev/null +++ b/Tests/base/NSURLSession/gdbinit @@ -0,0 +1,79 @@ +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/test01.m b/Tests/base/NSURLSession/test01.m new file mode 100644 index 000000000..3fd8c8c6e --- /dev/null +++ b/Tests/base/NSURLSession/test01.m @@ -0,0 +1,132 @@ +#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; + + 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") + +#endif + END_SET("NSURLSession test01") + return 0; +} diff --git a/Tests/base/NSURLSession/test02.m b/Tests/base/NSURLSession/test02.m new file mode 100644 index 000000000..35f58a72b --- /dev/null +++ b/Tests/base/NSURLSession/test02.m @@ -0,0 +1,89 @@ +#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 *defaultConfigObject; + NSURLSession *defaultSession; + NSURLSessionDataTask *dataTask; + NSMutableURLRequest *urlRequest; + NSURL *url; + NSOperationQueue *mainQueue; + NSString *params; + MyDelegate *object; + + object = AUTORELEASE([MyDelegate new]); + mainQueue = [NSOperationQueue mainQueue]; + defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration]; + defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject + delegate: object + delegateQueue: mainQueue]; + 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/base.make.in b/base.make.in index 32abca0f8..bd5ef148b 100644 --- a/base.make.in +++ b/base.make.in @@ -61,7 +61,7 @@ ifeq ($(BASE_MAKE_LOADED),) endif # Now we have definitions to show whether important dependencies have - # been met ... if thse are 0 then some core functi0nailyt is missing. + # been met ... if thse are 0 then some core functionality is missing. # Has GNUTLS been found (for TLS/SSL support throughout)? GNUSTEP_BASE_HAVE_GNUTLS=@HAVE_GNUTLS@ diff --git a/config.mak.in b/config.mak.in index 7f83c1cc4..529a1c623 100644 --- a/config.mak.in +++ b/config.mak.in @@ -35,13 +35,14 @@ endif GNUSTEP_INSTALL_GDOMAP_AS_SETUID=@GNUSTEP_INSTALL_GDOMAP_AS_SETUID@ GNUSTEP_GDOMAP_PORT_OVERRIDE=@GNUSTEP_GDOMAP_PORT_OVERRIDE@ -GNUSTEP_BASE_HAVE_LIBXML=@HAVE_LIBXML@ -GNUSTEP_BASE_HAVE_GNUTLS=@HAVE_GNUTLS@ -GNUSTEP_BASE_HAVE_MDNS=@HAVE_MDNS@ GNUSTEP_BASE_HAVE_AVAHI=@HAVE_AVAHI@ +GNUSTEP_BASE_HAVE_GNUTLS=@HAVE_GNUTLS@ GNUSTEP_BASE_HAVE_ICU=@HAVE_ICU@ +GNUSTEP_BASE_HAVE_LIBCURL=@HAVE_LIBCURL@ GNUSTEP_BASE_HAVE_LIBDISPATCH=@HAVE_LIBDISPATCH@ GNUSTEP_BASE_HAVE_LIBDISPATCH_RUNLOOP=@HAVE_LIBDISPATCH_RUNLOOP@ +GNUSTEP_BASE_HAVE_LIBXML=@HAVE_LIBXML@ +GNUSTEP_BASE_HAVE_MDNS=@HAVE_MDNS@ # Default to building only -baseadd # on non *-gnu-* library combos diff --git a/config/addlibrarypath.m4 b/config/addlibrarypath.m4 index cea8a66d5..2c3fecaa3 100644 --- a/config/addlibrarypath.m4 +++ b/config/addlibrarypath.m4 @@ -6,15 +6,16 @@ dnl notice and this notice are preserved. dnl dnl Written by Andrew Ruder dnl GS_ADD_LIBRARY_PATH -dnl Adds -L$1 -Wl,-R$1 on netbsd and -L$1 elsewhere to LDFLAGS and LDIR_FLAGS +dnl Adds -L$1 -Wl,-R$1 on netbsd and -Wl,-rpath,$1 -L$1 elsewhere +dnl to LDFLAGS and LDIR_FLAGS AC_DEFUN([GS_ADD_LIBRARY_PATH], [ case "$target_os" in netbsd*) LDFLAGS="$LDFLAGS -L$1 -Wl,-R$1" LDIR_FLAGS="$LDIR_FLAGS -Wl,-R$1 -L$1";; *) - LDFLAGS="$LDFLAGS -L$1" - LDIR_FLAGS="$LDIR_FLAGS -L$1";; + LDFLAGS="$LDFLAGS -Wl,-rpath,$1 -L$1" + LDIR_FLAGS="$LDIR_FLAGS -Wl,-rpath,$1 -L$1";; esac ]) diff --git a/configure b/configure index 562a2ad0d..25a021368 100755 --- a/configure +++ b/configure @@ -632,6 +632,8 @@ WARN_FLAGS LDIR_FLAGS INCLUDE_FLAGS USE_GMP +GS_HAVE_NSURLSESSION +HAVE_LIBCURL HAVE_LIBDISPATCH_RUNLOOP HAVE_LIBDISPATCH HAVE_ICU @@ -820,6 +822,10 @@ enable_zeroconf with_zeroconf_api enable_icu enable_libdispatch +with_dispatch_include +with_dispatch_library +with_curl +enable_nsurlsession with_gmp_include with_gmp_library with_gdomap_port @@ -1500,6 +1506,7 @@ Optional Features: --disable-zeroconf Disable NSNetServices support --disable-icu Disable International Components for Unicode --disable-libdispatch Disable dispatching blocks via libdispatch + --disable-nsurlsession Disable support for NSURLSession --enable-setuid-gdomap Enable installing gdomap as a setuid executable. By default, it is installed @@ -1571,6 +1578,9 @@ Optional Packages: --with-xml-prefix=PFX Prefix where libxml is installed (optional) --with-tls-prefix=PFX Prefix where libgnutls is installed (optional) --with-zeroconf-api=API force use of a specific zeroconf API (mdns or avahi) + --with-dispatch-include=PATH Include path for dispatch header + --with-dispatch-library=PATH Library path for dispatch lib + --with-curl= use curl installed in directory --with-gmp-include=PATH include path for gmp headers --with-gmp-library=PATH library path for gmp libraries --with-gdomap-port=PORT alternative port for gdomap @@ -10762,8 +10772,8 @@ case "$target_os" in LDFLAGS="$LDFLAGS -L${ffi_libdir} -Wl,-R${ffi_libdir}" LDIR_FLAGS="$LDIR_FLAGS -Wl,-R${ffi_libdir} -L${ffi_libdir}";; *) - LDFLAGS="$LDFLAGS -L${ffi_libdir}" - LDIR_FLAGS="$LDIR_FLAGS -L${ffi_libdir}";; + LDFLAGS="$LDFLAGS -Wl,-rpath,${ffi_libdir} -L${ffi_libdir}" + LDIR_FLAGS="$LDIR_FLAGS -Wl,-rpath,${ffi_libdir} -L${ffi_libdir}";; esac fi @@ -11093,8 +11103,8 @@ case "$target_os" in LDFLAGS="$LDFLAGS -L${libiconv_libdir} -Wl,-R${libiconv_libdir}" LDIR_FLAGS="$LDIR_FLAGS -Wl,-R${libiconv_libdir} -L${libiconv_libdir}";; *) - LDFLAGS="$LDFLAGS -L${libiconv_libdir}" - LDIR_FLAGS="$LDIR_FLAGS -L${libiconv_libdir}";; + LDFLAGS="$LDFLAGS -Wl,-rpath,${libiconv_libdir} -L${libiconv_libdir}" + LDIR_FLAGS="$LDIR_FLAGS -Wl,-rpath,${libiconv_libdir} -L${libiconv_libdir}";; esac fi @@ -11251,8 +11261,8 @@ case "$target_os" in LDFLAGS="$LDFLAGS -L${libiconv_libdir} -Wl,-R${libiconv_libdir}" LDIR_FLAGS="$LDIR_FLAGS -Wl,-R${libiconv_libdir} -L${libiconv_libdir}";; *) - LDFLAGS="$LDFLAGS -L${libiconv_libdir}" - LDIR_FLAGS="$LDIR_FLAGS -L${libiconv_libdir}";; + LDFLAGS="$LDFLAGS -Wl,-rpath,${libiconv_libdir} -L${libiconv_libdir}" + LDIR_FLAGS="$LDIR_FLAGS -Wl,-rpath,${libiconv_libdir} -L${libiconv_libdir}";; esac fi @@ -12885,6 +12895,7 @@ fi # Check for libdispatch # See DEPENDENCIES POLICY at the start of this file. #-------------------------------------------------------------------- + HAVE_LIBDISPATCH=0 # Check whether --enable-libdispatch was given. if test "${enable_libdispatch+set}" = set; then : @@ -12895,6 +12906,40 @@ fi if test $enable_libdispatch = yes; then + +# Check whether --with-dispatch-include was given. +if test "${with_dispatch_include+set}" = set; then : + withval=$with_dispatch_include; dispatch_incdir="$withval" +else + dispatch_incdir="no" +fi + + if test ${dispatch_incdir} != "no"; then + CPPFLAGS="$CPPFLAGS -I${dispatch_incdir}" + INCLUDE_FLAGS="$INCLUDE_FLAGS -I${dispatch_incdir}" + fi + + +# Check whether --with-dispatch-library was given. +if test "${with_dispatch_library+set}" = set; then : + withval=$with_dispatch_library; dispatch_libdir="$withval" +else + dispatch_libdir="no" +fi + + if test ${dispatch_libdir} != "no"; then + +case "$target_os" in + netbsd*) + LDFLAGS="$LDFLAGS -L${dispatch_libdir} -Wl,-R${dispatch_libdir}" + LDIR_FLAGS="$LDIR_FLAGS -Wl,-R${dispatch_libdir} -L${dispatch_libdir}";; + *) + LDFLAGS="$LDFLAGS -Wl,-rpath,${dispatch_libdir} -L${dispatch_libdir}" + LDIR_FLAGS="$LDIR_FLAGS -Wl,-rpath,${dispatch_libdir} -L${dispatch_libdir}";; +esac + + fi + for ac_header in dispatch.h do : ac_fn_c_check_header_mongrel "$LINENO" "dispatch.h" "ac_cv_header_dispatch_h" "$ac_includes_default" @@ -13019,8 +13064,8 @@ $as_echo "no" >&6; }; fi fi else - HAVE_LIBDISPATCH=0; - # just ignore libdispatch if it's not there + HAVE_LIBDISPATCH=0; + # just ignore libdispatch if it's not there fi fi @@ -13064,6 +13109,204 @@ done fi + +#-------------------------------------------------------------------- +# Check for libcurl +# See DEPENDENCIES POLICY at the start of this file. +#-------------------------------------------------------------------- +CURL_CONFIG="curl-config" + +# Check whether --with-curl was given. +if test "${with_curl+set}" = set; then : + withval=$with_curl; +fi + +if test "$with_curl" != ""; then + CURL_CONFIG="$with_curl/bin/curl-config" +fi + +HAVE_LIBCURL=0 +curl_all=no +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for libcurl" >&5 +$as_echo_n "checking for libcurl... " >&6; } +if eval $CURL_CONFIG --version 2>/dev/null >/dev/null; then + curl_ver=`$CURL_CONFIG --version | sed -e "s/libcurl //g"` + curl_maj=`echo $curl_ver | sed -e "s/^\(.*\)\.\(.*\)\.\(.*\)$/\1/"` + curl_min=`echo $curl_ver | sed -e "s/^\(.*\)\.\(.*\)\.\(.*\)$/\2/"` + if test $curl_maj -lt 7 -o \( $curl_maj -eq 7 -a $curl_min -lt 49 \); then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: FAILED (version too old to use" >&5 +$as_echo "FAILED (version too old to use" >&6; } + else + CURLCFLAGS=`$CURL_CONFIG --cflags` + CURLLIBS=`$CURL_CONFIG --libs` + CFLAGS="$CFLAGS $CURLCFLAGS" + LIBS="$LIBS $CURLLIBS" + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes ... version $curl_ver" >&5 +$as_echo "yes ... version $curl_ver" >&6; } + HAVE_LIBCURL=1 + curl_all=yes + if test \( $curl_maj -eq 7 -a $curl_min -lt 56 \); then + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: LOSS OF FUNCTIONALITY (version too old for SSL backend control)" >&5 +$as_echo "$as_me: WARNING: LOSS OF FUNCTIONALITY (version too old for SSL backend control)" >&2;} + curl_all=no + elif test \( $curl_maj -eq 7 -a $curl_min -lt 65 \); then + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: LOSS OF FUNCTIONALITY (version too old for connection lifetime control)" >&5 +$as_echo "$as_me: WARNING: LOSS OF FUNCTIONALITY (version too old for connection lifetime control)" >&2;} + curl_all=no + fi + fi +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: FAILED (curl-config not found)" >&5 +$as_echo "FAILED (curl-config not found)" >&6; } +fi + +if test $HAVE_LIBCURL = 1; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for libcurl TLS support with gnutls" >&5 +$as_echo_n "checking for libcurl TLS support with gnutls... " >&6; } + if $CURL_CONFIG --configure | grep -q with-gnutls; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } + curl_all=no + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: LOSS OF FUNCTIONALITY (curl lacks gnutls support)" >&5 +$as_echo "$as_me: WARNING: LOSS OF FUNCTIONALITY (curl lacks gnutls support)" >&2;} + fi +fi + +if test $HAVE_LIBCURL = 1; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_multi_perform in -lcurl" >&5 +$as_echo_n "checking for curl_multi_perform in -lcurl... " >&6; } +if ${ac_cv_lib_curl_curl_multi_perform+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lcurl $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char curl_multi_perform (); +int +main () +{ +return curl_multi_perform (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_curl_curl_multi_perform=yes +else + ac_cv_lib_curl_curl_multi_perform=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_perform" >&5 +$as_echo "$ac_cv_lib_curl_curl_multi_perform" >&6; } +if test "x$ac_cv_lib_curl_curl_multi_perform" = xyes; then : + curl_ok=yes +else + curl_ok=no +fi + +fi +if test $HAVE_LIBCURL = 1; then + for ac_header in curl/curl.h +do : + ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" +if test "x$ac_cv_header_curl_curl_h" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_CURL_CURL_H 1 +_ACEOF + curl_ok=yes +else + curl_ok=no +fi + +done + +fi +if test $HAVE_LIBCURL = 1; then + ac_fn_c_check_decl "$LINENO" "CURLOPT_CONNECT_TO" "ac_cv_have_decl_CURLOPT_CONNECT_TO" "#include +" +if test "x$ac_cv_have_decl_CURLOPT_CONNECT_TO" = xyes; then : + curl_ok=yes +else + curl_ok=no +fi + +fi +if test $HAVE_LIBCURL = 1; then + # If curl supports runtime selection of the TLS module we will need + # to use curl_global_sslset to pick GNUTLS + for ac_func in curl_global_sslset +do : + ac_fn_c_check_func "$LINENO" "curl_global_sslset" "ac_cv_func_curl_global_sslset" +if test "x$ac_cv_func_curl_global_sslset" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_CURL_GLOBAL_SSLSET 1 +_ACEOF + +fi +done + +fi + + +#-------------------------------------------------------------------- +# Check dependencies of NSURLSession API +#-------------------------------------------------------------------- +nsurlsessiondefault=no +if test "$OBJC_RUNTIME_LIB" = "ng" -a $HAVE_BLOCKS = 1 -a $HAVE_LIBDISPATCH = 1 -a $HAVE_LIBCURL = 1; then + nsurlsessiondefault=yes +fi + +GS_HAVE_NSURLSESSION=0 +# Check whether --enable-nsurlsession was given. +if test "${enable_nsurlsession+set}" = set; then : + enableval=$enable_nsurlsession; +else + enable_nsurlsession=$nsurlsessiondefault +fi + +if test $enable_nsurlsession = yes; then + if test "$OBJC_RUNTIME_LIB" != "ng"; then + as_fn_error $? "Missing ng runtime (needed for NSURLSession). To build without NSURLSession support, please run configure again with the --disable-nsurlsession option." "$LINENO" 5 + fi + if test $HAVE_BLOCKS = 0; then + as_fn_error $? "Missing blocks support (needed for NSURLSession). To build without NSURLSession support, please run configure again with the --disable-nsurlsession option." "$LINENO" 5 + fi + if test $HAVE_LIBDISPATCH = 0; then + as_fn_error $? "Missing libdispatch (needed for NSURLSession). To build without NSURLSession support, please run configure again with the --disable-nsurlsession option." "$LINENO" 5 + fi + # Check for dispatch_queue_create_with_target needed for NSURLSession + for ac_func in dispatch_queue_create_with_target +do : + ac_fn_c_check_func "$LINENO" "dispatch_queue_create_with_target" "ac_cv_func_dispatch_queue_create_with_target" +if test "x$ac_cv_func_dispatch_queue_create_with_target" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_DISPATCH_QUEUE_CREATE_WITH_TARGET 1 +_ACEOF + +fi +done + + if test $HAVE_LIBCURL = 0; then + as_fn_error $? "Missing libcurl (needed for NSURLSession). To build without NSURLSession support, please run configure again with the --disable-nsurlsession option." "$LINENO" 5 + fi + GS_HAVE_NSURLSESSION=1 +fi + + #-------------------------------------------------------------------- # Check GMP for NSDecimal #-------------------------------------------------------------------- @@ -13098,8 +13341,8 @@ case "$target_os" in LDFLAGS="$LDFLAGS -L${gmp_libdir} -Wl,-R${gmp_libdir}" LDIR_FLAGS="$LDIR_FLAGS -Wl,-R${gmp_libdir} -L${gmp_libdir}";; *) - LDFLAGS="$LDFLAGS -L${gmp_libdir}" - LDIR_FLAGS="$LDIR_FLAGS -L${gmp_libdir}";; + LDFLAGS="$LDFLAGS -Wl,-rpath,${gmp_libdir} -L${gmp_libdir}" + LDIR_FLAGS="$LDIR_FLAGS -Wl,-rpath,${gmp_libdir} -L${gmp_libdir}";; esac fi diff --git a/configure.ac b/configure.ac index 110e30262..6dd78ce76 100644 --- a/configure.ac +++ b/configure.ac @@ -2837,7 +2837,7 @@ AC_ARG_WITH(ffi-library, [ --with-ffi-library=PATH Library path for ffi libs], ffi_libdir="$withval", ffi_libdir="no") if test ${ffi_libdir} != "no"; then -GS_ADD_LIBRARY_PATH([${ffi_libdir}]) + GS_ADD_LIBRARY_PATH([${ffi_libdir}]) fi if test "$do_broken_libffi" = "no"; then @@ -3369,6 +3369,7 @@ AC_SUBST(HAVE_ICU) # Check for libdispatch # See DEPENDENCIES POLICY at the start of this file. #-------------------------------------------------------------------- + HAVE_LIBDISPATCH=0 AC_ARG_ENABLE(libdispatch, [ --disable-libdispatch Disable dispatching blocks via libdispatch], @@ -3376,6 +3377,21 @@ AC_ARG_ENABLE(libdispatch, enable_libdispatch=yes) if test $enable_libdispatch = yes; then + AC_ARG_WITH(dispatch-include, + [ --with-dispatch-include=PATH Include path for dispatch header], + dispatch_incdir="$withval", dispatch_incdir="no") + if test ${dispatch_incdir} != "no"; then + CPPFLAGS="$CPPFLAGS -I${dispatch_incdir}" + INCLUDE_FLAGS="$INCLUDE_FLAGS -I${dispatch_incdir}" + fi + + AC_ARG_WITH(dispatch-library, + [ --with-dispatch-library=PATH Library path for dispatch lib], + dispatch_libdir="$withval", dispatch_libdir="no") + if test ${dispatch_libdir} != "no"; then + GS_ADD_LIBRARY_PATH([${dispatch_libdir}]) + fi + AC_CHECK_HEADERS(dispatch.h, have_dispatch=yes, have_dispatch=no) if test "$have_dispatch" = "no"; then AC_CHECK_HEADERS(dispatch/dispatch.h, have_dispatch=yes, have_dispatch=no) @@ -3402,8 +3418,8 @@ if test $enable_libdispatch = yes; then fi fi else - HAVE_LIBDISPATCH=0; - # just ignore libdispatch if it's not there + HAVE_LIBDISPATCH=0; + # just ignore libdispatch if it's not there fi fi AC_SUBST(HAVE_LIBDISPATCH) @@ -3425,6 +3441,106 @@ if test $HAVE_LIBDISPATCH = 1; then fi AC_SUBST(HAVE_LIBDISPATCH_RUNLOOP) + +#-------------------------------------------------------------------- +# Check for libcurl +# See DEPENDENCIES POLICY at the start of this file. +#-------------------------------------------------------------------- +CURL_CONFIG="curl-config" +AC_ARG_WITH(curl, AS_HELP_STRING([--with-curl=], + [use curl installed in directory ])) +if test "$with_curl" != ""; then + CURL_CONFIG="$with_curl/bin/curl-config" +fi + +HAVE_LIBCURL=0 +curl_all=no +AC_MSG_CHECKING([for libcurl]) +if eval $CURL_CONFIG --version 2>/dev/null >/dev/null; then + curl_ver=`$CURL_CONFIG --version | sed -e "s/libcurl //g"` + curl_maj=`echo $curl_ver | sed -e "s/^\(.*\)\.\(.*\)\.\(.*\)$/\1/"` + curl_min=`echo $curl_ver | sed -e "s/^\(.*\)\.\(.*\)\.\(.*\)$/\2/"` + if test $curl_maj -lt 7 -o \( $curl_maj -eq 7 -a $curl_min -lt 49 \); then + AC_MSG_RESULT([FAILED (version too old to use]) + else + CURLCFLAGS=`$CURL_CONFIG --cflags` + CURLLIBS=`$CURL_CONFIG --libs` + CFLAGS="$CFLAGS $CURLCFLAGS" + LIBS="$LIBS $CURLLIBS" + AC_MSG_RESULT(yes ... version $curl_ver) + HAVE_LIBCURL=1 + curl_all=yes + if test \( $curl_maj -eq 7 -a $curl_min -lt 56 \); then + AC_MSG_WARN([LOSS OF FUNCTIONALITY (version too old for SSL backend control)]) + curl_all=no + elif test \( $curl_maj -eq 7 -a $curl_min -lt 65 \); then + AC_MSG_WARN([LOSS OF FUNCTIONALITY (version too old for connection lifetime control)]) + curl_all=no + fi + fi +else + AC_MSG_RESULT([FAILED (curl-config not found)]) +fi + +if test $HAVE_LIBCURL = 1; then + AC_MSG_CHECKING([for libcurl TLS support with gnutls]) + if $CURL_CONFIG --configure | grep -q with-gnutls; then + AC_MSG_RESULT(yes) + else + AC_MSG_RESULT(no) + curl_all=no + AC_MSG_WARN([LOSS OF FUNCTIONALITY (curl lacks gnutls support)]) + fi +fi + +if test $HAVE_LIBCURL = 1; then + AC_CHECK_LIB(curl, curl_multi_perform, curl_ok=yes, curl_ok=no) +fi +if test $HAVE_LIBCURL = 1; then + AC_CHECK_HEADERS(curl/curl.h, curl_ok=yes, curl_ok=no) +fi +if test $HAVE_LIBCURL = 1; then + AC_CHECK_DECL(CURLOPT_CONNECT_TO, curl_ok=yes, curl_ok=no, + [#include ]) +fi +if test $HAVE_LIBCURL = 1; then + # If curl supports runtime selection of the TLS module we will need + # to use curl_global_sslset to pick GNUTLS + AC_CHECK_FUNCS(curl_global_sslset) +fi +AC_SUBST(HAVE_LIBCURL) + +#-------------------------------------------------------------------- +# Check dependencies of NSURLSession API +#-------------------------------------------------------------------- +nsurlsessiondefault=no +if test "$OBJC_RUNTIME_LIB" = "ng" -a $HAVE_BLOCKS = 1 -a $HAVE_LIBDISPATCH = 1 -a $HAVE_LIBCURL = 1; then + nsurlsessiondefault=yes +fi + +GS_HAVE_NSURLSESSION=0 +AC_ARG_ENABLE(nsurlsession, + [ --disable-nsurlsession Disable support for NSURLSession],, + enable_nsurlsession=$nsurlsessiondefault) +if test $enable_nsurlsession = yes; then + if test "$OBJC_RUNTIME_LIB" != "ng"; then + AC_MSG_ERROR([Missing ng runtime (needed for NSURLSession). To build without NSURLSession support, please run configure again with the --disable-nsurlsession option.]) + fi + if test $HAVE_BLOCKS = 0; then + AC_MSG_ERROR([Missing blocks support (needed for NSURLSession). To build without NSURLSession support, please run configure again with the --disable-nsurlsession option.]) + fi + if test $HAVE_LIBDISPATCH = 0; then + AC_MSG_ERROR([Missing libdispatch (needed for NSURLSession). To build without NSURLSession support, please run configure again with the --disable-nsurlsession option.]) + fi + # Check for dispatch_queue_create_with_target needed for NSURLSession + AC_CHECK_FUNCS(dispatch_queue_create_with_target) + if test $HAVE_LIBCURL = 0; then + AC_MSG_ERROR([Missing libcurl (needed for NSURLSession). To build without NSURLSession support, please run configure again with the --disable-nsurlsession option.]) + fi + GS_HAVE_NSURLSESSION=1 +fi +AC_SUBST(GS_HAVE_NSURLSESSION) + #-------------------------------------------------------------------- # Check GMP for NSDecimal #--------------------------------------------------------------------