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!');