diff --git a/plugin.xml b/plugin.xml index 31884ce..a575ea6 100644 --- a/plugin.xml +++ b/plugin.xml @@ -6,8 +6,6 @@ - - Webserver for Cordova Apps webserver,cordova,http, request, response,server @@ -28,6 +26,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ios/GCDWebServer/Core/GCDWebServer.h b/src/ios/GCDWebServer/Core/GCDWebServer.h new file mode 100755 index 0000000..beec7b8 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServer.h @@ -0,0 +1,623 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +#import "GCDWebServerRequest.h" +#import "GCDWebServerResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerMatchBlock is called for every handler added to the + * GCDWebServer whenever a new HTTP request has started (i.e. HTTP headers have + * been received). The block is passed the basic info for the request (HTTP method, + * URL, headers...) and must decide if it wants to handle it or not. + * + * If the handler can handle the request, the block must return a new + * GCDWebServerRequest instance created with the same basic info. + * Otherwise, it simply returns nil. + */ +typedef GCDWebServerRequest* _Nullable (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery); + +/** + * The GCDWebServerProcessBlock is called after the HTTP request has been fully + * received (i.e. the entire HTTP body has been read). The block is passed the + * GCDWebServerRequest created at the previous step by the GCDWebServerMatchBlock. + * + * The block must return a GCDWebServerResponse or nil on error, which will + * result in a 500 HTTP status code returned to the client. It's however + * recommended to return a GCDWebServerErrorResponse on error so more useful + * information can be returned to the client. + */ +typedef GCDWebServerResponse* _Nullable (^GCDWebServerProcessBlock)(__kindof GCDWebServerRequest* request); + +/** + * The GCDWebServerAsynchronousProcessBlock works like the GCDWebServerProcessBlock + * except the GCDWebServerResponse can be returned to the server at a later time + * allowing for asynchronous generation of the response. + * + * The block must eventually call "completionBlock" passing a GCDWebServerResponse + * or nil on error, which will result in a 500 HTTP status code returned to the client. + * It's however recommended to return a GCDWebServerErrorResponse on error so more + * useful information can be returned to the client. + */ +typedef void (^GCDWebServerCompletionBlock)(GCDWebServerResponse* _Nullable response); +typedef void (^GCDWebServerAsyncProcessBlock)(__kindof GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock); + +/** + * The port used by the GCDWebServer (NSNumber / NSUInteger). + * + * The default value is 0 i.e. let the OS pick a random port. + */ +extern NSString* const GCDWebServerOption_Port; + +/** + * The Bonjour name used by the GCDWebServer (NSString). If set to an empty string, + * the name will automatically take the value of the GCDWebServerOption_ServerName + * option. If this option is set to nil, Bonjour will be disabled. + * + * The default value is nil. + */ +extern NSString* const GCDWebServerOption_BonjourName; + +/** + * The Bonjour service type used by the GCDWebServer (NSString). + * + * The default value is "_http._tcp", the service type for HTTP web servers. + */ +extern NSString* const GCDWebServerOption_BonjourType; + +/** + * Request a port mapping in the NAT gateway (NSNumber / BOOL). + * + * This uses the DNSService API under the hood which supports IPv4 mappings only. + * + * The default value is NO. + * + * @warning The external port set up by the NAT gateway may be different than + * the one used by the GCDWebServer. + */ +extern NSString* const GCDWebServerOption_RequestNATPortMapping; + +/** + * Only accept HTTP requests coming from localhost i.e. not from the outside + * network (NSNumber / BOOL). + * + * The default value is NO. + * + * @warning Bonjour and NAT port mapping should be disabled if using this option + * since the server will not be reachable from the outside network anyway. + */ +extern NSString* const GCDWebServerOption_BindToLocalhost; + +/** + * The maximum number of incoming HTTP requests that can be queued waiting to + * be handled before new ones are dropped (NSNumber / NSUInteger). + * + * The default value is 16. + */ +extern NSString* const GCDWebServerOption_MaxPendingConnections; + +/** + * The value for "Server" HTTP header used by the GCDWebServer (NSString). + * + * The default value is the GCDWebServer class name. + */ +extern NSString* const GCDWebServerOption_ServerName; + +/** + * The authentication method used by the GCDWebServer + * (one of "GCDWebServerAuthenticationMethod_..."). + * + * The default value is nil i.e. authentication is disabled. + */ +extern NSString* const GCDWebServerOption_AuthenticationMethod; + +/** + * The authentication realm used by the GCDWebServer (NSString). + * + * The default value is the same as the GCDWebServerOption_ServerName option. + */ +extern NSString* const GCDWebServerOption_AuthenticationRealm; + +/** + * The authentication accounts used by the GCDWebServer + * (NSDictionary of username / password pairs). + * + * The default value is nil i.e. no accounts. + */ +extern NSString* const GCDWebServerOption_AuthenticationAccounts; + +/** + * The class used by the GCDWebServer when instantiating GCDWebServerConnection + * (subclass of GCDWebServerConnection). + * + * The default value is the GCDWebServerConnection class. + */ +extern NSString* const GCDWebServerOption_ConnectionClass; + +/** + * Allow the GCDWebServer to pretend "HEAD" requests are actually "GET" ones + * and automatically discard the HTTP body of the response (NSNumber / BOOL). + * + * The default value is YES. + */ +extern NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET; + +/** + * The interval expressed in seconds used by the GCDWebServer to decide how to + * coalesce calls to -webServerDidConnect: and -webServerDidDisconnect: + * (NSNumber / double). Coalescing will be disabled if the interval is <= 0.0. + * + * The default value is 1.0 second. + */ +extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval; + +/** + * Set the dispatch queue priority on which server connection will be + * run (NSNumber / long). + * + * + * The default value is DISPATCH_QUEUE_PRIORITY_DEFAULT. + */ +extern NSString* const GCDWebServerOption_DispatchQueuePriority; + +#if TARGET_OS_IPHONE + +/** + * Enables the GCDWebServer to automatically suspend itself (as if -stop was + * called) when the iOS app goes into the background and the last + * GCDWebServerConnection is closed, then resume itself (as if -start was called) + * when the iOS app comes back to the foreground (NSNumber / BOOL). + * + * See the README.md file for more information about this option. + * + * The default value is YES. + * + * @warning The running property will be NO while the GCDWebServer is suspended. + */ +extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground; + +#endif + +/** + * HTTP Basic Authentication scheme (see https://tools.ietf.org/html/rfc2617). + * + * @warning Use of this authentication scheme is not recommended as the + * passwords are sent in clear. + */ +extern NSString* const GCDWebServerAuthenticationMethod_Basic; + +/** + * HTTP Digest Access Authentication scheme (see https://tools.ietf.org/html/rfc2617). + */ +extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; + +@class GCDWebServer; + +/** + * Delegate methods for GCDWebServer. + * + * @warning These methods are always called on the main thread in a serialized way. + */ +@protocol GCDWebServerDelegate +@optional + +/** + * This method is called after the server has successfully started. + */ +- (void)webServerDidStart:(GCDWebServer*)server; + +/** + * This method is called after the Bonjour registration for the server has + * successfully completed. + * + * Use the "bonjourServerURL" property to retrieve the Bonjour address of the + * server. + */ +- (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server; + +/** + * This method is called after the NAT port mapping for the server has been + * updated. + * + * Use the "publicServerURL" property to retrieve the public address of the + * server. + */ +- (void)webServerDidUpdateNATPortMapping:(GCDWebServer*)server; + +/** + * This method is called when the first GCDWebServerConnection is opened by the + * server to serve a series of HTTP requests. + * + * A series of HTTP requests is considered ongoing as long as new HTTP requests + * keep coming (and new GCDWebServerConnection instances keep being opened), + * until before the last HTTP request has been responded to (and the + * corresponding last GCDWebServerConnection closed). + */ +- (void)webServerDidConnect:(GCDWebServer*)server; + +/** + * This method is called when the last GCDWebServerConnection is closed after + * the server has served a series of HTTP requests. + * + * The GCDWebServerOption_ConnectedStateCoalescingInterval option can be used + * to have the server wait some extra delay before considering that the series + * of HTTP requests has ended (in case there some latency between consecutive + * requests). This effectively coalesces the calls to -webServerDidConnect: + * and -webServerDidDisconnect:. + */ +- (void)webServerDidDisconnect:(GCDWebServer*)server; + +/** + * This method is called after the server has stopped. + */ +- (void)webServerDidStop:(GCDWebServer*)server; + +@end + +/** + * The GCDWebServer class listens for incoming HTTP requests on a given port, + * then passes each one to a "handler" capable of generating an HTTP response + * for it, which is then sent back to the client. + * + * GCDWebServer instances can be created and used from any thread but it's + * recommended to have the main thread's runloop be running so internal callbacks + * can be handled e.g. for Bonjour registration. + * + * See the README.md file for more information about the architecture of GCDWebServer. + */ +@interface GCDWebServer : NSObject + +/** + * Sets the delegate for the server. + */ +@property(nonatomic, weak, nullable) id delegate; + +/** + * Returns YES if the server is currently running. + */ +@property(nonatomic, readonly, getter=isRunning) BOOL running; + +/** + * Returns the port used by the server. + * + * @warning This property is only valid if the server is running. + */ +@property(nonatomic, readonly) NSUInteger port; + +/** + * Returns the Bonjour name used by the server. + * + * @warning This property is only valid if the server is running and Bonjour + * registration has successfully completed, which can take up to a few seconds. + */ +@property(nonatomic, readonly, nullable) NSString* bonjourName; + +/** + * Returns the Bonjour service type used by the server. + * + * @warning This property is only valid if the server is running and Bonjour + * registration has successfully completed, which can take up to a few seconds. + */ +@property(nonatomic, readonly, nullable) NSString* bonjourType; + +/** + * This method is the designated initializer for the class. + */ +- (instancetype)init; + +/** + * Adds to the server a handler that generates responses synchronously when handling incoming HTTP requests. + * + * Handlers are called in a LIFO queue, so if multiple handlers can potentially + * respond to a given request, the latest added one wins. + * + * @warning Addling handlers while the server is running is not allowed. + */ +- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; + +/** + * Adds to the server a handler that generates responses asynchronously when handling incoming HTTP requests. + * + * Handlers are called in a LIFO queue, so if multiple handlers can potentially + * respond to a given request, the latest added one wins. + * + * @warning Addling handlers while the server is running is not allowed. + */ +- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock; + +/** + * Removes all handlers previously added to the server. + * + * @warning Removing handlers while the server is running is not allowed. + */ +- (void)removeAllHandlers; + +/** + * Starts the server with explicit options. This method is the designated way + * to start the server. + * + * Returns NO if the server failed to start and sets "error" argument if not NULL. + */ +- (BOOL)startWithOptions:(nullable NSDictionary*)options error:(NSError** _Nullable)error; + +/** + * Stops the server and prevents it to accepts new HTTP requests. + * + * @warning Stopping the server does not abort GCDWebServerConnection instances + * currently handling already received HTTP requests. These connections will + * continue to execute normally until completion. + */ +- (void)stop; + +@end + +@interface GCDWebServer (Extensions) + +/** + * Returns the server's URL. + * + * @warning This property is only valid if the server is running. + */ +@property(nonatomic, readonly, nullable) NSURL* serverURL; + +/** + * Returns the server's Bonjour URL. + * + * @warning This property is only valid if the server is running and Bonjour + * registration has successfully completed, which can take up to a few seconds. + * Also be aware this property will not automatically update if the Bonjour hostname + * has been dynamically changed after the server started running (this should be rare). + */ +@property(nonatomic, readonly, nullable) NSURL* bonjourServerURL; + +/** + * Returns the server's public URL. + * + * @warning This property is only valid if the server is running and NAT port + * mapping is active. + */ +@property(nonatomic, readonly, nullable) NSURL* publicServerURL; + +/** + * Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS) + * using the default Bonjour name. + * + * Returns NO if the server failed to start. + */ +- (BOOL)start; + +/** + * Starts the server on a given port and with a specific Bonjour name. + * Pass a nil Bonjour name to disable Bonjour entirely or an empty string to + * use the default name. + * + * Returns NO if the server failed to start. + */ +- (BOOL)startWithPort:(NSUInteger)port bonjourName:(nullable NSString*)name; + +#if !TARGET_OS_IPHONE + +/** + * Runs the server synchronously using -startWithPort:bonjourName: until a + * SIGINT signal is received i.e. Ctrl-C. This method is intended to be used + * by command line tools. + * + * Returns NO if the server failed to start. + * + * @warning This method must be used from the main thread only. + */ +- (BOOL)runWithPort:(NSUInteger)port bonjourName:(nullable NSString*)name; + +/** + * Runs the server synchronously using -startWithOptions: until a SIGTERM or + * SIGINT signal is received i.e. Ctrl-C in Terminal. This method is intended to + * be used by command line tools. + * + * Returns NO if the server failed to start and sets "error" argument if not NULL. + * + * @warning This method must be used from the main thread only. + */ +- (BOOL)runWithOptions:(nullable NSDictionary*)options error:(NSError** _Nullable)error; + +#endif + +@end + +@interface GCDWebServer (Handlers) + +/** + * Adds a default handler to the server to handle all incoming HTTP requests + * with a given HTTP method and generate responses synchronously. + */ +- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; + +/** + * Adds a default handler to the server to handle all incoming HTTP requests + * with a given HTTP method and generate responses asynchronously. + */ +- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block; + +/** + * Adds a handler to the server to handle incoming HTTP requests with a given + * HTTP method and a specific case-insensitive path and generate responses + * synchronously. + */ +- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; + +/** + * Adds a handler to the server to handle incoming HTTP requests with a given + * HTTP method and a specific case-insensitive path and generate responses + * asynchronously. + */ +- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block; + +/** + * Adds a handler to the server to handle incoming HTTP requests with a given + * HTTP method and a path matching a case-insensitive regular expression and + * generate responses synchronously. + */ +- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; + +/** + * Adds a handler to the server to handle incoming HTTP requests with a given + * HTTP method and a path matching a case-insensitive regular expression and + * generate responses asynchronously. + */ +- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block; + +@end + +@interface GCDWebServer (GETHandlers) + +/** + * Adds a handler to the server to respond to incoming "GET" HTTP requests + * with a specific case-insensitive path with in-memory data. + */ +- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(nullable NSString*)contentType cacheAge:(NSUInteger)cacheAge; + +/** + * Adds a handler to the server to respond to incoming "GET" HTTP requests + * with a specific case-insensitive path with a file. + */ +- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; + +/** + * Adds a handler to the server to respond to incoming "GET" HTTP requests + * with a case-insensitive path inside a base path with the corresponding file + * inside a local directory. If no local file matches the request path, a 401 + * HTTP status code is returned to the client. + * + * The "indexFilename" argument allows to specify an "index" file name to use + * when the request path corresponds to a directory. + */ +- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(nullable NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; + +@end + +/** + * GCDWebServer provides its own built-in logging facility which is used by + * default. It simply sends log messages to stderr assuming it is connected + * to a terminal type device. + * + * GCDWebServer is also compatible with a limited set of third-party logging + * facilities. If one of them is available at compile time, GCDWebServer will + * automatically use it in place of the built-in one. + * + * Currently supported third-party logging facilities are: + * - XLFacility (by the same author as GCDWebServer): https://github.com/swisspol/XLFacility + * - CocoaLumberjack: https://github.com/CocoaLumberjack/CocoaLumberjack + * + * For both the built-in logging facility and CocoaLumberjack, the default + * logging level is INFO (or DEBUG if the preprocessor constant "DEBUG" + * evaluates to non-zero at compile time). + * + * It's possible to have GCDWebServer use a custom logging facility by defining + * the "__GCDWEBSERVER_LOGGING_HEADER__" preprocessor constant in Xcode build + * settings to the name of a custom header file (escaped like \"MyLogging.h\"). + * This header file must define the following set of macros: + * + * GWS_LOG_DEBUG(...) + * GWS_LOG_VERBOSE(...) + * GWS_LOG_INFO(...) + * GWS_LOG_WARNING(...) + * GWS_LOG_ERROR(...) + * + * IMPORTANT: These macros must behave like NSLog(). Furthermore the GWS_LOG_DEBUG() + * macro should not do anything unless the preprocessor constant "DEBUG" evaluates + * to non-zero. + * + * The logging methods below send log messages to the same logging facility + * used by GCDWebServer. They can be used for consistency wherever you interact + * with GCDWebServer in your code (e.g. in the implementation of handlers). + */ +@interface GCDWebServer (Logging) + +/** + * Sets the log level of the logging facility below which log messages are discarded. + * + * @warning The interpretation of the "level" argument depends on the logging + * facility used at compile time. + * + * If using the built-in logging facility, the log levels are as follow: + * DEBUG = 0 + * VERBOSE = 1 + * INFO = 2 + * WARNING = 3 + * ERROR = 4 + */ ++ (void)setLogLevel:(int)level; + +/** + * Logs a message to the logging facility at the VERBOSE level. + */ +- (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1, 2); + +/** + * Logs a message to the logging facility at the INFO level. + */ +- (void)logInfo:(NSString*)format, ... NS_FORMAT_FUNCTION(1, 2); + +/** + * Logs a message to the logging facility at the WARNING level. + */ +- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1, 2); + +/** + * Logs a message to the logging facility at the ERROR level. + */ +- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1, 2); + +@end + +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + +@interface GCDWebServer (Testing) + +/** + * Activates recording of HTTP requests and responses which create files in the + * current directory containing the raw data for all requests and responses. + * + * @warning The current directory must not contain any prior recording files. + */ +@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled; + +/** + * Runs tests by playing back pre-recorded HTTP requests in the given directory + * and comparing the generated responses with the pre-recorded ones. + * + * Returns the number of failed tests or -1 if server failed to start. + */ +- (NSInteger)runTestsWithOptions:(nullable NSDictionary*)options inDirectory:(NSString*)path; + +@end + +#endif + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Core/GCDWebServer.m b/src/ios/GCDWebServer/Core/GCDWebServer.m new file mode 100755 index 0000000..837c083 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServer.m @@ -0,0 +1,1320 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import +#if TARGET_OS_IPHONE +#import +#else +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ +#import +#endif +#endif +#import +#import + +#import "GCDWebServerPrivate.h" + +#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR +#define kDefaultPort 80 +#else +#define kDefaultPort 8080 +#endif + +#define kBonjourResolutionTimeout 5.0 + +NSString* const GCDWebServerOption_Port = @"Port"; +NSString* const GCDWebServerOption_BonjourName = @"BonjourName"; +NSString* const GCDWebServerOption_BonjourType = @"BonjourType"; +NSString* const GCDWebServerOption_RequestNATPortMapping = @"RequestNATPortMapping"; +NSString* const GCDWebServerOption_BindToLocalhost = @"BindToLocalhost"; +NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections"; +NSString* const GCDWebServerOption_ServerName = @"ServerName"; +NSString* const GCDWebServerOption_AuthenticationMethod = @"AuthenticationMethod"; +NSString* const GCDWebServerOption_AuthenticationRealm = @"AuthenticationRealm"; +NSString* const GCDWebServerOption_AuthenticationAccounts = @"AuthenticationAccounts"; +NSString* const GCDWebServerOption_ConnectionClass = @"ConnectionClass"; +NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET = @"AutomaticallyMapHEADToGET"; +NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval = @"ConnectedStateCoalescingInterval"; +NSString* const GCDWebServerOption_DispatchQueuePriority = @"DispatchQueuePriority"; +#if TARGET_OS_IPHONE +NSString* const GCDWebServerOption_AutomaticallySuspendInBackground = @"AutomaticallySuspendInBackground"; +#endif + +NSString* const GCDWebServerAuthenticationMethod_Basic = @"Basic"; +NSString* const GCDWebServerAuthenticationMethod_DigestAccess = @"DigestAccess"; + +#if defined(__GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__) +#if DEBUG +GCDWebServerLoggingLevel GCDWebServerLogLevel = kGCDWebServerLoggingLevel_Debug; +#else +GCDWebServerLoggingLevel GCDWebServerLogLevel = kGCDWebServerLoggingLevel_Info; +#endif +#elif defined(__GCDWEBSERVER_LOGGING_FACILITY_COCOALUMBERJACK__) +#if DEBUG +DDLogLevel GCDWebServerLogLevel = DDLogLevelDebug; +#else +DDLogLevel GCDWebServerLogLevel = DDLogLevelInfo; +#endif +#endif + +#if !TARGET_OS_IPHONE +static BOOL _run; +#endif + +#ifdef __GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__ + +void GCDWebServerLogMessage(GCDWebServerLoggingLevel level, NSString* format, ...) { + static const char* levelNames[] = {"DEBUG", "VERBOSE", "INFO", "WARNING", "ERROR"}; + static int enableLogging = -1; + if (enableLogging < 0) { + enableLogging = (isatty(STDERR_FILENO) ? 1 : 0); + } + if (enableLogging) { + va_list arguments; + va_start(arguments, format); + NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; + va_end(arguments); + fprintf(stderr, "[%s] %s\n", levelNames[level], [message UTF8String]); + } +} + +#endif + +#if !TARGET_OS_IPHONE + +static void _SignalHandler(int signal) { + _run = NO; + printf("\n"); +} + +#endif + +#if !TARGET_OS_IPHONE || defined(__GCDWEBSERVER_ENABLE_TESTING__) + +// This utility function is used to ensure scheduled callbacks on the main thread are called when running the server synchronously +// https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html +// The main queue works with the application’s run loop to interleave the execution of queued tasks with the execution of other event sources attached to the run loop +// TODO: Ensure all scheduled blocks on the main queue are also executed +static void _ExecuteMainThreadRunLoopSources() { + SInt32 result; + do { + result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true); + } while (result == kCFRunLoopRunHandledSource); +} + +#endif + +@implementation GCDWebServerHandler + +- (instancetype)initWithMatchBlock:(GCDWebServerMatchBlock _Nonnull)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock _Nonnull)processBlock { + if ((self = [super init])) { + _matchBlock = [matchBlock copy]; + _asyncProcessBlock = [processBlock copy]; + } + return self; +} + +@end + +@implementation GCDWebServer { + dispatch_queue_t _syncQueue; + dispatch_group_t _sourceGroup; + NSMutableArray* _handlers; + NSInteger _activeConnections; // Accessed through _syncQueue only + BOOL _connected; // Accessed on main thread only + CFRunLoopTimerRef _disconnectTimer; // Accessed on main thread only + + NSDictionary* _options; + NSMutableDictionary* _authenticationBasicAccounts; + NSMutableDictionary* _authenticationDigestAccounts; + Class _connectionClass; + CFTimeInterval _disconnectDelay; + dispatch_source_t _source4; + dispatch_source_t _source6; + CFNetServiceRef _registrationService; + CFNetServiceRef _resolutionService; + DNSServiceRef _dnsService; + CFSocketRef _dnsSocket; + CFRunLoopSourceRef _dnsSource; + NSString* _dnsAddress; + NSUInteger _dnsPort; + BOOL _bindToLocalhost; +#if TARGET_OS_IPHONE + BOOL _suspendInBackground; + UIBackgroundTaskIdentifier _backgroundTask; +#endif +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + BOOL _recording; +#endif +} + ++ (void)initialize { + GCDWebServerInitializeFunctions(); +} + +- (instancetype)init { + if ((self = [super init])) { + _syncQueue = dispatch_queue_create([NSStringFromClass([self class]) UTF8String], DISPATCH_QUEUE_SERIAL); + _sourceGroup = dispatch_group_create(); + _handlers = [[NSMutableArray alloc] init]; +#if TARGET_OS_IPHONE + _backgroundTask = UIBackgroundTaskInvalid; +#endif + } + return self; +} + +- (void)dealloc { + GWS_DCHECK(_connected == NO); + GWS_DCHECK(_activeConnections == 0); + GWS_DCHECK(_options == nil); // The server can never be dealloc'ed while running because of the retain-cycle with the dispatch source + GWS_DCHECK(_disconnectTimer == NULL); // The server can never be dealloc'ed while the disconnect timer is pending because of the retain-cycle + +#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE + dispatch_release(_sourceGroup); + dispatch_release(_syncQueue); +#endif +} + +#if TARGET_OS_IPHONE + +// Always called on main thread +- (void)_startBackgroundTask { + GWS_DCHECK([NSThread isMainThread]); + if (_backgroundTask == UIBackgroundTaskInvalid) { + GWS_LOG_DEBUG(@"Did start background task"); + _backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + + GWS_LOG_WARNING(@"Application is being suspended while %@ is still connected", [self class]); + [self _endBackgroundTask]; + + }]; + } else { + GWS_DNOT_REACHED(); + } +} + +#endif + +// Always called on main thread +- (void)_didConnect { + GWS_DCHECK([NSThread isMainThread]); + GWS_DCHECK(_connected == NO); + _connected = YES; + GWS_LOG_DEBUG(@"Did connect"); + +#if TARGET_OS_IPHONE + if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) { + [self _startBackgroundTask]; + } +#endif + + if ([_delegate respondsToSelector:@selector(webServerDidConnect:)]) { + [_delegate webServerDidConnect:self]; + } +} + +- (void)willStartConnection:(GCDWebServerConnection*)connection { + dispatch_sync(_syncQueue, ^{ + + GWS_DCHECK(_activeConnections >= 0); + if (_activeConnections == 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (_disconnectTimer) { + CFRunLoopTimerInvalidate(_disconnectTimer); + CFRelease(_disconnectTimer); + _disconnectTimer = NULL; + } + if (_connected == NO) { + [self _didConnect]; + } + }); + } + _activeConnections += 1; + + }); +} + +#if TARGET_OS_IPHONE + +// Always called on main thread +- (void)_endBackgroundTask { + GWS_DCHECK([NSThread isMainThread]); + if (_backgroundTask != UIBackgroundTaskInvalid) { + if (_suspendInBackground && ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) && _source4) { + [self _stop]; + } + [[UIApplication sharedApplication] endBackgroundTask:_backgroundTask]; + _backgroundTask = UIBackgroundTaskInvalid; + GWS_LOG_DEBUG(@"Did end background task"); + } +} + +#endif + +// Always called on main thread +- (void)_didDisconnect { + GWS_DCHECK([NSThread isMainThread]); + GWS_DCHECK(_connected == YES); + _connected = NO; + GWS_LOG_DEBUG(@"Did disconnect"); + +#if TARGET_OS_IPHONE + [self _endBackgroundTask]; +#endif + + if ([_delegate respondsToSelector:@selector(webServerDidDisconnect:)]) { + [_delegate webServerDidDisconnect:self]; + } +} + +- (void)didEndConnection:(GCDWebServerConnection*)connection { + dispatch_sync(_syncQueue, ^{ + GWS_DCHECK(_activeConnections > 0); + _activeConnections -= 1; + if (_activeConnections == 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + if ((_disconnectDelay > 0.0) && (_source4 != NULL)) { + if (_disconnectTimer) { + CFRunLoopTimerInvalidate(_disconnectTimer); + CFRelease(_disconnectTimer); + } + _disconnectTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + _disconnectDelay, 0.0, 0, 0, ^(CFRunLoopTimerRef timer) { + GWS_DCHECK([NSThread isMainThread]); + [self _didDisconnect]; + CFRelease(_disconnectTimer); + _disconnectTimer = NULL; + }); + CFRunLoopAddTimer(CFRunLoopGetMain(), _disconnectTimer, kCFRunLoopCommonModes); + } else { + [self _didDisconnect]; + } + }); + } + }); +} + +- (NSString*)bonjourName { + CFStringRef name = _resolutionService ? CFNetServiceGetName(_resolutionService) : NULL; + return name && CFStringGetLength(name) ? CFBridgingRelease(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil; +} + +- (NSString*)bonjourType { + CFStringRef type = _resolutionService ? CFNetServiceGetType(_resolutionService) : NULL; + return type && CFStringGetLength(type) ? CFBridgingRelease(CFStringCreateCopy(kCFAllocatorDefault, type)) : nil; +} + +- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock { + [self addHandlerWithMatchBlock:matchBlock + asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { + completionBlock(processBlock(request)); + }]; +} + +- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock { + GWS_DCHECK(_options == nil); + GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock asyncProcessBlock:processBlock]; + [_handlers insertObject:handler atIndex:0]; +} + +- (void)removeAllHandlers { + GWS_DCHECK(_options == nil); + [_handlers removeAllObjects]; +} + +static void _NetServiceRegisterCallBack(CFNetServiceRef service, CFStreamError* error, void* info) { + GWS_DCHECK([NSThread isMainThread]); + @autoreleasepool { + if (error->error) { + GWS_LOG_ERROR(@"Bonjour registration error %i (domain %i)", (int)error->error, (int)error->domain); + } else { + GCDWebServer* server = (__bridge GCDWebServer*)info; + GWS_LOG_VERBOSE(@"Bonjour registration complete for %@", [server class]); + if (!CFNetServiceResolveWithTimeout(server->_resolutionService, kBonjourResolutionTimeout, NULL)) { + GWS_LOG_ERROR(@"Failed starting Bonjour resolution"); + GWS_DNOT_REACHED(); + } + } + } +} + +static void _NetServiceResolveCallBack(CFNetServiceRef service, CFStreamError* error, void* info) { + GWS_DCHECK([NSThread isMainThread]); + @autoreleasepool { + if (error->error) { + if ((error->domain != kCFStreamErrorDomainNetServices) && (error->error != kCFNetServicesErrorTimeout)) { + GWS_LOG_ERROR(@"Bonjour resolution error %i (domain %i)", (int)error->error, (int)error->domain); + } + } else { + GCDWebServer* server = (__bridge GCDWebServer*)info; + GWS_LOG_INFO(@"%@ now locally reachable at %@", [server class], server.bonjourServerURL); + if ([server.delegate respondsToSelector:@selector(webServerDidCompleteBonjourRegistration:)]) { + [server.delegate webServerDidCompleteBonjourRegistration:server]; + } + } + } +} + +static void _DNSServiceCallBack(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, uint32_t externalAddress, DNSServiceProtocol protocol, uint16_t internalPort, uint16_t externalPort, uint32_t ttl, void* context) { + GWS_DCHECK([NSThread isMainThread]); + @autoreleasepool { + GCDWebServer* server = (__bridge GCDWebServer*)context; + if ((errorCode == kDNSServiceErr_NoError) || (errorCode == kDNSServiceErr_DoubleNAT)) { + struct sockaddr_in addr4; + bzero(&addr4, sizeof(addr4)); + addr4.sin_len = sizeof(addr4); + addr4.sin_family = AF_INET; + addr4.sin_addr.s_addr = externalAddress; // Already in network byte order + server->_dnsAddress = GCDWebServerStringFromSockAddr((const struct sockaddr*)&addr4, NO); + server->_dnsPort = ntohs(externalPort); + GWS_LOG_INFO(@"%@ now publicly reachable at %@", [server class], server.publicServerURL); + } else { + GWS_LOG_ERROR(@"DNS service error %i", errorCode); + server->_dnsAddress = nil; + server->_dnsPort = 0; + } + if ([server.delegate respondsToSelector:@selector(webServerDidUpdateNATPortMapping:)]) { + [server.delegate webServerDidUpdateNATPortMapping:server]; + } + } +} + +static void _SocketCallBack(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void* data, void* info) { + GWS_DCHECK([NSThread isMainThread]); + @autoreleasepool { + GCDWebServer* server = (__bridge GCDWebServer*)info; + DNSServiceErrorType status = DNSServiceProcessResult(server->_dnsService); + if (status != kDNSServiceErr_NoError) { + GWS_LOG_ERROR(@"DNS service error %i", status); + } + } +} + +static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValue) { + id value = [options objectForKey:key]; + return value ? value : defaultValue; +} + +static inline NSString* _EncodeBase64(NSString* string) { + NSData* data = [string dataUsingEncoding:NSUTF8StringEncoding]; +#if (TARGET_OS_IPHONE && !(__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0)) || (!TARGET_OS_IPHONE && !(__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9)) + if (![data respondsToSelector:@selector(base64EncodedDataWithOptions:)]) { + return [data base64Encoding]; + } +#endif + return [[NSString alloc] initWithData:[data base64EncodedDataWithOptions:0] encoding:NSASCIIStringEncoding]; +} + +- (int)_createListeningSocket:(BOOL)useIPv6 + localAddress:(const void*)address + length:(socklen_t)length + maxPendingConnections:(NSUInteger)maxPendingConnections + error:(NSError**)error { + int listeningSocket = socket(useIPv6 ? PF_INET6 : PF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listeningSocket > 0) { + int yes = 1; + setsockopt(listeningSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + + if (bind(listeningSocket, address, length) == 0) { + if (listen(listeningSocket, (int)maxPendingConnections) == 0) { + GWS_LOG_DEBUG(@"Did open %s listening socket %i", useIPv6 ? "IPv6" : "IPv4", listeningSocket); + return listeningSocket; + } else { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + GWS_LOG_ERROR(@"Failed starting %s listening socket: %s (%i)", useIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); + close(listeningSocket); + } + } else { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + GWS_LOG_ERROR(@"Failed binding %s listening socket: %s (%i)", useIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); + close(listeningSocket); + } + + } else { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + GWS_LOG_ERROR(@"Failed creating %s listening socket: %s (%i)", useIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); + } + return -1; +} + +- (dispatch_source_t)_createDispatchSourceWithListeningSocket:(int)listeningSocket isIPv6:(BOOL)isIPv6 { + dispatch_group_enter(_sourceGroup); + dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, dispatch_get_global_queue(_dispatchQueuePriority, 0)); + dispatch_source_set_cancel_handler(source, ^{ + + @autoreleasepool { + int result = close(listeningSocket); + if (result != 0) { + GWS_LOG_ERROR(@"Failed closing %s listening socket: %s (%i)", isIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); + } else { + GWS_LOG_DEBUG(@"Did close %s listening socket %i", isIPv6 ? "IPv6" : "IPv4", listeningSocket); + } + } + dispatch_group_leave(_sourceGroup); + + }); + dispatch_source_set_event_handler(source, ^{ + + @autoreleasepool { + struct sockaddr_storage remoteSockAddr; + socklen_t remoteAddrLen = sizeof(remoteSockAddr); + int socket = accept(listeningSocket, (struct sockaddr*)&remoteSockAddr, &remoteAddrLen); + if (socket > 0) { + NSData* remoteAddress = [NSData dataWithBytes:&remoteSockAddr length:remoteAddrLen]; + + struct sockaddr_storage localSockAddr; + socklen_t localAddrLen = sizeof(localSockAddr); + NSData* localAddress = nil; + if (getsockname(socket, (struct sockaddr*)&localSockAddr, &localAddrLen) == 0) { + localAddress = [NSData dataWithBytes:&localSockAddr length:localAddrLen]; + GWS_DCHECK((!isIPv6 && localSockAddr.ss_family == AF_INET) || (isIPv6 && localSockAddr.ss_family == AF_INET6)); + } else { + GWS_DNOT_REACHED(); + } + + int noSigPipe = 1; + setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); // Make sure this socket cannot generate SIG_PIPE + + GCDWebServerConnection* connection = [[_connectionClass alloc] initWithServer:self localAddress:localAddress remoteAddress:remoteAddress socket:socket]; // Connection will automatically retain itself while opened + [connection self]; // Prevent compiler from complaining about unused variable / useless statement + } else { + GWS_LOG_ERROR(@"Failed accepting %s socket: %s (%i)", isIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); + } + } + + }); + return source; +} + +- (BOOL)_start:(NSError**)error { + GWS_DCHECK(_source4 == NULL); + + NSUInteger port = [_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue]; + BOOL bindToLocalhost = [_GetOption(_options, GCDWebServerOption_BindToLocalhost, @NO) boolValue]; + NSUInteger maxPendingConnections = [_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue]; + + struct sockaddr_in addr4; + bzero(&addr4, sizeof(addr4)); + addr4.sin_len = sizeof(addr4); + addr4.sin_family = AF_INET; + addr4.sin_port = htons(port); + addr4.sin_addr.s_addr = bindToLocalhost ? htonl(INADDR_LOOPBACK) : htonl(INADDR_ANY); + int listeningSocket4 = [self _createListeningSocket:NO localAddress:&addr4 length:sizeof(addr4) maxPendingConnections:maxPendingConnections error:error]; + if (listeningSocket4 <= 0) { + return NO; + } + if (port == 0) { + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + if (getsockname(listeningSocket4, (struct sockaddr*)&addr, &addrlen) == 0) { + port = ntohs(addr.sin_port); + } else { + GWS_LOG_ERROR(@"Failed retrieving socket address: %s (%i)", strerror(errno), errno); + } + } + + struct sockaddr_in6 addr6; + bzero(&addr6, sizeof(addr6)); + addr6.sin6_len = sizeof(addr6); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons(port); + addr6.sin6_addr = bindToLocalhost ? in6addr_loopback : in6addr_any; + int listeningSocket6 = [self _createListeningSocket:YES localAddress:&addr6 length:sizeof(addr6) maxPendingConnections:maxPendingConnections error:error]; + if (listeningSocket6 <= 0) { + close(listeningSocket4); + return NO; + } + + _serverName = [_GetOption(_options, GCDWebServerOption_ServerName, NSStringFromClass([self class])) copy]; + NSString* authenticationMethod = _GetOption(_options, GCDWebServerOption_AuthenticationMethod, nil); + if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_Basic]) { + _authenticationRealm = [_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy]; + _authenticationBasicAccounts = [[NSMutableDictionary alloc] init]; + NSDictionary* accounts = _GetOption(_options, GCDWebServerOption_AuthenticationAccounts, @{}); + [accounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* password, BOOL* stop) { + [_authenticationBasicAccounts setObject:_EncodeBase64([NSString stringWithFormat:@"%@:%@", username, password]) forKey:username]; + }]; + } else if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_DigestAccess]) { + _authenticationRealm = [_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy]; + _authenticationDigestAccounts = [[NSMutableDictionary alloc] init]; + NSDictionary* accounts = _GetOption(_options, GCDWebServerOption_AuthenticationAccounts, @{}); + [accounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* password, BOOL* stop) { + [_authenticationDigestAccounts setObject:GCDWebServerComputeMD5Digest(@"%@:%@:%@", username, _authenticationRealm, password) forKey:username]; + }]; + } + _connectionClass = _GetOption(_options, GCDWebServerOption_ConnectionClass, [GCDWebServerConnection class]); + _shouldAutomaticallyMapHEADToGET = [_GetOption(_options, GCDWebServerOption_AutomaticallyMapHEADToGET, @YES) boolValue]; + _disconnectDelay = [_GetOption(_options, GCDWebServerOption_ConnectedStateCoalescingInterval, @1.0) doubleValue]; + _dispatchQueuePriority = [_GetOption(_options, GCDWebServerOption_DispatchQueuePriority, @(DISPATCH_QUEUE_PRIORITY_DEFAULT)) longValue]; + + _source4 = [self _createDispatchSourceWithListeningSocket:listeningSocket4 isIPv6:NO]; + _source6 = [self _createDispatchSourceWithListeningSocket:listeningSocket6 isIPv6:YES]; + _port = port; + _bindToLocalhost = bindToLocalhost; + + NSString* bonjourName = _GetOption(_options, GCDWebServerOption_BonjourName, nil); + NSString* bonjourType = _GetOption(_options, GCDWebServerOption_BonjourType, @"_http._tcp"); + if (bonjourName) { + _registrationService = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), (__bridge CFStringRef)bonjourType, (__bridge CFStringRef)(bonjourName.length ? bonjourName : _serverName), (SInt32)_port); + if (_registrationService) { + CFNetServiceClientContext context = {0, (__bridge void*)self, NULL, NULL, NULL}; + + CFNetServiceSetClient(_registrationService, _NetServiceRegisterCallBack, &context); + CFNetServiceScheduleWithRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes); + CFStreamError streamError = {0}; + CFNetServiceRegisterWithOptions(_registrationService, 0, &streamError); + + _resolutionService = CFNetServiceCreateCopy(kCFAllocatorDefault, _registrationService); + if (_resolutionService) { + CFNetServiceSetClient(_resolutionService, _NetServiceResolveCallBack, &context); + CFNetServiceScheduleWithRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes); + } else { + GWS_LOG_ERROR(@"Failed creating CFNetService for resolution"); + } + } else { + GWS_LOG_ERROR(@"Failed creating CFNetService for registration"); + } + } + + if ([_GetOption(_options, GCDWebServerOption_RequestNATPortMapping, @NO) boolValue]) { + DNSServiceErrorType status = DNSServiceNATPortMappingCreate(&_dnsService, 0, 0, kDNSServiceProtocol_TCP, htons(port), htons(port), 0, _DNSServiceCallBack, (__bridge void*)self); + if (status == kDNSServiceErr_NoError) { + CFSocketContext context = {0, (__bridge void*)self, NULL, NULL, NULL}; + _dnsSocket = CFSocketCreateWithNative(kCFAllocatorDefault, DNSServiceRefSockFD(_dnsService), kCFSocketReadCallBack, _SocketCallBack, &context); + if (_dnsSocket) { + CFSocketSetSocketFlags(_dnsSocket, CFSocketGetSocketFlags(_dnsSocket) & ~kCFSocketCloseOnInvalidate); + _dnsSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _dnsSocket, 0); + if (_dnsSource) { + CFRunLoopAddSource(CFRunLoopGetMain(), _dnsSource, kCFRunLoopCommonModes); + } else { + GWS_LOG_ERROR(@"Failed creating CFRunLoopSource"); + GWS_DNOT_REACHED(); + } + } else { + GWS_LOG_ERROR(@"Failed creating CFSocket"); + GWS_DNOT_REACHED(); + } + } else { + GWS_LOG_ERROR(@"Failed creating NAT port mapping (%i)", status); + } + } + + dispatch_resume(_source4); + dispatch_resume(_source6); + GWS_LOG_INFO(@"%@ started on port %i and reachable at %@", [self class], (int)_port, self.serverURL); + if ([_delegate respondsToSelector:@selector(webServerDidStart:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate webServerDidStart:self]; + }); + } + + return YES; +} + +- (void)_stop { + GWS_DCHECK(_source4 != NULL); + + if (_dnsService) { + _dnsAddress = nil; + _dnsPort = 0; + if (_dnsSource) { + CFRunLoopSourceInvalidate(_dnsSource); + CFRelease(_dnsSource); + _dnsSource = NULL; + } + if (_dnsSocket) { + CFRelease(_dnsSocket); + _dnsSocket = NULL; + } + DNSServiceRefDeallocate(_dnsService); + _dnsService = NULL; + } + + if (_registrationService) { + if (_resolutionService) { + CFNetServiceUnscheduleFromRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes); + CFNetServiceSetClient(_resolutionService, NULL, NULL); + CFNetServiceCancel(_resolutionService); + CFRelease(_resolutionService); + _resolutionService = NULL; + } + CFNetServiceUnscheduleFromRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes); + CFNetServiceSetClient(_registrationService, NULL, NULL); + CFNetServiceCancel(_registrationService); + CFRelease(_registrationService); + _registrationService = NULL; + } + + dispatch_source_cancel(_source6); + dispatch_source_cancel(_source4); + dispatch_group_wait(_sourceGroup, DISPATCH_TIME_FOREVER); // Wait until the cancellation handlers have been called which guarantees the listening sockets are closed +#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE + dispatch_release(_source6); +#endif + _source6 = NULL; +#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE + dispatch_release(_source4); +#endif + _source4 = NULL; + _port = 0; + _bindToLocalhost = NO; + + _serverName = nil; + _authenticationRealm = nil; + _authenticationBasicAccounts = nil; + _authenticationDigestAccounts = nil; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (_disconnectTimer) { + CFRunLoopTimerInvalidate(_disconnectTimer); + CFRelease(_disconnectTimer); + _disconnectTimer = NULL; + [self _didDisconnect]; + } + }); + + GWS_LOG_INFO(@"%@ stopped", [self class]); + if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate webServerDidStop:self]; + }); + } +} + +#if TARGET_OS_IPHONE + +- (void)_didEnterBackground:(NSNotification*)notification { + GWS_DCHECK([NSThread isMainThread]); + GWS_LOG_DEBUG(@"Did enter background"); + if ((_backgroundTask == UIBackgroundTaskInvalid) && _source4) { + [self _stop]; + } +} + +- (void)_willEnterForeground:(NSNotification*)notification { + GWS_DCHECK([NSThread isMainThread]); + GWS_LOG_DEBUG(@"Will enter foreground"); + if (!_source4) { + [self _start:NULL]; // TODO: There's probably nothing we can do on failure + } +} + +#endif + +- (BOOL)startWithOptions:(NSDictionary*)options error:(NSError**)error { + if (_options == nil) { + _options = options ? [options copy] : @{}; +#if TARGET_OS_IPHONE + _suspendInBackground = [_GetOption(_options, GCDWebServerOption_AutomaticallySuspendInBackground, @YES) boolValue]; + if (((_suspendInBackground == NO) || ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground)) && ![self _start:error]) +#else + if (![self _start:error]) +#endif + { + _options = nil; + return NO; + } +#if TARGET_OS_IPHONE + if (_suspendInBackground) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; + } +#endif + return YES; + } else { + GWS_DNOT_REACHED(); + } + return NO; +} + +- (BOOL)isRunning { + return (_options ? YES : NO); +} + +- (void)stop { + if (_options) { +#if TARGET_OS_IPHONE + if (_suspendInBackground) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; + } +#endif + if (_source4) { + [self _stop]; + } + _options = nil; + } else { + GWS_DNOT_REACHED(); + } +} + +@end + +@implementation GCDWebServer (Extensions) + +- (NSURL*)serverURL { + if (_source4) { + NSString* ipAddress = _bindToLocalhost ? @"localhost" : GCDWebServerGetPrimaryIPAddress(NO); // We can't really use IPv6 anyway as it doesn't work great with HTTP URLs in practice + if (ipAddress) { + if (_port != 80) { + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", ipAddress, (int)_port]]; + } else { + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", ipAddress]]; + } + } + } + return nil; +} + +- (NSURL*)bonjourServerURL { + if (_source4 && _resolutionService) { + NSString* name = (__bridge NSString*)CFNetServiceGetTargetHost(_resolutionService); + if (name.length) { + name = [name substringToIndex:(name.length - 1)]; // Strip trailing period at end of domain + if (_port != 80) { + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", name, (int)_port]]; + } else { + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", name]]; + } + } + } + return nil; +} + +- (NSURL*)publicServerURL { + if (_source4 && _dnsService && _dnsAddress && _dnsPort) { + if (_dnsPort != 80) { + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", _dnsAddress, (int)_dnsPort]]; + } else { + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", _dnsAddress]]; + } + } + return nil; +} + +- (BOOL)start { + return [self startWithPort:kDefaultPort bonjourName:@""]; +} + +- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name { + NSMutableDictionary* options = [NSMutableDictionary dictionary]; + [options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port]; + [options setValue:name forKey:GCDWebServerOption_BonjourName]; + return [self startWithOptions:options error:NULL]; +} + +#if !TARGET_OS_IPHONE + +- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name { + NSMutableDictionary* options = [NSMutableDictionary dictionary]; + [options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port]; + [options setValue:name forKey:GCDWebServerOption_BonjourName]; + return [self runWithOptions:options error:NULL]; +} + +- (BOOL)runWithOptions:(NSDictionary*)options error:(NSError**)error { + GWS_DCHECK([NSThread isMainThread]); + BOOL success = NO; + _run = YES; + void (*termHandler)(int) = signal(SIGTERM, _SignalHandler); + void (*intHandler)(int) = signal(SIGINT, _SignalHandler); + if ((termHandler != SIG_ERR) && (intHandler != SIG_ERR)) { + if ([self startWithOptions:options error:error]) { + while (_run) { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true); + } + [self stop]; + success = YES; + } + _ExecuteMainThreadRunLoopSources(); + signal(SIGINT, intHandler); + signal(SIGTERM, termHandler); + } + return success; +} + +#endif + +@end + +@implementation GCDWebServer (Handlers) + +- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { + [self addDefaultHandlerForMethod:method + requestClass:aClass + asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { + completionBlock(block(request)); + }]; +} + +- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { + [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { + + if (![requestMethod isEqualToString:method]) { + return nil; + } + return [[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; + + } + asyncProcessBlock:block]; +} + +- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { + [self addHandlerForMethod:method + path:path + requestClass:aClass + asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { + completionBlock(block(request)); + }]; +} + +- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { + if ([path hasPrefix:@"/"] && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) { + [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { + + if (![requestMethod isEqualToString:method]) { + return nil; + } + if ([urlPath caseInsensitiveCompare:path] != NSOrderedSame) { + return nil; + } + return [[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; + + } + asyncProcessBlock:block]; + } else { + GWS_DNOT_REACHED(); + } +} + +- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { + [self addHandlerForMethod:method + pathRegex:regex + requestClass:aClass + asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { + completionBlock(block(request)); + }]; +} + +- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { + NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL]; + if (expression && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) { + [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { + + if (![requestMethod isEqualToString:method]) { + return nil; + } + + NSArray* matches = [expression matchesInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)]; + if (matches.count == 0) { + return nil; + } + + NSMutableArray* captures = [NSMutableArray array]; + for (NSTextCheckingResult* result in matches) { + // Start at 1; index 0 is the whole string + for (NSUInteger i = 1; i < result.numberOfRanges; i++) { + NSRange range = [result rangeAtIndex:i]; + // range is {NSNotFound, 0} "if one of the capture groups did not participate in this particular match" + // see discussion in -[NSRegularExpression firstMatchInString:options:range:] + if (range.location != NSNotFound) { + [captures addObject:[urlPath substringWithRange:range]]; + } + } + } + + GCDWebServerRequest* request = [[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; + [request setAttribute:captures forKey:GCDWebServerRequestAttribute_RegexCaptures]; + return request; + + } + asyncProcessBlock:block]; + } else { + GWS_DNOT_REACHED(); + } +} + +@end + +@implementation GCDWebServer (GETHandlers) + +- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge { + [self addHandlerForMethod:@"GET" + path:path + requestClass:[GCDWebServerRequest class] + processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) { + + GCDWebServerResponse* response = [GCDWebServerDataResponse responseWithData:staticData contentType:contentType]; + response.cacheControlMaxAge = cacheAge; + return response; + + }]; +} + +- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests { + [self addHandlerForMethod:@"GET" + path:path + requestClass:[GCDWebServerRequest class] + processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) { + + GCDWebServerResponse* response = nil; + if (allowRangeRequests) { + response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange isAttachment:isAttachment]; + [response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"]; + } else { + response = [GCDWebServerFileResponse responseWithFile:filePath isAttachment:isAttachment]; + } + response.cacheControlMaxAge = cacheAge; + return response; + + }]; +} + +- (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path { + NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path]; + if (enumerator == nil) { + return nil; + } + NSMutableString* html = [NSMutableString string]; + [html appendString:@"\n"]; + [html appendString:@"\n"]; + [html appendString:@"
    \n"]; + for (NSString* file in enumerator) { + if (![file hasPrefix:@"."]) { + NSString* type = [[enumerator fileAttributes] objectForKey:NSFileType]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSString* escapedFile = [file stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; +#pragma clang diagnostic pop + GWS_DCHECK(escapedFile); + if ([type isEqualToString:NSFileTypeRegular]) { + [html appendFormat:@"
  • %@
  • \n", escapedFile, file]; + } else if ([type isEqualToString:NSFileTypeDirectory]) { + [html appendFormat:@"
  • %@/
  • \n", escapedFile, file]; + } + } + [enumerator skipDescendents]; + } + [html appendString:@"
\n"]; + [html appendString:@"\n"]; + return [GCDWebServerDataResponse responseWithHTML:html]; +} + +- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests { + if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) { + GCDWebServer* __unsafe_unretained server = self; + [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { + + if (![requestMethod isEqualToString:@"GET"]) { + return nil; + } + if (![urlPath hasPrefix:basePath]) { + return nil; + } + return [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; + + } + processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) { + + GCDWebServerResponse* response = nil; + NSString* filePath = [directoryPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]]; + NSString* fileType = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL] fileType]; + if (fileType) { + if ([fileType isEqualToString:NSFileTypeDirectory]) { + if (indexFilename) { + NSString* indexPath = [filePath stringByAppendingPathComponent:indexFilename]; + NSString* indexType = [[[NSFileManager defaultManager] attributesOfItemAtPath:indexPath error:NULL] fileType]; + if ([indexType isEqualToString:NSFileTypeRegular]) { + return [GCDWebServerFileResponse responseWithFile:indexPath]; + } + } + response = [server _responseWithContentsOfDirectory:filePath]; + } else if ([fileType isEqualToString:NSFileTypeRegular]) { + if (allowRangeRequests) { + response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange]; + [response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"]; + } else { + response = [GCDWebServerFileResponse responseWithFile:filePath]; + } + } + } + if (response) { + response.cacheControlMaxAge = cacheAge; + } else { + response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NotFound]; + } + return response; + + }]; + } else { + GWS_DNOT_REACHED(); + } +} + +@end + +@implementation GCDWebServer (Logging) + ++ (void)setLogLevel:(int)level { +#if defined(__GCDWEBSERVER_LOGGING_FACILITY_XLFACILITY__) + [XLSharedFacility setMinLogLevel:level]; +#elif defined(__GCDWEBSERVER_LOGGING_FACILITY_COCOALUMBERJACK__) + GCDWebServerLogLevel = level; +#elif defined(__GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__) + GCDWebServerLogLevel = level; +#endif +} + +- (void)logVerbose:(NSString*)format, ... { + va_list arguments; + va_start(arguments, format); + GWS_LOG_VERBOSE(@"%@", [[NSString alloc] initWithFormat:format arguments:arguments]); + va_end(arguments); +} + +- (void)logInfo:(NSString*)format, ... { + va_list arguments; + va_start(arguments, format); + GWS_LOG_INFO(@"%@", [[NSString alloc] initWithFormat:format arguments:arguments]); + va_end(arguments); +} + +- (void)logWarning:(NSString*)format, ... { + va_list arguments; + va_start(arguments, format); + GWS_LOG_WARNING(@"%@", [[NSString alloc] initWithFormat:format arguments:arguments]); + va_end(arguments); +} + +- (void)logError:(NSString*)format, ... { + va_list arguments; + va_start(arguments, format); + GWS_LOG_ERROR(@"%@", [[NSString alloc] initWithFormat:format arguments:arguments]); + va_end(arguments); +} + +@end + +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + +@implementation GCDWebServer (Testing) + +- (void)setRecordingEnabled:(BOOL)flag { + _recording = flag; +} + +- (BOOL)isRecordingEnabled { + return _recording; +} + +static CFHTTPMessageRef _CreateHTTPMessageFromData(NSData* data, BOOL isRequest) { + CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, isRequest); + if (CFHTTPMessageAppendBytes(message, data.bytes, data.length)) { + return message; + } + CFRelease(message); + return NULL; +} + +static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData, NSUInteger port) { + CFHTTPMessageRef response = NULL; + int httpSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); + if (httpSocket > 0) { + struct sockaddr_in addr4; + bzero(&addr4, sizeof(addr4)); + addr4.sin_len = sizeof(port); + addr4.sin_family = AF_INET; + addr4.sin_port = htons(8080); + addr4.sin_addr.s_addr = htonl(INADDR_ANY); + if (connect(httpSocket, (void*)&addr4, sizeof(addr4)) == 0) { + if (write(httpSocket, inData.bytes, inData.length) == (ssize_t)inData.length) { + NSMutableData* outData = [[NSMutableData alloc] initWithLength:(256 * 1024)]; + NSUInteger length = 0; + while (1) { + ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length); + if (result < 0) { + length = NSUIntegerMax; + break; + } else if (result == 0) { + break; + } + length += result; + if (length >= outData.length) { + outData.length = 2 * outData.length; + } + } + if (length != NSUIntegerMax) { + outData.length = length; + response = _CreateHTTPMessageFromData(outData, NO); + } else { + GWS_DNOT_REACHED(); + } + } + } + close(httpSocket); + } + return response; +} + +static void _LogResult(NSString* format, ...) { + va_list arguments; + va_start(arguments, format); + NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; + va_end(arguments); + fprintf(stdout, "%s\n", [message UTF8String]); +} + +- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path { + GWS_DCHECK([NSThread isMainThread]); + NSArray* ignoredHeaders = @[ @"Date", @"Etag" ]; // Dates are always different by definition and ETags depend on file system node IDs + NSInteger result = -1; + if ([self startWithOptions:options error:NULL]) { + _ExecuteMainThreadRunLoopSources(); + + result = 0; + NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL]; + for (NSString* requestFile in files) { + if (![requestFile hasSuffix:@".request"]) { + continue; + } + @autoreleasepool { + NSString* index = [[requestFile componentsSeparatedByString:@"-"] firstObject]; + BOOL success = NO; + NSData* requestData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:requestFile]]; + if (requestData) { + CFHTTPMessageRef request = _CreateHTTPMessageFromData(requestData, YES); + if (request) { + NSString* requestMethod = CFBridgingRelease(CFHTTPMessageCopyRequestMethod(request)); + NSURL* requestURL = CFBridgingRelease(CFHTTPMessageCopyRequestURL(request)); + _LogResult(@"[%i] %@ %@", (int)[index integerValue], requestMethod, requestURL.path); + NSString* prefix = [index stringByAppendingString:@"-"]; + for (NSString* responseFile in files) { + if ([responseFile hasPrefix:prefix] && [responseFile hasSuffix:@".response"]) { + NSData* responseData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:responseFile]]; + if (responseData) { + CFHTTPMessageRef expectedResponse = _CreateHTTPMessageFromData(responseData, NO); + if (expectedResponse) { + CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromPerformingRequest(requestData, self.port); + if (actualResponse) { + success = YES; + + CFIndex expectedStatusCode = CFHTTPMessageGetResponseStatusCode(expectedResponse); + CFIndex actualStatusCode = CFHTTPMessageGetResponseStatusCode(actualResponse); + if (actualStatusCode != expectedStatusCode) { + _LogResult(@" Status code not matching:\n Expected: %i\n Actual: %i", (int)expectedStatusCode, (int)actualStatusCode); + success = NO; + } + + NSDictionary* expectedHeaders = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(expectedResponse)); + NSDictionary* actualHeaders = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(actualResponse)); + for (NSString* expectedHeader in expectedHeaders) { + if ([ignoredHeaders containsObject:expectedHeader]) { + continue; + } + NSString* expectedValue = [expectedHeaders objectForKey:expectedHeader]; + NSString* actualValue = [actualHeaders objectForKey:expectedHeader]; + if (![actualValue isEqualToString:expectedValue]) { + _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", expectedHeader, expectedValue, actualValue); + success = NO; + } + } + for (NSString* actualHeader in actualHeaders) { + if (![expectedHeaders objectForKey:actualHeader]) { + _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", actualHeader, nil, [actualHeaders objectForKey:actualHeader]); + success = NO; + } + } + + NSString* expectedContentLength = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(expectedResponse, CFSTR("Content-Length"))); + NSData* expectedBody = CFBridgingRelease(CFHTTPMessageCopyBody(expectedResponse)); + NSString* actualContentLength = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(actualResponse, CFSTR("Content-Length"))); + NSData* actualBody = CFBridgingRelease(CFHTTPMessageCopyBody(actualResponse)); + if ([actualContentLength isEqualToString:expectedContentLength] && (actualBody.length > expectedBody.length)) { // Handle web browser closing connection before retrieving entire body (e.g. when playing a video file) + actualBody = [actualBody subdataWithRange:NSMakeRange(0, expectedBody.length)]; + } + if (![actualBody isEqualToData:expectedBody]) { + _LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length); + success = NO; +#if !TARGET_OS_IPHONE +#if DEBUG + if (GCDWebServerIsTextContentType((NSString*)[expectedHeaders objectForKey:@"Content-Type"])) { + NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:(NSString*)[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; + NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:(NSString*)[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; + if ([expectedBody writeToFile:expectedPath atomically:YES] && [actualBody writeToFile:actualPath atomically:YES]) { + NSTask* task = [[NSTask alloc] init]; + [task setLaunchPath:@"/usr/bin/opendiff"]; + [task setArguments:@[ expectedPath, actualPath ]]; + [task launch]; + } + } +#endif +#endif + } + + CFRelease(actualResponse); + } + CFRelease(expectedResponse); + } + } else { + GWS_DNOT_REACHED(); + } + break; + } + } + CFRelease(request); + } + } else { + GWS_DNOT_REACHED(); + } + _LogResult(@""); + if (!success) { + ++result; + } + } + _ExecuteMainThreadRunLoopSources(); + } + + [self stop]; + + _ExecuteMainThreadRunLoopSources(); + } + return result; +} + +@end + +#endif diff --git a/src/ios/GCDWebServer/Core/GCDWebServerConnection.h b/src/ios/GCDWebServer/Core/GCDWebServerConnection.h new file mode 100755 index 0000000..420d12a --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerConnection.h @@ -0,0 +1,183 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServer.h" + +NS_ASSUME_NONNULL_BEGIN + +@class GCDWebServerHandler; + +/** + * The GCDWebServerConnection class is instantiated by GCDWebServer to handle + * each new HTTP connection. Each instance stays alive until the connection is + * closed. + * + * You cannot use this class directly, but it is made public so you can + * subclass it to override some hooks. Use the GCDWebServerOption_ConnectionClass + * option for GCDWebServer to install your custom subclass. + * + * @warning The GCDWebServerConnection retains the GCDWebServer until the + * connection is closed. + */ +@interface GCDWebServerConnection : NSObject + +/** + * Returns the GCDWebServer that owns the connection. + */ +@property(nonatomic, readonly) GCDWebServer* server; + +/** + * Returns YES if the connection is using IPv6. + */ +@property(nonatomic, readonly, getter=isUsingIPv6) BOOL usingIPv6; + +/** + * Returns the address of the local peer (i.e. server) of the connection + * as a raw "struct sockaddr". + */ +@property(nonatomic, readonly) NSData* localAddressData; + +/** + * Returns the address of the local peer (i.e. server) of the connection + * as a string. + */ +@property(nonatomic, readonly) NSString* localAddressString; + +/** + * Returns the address of the remote peer (i.e. client) of the connection + * as a raw "struct sockaddr". + */ +@property(nonatomic, readonly) NSData* remoteAddressData; + +/** + * Returns the address of the remote peer (i.e. client) of the connection + * as a string. + */ +@property(nonatomic, readonly) NSString* remoteAddressString; + +/** + * Returns the total number of bytes received from the remote peer (i.e. client) + * so far. + */ +@property(nonatomic, readonly) NSUInteger totalBytesRead; + +/** + * Returns the total number of bytes sent to the remote peer (i.e. client) so far. + */ +@property(nonatomic, readonly) NSUInteger totalBytesWritten; + +@end + +/** + * Hooks to customize the behavior of GCDWebServer HTTP connections. + * + * @warning These methods can be called on any GCD thread. + * Be sure to also call "super" when overriding them. + */ +@interface GCDWebServerConnection (Subclassing) + +/** + * This method is called when the connection is opened. + * + * Return NO to reject the connection e.g. after validating the local + * or remote address. + */ +- (BOOL)open; + +/** + * This method is called whenever data has been received + * from the remote peer (i.e. client). + * + * @warning Do not attempt to modify this data. + */ +- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length; + +/** + * This method is called whenever data has been sent + * to the remote peer (i.e. client). + * + * @warning Do not attempt to modify this data. + */ +- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length; + +/** + * This method is called after the HTTP headers have been received to + * allow replacing the request URL by another one. + * + * The default implementation returns the original URL. + */ +- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers; + +/** + * Assuming a valid HTTP request was received, this method is called before + * the request is processed. + * + * Return a non-nil GCDWebServerResponse to bypass the request processing entirely. + * + * The default implementation checks for HTTP authentication if applicable + * and returns a barebone 401 status code response if authentication failed. + */ +- (nullable GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request; + +/** + * Assuming a valid HTTP request was received and -preflightRequest: returned nil, + * this method is called to process the request by executing the handler's + * process block. + */ +- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion; + +/** + * Assuming a valid HTTP request was received and either -preflightRequest: + * or -processRequest:completion: returned a non-nil GCDWebServerResponse, + * this method is called to override the response. + * + * You can either modify the current response and return it, or return a + * completely new one. + * + * The default implementation replaces any response matching the "ETag" or + * "Last-Modified-Date" header of the request by a barebone "Not-Modified" (304) + * one. + */ +- (GCDWebServerResponse*)overrideResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request; + +/** + * This method is called if any error happens while validing or processing + * the request or if no GCDWebServerResponse was generated during processing. + * + * @warning If the request was invalid (e.g. the HTTP headers were malformed), + * the "request" argument will be nil. + */ +- (void)abortRequest:(nullable GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; + +/** + * Called when the connection is closed. + */ +- (void)close; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Core/GCDWebServerConnection.m b/src/ios/GCDWebServer/Core/GCDWebServerConnection.m new file mode 100755 index 0000000..b59f3f4 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerConnection.m @@ -0,0 +1,868 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import +#import +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ +#import +#endif + +#import "GCDWebServerPrivate.h" + +#define kHeadersReadCapacity (1 * 1024) +#define kBodyReadCapacity (256 * 1024) + +typedef void (^ReadDataCompletionBlock)(BOOL success); +typedef void (^ReadHeadersCompletionBlock)(NSData* extraData); +typedef void (^ReadBodyCompletionBlock)(BOOL success); + +typedef void (^WriteDataCompletionBlock)(BOOL success); +typedef void (^WriteHeadersCompletionBlock)(BOOL success); +typedef void (^WriteBodyCompletionBlock)(BOOL success); + +static NSData* _CRLFData = nil; +static NSData* _CRLFCRLFData = nil; +static NSData* _continueData = nil; +static NSData* _lastChunkData = nil; +static NSString* _digestAuthenticationNonce = nil; +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ +static int32_t _connectionCounter = 0; +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface GCDWebServerConnection (Read) +- (void)readData:(NSMutableData*)data withLength:(NSUInteger)length completionBlock:(ReadDataCompletionBlock)block; +- (void)readHeaders:(NSMutableData*)headersData withCompletionBlock:(ReadHeadersCompletionBlock)block; +- (void)readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block; +- (void)readNextBodyChunk:(NSMutableData*)chunkData completionBlock:(ReadBodyCompletionBlock)block; +@end + +@interface GCDWebServerConnection (Write) +- (void)writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block; +- (void)writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block; +- (void)writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block; +@end + +NS_ASSUME_NONNULL_END + +@implementation GCDWebServerConnection { + CFSocketNativeHandle _socket; + BOOL _virtualHEAD; + + CFHTTPMessageRef _requestMessage; + GCDWebServerRequest* _request; + GCDWebServerHandler* _handler; + CFHTTPMessageRef _responseMessage; + GCDWebServerResponse* _response; + NSInteger _statusCode; + + BOOL _opened; +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + NSUInteger _connectionIndex; + NSString* _requestPath; + int _requestFD; + NSString* _responsePath; + int _responseFD; +#endif +} + ++ (void)initialize { + if (_CRLFData == nil) { + _CRLFData = [[NSData alloc] initWithBytes:"\r\n" length:2]; + GWS_DCHECK(_CRLFData); + } + if (_CRLFCRLFData == nil) { + _CRLFCRLFData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; + GWS_DCHECK(_CRLFCRLFData); + } + if (_continueData == nil) { + CFHTTPMessageRef message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, NULL, kCFHTTPVersion1_1); + _continueData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(message)); + CFRelease(message); + GWS_DCHECK(_continueData); + } + if (_lastChunkData == nil) { + _lastChunkData = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5]; + } + if (_digestAuthenticationNonce == nil) { + CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); + _digestAuthenticationNonce = GCDWebServerComputeMD5Digest(@"%@", CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid))); + CFRelease(uuid); + } +} + +- (BOOL)isUsingIPv6 { + const struct sockaddr* localSockAddr = _localAddressData.bytes; + return (localSockAddr->sa_family == AF_INET6); +} + +- (void)_initializeResponseHeadersWithStatusCode:(NSInteger)statusCode { + _statusCode = statusCode; + _responseMessage = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_1); + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Connection"), CFSTR("Close")); + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Server"), (__bridge CFStringRef)_server.serverName); + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (__bridge CFStringRef)GCDWebServerFormatRFC822([NSDate date])); +} + +- (void)_startProcessingRequest { + GWS_DCHECK(_responseMessage == NULL); + + GCDWebServerResponse* preflightResponse = [self preflightRequest:_request]; + if (preflightResponse) { + [self _finishProcessingRequest:preflightResponse]; + } else { + [self processRequest:_request + completion:^(GCDWebServerResponse* processResponse) { + [self _finishProcessingRequest:processResponse]; + }]; + } +} + +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html +- (void)_finishProcessingRequest:(GCDWebServerResponse*)response { + GWS_DCHECK(_responseMessage == NULL); + BOOL hasBody = NO; + + if (response) { + response = [self overrideResponse:response forRequest:_request]; + } + if (response) { + if ([response hasBody]) { + [response prepareForReading]; + hasBody = !_virtualHEAD; + } + NSError* error = nil; + if (hasBody && ![response performOpen:&error]) { + GWS_LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error); + } else { + _response = response; + } + } + + if (_response) { + [self _initializeResponseHeadersWithStatusCode:_response.statusCode]; + if (_response.lastModifiedDate) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Last-Modified"), (__bridge CFStringRef)GCDWebServerFormatRFC822((NSDate*)_response.lastModifiedDate)); + } + if (_response.eTag) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("ETag"), (__bridge CFStringRef)_response.eTag); + } + if ((_response.statusCode >= 200) && (_response.statusCode < 300)) { + if (_response.cacheControlMaxAge > 0) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (__bridge CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)_response.cacheControlMaxAge]); + } else { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache")); + } + } + if (_response.contentType != nil) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (__bridge CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType)); + } + if (_response.contentLength != NSUIntegerMax) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (__bridge CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]); + } + if (_response.usesChunkedTransferEncoding) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Transfer-Encoding"), CFSTR("chunked")); + } + [_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); + }]; + [self writeHeadersWithCompletionBlock:^(BOOL success) { + + if (success) { + if (hasBody) { + [self writeBodyWithCompletionBlock:^(BOOL successInner) { + + [_response performClose]; // TODO: There's nothing we can do on failure as headers have already been sent + + }]; + } + } else if (hasBody) { + [_response performClose]; + } + + }]; + } else { + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + } +} + +- (void)_readBodyWithLength:(NSUInteger)length initialData:(NSData*)initialData { + NSError* error = nil; + if (![_request performOpen:&error]) { + GWS_LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + return; + } + + if (initialData.length) { + if (![_request performWriteData:initialData error:&error]) { + GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); + if (![_request performClose:&error]) { + GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); + } + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + return; + } + length -= initialData.length; + } + + if (length) { + [self readBodyWithRemainingLength:length + completionBlock:^(BOOL success) { + + NSError* localError = nil; + if ([_request performClose:&localError]) { + [self _startProcessingRequest]; + } else { + GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + } + + }]; + } else { + if ([_request performClose:&error]) { + [self _startProcessingRequest]; + } else { + GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + } + } +} + +- (void)_readChunkedBodyWithInitialData:(NSData*)initialData { + NSError* error = nil; + if (![_request performOpen:&error]) { + GWS_LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + return; + } + + NSMutableData* chunkData = [[NSMutableData alloc] initWithData:initialData]; + [self readNextBodyChunk:chunkData + completionBlock:^(BOOL success) { + + NSError* localError = nil; + if ([_request performClose:&localError]) { + [self _startProcessingRequest]; + } else { + GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + } + + }]; +} + +- (void)_readRequestHeaders { + _requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true); + NSMutableData* headersData = [[NSMutableData alloc] initWithCapacity:kHeadersReadCapacity]; + [self readHeaders:headersData + withCompletionBlock:^(NSData* extraData) { + + if (extraData) { + NSString* requestMethod = CFBridgingRelease(CFHTTPMessageCopyRequestMethod(_requestMessage)); // Method verbs are case-sensitive and uppercase + if (_server.shouldAutomaticallyMapHEADToGET && [requestMethod isEqualToString:@"HEAD"]) { + requestMethod = @"GET"; + _virtualHEAD = YES; + } + NSDictionary* requestHeaders = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones + NSURL* requestURL = CFBridgingRelease(CFHTTPMessageCopyRequestURL(_requestMessage)); + if (requestURL) { + requestURL = [self rewriteRequestURL:requestURL withMethod:requestMethod headers:requestHeaders]; + GWS_DCHECK(requestURL); + } + NSString* urlPath = requestURL ? CFBridgingRelease(CFURLCopyPath((CFURLRef)requestURL)) : nil; // Don't use -[NSURL path] which strips the ending slash + NSString* requestPath = urlPath ? GCDWebServerUnescapeURLString(urlPath) : nil; + NSString* queryString = requestURL ? CFBridgingRelease(CFURLCopyQueryString((CFURLRef)requestURL, NULL)) : nil; // Don't use -[NSURL query] to make sure query is not unescaped; + NSDictionary* requestQuery = queryString ? GCDWebServerParseURLEncodedForm(queryString) : @{}; + if (requestMethod && requestURL && requestHeaders && requestPath && requestQuery) { + for (_handler in _server.handlers) { + _request = _handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery); + if (_request) { + break; + } + } + if (_request) { + _request.localAddressData = self.localAddressData; + _request.remoteAddressData = self.remoteAddressData; + if ([_request hasBody]) { + [_request prepareForWriting]; + if (_request.usesChunkedTransferEncoding || (extraData.length <= _request.contentLength)) { + NSString* expectHeader = [requestHeaders objectForKey:@"Expect"]; + if (expectHeader) { + if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) { // TODO: Actually validate request before continuing + [self writeData:_continueData + withCompletionBlock:^(BOOL success) { + + if (success) { + if (_request.usesChunkedTransferEncoding) { + [self _readChunkedBodyWithInitialData:extraData]; + } else { + [self _readBodyWithLength:_request.contentLength initialData:extraData]; + } + } + + }]; + } else { + GWS_LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_ExpectationFailed]; + } + } else { + if (_request.usesChunkedTransferEncoding) { + [self _readChunkedBodyWithInitialData:extraData]; + } else { + [self _readBodyWithLength:_request.contentLength initialData:extraData]; + } + } + } else { + GWS_LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest]; + } + } else { + [self _startProcessingRequest]; + } + } else { + _request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery]; + GWS_DCHECK(_request); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_MethodNotAllowed]; + } + } else { + [self abortRequest:nil withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + GWS_DNOT_REACHED(); + } + } else { + [self abortRequest:nil withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + } + + }]; +} + +- (instancetype)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket { + if ((self = [super init])) { + _server = server; + _localAddressData = localAddress; + _remoteAddressData = remoteAddress; + _socket = socket; + GWS_LOG_DEBUG(@"Did open connection on socket %i", _socket); + + [_server willStartConnection:self]; + + if (![self open]) { + close(_socket); + return nil; + } + _opened = YES; + + [self _readRequestHeaders]; + } + return self; +} + +- (NSString*)localAddressString { + return GCDWebServerStringFromSockAddr(_localAddressData.bytes, YES); +} + +- (NSString*)remoteAddressString { + return GCDWebServerStringFromSockAddr(_remoteAddressData.bytes, YES); +} + +- (void)dealloc { + int result = close(_socket); + if (result != 0) { + GWS_LOG_ERROR(@"Failed closing socket %i for connection: %s (%i)", _socket, strerror(errno), errno); + } else { + GWS_LOG_DEBUG(@"Did close connection on socket %i", _socket); + } + + if (_opened) { + [self close]; + } + + [_server didEndConnection:self]; + + if (_requestMessage) { + CFRelease(_requestMessage); + } + + if (_responseMessage) { + CFRelease(_responseMessage); + } +} + +@end + +@implementation GCDWebServerConnection (Read) + +- (void)readData:(NSMutableData*)data withLength:(NSUInteger)length completionBlock:(ReadDataCompletionBlock)block { + dispatch_read(_socket, length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(dispatch_data_t buffer, int error) { + + @autoreleasepool { + if (error == 0) { + size_t size = dispatch_data_get_size(buffer); + if (size > 0) { + NSUInteger originalLength = data.length; + dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { + [data appendBytes:chunkBytes length:chunkSize]; + return true; + }); + [self didReadBytes:((char*)data.bytes + originalLength) length:(data.length - originalLength)]; + block(YES); + } else { + if (_totalBytesRead > 0) { + GWS_LOG_ERROR(@"No more data available on socket %i", _socket); + } else { + GWS_LOG_WARNING(@"No data received from socket %i", _socket); + } + block(NO); + } + } else { + GWS_LOG_ERROR(@"Error while reading from socket %i: %s (%i)", _socket, strerror(error), error); + block(NO); + } + } + + }); +} + +- (void)readHeaders:(NSMutableData*)headersData withCompletionBlock:(ReadHeadersCompletionBlock)block { + GWS_DCHECK(_requestMessage); + [self readData:headersData + withLength:NSUIntegerMax + completionBlock:^(BOOL success) { + + if (success) { + NSRange range = [headersData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, headersData.length)]; + if (range.location == NSNotFound) { + [self readHeaders:headersData withCompletionBlock:block]; + } else { + NSUInteger length = range.location + range.length; + if (CFHTTPMessageAppendBytes(_requestMessage, headersData.bytes, length)) { + if (CFHTTPMessageIsHeaderComplete(_requestMessage)) { + block([headersData subdataWithRange:NSMakeRange(length, headersData.length - length)]); + } else { + GWS_LOG_ERROR(@"Failed parsing request headers from socket %i", _socket); + block(nil); + } + } else { + GWS_LOG_ERROR(@"Failed appending request headers data from socket %i", _socket); + block(nil); + } + } + } else { + block(nil); + } + + }]; +} + +- (void)readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block { + GWS_DCHECK([_request hasBody] && ![_request usesChunkedTransferEncoding]); + NSMutableData* bodyData = [[NSMutableData alloc] initWithCapacity:kBodyReadCapacity]; + [self readData:bodyData + withLength:length + completionBlock:^(BOOL success) { + + if (success) { + if (bodyData.length <= length) { + NSError* error = nil; + if ([_request performWriteData:bodyData error:&error]) { + NSUInteger remainingLength = length - bodyData.length; + if (remainingLength) { + [self readBodyWithRemainingLength:remainingLength completionBlock:block]; + } else { + block(YES); + } + } else { + GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); + block(NO); + } + } else { + GWS_LOG_ERROR(@"Unexpected extra content reading request body on socket %i", _socket); + block(NO); + GWS_DNOT_REACHED(); + } + } else { + block(NO); + } + + }]; +} + +static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { + char buffer[size + 1]; + bcopy(bytes, buffer, size); + buffer[size] = 0; + char* end = NULL; + long result = strtol(buffer, &end, 16); + return ((end != NULL) && (*end == 0) && (result >= 0) ? result : NSNotFound); +} + +- (void)readNextBodyChunk:(NSMutableData*)chunkData completionBlock:(ReadBodyCompletionBlock)block { + GWS_DCHECK([_request hasBody] && [_request usesChunkedTransferEncoding]); + + while (1) { + NSRange range = [chunkData rangeOfData:_CRLFData options:0 range:NSMakeRange(0, chunkData.length)]; + if (range.location == NSNotFound) { + break; + } + NSRange extensionRange = [chunkData rangeOfData:[NSData dataWithBytes:";" length:1] options:0 range:NSMakeRange(0, range.location)]; // Ignore chunk extensions + NSUInteger length = _ScanHexNumber((char*)chunkData.bytes, extensionRange.location != NSNotFound ? extensionRange.location : range.location); + if (length != NSNotFound) { + if (length) { + if (chunkData.length < range.location + range.length + length + 2) { + break; + } + const char* ptr = (char*)chunkData.bytes + range.location + range.length + length; + if ((*ptr == '\r') && (*(ptr + 1) == '\n')) { + NSError* error = nil; + if ([_request performWriteData:[chunkData subdataWithRange:NSMakeRange(range.location + range.length, length)] error:&error]) { + [chunkData replaceBytesInRange:NSMakeRange(0, range.location + range.length + length + 2) withBytes:NULL length:0]; + } else { + GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); + block(NO); + return; + } + } else { + GWS_LOG_ERROR(@"Missing terminating CRLF sequence for chunk reading request body on socket %i", _socket); + block(NO); + return; + } + } else { + NSRange trailerRange = [chunkData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(range.location, chunkData.length - range.location)]; // Ignore trailers + if (trailerRange.location != NSNotFound) { + block(YES); + return; + } + } + } else { + GWS_LOG_ERROR(@"Invalid chunk length reading request body on socket %i", _socket); + block(NO); + return; + } + } + + [self readData:chunkData + withLength:NSUIntegerMax + completionBlock:^(BOOL success) { + + if (success) { + [self readNextBodyChunk:chunkData completionBlock:block]; + } else { + block(NO); + } + + }]; +} + +@end + +@implementation GCDWebServerConnection (Write) + +- (void)writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block { + dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^{ + [data self]; // Keeps ARC from releasing data too early + }); + dispatch_write(_socket, buffer, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(dispatch_data_t remainingData, int error) { + + @autoreleasepool { + if (error == 0) { + GWS_DCHECK(remainingData == NULL); + [self didWriteBytes:data.bytes length:data.length]; + block(YES); + } else { + GWS_LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error); + block(NO); + } + } + + }); +#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE + dispatch_release(buffer); +#endif +} + +- (void)writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block { + GWS_DCHECK(_responseMessage); + CFDataRef data = CFHTTPMessageCopySerializedMessage(_responseMessage); + [self writeData:(__bridge NSData*)data withCompletionBlock:block]; + CFRelease(data); +} + +- (void)writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block { + GWS_DCHECK([_response hasBody]); + [_response performReadDataWithCompletion:^(NSData* data, NSError* error) { + + if (data) { + if (data.length) { + if (_response.usesChunkedTransferEncoding) { + const char* hexString = [[NSString stringWithFormat:@"%lx", (unsigned long)data.length] UTF8String]; + size_t hexLength = strlen(hexString); + NSData* chunk = [NSMutableData dataWithLength:(hexLength + 2 + data.length + 2)]; + if (chunk == nil) { + GWS_LOG_ERROR(@"Failed allocating memory for response body chunk for socket %i: %@", _socket, error); + block(NO); + return; + } + char* ptr = (char*)[(NSMutableData*)chunk mutableBytes]; + bcopy(hexString, ptr, hexLength); + ptr += hexLength; + *ptr++ = '\r'; + *ptr++ = '\n'; + bcopy(data.bytes, ptr, data.length); + ptr += data.length; + *ptr++ = '\r'; + *ptr = '\n'; + data = chunk; + } + [self writeData:data + withCompletionBlock:^(BOOL success) { + + if (success) { + [self writeBodyWithCompletionBlock:block]; + } else { + block(NO); + } + + }]; + } else { + if (_response.usesChunkedTransferEncoding) { + [self writeData:_lastChunkData + withCompletionBlock:^(BOOL success) { + + block(success); + + }]; + } else { + block(YES); + } + } + } else { + GWS_LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error); + block(NO); + } + + }]; +} + +@end + +@implementation GCDWebServerConnection (Subclassing) + +- (BOOL)open { +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + if (_server.recordingEnabled) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + _connectionIndex = OSAtomicIncrement32(&_connectionCounter); +#pragma clang diagnostic pop + + _requestPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; + _requestFD = open([_requestPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + GWS_DCHECK(_requestFD > 0); + + _responsePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; + _responseFD = open([_responsePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + GWS_DCHECK(_responseFD > 0); + } +#endif + + return YES; +} + +- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length { + GWS_LOG_DEBUG(@"Connection received %lu bytes on socket %i", (unsigned long)length, _socket); + _totalBytesRead += length; + +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + if ((_requestFD > 0) && (write(_requestFD, bytes, length) != (ssize_t)length)) { + GWS_LOG_ERROR(@"Failed recording request data: %s (%i)", strerror(errno), errno); + close(_requestFD); + _requestFD = 0; + } +#endif +} + +- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length { + GWS_LOG_DEBUG(@"Connection sent %lu bytes on socket %i", (unsigned long)length, _socket); + _totalBytesWritten += length; + +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + if ((_responseFD > 0) && (write(_responseFD, bytes, length) != (ssize_t)length)) { + GWS_LOG_ERROR(@"Failed recording response data: %s (%i)", strerror(errno), errno); + close(_responseFD); + _responseFD = 0; + } +#endif +} + +- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers { + return url; +} + +// https://tools.ietf.org/html/rfc2617 +- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request { + GWS_LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_totalBytesRead); + GCDWebServerResponse* response = nil; + if (_server.authenticationBasicAccounts) { + __block BOOL authenticated = NO; + NSString* authorizationHeader = [request.headers objectForKey:@"Authorization"]; + if ([authorizationHeader hasPrefix:@"Basic "]) { + NSString* basicAccount = [authorizationHeader substringFromIndex:6]; + [_server.authenticationBasicAccounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* digest, BOOL* stop) { + if ([basicAccount isEqualToString:digest]) { + authenticated = YES; + *stop = YES; + } + }]; + } + if (!authenticated) { + response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Unauthorized]; + [response setValue:[NSString stringWithFormat:@"Basic realm=\"%@\"", _server.authenticationRealm] forAdditionalHeader:@"WWW-Authenticate"]; + } + } else if (_server.authenticationDigestAccounts) { + BOOL authenticated = NO; + BOOL isStaled = NO; + NSString* authorizationHeader = [request.headers objectForKey:@"Authorization"]; + if ([authorizationHeader hasPrefix:@"Digest "]) { + NSString* realm = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"realm"); + if ([realm isEqualToString:_server.authenticationRealm]) { + NSString* nonce = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"nonce"); + if ([nonce isEqualToString:_digestAuthenticationNonce]) { + NSString* username = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"username"); + NSString* uri = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"uri"); + NSString* actualResponse = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"response"); + NSString* ha1 = [_server.authenticationDigestAccounts objectForKey:username]; + NSString* ha2 = GCDWebServerComputeMD5Digest(@"%@:%@", request.method, uri); // We cannot use "request.path" as the query string is required + NSString* expectedResponse = GCDWebServerComputeMD5Digest(@"%@:%@:%@", ha1, _digestAuthenticationNonce, ha2); + if ([actualResponse isEqualToString:expectedResponse]) { + authenticated = YES; + } + } else if (nonce.length) { + isStaled = YES; + } + } + } + if (!authenticated) { + response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Unauthorized]; + [response setValue:[NSString stringWithFormat:@"Digest realm=\"%@\", nonce=\"%@\"%@", _server.authenticationRealm, _digestAuthenticationNonce, isStaled ? @", stale=TRUE" : @""] forAdditionalHeader:@"WWW-Authenticate"]; // TODO: Support Quality of Protection ("qop") + } + } + return response; +} + +- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion { + GWS_LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_totalBytesRead); + _handler.asyncProcessBlock(request, [completion copy]); +} + +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25 +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 +static inline BOOL _CompareResources(NSString* responseETag, NSString* requestETag, NSDate* responseLastModified, NSDate* requestLastModified) { + if (requestLastModified && responseLastModified) { + if ([responseLastModified compare:requestLastModified] != NSOrderedDescending) { + return YES; + } + } + if (requestETag && responseETag) { // Per the specs "If-None-Match" must be checked after "If-Modified-Since" + if ([requestETag isEqualToString:@"*"]) { + return YES; + } + if ([responseETag isEqualToString:requestETag]) { + return YES; + } + } + return NO; +} + +- (GCDWebServerResponse*)overrideResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request { + if ((response.statusCode >= 200) && (response.statusCode < 300) && _CompareResources(response.eTag, request.ifNoneMatch, response.lastModifiedDate, request.ifModifiedSince)) { + NSInteger code = [request.method isEqualToString:@"HEAD"] || [request.method isEqualToString:@"GET"] ? kGCDWebServerHTTPStatusCode_NotModified : kGCDWebServerHTTPStatusCode_PreconditionFailed; + GCDWebServerResponse* newResponse = [GCDWebServerResponse responseWithStatusCode:code]; + newResponse.cacheControlMaxAge = response.cacheControlMaxAge; + newResponse.lastModifiedDate = response.lastModifiedDate; + newResponse.eTag = response.eTag; + GWS_DCHECK(newResponse); + return newResponse; + } + return response; +} + +- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode { + GWS_DCHECK(_responseMessage == NULL); + GWS_DCHECK((statusCode >= 400) && (statusCode < 600)); + [self _initializeResponseHeadersWithStatusCode:statusCode]; + [self writeHeadersWithCompletionBlock:^(BOOL success) { + ; // Nothing more to do + }]; + GWS_LOG_DEBUG(@"Connection aborted with status code %i on socket %i", (int)statusCode, _socket); +} + +- (void)close { +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + if (_requestPath) { + BOOL success = NO; + NSError* error = nil; + if (_requestFD > 0) { + close(_requestFD); + NSString* name = [NSString stringWithFormat:@"%03lu-%@.request", (unsigned long)_connectionIndex, _virtualHEAD ? @"HEAD" : _request.method]; + success = [[NSFileManager defaultManager] moveItemAtPath:_requestPath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error]; + } + if (!success) { + GWS_LOG_ERROR(@"Failed saving recorded request: %@", error); + GWS_DNOT_REACHED(); + } + unlink([_requestPath fileSystemRepresentation]); + } + + if (_responsePath) { + BOOL success = NO; + NSError* error = nil; + if (_responseFD > 0) { + close(_responseFD); + NSString* name = [NSString stringWithFormat:@"%03lu-%i.response", (unsigned long)_connectionIndex, (int)_statusCode]; + success = [[NSFileManager defaultManager] moveItemAtPath:_responsePath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error]; + } + if (!success) { + GWS_LOG_ERROR(@"Failed saving recorded response: %@", error); + GWS_DNOT_REACHED(); + } + unlink([_responsePath fileSystemRepresentation]); + } +#endif + + if (_request) { + GWS_LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_totalBytesRead, (unsigned long)_totalBytesWritten); + } else { + GWS_LOG_VERBOSE(@"[%@] %@ %i \"(invalid request)\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, (unsigned long)_totalBytesRead, (unsigned long)_totalBytesWritten); + } +} + +@end diff --git a/src/ios/GCDWebServer/Core/GCDWebServerFunctions.h b/src/ios/GCDWebServer/Core/GCDWebServerFunctions.h new file mode 100755 index 0000000..4235ecc --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerFunctions.h @@ -0,0 +1,109 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Converts a file extension to the corresponding MIME type. + * If there is no match, "application/octet-stream" is returned. + * + * Overrides allow to customize the built-in mapping from extensions to MIME + * types. Keys of the dictionary must be lowercased file extensions without + * the period, and the values must be the corresponding MIME types. + */ +NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension, NSDictionary* _Nullable overrides); + +/** + * Add percent-escapes to a string so it can be used in a URL. + * The legal characters ":@/?&=+" are also escaped to ensure compatibility + * with URL encoded forms and URL queries. + */ +NSString* _Nullable GCDWebServerEscapeURLString(NSString* string); + +/** + * Unescapes a URL percent-encoded string. + */ +NSString* _Nullable GCDWebServerUnescapeURLString(NSString* string); + +/** + * Extracts the unescaped names and values from an + * "application/x-www-form-urlencoded" form. + * http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 + */ +NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form); + +/** + * On OS X, returns the IPv4 or IPv6 address as a string of the primary + * connected service or nil if not available. + * + * On iOS, returns the IPv4 or IPv6 address as a string of the WiFi + * interface if connected or nil otherwise. + */ +NSString* _Nullable GCDWebServerGetPrimaryIPAddress(BOOL useIPv6); + +/** + * Converts a date into a string using RFC822 formatting. + * https://tools.ietf.org/html/rfc822#section-5 + * https://tools.ietf.org/html/rfc1123#section-5.2.14 + */ +NSString* GCDWebServerFormatRFC822(NSDate* date); + +/** + * Converts a RFC822 formatted string into a date. + * https://tools.ietf.org/html/rfc822#section-5 + * https://tools.ietf.org/html/rfc1123#section-5.2.14 + * + * @warning Timezones other than GMT are not supported by this function. + */ +NSDate* _Nullable GCDWebServerParseRFC822(NSString* string); + +/** + * Converts a date into a string using IOS 8601 formatting. + * http://tools.ietf.org/html/rfc3339#section-5.6 + */ +NSString* GCDWebServerFormatISO8601(NSDate* date); + +/** + * Converts a ISO 8601 formatted string into a date. + * http://tools.ietf.org/html/rfc3339#section-5.6 + * + * @warning Only "calendar" variant is supported at this time and timezones + * other than GMT are not supported either. + */ +NSDate* _Nullable GCDWebServerParseISO8601(NSString* string); + +#ifdef __cplusplus +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Core/GCDWebServerFunctions.m b/src/ios/GCDWebServer/Core/GCDWebServerFunctions.m new file mode 100755 index 0000000..ec50086 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerFunctions.m @@ -0,0 +1,316 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import +#if TARGET_OS_IPHONE +#import +#else +#import +#endif +#import + +#import +#import +#import + +#import "GCDWebServerPrivate.h" + +static NSDateFormatter* _dateFormatterRFC822 = nil; +static NSDateFormatter* _dateFormatterISO8601 = nil; +static dispatch_queue_t _dateFormatterQueue = NULL; + +// TODO: Handle RFC 850 and ANSI C's asctime() format +void GCDWebServerInitializeFunctions() { + GWS_DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread + if (_dateFormatterRFC822 == nil) { + _dateFormatterRFC822 = [[NSDateFormatter alloc] init]; + _dateFormatterRFC822.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; + _dateFormatterRFC822.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"; + _dateFormatterRFC822.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; + GWS_DCHECK(_dateFormatterRFC822); + } + if (_dateFormatterISO8601 == nil) { + _dateFormatterISO8601 = [[NSDateFormatter alloc] init]; + _dateFormatterISO8601.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; + _dateFormatterISO8601.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'"; + _dateFormatterISO8601.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; + GWS_DCHECK(_dateFormatterISO8601); + } + if (_dateFormatterQueue == NULL) { + _dateFormatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + GWS_DCHECK(_dateFormatterQueue); + } +} + +NSString* GCDWebServerNormalizeHeaderValue(NSString* value) { + if (value) { + NSRange range = [value rangeOfString:@";"]; // Assume part before ";" separator is case-insensitive + if (range.location != NSNotFound) { + value = [[[value substringToIndex:range.location] lowercaseString] stringByAppendingString:[value substringFromIndex:range.location]]; + } else { + value = [value lowercaseString]; + } + } + return value; +} + +NSString* GCDWebServerTruncateHeaderValue(NSString* value) { + if (value) { + NSRange range = [value rangeOfString:@";"]; + if (range.location != NSNotFound) { + return [value substringToIndex:range.location]; + } + } + return value; +} + +NSString* GCDWebServerExtractHeaderValueParameter(NSString* value, NSString* name) { + NSString* parameter = nil; + if (value) { + NSScanner* scanner = [[NSScanner alloc] initWithString:value]; + [scanner setCaseSensitive:NO]; // Assume parameter names are case-insensitive + NSString* string = [NSString stringWithFormat:@"%@=", name]; + if ([scanner scanUpToString:string intoString:NULL]) { + [scanner scanString:string intoString:NULL]; + if ([scanner scanString:@"\"" intoString:NULL]) { + [scanner scanUpToString:@"\"" intoString:¶meter]; + } else { + [scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:¶meter]; + } + } + } + return parameter; +} + +// http://www.w3schools.com/tags/ref_charactersets.asp +NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset) { + NSStringEncoding encoding = kCFStringEncodingInvalidId; + if (charset) { + encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)charset)); + } + return (encoding != kCFStringEncodingInvalidId ? encoding : NSUTF8StringEncoding); +} + +NSString* GCDWebServerFormatRFC822(NSDate* date) { + __block NSString* string; + dispatch_sync(_dateFormatterQueue, ^{ + string = [_dateFormatterRFC822 stringFromDate:date]; + }); + return string; +} + +NSDate* GCDWebServerParseRFC822(NSString* string) { + __block NSDate* date; + dispatch_sync(_dateFormatterQueue, ^{ + date = [_dateFormatterRFC822 dateFromString:string]; + }); + return date; +} + +NSString* GCDWebServerFormatISO8601(NSDate* date) { + __block NSString* string; + dispatch_sync(_dateFormatterQueue, ^{ + string = [_dateFormatterISO8601 stringFromDate:date]; + }); + return string; +} + +NSDate* GCDWebServerParseISO8601(NSString* string) { + __block NSDate* date; + dispatch_sync(_dateFormatterQueue, ^{ + date = [_dateFormatterISO8601 dateFromString:string]; + }); + return date; +} + +BOOL GCDWebServerIsTextContentType(NSString* type) { + return ([type hasPrefix:@"text/"] || [type hasPrefix:@"application/json"] || [type hasPrefix:@"application/xml"]); +} + +NSString* GCDWebServerDescribeData(NSData* data, NSString* type) { + if (GCDWebServerIsTextContentType(type)) { + NSString* charset = GCDWebServerExtractHeaderValueParameter(type, @"charset"); + NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)]; + if (string) { + return string; + } + } + return [NSString stringWithFormat:@"<%lu bytes>", (unsigned long)data.length]; +} + +NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension, NSDictionary* overrides) { + NSDictionary* builtInOverrides = @{@"css": @"text/css"}; + NSString* mimeType = nil; + extension = [extension lowercaseString]; + if (extension.length) { + mimeType = [overrides objectForKey:extension]; + if (mimeType == nil) { + mimeType = [builtInOverrides objectForKey:extension]; + } + if (mimeType == nil) { + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL); + if (uti) { + mimeType = CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)); + CFRelease(uti); + } + } + } + return mimeType ? mimeType : kGCDWebServerDefaultMimeType; +} + +NSString* GCDWebServerEscapeURLString(NSString* string) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":@/?&=+"), kCFStringEncodingUTF8)); +#pragma clang diagnostic pop +} + +NSString* GCDWebServerUnescapeURLString(NSString* string) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return CFBridgingRelease(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8)); +#pragma clang diagnostic pop +} + +NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) { + NSMutableDictionary* parameters = [NSMutableDictionary dictionary]; + NSScanner* scanner = [[NSScanner alloc] initWithString:form]; + [scanner setCharactersToBeSkipped:nil]; + while (1) { + NSString* key = nil; + if (![scanner scanUpToString:@"=" intoString:&key] || [scanner isAtEnd]) { + break; + } + [scanner setScanLocation:([scanner scanLocation] + 1)]; + + NSString* value = nil; + [scanner scanUpToString:@"&" intoString:&value]; + if (value == nil) { + value = @""; + } + + key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + NSString* unescapedKey = key ? GCDWebServerUnescapeURLString(key) : nil; + value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + NSString* unescapedValue = value ? GCDWebServerUnescapeURLString(value) : nil; + if (unescapedKey && unescapedValue) { + [parameters setObject:unescapedValue forKey:unescapedKey]; + } else { + GWS_LOG_WARNING(@"Failed parsing URL encoded form for key \"%@\" and value \"%@\"", key, value); + GWS_DNOT_REACHED(); + } + + if ([scanner isAtEnd]) { + break; + } + [scanner setScanLocation:([scanner scanLocation] + 1)]; + } + return parameters; +} + +NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOOL includeService) { + char hostBuffer[NI_MAXHOST]; + char serviceBuffer[NI_MAXSERV]; + if (getnameinfo(addr, addr->sa_len, hostBuffer, sizeof(hostBuffer), serviceBuffer, sizeof(serviceBuffer), NI_NUMERICHOST | NI_NUMERICSERV | NI_NOFQDN) != 0) { +#if DEBUG + GWS_DNOT_REACHED(); +#else + return @""; +#endif + } + return includeService ? [NSString stringWithFormat:@"%s:%s", hostBuffer, serviceBuffer] : (NSString*)[NSString stringWithUTF8String:hostBuffer]; +} + +NSString* GCDWebServerGetPrimaryIPAddress(BOOL useIPv6) { + NSString* address = nil; +#if TARGET_OS_IPHONE +#if !TARGET_IPHONE_SIMULATOR && !TARGET_OS_TV + const char* primaryInterface = "en0"; // WiFi interface on iOS +#endif +#else + const char* primaryInterface = NULL; + SCDynamicStoreRef store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("GCDWebServer"), NULL, NULL); + if (store) { + CFPropertyListRef info = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4")); // There is no equivalent for IPv6 but the primary interface should be the same + if (info) { + NSString* interface = [(__bridge NSDictionary*)info objectForKey:@"PrimaryInterface"]; + if (interface) { + primaryInterface = [[NSString stringWithString:interface] UTF8String]; // Copy string to auto-release pool + } + CFRelease(info); + } + CFRelease(store); + } + if (primaryInterface == NULL) { + primaryInterface = "lo0"; + } +#endif + struct ifaddrs* list; + if (getifaddrs(&list) >= 0) { + for (struct ifaddrs* ifap = list; ifap; ifap = ifap->ifa_next) { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_TV + // Assume en0 is Ethernet and en1 is WiFi since there is no way to use SystemConfiguration framework in iOS Simulator + // Assumption holds for Apple TV running tvOS + if (strcmp(ifap->ifa_name, "en0") && strcmp(ifap->ifa_name, "en1")) +#else + if (strcmp(ifap->ifa_name, primaryInterface)) +#endif + { + continue; + } + if ((ifap->ifa_flags & IFF_UP) && ((!useIPv6 && (ifap->ifa_addr->sa_family == AF_INET)) || (useIPv6 && (ifap->ifa_addr->sa_family == AF_INET6)))) { + address = GCDWebServerStringFromSockAddr(ifap->ifa_addr, NO); + break; + } + } + freeifaddrs(list); + } + return address; +} + +NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) { + va_list arguments; + va_start(arguments, format); + const char* string = [[[NSString alloc] initWithFormat:format arguments:arguments] UTF8String]; + va_end(arguments); + unsigned char md5[CC_MD5_DIGEST_LENGTH]; + CC_MD5(string, (CC_LONG)strlen(string), md5); + char buffer[2 * CC_MD5_DIGEST_LENGTH + 1]; + for (int i = 0; i < CC_MD5_DIGEST_LENGTH; ++i) { + unsigned char byte = md5[i]; + unsigned char byteHi = (byte & 0xF0) >> 4; + buffer[2 * i + 0] = byteHi >= 10 ? 'a' + byteHi - 10 : '0' + byteHi; + unsigned char byteLo = byte & 0x0F; + buffer[2 * i + 1] = byteLo >= 10 ? 'a' + byteLo - 10 : '0' + byteLo; + } + buffer[2 * CC_MD5_DIGEST_LENGTH] = 0; + return (NSString*)[NSString stringWithUTF8String:buffer]; +} diff --git a/src/ios/GCDWebServer/Core/GCDWebServerHTTPStatusCodes.h b/src/ios/GCDWebServer/Core/GCDWebServerHTTPStatusCodes.h new file mode 100755 index 0000000..6e98381 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerHTTPStatusCodes.h @@ -0,0 +1,116 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html +// http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + +#import + +/** + * Convenience constants for "informational" HTTP status codes. + */ +typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) { + kGCDWebServerHTTPStatusCode_Continue = 100, + kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101, + kGCDWebServerHTTPStatusCode_Processing = 102 +}; + +/** + * Convenience constants for "successful" HTTP status codes. + */ +typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) { + kGCDWebServerHTTPStatusCode_OK = 200, + kGCDWebServerHTTPStatusCode_Created = 201, + kGCDWebServerHTTPStatusCode_Accepted = 202, + kGCDWebServerHTTPStatusCode_NonAuthoritativeInformation = 203, + kGCDWebServerHTTPStatusCode_NoContent = 204, + kGCDWebServerHTTPStatusCode_ResetContent = 205, + kGCDWebServerHTTPStatusCode_PartialContent = 206, + kGCDWebServerHTTPStatusCode_MultiStatus = 207, + kGCDWebServerHTTPStatusCode_AlreadyReported = 208 +}; + +/** + * Convenience constants for "redirection" HTTP status codes. + */ +typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) { + kGCDWebServerHTTPStatusCode_MultipleChoices = 300, + kGCDWebServerHTTPStatusCode_MovedPermanently = 301, + kGCDWebServerHTTPStatusCode_Found = 302, + kGCDWebServerHTTPStatusCode_SeeOther = 303, + kGCDWebServerHTTPStatusCode_NotModified = 304, + kGCDWebServerHTTPStatusCode_UseProxy = 305, + kGCDWebServerHTTPStatusCode_TemporaryRedirect = 307, + kGCDWebServerHTTPStatusCode_PermanentRedirect = 308 +}; + +/** + * Convenience constants for "client error" HTTP status codes. + */ +typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) { + kGCDWebServerHTTPStatusCode_BadRequest = 400, + kGCDWebServerHTTPStatusCode_Unauthorized = 401, + kGCDWebServerHTTPStatusCode_PaymentRequired = 402, + kGCDWebServerHTTPStatusCode_Forbidden = 403, + kGCDWebServerHTTPStatusCode_NotFound = 404, + kGCDWebServerHTTPStatusCode_MethodNotAllowed = 405, + kGCDWebServerHTTPStatusCode_NotAcceptable = 406, + kGCDWebServerHTTPStatusCode_ProxyAuthenticationRequired = 407, + kGCDWebServerHTTPStatusCode_RequestTimeout = 408, + kGCDWebServerHTTPStatusCode_Conflict = 409, + kGCDWebServerHTTPStatusCode_Gone = 410, + kGCDWebServerHTTPStatusCode_LengthRequired = 411, + kGCDWebServerHTTPStatusCode_PreconditionFailed = 412, + kGCDWebServerHTTPStatusCode_RequestEntityTooLarge = 413, + kGCDWebServerHTTPStatusCode_RequestURITooLong = 414, + kGCDWebServerHTTPStatusCode_UnsupportedMediaType = 415, + kGCDWebServerHTTPStatusCode_RequestedRangeNotSatisfiable = 416, + kGCDWebServerHTTPStatusCode_ExpectationFailed = 417, + kGCDWebServerHTTPStatusCode_UnprocessableEntity = 422, + kGCDWebServerHTTPStatusCode_Locked = 423, + kGCDWebServerHTTPStatusCode_FailedDependency = 424, + kGCDWebServerHTTPStatusCode_UpgradeRequired = 426, + kGCDWebServerHTTPStatusCode_PreconditionRequired = 428, + kGCDWebServerHTTPStatusCode_TooManyRequests = 429, + kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431 +}; + +/** + * Convenience constants for "server error" HTTP status codes. + */ +typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) { + kGCDWebServerHTTPStatusCode_InternalServerError = 500, + kGCDWebServerHTTPStatusCode_NotImplemented = 501, + kGCDWebServerHTTPStatusCode_BadGateway = 502, + kGCDWebServerHTTPStatusCode_ServiceUnavailable = 503, + kGCDWebServerHTTPStatusCode_GatewayTimeout = 504, + kGCDWebServerHTTPStatusCode_HTTPVersionNotSupported = 505, + kGCDWebServerHTTPStatusCode_InsufficientStorage = 507, + kGCDWebServerHTTPStatusCode_LoopDetected = 508, + kGCDWebServerHTTPStatusCode_NotExtended = 510, + kGCDWebServerHTTPStatusCode_NetworkAuthenticationRequired = 511 +}; diff --git a/src/ios/GCDWebServer/Core/GCDWebServerPrivate.h b/src/ios/GCDWebServer/Core/GCDWebServerPrivate.h new file mode 100755 index 0000000..e1e6353 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerPrivate.h @@ -0,0 +1,245 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import +#import + +/** + * All GCDWebServer headers. + */ + +#import "GCDWebServerHTTPStatusCodes.h" +#import "GCDWebServerFunctions.h" + +#import "GCDWebServer.h" +#import "GCDWebServerConnection.h" + +#import "GCDWebServerDataRequest.h" +#import "GCDWebServerFileRequest.h" +#import "GCDWebServerMultiPartFormRequest.h" +#import "GCDWebServerURLEncodedFormRequest.h" + +#import "GCDWebServerDataResponse.h" +#import "GCDWebServerErrorResponse.h" +#import "GCDWebServerFileResponse.h" +#import "GCDWebServerStreamedResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Check if a custom logging facility should be used instead. + */ + +#if defined(__GCDWEBSERVER_LOGGING_HEADER__) + +#define __GCDWEBSERVER_LOGGING_FACILITY_CUSTOM__ + +#import __GCDWEBSERVER_LOGGING_HEADER__ + +/** + * Automatically detect if XLFacility is available and if so use it as a + * logging facility. + */ + +#elif defined(__has_include) && __has_include("XLFacilityMacros.h") + +#define __GCDWEBSERVER_LOGGING_FACILITY_XLFACILITY__ + +#undef XLOG_TAG +#define XLOG_TAG @"gcdwebserver.internal" + +#import "XLFacilityMacros.h" + +#define GWS_LOG_DEBUG(...) XLOG_DEBUG(__VA_ARGS__) +#define GWS_LOG_VERBOSE(...) XLOG_VERBOSE(__VA_ARGS__) +#define GWS_LOG_INFO(...) XLOG_INFO(__VA_ARGS__) +#define GWS_LOG_WARNING(...) XLOG_WARNING(__VA_ARGS__) +#define GWS_LOG_ERROR(...) XLOG_ERROR(__VA_ARGS__) + +#define GWS_DCHECK(__CONDITION__) XLOG_DEBUG_CHECK(__CONDITION__) +#define GWS_DNOT_REACHED() XLOG_DEBUG_UNREACHABLE() + +/** + * Automatically detect if CocoaLumberJack is available and if so use + * it as a logging facility. + */ + +#elif defined(__has_include) && __has_include("CocoaLumberjack/CocoaLumberjack.h") + +#import + +#define __GCDWEBSERVER_LOGGING_FACILITY_COCOALUMBERJACK__ + +#undef LOG_LEVEL_DEF +#define LOG_LEVEL_DEF GCDWebServerLogLevel +extern DDLogLevel GCDWebServerLogLevel; + +#define GWS_LOG_DEBUG(...) DDLogDebug(__VA_ARGS__) +#define GWS_LOG_VERBOSE(...) DDLogVerbose(__VA_ARGS__) +#define GWS_LOG_INFO(...) DDLogInfo(__VA_ARGS__) +#define GWS_LOG_WARNING(...) DDLogWarn(__VA_ARGS__) +#define GWS_LOG_ERROR(...) DDLogError(__VA_ARGS__) + +/** + * If all of the above fail, then use GCDWebServer built-in + * logging facility. + */ + +#else + +#define __GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__ + +typedef NS_ENUM(int, GCDWebServerLoggingLevel) { + kGCDWebServerLoggingLevel_Debug = 0, + kGCDWebServerLoggingLevel_Verbose, + kGCDWebServerLoggingLevel_Info, + kGCDWebServerLoggingLevel_Warning, + kGCDWebServerLoggingLevel_Error +}; + +extern GCDWebServerLoggingLevel GCDWebServerLogLevel; +extern void GCDWebServerLogMessage(GCDWebServerLoggingLevel level, NSString* format, ...) NS_FORMAT_FUNCTION(2, 3); + +#if DEBUG +#define GWS_LOG_DEBUG(...) \ + do { \ + if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Debug) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Debug, __VA_ARGS__); \ + } while (0) +#else +#define GWS_LOG_DEBUG(...) +#endif +#define GWS_LOG_VERBOSE(...) \ + do { \ + if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Verbose) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Verbose, __VA_ARGS__); \ + } while (0) +#define GWS_LOG_INFO(...) \ + do { \ + if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Info) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Info, __VA_ARGS__); \ + } while (0) +#define GWS_LOG_WARNING(...) \ + do { \ + if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Warning) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Warning, __VA_ARGS__); \ + } while (0) +#define GWS_LOG_ERROR(...) \ + do { \ + if (GCDWebServerLogLevel <= kGCDWebServerLoggingLevel_Error) GCDWebServerLogMessage(kGCDWebServerLoggingLevel_Error, __VA_ARGS__); \ + } while (0) + +#endif + +/** + * Consistency check macros used when building Debug only. + */ + +#if !defined(GWS_DCHECK) || !defined(GWS_DNOT_REACHED) + +#if DEBUG + +#define GWS_DCHECK(__CONDITION__) \ + do { \ + if (!(__CONDITION__)) { \ + abort(); \ + } \ + } while (0) +#define GWS_DNOT_REACHED() abort() + +#else + +#define GWS_DCHECK(__CONDITION__) +#define GWS_DNOT_REACHED() + +#endif + +#endif + +/** + * GCDWebServer internal constants and APIs. + */ + +#define kGCDWebServerDefaultMimeType @"application/octet-stream" +#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain" + +static inline BOOL GCDWebServerIsValidByteRange(NSRange range) { + return ((range.location != NSUIntegerMax) || (range.length > 0)); +} + +static inline NSError* GCDWebServerMakePosixError(int code) { + return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithUTF8String:strerror(code)]}]; +} + +extern void GCDWebServerInitializeFunctions(); +extern NSString* _Nullable GCDWebServerNormalizeHeaderValue(NSString* _Nullable value); +extern NSString* _Nullable GCDWebServerTruncateHeaderValue(NSString* _Nullable value); +extern NSString* _Nullable GCDWebServerExtractHeaderValueParameter(NSString* _Nullable value, NSString* attribute); +extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset); +extern BOOL GCDWebServerIsTextContentType(NSString* type); +extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType); +extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_FUNCTION(1, 2); +extern NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOOL includeService); + +@interface GCDWebServerConnection () +- (instancetype)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket; +@end + +@interface GCDWebServer () +@property(nonatomic, readonly) NSMutableArray* handlers; +@property(nonatomic, readonly) NSString* serverName; +@property(nonatomic, readonly) NSString* authenticationRealm; +@property(nonatomic, readonly) NSMutableDictionary* authenticationBasicAccounts; +@property(nonatomic, readonly) NSMutableDictionary* authenticationDigestAccounts; +@property(nonatomic, readonly) BOOL shouldAutomaticallyMapHEADToGET; +@property(nonatomic, readonly) dispatch_queue_priority_t dispatchQueuePriority; +- (void)willStartConnection:(GCDWebServerConnection*)connection; +- (void)didEndConnection:(GCDWebServerConnection*)connection; +@end + +@interface GCDWebServerHandler : NSObject +@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock; +@property(nonatomic, readonly) GCDWebServerAsyncProcessBlock asyncProcessBlock; +@end + +@interface GCDWebServerRequest () +@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding; +@property(nonatomic) NSData* localAddressData; +@property(nonatomic) NSData* remoteAddressData; +- (void)prepareForWriting; +- (BOOL)performOpen:(NSError**)error; +- (BOOL)performWriteData:(NSData*)data error:(NSError**)error; +- (BOOL)performClose:(NSError**)error; +- (void)setAttribute:(nullable id)attribute forKey:(NSString*)key; +@end + +@interface GCDWebServerResponse () +@property(nonatomic, readonly) NSDictionary* additionalHeaders; +@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding; +- (void)prepareForReading; +- (BOOL)performOpen:(NSError**)error; +- (void)performReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block; +- (void)performClose; +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Core/GCDWebServerRequest.h b/src/ios/GCDWebServer/Core/GCDWebServerRequest.h new file mode 100755 index 0000000..3fe9029 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerRequest.h @@ -0,0 +1,210 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Attribute key to retrieve an NSArray containing NSStrings from a GCDWebServerRequest + * with the contents of any regular expression captures done on the request path. + * + * @warning This attribute will only be set on the request if adding a handler using + * -addHandlerForMethod:pathRegex:requestClass:processBlock:. + */ +extern NSString* const GCDWebServerRequestAttribute_RegexCaptures; + +/** + * This protocol is used by the GCDWebServerConnection to communicate with + * the GCDWebServerRequest and write the received HTTP body data. + * + * Note that multiple GCDWebServerBodyWriter objects can be chained together + * internally e.g. to automatically decode gzip encoded content before + * passing it on to the GCDWebServerRequest. + * + * @warning These methods can be called on any GCD thread. + */ +@protocol GCDWebServerBodyWriter + +/** + * This method is called before any body data is received. + * + * It should return YES on success or NO on failure and set the "error" argument + * which is guaranteed to be non-NULL. + */ +- (BOOL)open:(NSError**)error; + +/** + * This method is called whenever body data has been received. + * + * It should return YES on success or NO on failure and set the "error" argument + * which is guaranteed to be non-NULL. + */ +- (BOOL)writeData:(NSData*)data error:(NSError**)error; + +/** + * This method is called after all body data has been received. + * + * It should return YES on success or NO on failure and set the "error" argument + * which is guaranteed to be non-NULL. + */ +- (BOOL)close:(NSError**)error; + +@end + +/** + * The GCDWebServerRequest class is instantiated by the GCDWebServerConnection + * after the HTTP headers have been received. Each instance wraps a single HTTP + * request. If a body is present, the methods from the GCDWebServerBodyWriter + * protocol will be called by the GCDWebServerConnection to receive it. + * + * The default implementation of the GCDWebServerBodyWriter protocol on the class + * simply ignores the body data. + * + * @warning GCDWebServerRequest instances can be created and used on any GCD thread. + */ +@interface GCDWebServerRequest : NSObject + +/** + * Returns the HTTP method for the request. + */ +@property(nonatomic, readonly) NSString* method; + +/** + * Returns the URL for the request. + */ +@property(nonatomic, readonly) NSURL* URL; + +/** + * Returns the HTTP headers for the request. + */ +@property(nonatomic, readonly) NSDictionary* headers; + +/** + * Returns the path component of the URL for the request. + */ +@property(nonatomic, readonly) NSString* path; + +/** + * Returns the parsed and unescaped query component of the URL for the request. + * + * @warning This property will be nil if there is no query in the URL. + */ +@property(nonatomic, readonly, nullable) NSDictionary* query; + +/** + * Returns the content type for the body of the request parsed from the + * "Content-Type" header. + * + * This property will be nil if the request has no body or set to + * "application/octet-stream" if a body is present but there was no + * "Content-Type" header. + */ +@property(nonatomic, readonly, nullable) NSString* contentType; + +/** + * Returns the content length for the body of the request parsed from the + * "Content-Length" header. + * + * This property will be set to "NSUIntegerMax" if the request has no body or + * if there is a body but no "Content-Length" header, typically because + * chunked transfer encoding is used. + */ +@property(nonatomic, readonly) NSUInteger contentLength; + +/** + * Returns the parsed "If-Modified-Since" header or nil if absent or malformed. + */ +@property(nonatomic, readonly, nullable) NSDate* ifModifiedSince; + +/** + * Returns the parsed "If-None-Match" header or nil if absent or malformed. + */ +@property(nonatomic, readonly, nullable) NSString* ifNoneMatch; + +/** + * Returns the parsed "Range" header or (NSUIntegerMax, 0) if absent or malformed. + * The range will be set to (offset, length) if expressed from the beginning + * of the entity body, or (NSUIntegerMax, length) if expressed from its end. + */ +@property(nonatomic, readonly) NSRange byteRange; + +/** + * Returns YES if the client supports gzip content encoding according to the + * "Accept-Encoding" header. + */ +@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding; + +/** + * Returns the address of the local peer (i.e. server) for the request + * as a raw "struct sockaddr". + */ +@property(nonatomic, readonly) NSData* localAddressData; + +/** + * Returns the address of the local peer (i.e. server) for the request + * as a string. + */ +@property(nonatomic, readonly) NSString* localAddressString; + +/** + * Returns the address of the remote peer (i.e. client) for the request + * as a raw "struct sockaddr". + */ +@property(nonatomic, readonly) NSData* remoteAddressData; + +/** + * Returns the address of the remote peer (i.e. client) for the request + * as a string. + */ +@property(nonatomic, readonly) NSString* remoteAddressString; + +/** + * This method is the designated initializer for the class. + */ +- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(nullable NSDictionary*)query; + +/** + * Convenience method that checks if the contentType property is defined. + */ +- (BOOL)hasBody; + +/** + * Convenience method that checks if the byteRange property is defined. + */ +- (BOOL)hasByteRange; + +/** + * Retrieves an attribute associated with this request using the given key. + * + * @return The attribute value for the key. + */ +- (nullable id)attributeForKey:(NSString*)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Core/GCDWebServerRequest.m b/src/ios/GCDWebServer/Core/GCDWebServerRequest.m new file mode 100755 index 0000000..05988cd --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerRequest.m @@ -0,0 +1,303 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import + +#import "GCDWebServerPrivate.h" + +NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerRequestAttribute_RegexCaptures"; + +#define kZlibErrorDomain @"ZlibErrorDomain" +#define kGZipInitialBufferSize (256 * 1024) + +@interface GCDWebServerBodyDecoder : NSObject +@end + +@interface GCDWebServerGZipDecoder : GCDWebServerBodyDecoder +@end + +@implementation GCDWebServerBodyDecoder { + GCDWebServerRequest* __unsafe_unretained _request; + id __unsafe_unretained _writer; +} + +- (instancetype)initWithRequest:(GCDWebServerRequest* _Nonnull)request writer:(id _Nonnull)writer { + if ((self = [super init])) { + _request = request; + _writer = writer; + } + return self; +} + +- (BOOL)open:(NSError**)error { + return [_writer open:error]; +} + +- (BOOL)writeData:(NSData*)data error:(NSError**)error { + return [_writer writeData:data error:error]; +} + +- (BOOL)close:(NSError**)error { + return [_writer close:error]; +} + +@end + +@implementation GCDWebServerGZipDecoder { + z_stream _stream; + BOOL _finished; +} + +- (BOOL)open:(NSError**)error { + int result = inflateInit2(&_stream, 15 + 16); + if (result != Z_OK) { + if (error) { + *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil]; + } + return NO; + } + if (![super open:error]) { + inflateEnd(&_stream); + return NO; + } + return YES; +} + +- (BOOL)writeData:(NSData*)data error:(NSError**)error { + GWS_DCHECK(!_finished); + _stream.next_in = (Bytef*)data.bytes; + _stream.avail_in = (uInt)data.length; + NSMutableData* decodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize]; + if (decodedData == nil) { + GWS_DNOT_REACHED(); + return NO; + } + NSUInteger length = 0; + while (1) { + NSUInteger maxLength = decodedData.length - length; + _stream.next_out = (Bytef*)((char*)decodedData.mutableBytes + length); + _stream.avail_out = (uInt)maxLength; + int result = inflate(&_stream, Z_NO_FLUSH); + if ((result != Z_OK) && (result != Z_STREAM_END)) { + if (error) { + *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil]; + } + return NO; + } + length += maxLength - _stream.avail_out; + if (_stream.avail_out > 0) { + if (result == Z_STREAM_END) { + _finished = YES; + } + break; + } + decodedData.length = 2 * decodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available + } + decodedData.length = length; + BOOL success = length ? [super writeData:decodedData error:error] : YES; // No need to call writer if we have no data yet + return success; +} + +- (BOOL)close:(NSError**)error { + GWS_DCHECK(_finished); + inflateEnd(&_stream); + return [super close:error]; +} + +@end + +@implementation GCDWebServerRequest { + BOOL _opened; + NSMutableArray* _decoders; + id __unsafe_unretained _writer; + NSMutableDictionary* _attributes; +} + +- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { + if ((self = [super init])) { + _method = [method copy]; + _URL = url; + _headers = headers; + _path = [path copy]; + _query = query; + + _contentType = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]); + _usesChunkedTransferEncoding = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"]; + NSString* lengthHeader = [_headers objectForKey:@"Content-Length"]; + if (lengthHeader) { + NSInteger length = [lengthHeader integerValue]; + if (_usesChunkedTransferEncoding || (length < 0)) { + GWS_LOG_WARNING(@"Invalid 'Content-Length' header '%@' for '%@' request on \"%@\"", lengthHeader, _method, _URL); + GWS_DNOT_REACHED(); + return nil; + } + _contentLength = length; + if (_contentType == nil) { + _contentType = kGCDWebServerDefaultMimeType; + } + } else if (_usesChunkedTransferEncoding) { + if (_contentType == nil) { + _contentType = kGCDWebServerDefaultMimeType; + } + _contentLength = NSUIntegerMax; + } else { + if (_contentType) { + GWS_LOG_WARNING(@"Ignoring 'Content-Type' header for '%@' request on \"%@\"", _method, _URL); + _contentType = nil; // Content-Type without Content-Length or chunked-encoding doesn't make sense + } + _contentLength = NSUIntegerMax; + } + + NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"]; + if (modifiedHeader) { + _ifModifiedSince = [GCDWebServerParseRFC822(modifiedHeader) copy]; + } + _ifNoneMatch = [_headers objectForKey:@"If-None-Match"]; + + _byteRange = NSMakeRange(NSUIntegerMax, 0); + NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]); + if (rangeHeader) { + if ([rangeHeader hasPrefix:@"bytes="]) { + NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","]; + if (components.count == 1) { + components = [[components firstObject] componentsSeparatedByString:@"-"]; + if (components.count == 2) { + NSString* startString = [components objectAtIndex:0]; + NSInteger startValue = [startString integerValue]; + NSString* endString = [components objectAtIndex:1]; + NSInteger endValue = [endString integerValue]; + if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999" + _byteRange.location = startValue; + _byteRange.length = endValue - startValue + 1; + } else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-" + _byteRange.location = startValue; + _byteRange.length = NSUIntegerMax; + } else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500" + _byteRange.location = NSUIntegerMax; + _byteRange.length = endValue; + } + } + } + } + if ((_byteRange.location == NSUIntegerMax) && (_byteRange.length == 0)) { // Ignore "Range" header if syntactically invalid + GWS_LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url); + } + } + + if ([[_headers objectForKey:@"Accept-Encoding"] rangeOfString:@"gzip"].location != NSNotFound) { + _acceptsGzipContentEncoding = YES; + } + + _decoders = [[NSMutableArray alloc] init]; + _attributes = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL)hasBody { + return _contentType ? YES : NO; +} + +- (BOOL)hasByteRange { + return GCDWebServerIsValidByteRange(_byteRange); +} + +- (id)attributeForKey:(NSString*)key { + return [_attributes objectForKey:key]; +} + +- (BOOL)open:(NSError**)error { + return YES; +} + +- (BOOL)writeData:(NSData*)data error:(NSError**)error { + return YES; +} + +- (BOOL)close:(NSError**)error { + return YES; +} + +- (void)prepareForWriting { + _writer = self; + if ([GCDWebServerNormalizeHeaderValue([self.headers objectForKey:@"Content-Encoding"]) isEqualToString:@"gzip"]) { + GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer]; + [_decoders addObject:decoder]; + _writer = decoder; + } +} + +- (BOOL)performOpen:(NSError**)error { + GWS_DCHECK(_contentType); + GWS_DCHECK(_writer); + if (_opened) { + GWS_DNOT_REACHED(); + return NO; + } + _opened = YES; + return [_writer open:error]; +} + +- (BOOL)performWriteData:(NSData*)data error:(NSError**)error { + GWS_DCHECK(_opened); + return [_writer writeData:data error:error]; +} + +- (BOOL)performClose:(NSError**)error { + GWS_DCHECK(_opened); + return [_writer close:error]; +} + +- (void)setAttribute:(id)attribute forKey:(NSString*)key { + [_attributes setValue:attribute forKey:key]; +} + +- (NSString*)localAddressString { + return GCDWebServerStringFromSockAddr(_localAddressData.bytes, YES); +} + +- (NSString*)remoteAddressString { + return GCDWebServerStringFromSockAddr(_remoteAddressData.bytes, YES); +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path]; + for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [description appendFormat:@"\n %@ = %@", argument, [_query objectForKey:argument]]; + } + [description appendString:@"\n"]; + for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]]; + } + return description; +} + +@end diff --git a/src/ios/GCDWebServer/Core/GCDWebServerResponse.h b/src/ios/GCDWebServer/Core/GCDWebServerResponse.h new file mode 100755 index 0000000..1e5e8c9 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerResponse.h @@ -0,0 +1,212 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerBodyReaderCompletionBlock is passed by GCDWebServer to the + * GCDWebServerBodyReader object when reading data from it asynchronously. + */ +typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* _Nullable error); + +/** + * This protocol is used by the GCDWebServerConnection to communicate with + * the GCDWebServerResponse and read the HTTP body data to send. + * + * Note that multiple GCDWebServerBodyReader objects can be chained together + * internally e.g. to automatically apply gzip encoding to the content before + * passing it on to the GCDWebServerResponse. + * + * @warning These methods can be called on any GCD thread. + */ +@protocol GCDWebServerBodyReader + +@required + +/** + * This method is called before any body data is sent. + * + * It should return YES on success or NO on failure and set the "error" argument + * which is guaranteed to be non-NULL. + */ +- (BOOL)open:(NSError**)error; + +/** + * This method is called whenever body data is sent. + * + * It should return a non-empty NSData if there is body data available, + * or an empty NSData there is no more body data, or nil on error and set + * the "error" argument which is guaranteed to be non-NULL. + */ +- (nullable NSData*)readData:(NSError**)error; + +/** + * This method is called after all body data has been sent. + */ +- (void)close; + +@optional + +/** + * If this method is implemented, it will be preferred over -readData:. + * + * It must call the passed block when data is available, passing a non-empty + * NSData if there is body data available, or an empty NSData there is no more + * body data, or nil on error and pass an NSError along. + */ +- (void)asyncReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block; + +@end + +/** + * The GCDWebServerResponse class is used to wrap a single HTTP response. + * It is instantiated by the handler of the GCDWebServer that handled the request. + * If a body is present, the methods from the GCDWebServerBodyReader protocol + * will be called by the GCDWebServerConnection to send it. + * + * The default implementation of the GCDWebServerBodyReader protocol + * on the class simply returns an empty body. + * + * @warning GCDWebServerResponse instances can be created and used on any GCD thread. + */ +@interface GCDWebServerResponse : NSObject + +/** + * Sets the content type for the body of the response. + * + * The default value is nil i.e. the response has no body. + * + * @warning This property must be set if a body is present. + */ +@property(nonatomic, copy, nullable) NSString* contentType; + +/** + * Sets the content length for the body of the response. If a body is present + * but this property is set to "NSUIntegerMax", this means the length of the body + * cannot be known ahead of time. Chunked transfer encoding will be + * automatically enabled by the GCDWebServerConnection to comply with HTTP/1.1 + * specifications. + * + * The default value is "NSUIntegerMax" i.e. the response has no body or its length + * is undefined. + */ +@property(nonatomic) NSUInteger contentLength; + +/** + * Sets the HTTP status code for the response. + * + * The default value is 200 i.e. "OK". + */ +@property(nonatomic) NSInteger statusCode; + +/** + * Sets the caching hint for the response using the "Cache-Control" header. + * This value is expressed in seconds. + * + * The default value is 0 i.e. "no-cache". + */ +@property(nonatomic) NSUInteger cacheControlMaxAge; + +/** + * Sets the last modified date for the response using the "Last-Modified" header. + * + * The default value is nil. + */ +@property(nonatomic, nullable) NSDate* lastModifiedDate; + +/** + * Sets the ETag for the response using the "ETag" header. + * + * The default value is nil. + */ +@property(nonatomic, copy, nullable) NSString* eTag; + +/** + * Enables gzip encoding for the response body. + * + * The default value is NO. + * + * @warning Enabling gzip encoding will remove any "Content-Length" header + * since the length of the body is not known anymore. The client will still + * be able to determine the body length when connection is closed per + * HTTP/1.1 specifications. + */ +@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; + +/** + * Creates an empty response. + */ ++ (instancetype)response; + +/** + * This method is the designated initializer for the class. + */ +- (instancetype)init; + +/** + * Sets an additional HTTP header on the response. + * Pass a nil value to remove an additional header. + * + * @warning Do not attempt to override the primary headers used + * by GCDWebServerResponse like "Content-Type", "ETag", etc... + */ +- (void)setValue:(nullable NSString*)value forAdditionalHeader:(NSString*)header; + +/** + * Convenience method that checks if the contentType property is defined. + */ +- (BOOL)hasBody; + +@end + +@interface GCDWebServerResponse (Extensions) + +/** + * Creates a empty response with a specific HTTP status code. + */ ++ (instancetype)responseWithStatusCode:(NSInteger)statusCode; + +/** + * Creates an HTTP redirect response to a new URL. + */ ++ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent; + +/** + * Initializes an empty response with a specific HTTP status code. + */ +- (instancetype)initWithStatusCode:(NSInteger)statusCode; + +/** + * Initializes an HTTP redirect response to a new URL. + */ +- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Core/GCDWebServerResponse.m b/src/ios/GCDWebServer/Core/GCDWebServerResponse.m new file mode 100755 index 0000000..9153ff6 --- /dev/null +++ b/src/ios/GCDWebServer/Core/GCDWebServerResponse.m @@ -0,0 +1,284 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import + +#import "GCDWebServerPrivate.h" + +#define kZlibErrorDomain @"ZlibErrorDomain" +#define kGZipInitialBufferSize (256 * 1024) + +@interface GCDWebServerBodyEncoder : NSObject +@end + +@interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder +@end + +@implementation GCDWebServerBodyEncoder { + GCDWebServerResponse* __unsafe_unretained _response; + id __unsafe_unretained _reader; +} + +- (instancetype)initWithResponse:(GCDWebServerResponse* _Nonnull)response reader:(id _Nonnull)reader { + if ((self = [super init])) { + _response = response; + _reader = reader; + } + return self; +} + +- (BOOL)open:(NSError**)error { + return [_reader open:error]; +} + +- (NSData*)readData:(NSError**)error { + return [_reader readData:error]; +} + +- (void)close { + [_reader close]; +} + +@end + +@implementation GCDWebServerGZipEncoder { + z_stream _stream; + BOOL _finished; +} + +- (instancetype)initWithResponse:(GCDWebServerResponse* _Nonnull)response reader:(id _Nonnull)reader { + if ((self = [super initWithResponse:response reader:reader])) { + response.contentLength = NSUIntegerMax; // Make sure "Content-Length" header is not set since we don't know it + [response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"]; + } + return self; +} + +- (BOOL)open:(NSError**)error { + int result = deflateInit2(&_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); + if (result != Z_OK) { + if (error) { + *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil]; + } + return NO; + } + if (![super open:error]) { + deflateEnd(&_stream); + return NO; + } + return YES; +} + +- (NSData*)readData:(NSError**)error { + NSMutableData* encodedData; + if (_finished) { + encodedData = [[NSMutableData alloc] init]; + } else { + encodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize]; + if (encodedData == nil) { + GWS_DNOT_REACHED(); + return nil; + } + NSUInteger length = 0; + do { + NSData* data = [super readData:error]; + if (data == nil) { + return nil; + } + _stream.next_in = (Bytef*)data.bytes; + _stream.avail_in = (uInt)data.length; + while (1) { + NSUInteger maxLength = encodedData.length - length; + _stream.next_out = (Bytef*)((char*)encodedData.mutableBytes + length); + _stream.avail_out = (uInt)maxLength; + int result = deflate(&_stream, data.length ? Z_NO_FLUSH : Z_FINISH); + if (result == Z_STREAM_END) { + _finished = YES; + } else if (result != Z_OK) { + if (error) { + *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil]; + } + return nil; + } + length += maxLength - _stream.avail_out; + if (_stream.avail_out > 0) { + break; + } + encodedData.length = 2 * encodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available + } + GWS_DCHECK(_stream.avail_in == 0); + } while (length == 0); // Make sure we don't return an empty NSData if not in finished state + encodedData.length = length; + } + return encodedData; +} + +- (void)close { + deflateEnd(&_stream); + [super close]; +} + +@end + +@implementation GCDWebServerResponse { + BOOL _opened; + NSMutableArray* _encoders; + id __unsafe_unretained _reader; +} + ++ (instancetype)response { + return [[[self class] alloc] init]; +} + +- (instancetype)init { + if ((self = [super init])) { + _contentType = nil; + _contentLength = NSUIntegerMax; + _statusCode = kGCDWebServerHTTPStatusCode_OK; + _cacheControlMaxAge = 0; + _additionalHeaders = [[NSMutableDictionary alloc] init]; + _encoders = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header { + [_additionalHeaders setValue:value forKey:header]; +} + +- (BOOL)hasBody { + return _contentType ? YES : NO; +} + +- (BOOL)usesChunkedTransferEncoding { + return (_contentType != nil) && (_contentLength == NSUIntegerMax); +} + +- (BOOL)open:(NSError**)error { + return YES; +} + +- (NSData*)readData:(NSError**)error { + return [NSData data]; +} + +- (void)close { + ; +} + +- (void)prepareForReading { + _reader = self; + if (_gzipContentEncodingEnabled) { + GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader]; + [_encoders addObject:encoder]; + _reader = encoder; + } +} + +- (BOOL)performOpen:(NSError**)error { + GWS_DCHECK(_contentType); + GWS_DCHECK(_reader); + if (_opened) { + GWS_DNOT_REACHED(); + return NO; + } + _opened = YES; + return [_reader open:error]; +} + +- (void)performReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block { + GWS_DCHECK(_opened); + if ([_reader respondsToSelector:@selector(asyncReadDataWithCompletion:)]) { + [_reader asyncReadDataWithCompletion:[block copy]]; + } else { + NSError* error = nil; + NSData* data = [_reader readData:&error]; + block(data, error); + } +} + +- (void)performClose { + GWS_DCHECK(_opened); + [_reader close]; +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_statusCode]; + if (_contentType) { + [description appendFormat:@"\nContent Type = %@", _contentType]; + } + if (_contentLength != NSUIntegerMax) { + [description appendFormat:@"\nContent Length = %lu", (unsigned long)_contentLength]; + } + [description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_cacheControlMaxAge]; + if (_lastModifiedDate) { + [description appendFormat:@"\nLast Modified Date = %@", _lastModifiedDate]; + } + if (_eTag) { + [description appendFormat:@"\nETag = %@", _eTag]; + } + if (_additionalHeaders.count) { + [description appendString:@"\n"]; + for (NSString* header in [[_additionalHeaders allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [description appendFormat:@"\n%@: %@", header, [_additionalHeaders objectForKey:header]]; + } + } + return description; +} + +@end + +@implementation GCDWebServerResponse (Extensions) + ++ (instancetype)responseWithStatusCode:(NSInteger)statusCode { + return [[self alloc] initWithStatusCode:statusCode]; +} + ++ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent { + return [[self alloc] initWithRedirect:location permanent:permanent]; +} + +- (instancetype)initWithStatusCode:(NSInteger)statusCode { + if ((self = [self init])) { + self.statusCode = statusCode; + } + return self; +} + +- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent { + if ((self = [self init])) { + self.statusCode = permanent ? kGCDWebServerHTTPStatusCode_MovedPermanently : kGCDWebServerHTTPStatusCode_TemporaryRedirect; + [self setValue:[location absoluteString] forAdditionalHeader:@"Location"]; + } + return self; +} + +@end diff --git a/src/ios/GCDWebServer/Requests/GCDWebServerDataRequest.h b/src/ios/GCDWebServer/Requests/GCDWebServerDataRequest.h new file mode 100755 index 0000000..f21a4b7 --- /dev/null +++ b/src/ios/GCDWebServer/Requests/GCDWebServerDataRequest.h @@ -0,0 +1,64 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServerRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerDataRequest subclass of GCDWebServerRequest stores the body + * of the HTTP request in memory. + */ +@interface GCDWebServerDataRequest : GCDWebServerRequest + +/** + * Returns the data for the request body. + */ +@property(nonatomic, readonly) NSData* data; + +@end + +@interface GCDWebServerDataRequest (Extensions) + +/** + * Returns the data for the request body interpreted as text. If the content + * type of the body is not a text one, or if an error occurs, nil is returned. + * + * The text encoding used to interpret the data is extracted from the + * "Content-Type" header or defaults to UTF-8. + */ +@property(nonatomic, readonly, nullable) NSString* text; + +/** + * Returns the data for the request body interpreted as a JSON object. If the + * content type of the body is not JSON, or if an error occurs, nil is returned. + */ +@property(nonatomic, readonly, nullable) id jsonObject; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Requests/GCDWebServerDataRequest.m b/src/ios/GCDWebServer/Requests/GCDWebServerDataRequest.m new file mode 100755 index 0000000..3ea9bba --- /dev/null +++ b/src/ios/GCDWebServer/Requests/GCDWebServerDataRequest.m @@ -0,0 +1,104 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import "GCDWebServerPrivate.h" + +@interface GCDWebServerDataRequest () +@property(nonatomic) NSMutableData* data; +@end + +@implementation GCDWebServerDataRequest { + NSString* _text; + id _jsonObject; +} + +- (BOOL)open:(NSError**)error { + if (self.contentLength != NSUIntegerMax) { + _data = [[NSMutableData alloc] initWithCapacity:self.contentLength]; + } else { + _data = [[NSMutableData alloc] init]; + } + if (_data == nil) { + if (error) { + *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Failed allocating memory" }]; + } + return NO; + } + return YES; +} + +- (BOOL)writeData:(NSData*)data error:(NSError**)error { + [_data appendData:data]; + return YES; +} + +- (BOOL)close:(NSError**)error { + return YES; +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + if (_data) { + [description appendString:@"\n\n"]; + [description appendString:GCDWebServerDescribeData(_data, (NSString*)self.contentType)]; + } + return description; +} + +@end + +@implementation GCDWebServerDataRequest (Extensions) + +- (NSString*)text { + if (_text == nil) { + if ([self.contentType hasPrefix:@"text/"]) { + NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset"); + _text = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)]; + } else { + GWS_DNOT_REACHED(); + } + } + return _text; +} + +- (id)jsonObject { + if (_jsonObject == nil) { + NSString* mimeType = GCDWebServerTruncateHeaderValue(self.contentType); + if ([mimeType isEqualToString:@"application/json"] || [mimeType isEqualToString:@"text/json"] || [mimeType isEqualToString:@"text/javascript"]) { + _jsonObject = [NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL]; + } else { + GWS_DNOT_REACHED(); + } + } + return _jsonObject; +} + +@end diff --git a/src/ios/GCDWebServer/Requests/GCDWebServerFileRequest.h b/src/ios/GCDWebServer/Requests/GCDWebServerFileRequest.h new file mode 100755 index 0000000..8aceae4 --- /dev/null +++ b/src/ios/GCDWebServer/Requests/GCDWebServerFileRequest.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServerRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerFileRequest subclass of GCDWebServerRequest stores the body + * of the HTTP request to a file on disk. + */ +@interface GCDWebServerFileRequest : GCDWebServerRequest + +/** + * Returns the path to the temporary file containing the request body. + * + * @warning This temporary file will be automatically deleted when the + * GCDWebServerFileRequest is deallocated. If you want to preserve this file, + * you must move it to a different location beforehand. + */ +@property(nonatomic, readonly) NSString* temporaryPath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Requests/GCDWebServerFileRequest.m b/src/ios/GCDWebServer/Requests/GCDWebServerFileRequest.m new file mode 100755 index 0000000..8a47fcc --- /dev/null +++ b/src/ios/GCDWebServer/Requests/GCDWebServerFileRequest.m @@ -0,0 +1,102 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import "GCDWebServerPrivate.h" + +@implementation GCDWebServerFileRequest { + int _file; +} + +- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { + if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) { + _temporaryPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; + } + return self; +} + +- (void)dealloc { + unlink([_temporaryPath fileSystemRepresentation]); +} + +- (BOOL)open:(NSError**)error { + _file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (_file <= 0) { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + return NO; + } + return YES; +} + +- (BOOL)writeData:(NSData*)data error:(NSError**)error { + if (write(_file, data.bytes, data.length) != (ssize_t)data.length) { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + return NO; + } + return YES; +} + +- (BOOL)close:(NSError**)error { + if (close(_file) < 0) { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + return NO; + } +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + NSString* creationDateHeader = [self.headers objectForKey:@"X-GCDWebServer-CreationDate"]; + if (creationDateHeader) { + NSDate* date = GCDWebServerParseISO8601(creationDateHeader); + if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate : date} ofItemAtPath:_temporaryPath error:error]) { + return NO; + } + } + NSString* modifiedDateHeader = [self.headers objectForKey:@"X-GCDWebServer-ModifiedDate"]; + if (modifiedDateHeader) { + NSDate* date = GCDWebServerParseRFC822(modifiedDateHeader); + if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate : date} ofItemAtPath:_temporaryPath error:error]) { + return NO; + } + } +#endif + return YES; +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + [description appendFormat:@"\n\n{%@}", _temporaryPath]; + return description; +} + +@end diff --git a/src/ios/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.h b/src/ios/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.h new file mode 100755 index 0000000..93ac179 --- /dev/null +++ b/src/ios/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.h @@ -0,0 +1,136 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServerRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerMultiPart class is an abstract class that wraps the content + * of a part. + */ +@interface GCDWebServerMultiPart : NSObject + +/** + * Returns the control name retrieved from the part headers. + */ +@property(nonatomic, readonly) NSString* controlName; + +/** + * Returns the content type retrieved from the part headers or "text/plain" + * if not available (per HTTP specifications). + */ +@property(nonatomic, readonly) NSString* contentType; + +/** + * Returns the MIME type component of the content type for the part. + */ +@property(nonatomic, readonly) NSString* mimeType; + +@end + +/** + * The GCDWebServerMultiPartArgument subclass of GCDWebServerMultiPart wraps + * the content of a part as data in memory. + */ +@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart + +/** + * Returns the data for the part. + */ +@property(nonatomic, readonly) NSData* data; + +/** + * Returns the data for the part interpreted as text. If the content + * type of the part is not a text one, or if an error occurs, nil is returned. + * + * The text encoding used to interpret the data is extracted from the + * "Content-Type" header or defaults to UTF-8. + */ +@property(nonatomic, readonly, nullable) NSString* string; + +@end + +/** + * The GCDWebServerMultiPartFile subclass of GCDWebServerMultiPart wraps + * the content of a part as a file on disk. + */ +@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart + +/** + * Returns the file name retrieved from the part headers. + */ +@property(nonatomic, readonly) NSString* fileName; + +/** + * Returns the path to the temporary file containing the part data. + * + * @warning This temporary file will be automatically deleted when the + * GCDWebServerMultiPartFile is deallocated. If you want to preserve this file, + * you must move it to a different location beforehand. + */ +@property(nonatomic, readonly) NSString* temporaryPath; + +@end + +/** + * The GCDWebServerMultiPartFormRequest subclass of GCDWebServerRequest + * parses the body of the HTTP request as a multipart encoded form. + */ +@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest + +/** + * Returns the argument parts from the multipart encoded form as + * name / GCDWebServerMultiPartArgument pairs. + */ +@property(nonatomic, readonly) NSArray* arguments; + +/** + * Returns the files parts from the multipart encoded form as + * name / GCDWebServerMultiPartFile pairs. + */ +@property(nonatomic, readonly) NSArray* files; + +/** + * Returns the MIME type for multipart encoded forms + * i.e. "multipart/form-data". + */ ++ (NSString*)mimeType; + +/** + * Returns the first argument for a given control name or nil if not found. + */ +- (nullable GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name; + +/** + * Returns the first file for a given control name or nil if not found. + */ +- (nullable GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.m b/src/ios/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.m new file mode 100755 index 0000000..4e6bf09 --- /dev/null +++ b/src/ios/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.m @@ -0,0 +1,405 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import "GCDWebServerPrivate.h" + +#define kMultiPartBufferSize (256 * 1024) + +typedef enum { + kParserState_Undefined = 0, + kParserState_Start, + kParserState_Headers, + kParserState_Content, + kParserState_End +} ParserState; + +@interface GCDWebServerMIMEStreamParser : NSObject +@end + +static NSData* _newlineData = nil; +static NSData* _newlinesData = nil; +static NSData* _dashNewlineData = nil; + +@implementation GCDWebServerMultiPart + +- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type { + if ((self = [super init])) { + _controlName = [name copy]; + _contentType = [type copy]; + _mimeType = (NSString*)GCDWebServerTruncateHeaderValue(_contentType); + } + return self; +} + +@end + +@implementation GCDWebServerMultiPartArgument + +- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type data:(NSData* _Nonnull)data { + if ((self = [super initWithControlName:name contentType:type])) { + _data = data; + + if ([self.contentType hasPrefix:@"text/"]) { + NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset"); + _string = [[NSString alloc] initWithData:_data encoding:GCDWebServerStringEncodingFromCharset(charset)]; + } + } + return self; +} + +- (NSString*)description { + return [NSString stringWithFormat:@"<%@ | '%@' | %lu bytes>", [self class], self.mimeType, (unsigned long)_data.length]; +} + +@end + +@implementation GCDWebServerMultiPartFile + +- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type fileName:(NSString* _Nonnull)fileName temporaryPath:(NSString* _Nonnull)temporaryPath { + if ((self = [super initWithControlName:name contentType:type])) { + _fileName = [fileName copy]; + _temporaryPath = [temporaryPath copy]; + } + return self; +} + +- (void)dealloc { + unlink([_temporaryPath fileSystemRepresentation]); +} + +- (NSString*)description { + return [NSString stringWithFormat:@"<%@ | '%@' | '%@>'", [self class], self.mimeType, _fileName]; +} + +@end + +@implementation GCDWebServerMIMEStreamParser { + NSData* _boundary; + NSString* _defaultcontrolName; + ParserState _state; + NSMutableData* _data; + NSMutableArray* _arguments; + NSMutableArray* _files; + + NSString* _controlName; + NSString* _fileName; + NSString* _contentType; + NSString* _tmpPath; + int _tmpFile; + GCDWebServerMIMEStreamParser* _subParser; +} + ++ (void)initialize { + if (_newlineData == nil) { + _newlineData = [[NSData alloc] initWithBytes:"\r\n" length:2]; + GWS_DCHECK(_newlineData); + } + if (_newlinesData == nil) { + _newlinesData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; + GWS_DCHECK(_newlinesData); + } + if (_dashNewlineData == nil) { + _dashNewlineData = [[NSData alloc] initWithBytes:"--\r\n" length:4]; + GWS_DCHECK(_dashNewlineData); + } +} + +- (instancetype)initWithBoundary:(NSString* _Nonnull)boundary defaultControlName:(NSString* _Nullable)name arguments:(NSMutableArray* _Nonnull)arguments files:(NSMutableArray* _Nonnull)files { + NSData* data = boundary.length ? [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding] : nil; + if (data == nil) { + GWS_DNOT_REACHED(); + return nil; + } + if ((self = [super init])) { + _boundary = data; + _defaultcontrolName = name; + _arguments = arguments; + _files = files; + _data = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize]; + _state = kParserState_Start; + } + return self; +} + +- (void)dealloc { + if (_tmpFile > 0) { + close(_tmpFile); + unlink([_tmpPath fileSystemRepresentation]); + } +} + +// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 +- (BOOL)_parseData { + BOOL success = YES; + + if (_state == kParserState_Headers) { + NSRange range = [_data rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _data.length)]; + if (range.location != NSNotFound) { + _controlName = nil; + _fileName = nil; + _contentType = nil; + _tmpPath = nil; + _subParser = nil; + NSString* headers = [[NSString alloc] initWithData:[_data subdataWithRange:NSMakeRange(0, range.location)] encoding:NSUTF8StringEncoding]; + if (headers) { + for (NSString* header in [headers componentsSeparatedByString:@"\r\n"]) { + NSRange subRange = [header rangeOfString:@":"]; + if (subRange.location != NSNotFound) { + NSString* name = [header substringToIndex:subRange.location]; + NSString* value = [[header substringFromIndex:(subRange.location + subRange.length)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if ([name caseInsensitiveCompare:@"Content-Type"] == NSOrderedSame) { + _contentType = GCDWebServerNormalizeHeaderValue(value); + } else if ([name caseInsensitiveCompare:@"Content-Disposition"] == NSOrderedSame) { + NSString* contentDisposition = GCDWebServerNormalizeHeaderValue(value); + if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) { + _controlName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name"); + _fileName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"); + } else if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"file"]) { + _controlName = _defaultcontrolName; + _fileName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"); + } + } + } else { + GWS_DNOT_REACHED(); + } + } + if (_contentType == nil) { + _contentType = @"text/plain"; + } + } else { + GWS_LOG_ERROR(@"Failed decoding headers in part of 'multipart/form-data'"); + GWS_DNOT_REACHED(); + } + if (_controlName) { + if ([GCDWebServerTruncateHeaderValue(_contentType) isEqualToString:@"multipart/mixed"]) { + NSString* boundary = GCDWebServerExtractHeaderValueParameter(_contentType, @"boundary"); + _subParser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:_controlName arguments:_arguments files:_files]; + if (_subParser == nil) { + GWS_DNOT_REACHED(); + success = NO; + } + } else if (_fileName) { + NSString* path = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; + _tmpFile = open([path fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (_tmpFile > 0) { + _tmpPath = [path copy]; + } else { + GWS_DNOT_REACHED(); + success = NO; + } + } + } else { + GWS_DNOT_REACHED(); + success = NO; + } + + [_data replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0]; + _state = kParserState_Content; + } + } + + if ((_state == kParserState_Start) || (_state == kParserState_Content)) { + NSRange range = [_data rangeOfData:_boundary options:0 range:NSMakeRange(0, _data.length)]; + if (range.location != NSNotFound) { + NSRange subRange = NSMakeRange(range.location + range.length, _data.length - range.location - range.length); + NSRange subRange1 = [_data rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange]; + NSRange subRange2 = [_data rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange]; + if ((subRange1.location != NSNotFound) || (subRange2.location != NSNotFound)) { + if (_state == kParserState_Content) { + const void* dataBytes = _data.bytes; + NSUInteger dataLength = range.location - 2; + if (_subParser) { + if (![_subParser appendBytes:dataBytes length:(dataLength + 2)] || ![_subParser isAtEnd]) { + GWS_DNOT_REACHED(); + success = NO; + } + _subParser = nil; + } else if (_tmpPath) { + ssize_t result = write(_tmpFile, dataBytes, dataLength); + if (result == (ssize_t)dataLength) { + if (close(_tmpFile) == 0) { + _tmpFile = 0; + GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithControlName:_controlName contentType:_contentType fileName:_fileName temporaryPath:_tmpPath]; + [_files addObject:file]; + } else { + GWS_DNOT_REACHED(); + success = NO; + } + } else { + GWS_DNOT_REACHED(); + success = NO; + } + _tmpPath = nil; + } else { + NSData* data = [[NSData alloc] initWithBytes:(void*)dataBytes length:dataLength]; + GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithControlName:_controlName contentType:_contentType data:data]; + [_arguments addObject:argument]; + } + } + + if (subRange1.location != NSNotFound) { + [_data replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0]; + _state = kParserState_Headers; + success = [self _parseData]; + } else { + _state = kParserState_End; + } + } + } else { + NSUInteger margin = 2 * _boundary.length; + if (_data.length > margin) { + NSUInteger length = _data.length - margin; + if (_subParser) { + if ([_subParser appendBytes:_data.bytes length:length]) { + [_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0]; + } else { + GWS_DNOT_REACHED(); + success = NO; + } + } else if (_tmpPath) { + ssize_t result = write(_tmpFile, _data.bytes, length); + if (result == (ssize_t)length) { + [_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0]; + } else { + GWS_DNOT_REACHED(); + success = NO; + } + } + } + } + } + + return success; +} + +- (BOOL)appendBytes:(const void*)bytes length:(NSUInteger)length { + [_data appendBytes:bytes length:length]; + return [self _parseData]; +} + +- (BOOL)isAtEnd { + return (_state == kParserState_End); +} + +@end + +@interface GCDWebServerMultiPartFormRequest () +@property(nonatomic) NSMutableArray* arguments; +@property(nonatomic) NSMutableArray* files; +@end + +@implementation GCDWebServerMultiPartFormRequest { + GCDWebServerMIMEStreamParser* _parser; +} + ++ (NSString*)mimeType { + return @"multipart/form-data"; +} + +- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { + if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) { + _arguments = [[NSMutableArray alloc] init]; + _files = [[NSMutableArray alloc] init]; + } + return self; +} + +- (BOOL)open:(NSError**)error { + NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary"); + _parser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:nil arguments:_arguments files:_files]; + if (_parser == nil) { + if (error) { + *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Failed starting to parse multipart form data" }]; + } + return NO; + } + return YES; +} + +- (BOOL)writeData:(NSData*)data error:(NSError**)error { + if (![_parser appendBytes:data.bytes length:data.length]) { + if (error) { + *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Failed continuing to parse multipart form data" }]; + } + return NO; + } + return YES; +} + +- (BOOL)close:(NSError**)error { + BOOL atEnd = [_parser isAtEnd]; + _parser = nil; + if (!atEnd) { + if (error) { + *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Failed finishing to parse multipart form data" }]; + } + return NO; + } + return YES; +} + +- (GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name { + for (GCDWebServerMultiPartArgument* argument in _arguments) { + if ([argument.controlName isEqualToString:name]) { + return argument; + } + } + return nil; +} + +- (GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name { + for (GCDWebServerMultiPartFile* file in _files) { + if ([file.controlName isEqualToString:name]) { + return file; + } + } + return nil; +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + if (_arguments.count) { + [description appendString:@"\n"]; + for (GCDWebServerMultiPartArgument* argument in _arguments) { + [description appendFormat:@"\n%@ (%@)\n", argument.controlName, argument.contentType]; + [description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)]; + } + } + if (_files.count) { + [description appendString:@"\n"]; + for (GCDWebServerMultiPartFile* file in _files) { + [description appendFormat:@"\n%@ (%@): %@\n{%@}", file.controlName, file.contentType, file.fileName, file.temporaryPath]; + } + } + return description; +} + +@end diff --git a/src/ios/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.h b/src/ios/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.h new file mode 100755 index 0000000..fcf177e --- /dev/null +++ b/src/ios/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.h @@ -0,0 +1,55 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServerDataRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerURLEncodedFormRequest subclass of GCDWebServerRequest + * parses the body of the HTTP request as a URL encoded form using + * GCDWebServerParseURLEncodedForm(). + */ +@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest + +/** + * Returns the unescaped control names and values for the URL encoded form. + * + * The text encoding used to interpret the data is extracted from the + * "Content-Type" header or defaults to UTF-8. + */ +@property(nonatomic, readonly) NSDictionary* arguments; + +/** + * Returns the MIME type for URL encoded forms + * i.e. "application/x-www-form-urlencoded". + */ ++ (NSString*)mimeType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.m b/src/ios/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.m new file mode 100755 index 0000000..7e0137f --- /dev/null +++ b/src/ios/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.m @@ -0,0 +1,60 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import "GCDWebServerPrivate.h" + +@implementation GCDWebServerURLEncodedFormRequest + ++ (NSString*)mimeType { + return @"application/x-www-form-urlencoded"; +} + +- (BOOL)close:(NSError**)error { + if (![super close:error]) { + return NO; + } + + NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset"); + NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)]; + _arguments = GCDWebServerParseURLEncodedForm(string); + return YES; +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + [description appendString:@"\n"]; + for (NSString* argument in [[_arguments allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [description appendFormat:@"\n%@ = %@", argument, [_arguments objectForKey:argument]]; + } + return description; +} + +@end diff --git a/src/ios/GCDWebServer/Responses/GCDWebServerDataResponse.h b/src/ios/GCDWebServer/Responses/GCDWebServerDataResponse.h new file mode 100755 index 0000000..783f596 --- /dev/null +++ b/src/ios/GCDWebServer/Responses/GCDWebServerDataResponse.h @@ -0,0 +1,113 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServerResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerDataResponse subclass of GCDWebServerResponse reads the body + * of the HTTP response from memory. + */ +@interface GCDWebServerDataResponse : GCDWebServerResponse +@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null + +/** + * Creates a response with data in memory and a given content type. + */ ++ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type; + +/** + * This method is the designated initializer for the class. + */ +- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type; + +@end + +@interface GCDWebServerDataResponse (Extensions) + +/** + * Creates a data response from text encoded using UTF-8. + */ ++ (nullable instancetype)responseWithText:(NSString*)text; + +/** + * Creates a data response from HTML encoded using UTF-8. + */ ++ (nullable instancetype)responseWithHTML:(NSString*)html; + +/** + * Creates a data response from an HTML template encoded using UTF-8. + * See -initWithHTMLTemplate:variables: for details. + */ ++ (nullable instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; + +/** + * Creates a data response from a serialized JSON object and the default + * "application/json" content type. + */ ++ (nullable instancetype)responseWithJSONObject:(id)object; + +/** + * Creates a data response from a serialized JSON object and a custom + * content type. + */ ++ (nullable instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type; + +/** + * Initializes a data response from text encoded using UTF-8. + */ +- (nullable instancetype)initWithText:(NSString*)text; + +/** + * Initializes a data response from HTML encoded using UTF-8. + */ +- (nullable instancetype)initWithHTML:(NSString*)html; + +/** + * Initializes a data response from an HTML template encoded using UTF-8. + * + * All occurences of "%variable%" within the HTML template are replaced with + * their corresponding values. + */ +- (nullable instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; + +/** + * Initializes a data response from a serialized JSON object and the default + * "application/json" content type. + */ +- (nullable instancetype)initWithJSONObject:(id)object; + +/** + * Initializes a data response from a serialized JSON object and a custom + * content type. + */ +- (nullable instancetype)initWithJSONObject:(id)object contentType:(NSString*)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Responses/GCDWebServerDataResponse.m b/src/ios/GCDWebServer/Responses/GCDWebServerDataResponse.m new file mode 100755 index 0000000..b496847 --- /dev/null +++ b/src/ios/GCDWebServer/Responses/GCDWebServerDataResponse.m @@ -0,0 +1,136 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import "GCDWebServerPrivate.h" + +@implementation GCDWebServerDataResponse { + NSData* _data; + BOOL _done; +} + +@dynamic contentType; + ++ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type { + return [[[self class] alloc] initWithData:data contentType:type]; +} + +- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type { + if ((self = [super init])) { + _data = data; + + self.contentType = type; + self.contentLength = data.length; + } + return self; +} + +- (NSData*)readData:(NSError**)error { + NSData* data; + if (_done) { + data = [NSData data]; + } else { + data = _data; + _done = YES; + } + return data; +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + [description appendString:@"\n\n"]; + [description appendString:GCDWebServerDescribeData(_data, self.contentType)]; + return description; +} + +@end + +@implementation GCDWebServerDataResponse (Extensions) + ++ (instancetype)responseWithText:(NSString*)text { + return [[self alloc] initWithText:text]; +} + ++ (instancetype)responseWithHTML:(NSString*)html { + return [[self alloc] initWithHTML:html]; +} + ++ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables { + return [[self alloc] initWithHTMLTemplate:path variables:variables]; +} + ++ (instancetype)responseWithJSONObject:(id)object { + return [[self alloc] initWithJSONObject:object]; +} + ++ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type { + return [[self alloc] initWithJSONObject:object contentType:type]; +} + +- (instancetype)initWithText:(NSString*)text { + NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding]; + if (data == nil) { + GWS_DNOT_REACHED(); + return nil; + } + return [self initWithData:data contentType:@"text/plain; charset=utf-8"]; +} + +- (instancetype)initWithHTML:(NSString*)html { + NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding]; + if (data == nil) { + GWS_DNOT_REACHED(); + return nil; + } + return [self initWithData:data contentType:@"text/html; charset=utf-8"]; +} + +- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables { + NSMutableString* html = [[NSMutableString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; + [variables enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL* stop) { + [html replaceOccurrencesOfString:[NSString stringWithFormat:@"%%%@%%", key] withString:value options:0 range:NSMakeRange(0, html.length)]; + }]; + return [self initWithHTML:html]; +} + +- (instancetype)initWithJSONObject:(id)object { + return [self initWithJSONObject:object contentType:@"application/json"]; +} + +- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type { + NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL]; + if (data == nil) { + GWS_DNOT_REACHED(); + return nil; + } + return [self initWithData:data contentType:type]; +} + +@end diff --git a/src/ios/GCDWebServer/Responses/GCDWebServerErrorResponse.h b/src/ios/GCDWebServer/Responses/GCDWebServerErrorResponse.h new file mode 100755 index 0000000..92c834c --- /dev/null +++ b/src/ios/GCDWebServer/Responses/GCDWebServerErrorResponse.h @@ -0,0 +1,85 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServerDataResponse.h" +#import "GCDWebServerHTTPStatusCodes.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerDataResponse subclass of GCDWebServerDataResponse generates + * an HTML body from an HTTP status code and an error message. + */ +@interface GCDWebServerErrorResponse : GCDWebServerDataResponse + +/** + * Creates a client error response with the corresponding HTTP status code. + */ ++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3); + +/** + * Creates a server error response with the corresponding HTTP status code. + */ ++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3); + +/** + * Creates a client error response with the corresponding HTTP status code + * and an underlying NSError. + */ ++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); + +/** + * Creates a server error response with the corresponding HTTP status code + * and an underlying NSError. + */ ++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); + +/** + * Initializes a client error response with the corresponding HTTP status code. + */ +- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3); + +/** + * Initializes a server error response with the corresponding HTTP status code. + */ +- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3); + +/** + * Initializes a client error response with the corresponding HTTP status code + * and an underlying NSError. + */ +- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); + +/** + * Initializes a server error response with the corresponding HTTP status code + * and an underlying NSError. + */ +- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Responses/GCDWebServerErrorResponse.m b/src/ios/GCDWebServer/Responses/GCDWebServerErrorResponse.m new file mode 100755 index 0000000..f1cd202 --- /dev/null +++ b/src/ios/GCDWebServer/Responses/GCDWebServerErrorResponse.m @@ -0,0 +1,124 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import "GCDWebServerPrivate.h" + +@implementation GCDWebServerErrorResponse + ++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { + GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500)); + va_list arguments; + va_start(arguments, format); + GCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments]; + va_end(arguments); + return response; +} + ++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { + GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600)); + va_list arguments; + va_start(arguments, format); + GCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments]; + va_end(arguments); + return response; +} + ++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { + GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500)); + va_list arguments; + va_start(arguments, format); + GCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments]; + va_end(arguments); + return response; +} + ++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { + GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600)); + va_list arguments; + va_start(arguments, format); + GCDWebServerErrorResponse* response = [[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments]; + va_end(arguments); + return response; +} + +static inline NSString* _EscapeHTMLString(NSString* string) { + return [string stringByReplacingOccurrencesOfString:@"\"" withString:@"""]; +} + +- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments { + NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; + NSString* title = [NSString stringWithFormat:@"HTTP Error %i", (int)statusCode]; + NSString* error = underlyingError ? [NSString stringWithFormat:@"[%@] %@ (%li)", underlyingError.domain, _EscapeHTMLString(underlyingError.localizedDescription), (long)underlyingError.code] : @""; + NSString* html = [NSString stringWithFormat:@"%@

%@: %@

%@

", + title, title, _EscapeHTMLString(message), error]; + if ((self = [self initWithHTML:html])) { + self.statusCode = statusCode; + } + return self; +} + +- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { + GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500)); + va_list arguments; + va_start(arguments, format); + self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments]; + va_end(arguments); + return self; +} + +- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { + GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600)); + va_list arguments; + va_start(arguments, format); + self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments]; + va_end(arguments); + return self; +} + +- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { + GWS_DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500)); + va_list arguments; + va_start(arguments, format); + self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments]; + va_end(arguments); + return self; +} + +- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { + GWS_DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600)); + va_list arguments; + va_start(arguments, format); + self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments]; + va_end(arguments); + return self; +} + +@end diff --git a/src/ios/GCDWebServer/Responses/GCDWebServerFileResponse.h b/src/ios/GCDWebServer/Responses/GCDWebServerFileResponse.h new file mode 100755 index 0000000..9403835 --- /dev/null +++ b/src/ios/GCDWebServer/Responses/GCDWebServerFileResponse.h @@ -0,0 +1,108 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServerResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerFileResponse subclass of GCDWebServerResponse reads the body + * of the HTTP response from a file on disk. + * + * It will automatically set the contentType, lastModifiedDate and eTag + * properties of the GCDWebServerResponse according to the file extension and + * metadata. + */ +@interface GCDWebServerFileResponse : GCDWebServerResponse +@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null +@property(nonatomic) NSDate* lastModifiedDate; // Redeclare as non-null +@property(nonatomic, copy) NSString* eTag; // Redeclare as non-null + +/** + * Creates a response with the contents of a file. + */ ++ (nullable instancetype)responseWithFile:(NSString*)path; + +/** + * Creates a response like +responseWithFile: and sets the "Content-Disposition" + * HTTP header for a download if the "attachment" argument is YES. + */ ++ (nullable instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment; + +/** + * Creates a response like +responseWithFile: but restricts the file contents + * to a specific byte range. + * + * See -initWithFile:byteRange: for details. + */ ++ (nullable instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range; + +/** + * Creates a response like +responseWithFile:byteRange: and sets the + * "Content-Disposition" HTTP header for a download if the "attachment" + * argument is YES. + */ ++ (nullable instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment; + +/** + * Initializes a response with the contents of a file. + */ +- (nullable instancetype)initWithFile:(NSString*)path; + +/** + * Initializes a response like +responseWithFile: and sets the + * "Content-Disposition" HTTP header for a download if the "attachment" + * argument is YES. + */ +- (nullable instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment; + +/** + * Initializes a response like -initWithFile: but restricts the file contents + * to a specific byte range. This range should be set to (NSUIntegerMax, 0) for + * the full file, (offset, length) if expressed from the beginning of the file, + * or (NSUIntegerMax, length) if expressed from the end of the file. The "offset" + * and "length" values will be automatically adjusted to be compatible with the + * actual size of the file. + * + * This argument would typically be set to the value of the byteRange property + * of the current GCDWebServerRequest. + */ +- (nullable instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range; + +/** + * This method is the designated initializer for the class. + * + * If MIME type overrides are specified, they allow to customize the built-in + * mapping from extensions to MIME types. Keys of the dictionary must be lowercased + * file extensions without the period, and the values must be the corresponding + * MIME types. + */ +- (nullable instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment mimeTypeOverrides:(nullable NSDictionary*)overrides; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Responses/GCDWebServerFileResponse.m b/src/ios/GCDWebServer/Responses/GCDWebServerFileResponse.m new file mode 100755 index 0000000..bd07518 --- /dev/null +++ b/src/ios/GCDWebServer/Responses/GCDWebServerFileResponse.m @@ -0,0 +1,185 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import + +#import "GCDWebServerPrivate.h" + +#define kFileReadBufferSize (32 * 1024) + +@implementation GCDWebServerFileResponse { + NSString* _path; + NSUInteger _offset; + NSUInteger _size; + int _file; +} + +@dynamic contentType, lastModifiedDate, eTag; + ++ (instancetype)responseWithFile:(NSString*)path { + return [[[self class] alloc] initWithFile:path]; +} + ++ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment { + return [[[self class] alloc] initWithFile:path isAttachment:attachment]; +} + ++ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range { + return [[[self class] alloc] initWithFile:path byteRange:range]; +} + ++ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment { + return [[[self class] alloc] initWithFile:path byteRange:range isAttachment:attachment mimeTypeOverrides:nil]; +} + +- (instancetype)initWithFile:(NSString*)path { + return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:NO mimeTypeOverrides:nil]; +} + +- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment { + return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:attachment mimeTypeOverrides:nil]; +} + +- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range { + return [self initWithFile:path byteRange:range isAttachment:NO mimeTypeOverrides:nil]; +} + +static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) { + return [NSDate dateWithTimeIntervalSince1970:((NSTimeInterval)t->tv_sec + (NSTimeInterval)t->tv_nsec / 1000000000.0)]; +} + +- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment mimeTypeOverrides:(NSDictionary*)overrides { + struct stat info; + if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) { + GWS_DNOT_REACHED(); + return nil; + } +#ifndef __LP64__ + if (info.st_size >= (off_t)4294967295) { // In 32 bit mode, we can't handle files greater than 4 GiBs (don't use "NSUIntegerMax" here to avoid potential unsigned to signed conversion issues) + GWS_DNOT_REACHED(); + return nil; + } +#endif + NSUInteger fileSize = (NSUInteger)info.st_size; + + BOOL hasByteRange = GCDWebServerIsValidByteRange(range); + if (hasByteRange) { + if (range.location != NSUIntegerMax) { + range.location = MIN(range.location, fileSize); + range.length = MIN(range.length, fileSize - range.location); + } else { + range.length = MIN(range.length, fileSize); + range.location = fileSize - range.length; + } + if (range.length == 0) { + return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header + } + } else { + range.location = 0; + range.length = fileSize; + } + + if ((self = [super init])) { + _path = [path copy]; + _offset = range.location; + _size = range.length; + if (hasByteRange) { + [self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent]; + [self setValue:[NSString stringWithFormat:@"bytes %lu-%lu/%lu", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), (unsigned long)fileSize] forAdditionalHeader:@"Content-Range"]; + GWS_LOG_DEBUG(@"Using content bytes range [%lu-%lu] for file \"%@\"", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), path); + } + + if (attachment) { + NSString* fileName = [path lastPathComponent]; + NSData* data = [[fileName stringByReplacingOccurrencesOfString:@"\"" withString:@""] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES]; + NSString* lossyFileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil; + if (lossyFileName) { + NSString* value = [NSString stringWithFormat:@"attachment; filename=\"%@\"; filename*=UTF-8''%@", lossyFileName, GCDWebServerEscapeURLString(fileName)]; + [self setValue:value forAdditionalHeader:@"Content-Disposition"]; + } else { + GWS_DNOT_REACHED(); + } + } + + self.contentType = GCDWebServerGetMimeTypeForExtension([_path pathExtension], overrides); + self.contentLength = _size; + self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec); + self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec]; + } + return self; +} + +- (BOOL)open:(NSError**)error { + _file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY); + if (_file <= 0) { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + return NO; + } + if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + close(_file); + return NO; + } + return YES; +} + +- (NSData*)readData:(NSError**)error { + size_t length = MIN((NSUInteger)kFileReadBufferSize, _size); + NSMutableData* data = [[NSMutableData alloc] initWithLength:length]; + ssize_t result = read(_file, data.mutableBytes, length); + if (result < 0) { + if (error) { + *error = GCDWebServerMakePosixError(errno); + } + return nil; + } + if (result > 0) { + [data setLength:result]; + _size -= result; + } + return data; +} + +- (void)close { + close(_file); +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + [description appendFormat:@"\n\n{%@}", _path]; + return description; +} + +@end diff --git a/src/ios/GCDWebServer/Responses/GCDWebServerStreamedResponse.h b/src/ios/GCDWebServer/Responses/GCDWebServerStreamedResponse.h new file mode 100755 index 0000000..bb48e66 --- /dev/null +++ b/src/ios/GCDWebServer/Responses/GCDWebServerStreamedResponse.h @@ -0,0 +1,80 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "GCDWebServerResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The GCDWebServerStreamBlock is called to stream the data for the HTTP body. + * The block must return either a chunk of data, an empty NSData when done, or + * nil on error and set the "error" argument which is guaranteed to be non-NULL. + */ +typedef NSData* _Nullable (^GCDWebServerStreamBlock)(NSError** error); + +/** + * The GCDWebServerAsyncStreamBlock works like the GCDWebServerStreamBlock + * except the streamed data can be returned at a later time allowing for + * truly asynchronous generation of the data. + * + * The block must call "completionBlock" passing the new chunk of data when ready, + * an empty NSData when done, or nil on error and pass a NSError. + * + * The block cannot call "completionBlock" more than once per invocation. + */ +typedef void (^GCDWebServerAsyncStreamBlock)(GCDWebServerBodyReaderCompletionBlock completionBlock); + +/** + * The GCDWebServerStreamedResponse subclass of GCDWebServerResponse streams + * the body of the HTTP response using a GCD block. + */ +@interface GCDWebServerStreamedResponse : GCDWebServerResponse +@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null + +/** + * Creates a response with streamed data and a given content type. + */ ++ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block; + +/** + * Creates a response with async streamed data and a given content type. + */ ++ (instancetype)responseWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block; + +/** + * Initializes a response with streamed data and a given content type. + */ +- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block; + +/** + * This method is the designated initializer for the class. + */ +- (instancetype)initWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/GCDWebServer/Responses/GCDWebServerStreamedResponse.m b/src/ios/GCDWebServer/Responses/GCDWebServerStreamedResponse.m new file mode 100755 index 0000000..9387263 --- /dev/null +++ b/src/ios/GCDWebServer/Responses/GCDWebServerStreamedResponse.m @@ -0,0 +1,78 @@ +/* + Copyright (c) 2012-2015, Pierre-Olivier Latour + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of Pierre-Olivier Latour may not be used to endorse + or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if !__has_feature(objc_arc) +#error GCDWebServer requires ARC +#endif + +#import "GCDWebServerPrivate.h" + +@implementation GCDWebServerStreamedResponse { + GCDWebServerAsyncStreamBlock _block; +} + +@dynamic contentType; + ++ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { + return [[[self class] alloc] initWithContentType:type streamBlock:block]; +} + ++ (instancetype)responseWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block { + return [[[self class] alloc] initWithContentType:type asyncStreamBlock:block]; +} + +- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { + return [self initWithContentType:type + asyncStreamBlock:^(GCDWebServerBodyReaderCompletionBlock completionBlock) { + + NSError* error = nil; + NSData* data = block(&error); + completionBlock(data, error); + + }]; +} + +- (instancetype)initWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block { + if ((self = [super init])) { + _block = [block copy]; + + self.contentType = type; + } + return self; +} + +- (void)asyncReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block { + _block(block); +} + +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + [description appendString:@"\n\n"]; + return description; +} + +@end diff --git a/src/ios/Webserver-Bridging-Header.h b/src/ios/Webserver-Bridging-Header.h new file mode 100644 index 0000000..b7deef4 --- /dev/null +++ b/src/ios/Webserver-Bridging-Header.h @@ -0,0 +1 @@ +#import "GCDWebServer.h" diff --git a/src/ios/Webserver.swift b/src/ios/Webserver.swift index 9d5014a..17f6220 100644 --- a/src/ios/Webserver.swift +++ b/src/ios/Webserver.swift @@ -1,12 +1,20 @@ @objc(Webserver) class Webserver : CDVPlugin { - func start(command: CDVInvokedUrlCommand) { - var pluginResult = CDVPluginResult( - status: CDVCommandStatus_OK - ) - self.commandDelegate!.sendPluginResult( - pluginResult, - callbackId: command.callbackId - ) - } + var request_ids: [String] = [] + var webServer = GCDWebServer() + + override func pluginInitialize() { + self.request_ids = [] + } + + func start(_ command: CDVInvokedUrlCommand) { + self.request_ids.append("Hi") + + for request_id in self.request_ids { + print(request_id) + } + + let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK) + self.commandDelegate!.send(pluginResult, callbackId: command.callbackId) + } } diff --git a/tests/tests.js b/tests/tests.js index 0c3d272..0685a0d 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -19,7 +19,9 @@ exports.defineAutoTests = function() { }; exports.defineManualTests = function(contentEl, createActionButton) { - createActionButton('Start', function() { + createActionButton('Start Webserver', function() { + console.log(webserver); + webserver.start( function() { console.log('Success!');