diff --git a/GCDWebServer/Core/GCDWebServer.h b/GCDWebServer/Core/GCDWebServer.h index e9a5196..6597a37 100644 --- a/GCDWebServer/Core/GCDWebServer.h +++ b/GCDWebServer/Core/GCDWebServer.h @@ -30,8 +30,14 @@ #import "GCDWebServerRequest.h" #import "GCDWebServerResponse.h" +/** + * Log levels used by GCDWebServer. + * + * @warning kGCDWebServerLogLevel_Debug is only available if "NDEBUG" is not + * defined when building. + */ typedef NS_ENUM(int, GCDWebServerLogLevel) { - kGCDWebServerLogLevel_Debug = 0, // Only available if "NDEBUG" is not defined when building + kGCDWebServerLogLevel_Debug = 0, kGCDWebServerLogLevel_Verbose, kGCDWebServerLogLevel_Info, kGCDWebServerLogLevel_Warning, @@ -39,88 +45,420 @@ typedef NS_ENUM(int, GCDWebServerLogLevel) { kGCDWebServerLogLevel_Exception, }; +/** + * 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* (^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* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request); -extern NSString* const GCDWebServerOption_Port; // NSNumber / NSUInteger (default is 0 i.e. use a random port) -extern NSString* const GCDWebServerOption_BonjourName; // NSString (default is empty string i.e. use computer name) -extern NSString* const GCDWebServerOption_MaxPendingConnections; // NSNumber / NSUInteger (default is 16) -extern NSString* const GCDWebServerOption_ServerName; // NSString (default is server class name) -extern NSString* const GCDWebServerOption_AuthenticationMethod; // One of "GCDWebServerAuthenticationMethod_..." (default is nil i.e. no authentication) -extern NSString* const GCDWebServerOption_AuthenticationRealm; // NSString (default is server name) -extern NSString* const GCDWebServerOption_AuthenticationAccounts; // NSDictionary of username / password (default is nil i.e. no accounts) -extern NSString* const GCDWebServerOption_ConnectionClass; // Subclass of GCDWebServerConnection (default is GCDWebServerConnection class) -extern NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET; // NSNumber / BOOL (default is YES) -extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval; // NSNumber / double (default is 1.0 seconds - set to <=0.0 to disable coaslescing of -webServerDidConnect: / -webServerDidDisconnect:) +/** + * The port used by the GCDWebServer (NSNumber / NSUInteger). + * + * 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). + * + * Default value is an empty string i.e. use the computer / device name. + */ +extern NSString* const GCDWebServerOption_BonjourName; + +/** + * The maximum number of incoming HTTP requests that can be queued waiting to + * be handled before new ones are dropped (NSNumber / NSUInteger). + * + * Default value is 16. + */ +extern NSString* const GCDWebServerOption_MaxPendingConnections; + +/** + * The value for "Server" HTTP header used by the GCDWebServer (NSString). + * + * Default value is the GCDWebServer class name. + */ +extern NSString* const GCDWebServerOption_ServerName; + +/** + * The authentication method used by the GCDWebServer + * (one of "GCDWebServerAuthenticationMethod_..."). + * + * Default value is nil i.e. authentication disabled. + */ +extern NSString* const GCDWebServerOption_AuthenticationMethod; + +/** + * The authentication realm used by the GCDWebServer (NSString). + * + * Default value is the same as GCDWebServerOption_ServerName. + */ +extern NSString* const GCDWebServerOption_AuthenticationRealm; + +/** + * The authentication accounts used by the GCDWebServer + * (NSDictionary of username / password pairs). + * + * 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). + * + * Default value is 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). + * + * 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. + * + * Default value is 1.0 second. + */ +extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval; + #if TARGET_OS_IPHONE -extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground; // NSNumber / BOOL (default is YES) + +/** + * 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. + * + * Default value is YES. + * + * @warning The running property will be NO while the GCDWebServer is suspended. + */ +extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground; + #endif -extern NSString* const GCDWebServerAuthenticationMethod_Basic; // Not recommended as password is sent in clear +/** + * HTTP Basic Authentication scheme (see https://tools.ietf.org/html/rfc2617). + * + * @warning Use of this method 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; -// These methods are always called on main thread +/** + * 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 succesfully started. + */ - (void)webServerDidStart:(GCDWebServer*)server; -- (void)webServerDidConnect:(GCDWebServer*)server; // Called when first connection is opened -- (void)webServerDidDisconnect:(GCDWebServer*)server; // Called when last connection is closed + +/** + * This method is called when the first GCDWebServerConnection is opened by the + * server to serve a series of HTTP requests. A series is ongoing as long as + * new HTTP requests keep coming (and new GCDWebServerConnection instances keep + * being opened), 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 manages the socket that listens for HTTP requests and + * the list of handlers used to respond to them. + * + * See the README.md file for more information about the architecture of GCDWebServer. + */ @interface GCDWebServer : NSObject + +/** + * Sets the delegate for the server. + */ @property(nonatomic, assign) id delegate; + +/** + * Indicates if the server is currently running. + */ @property(nonatomic, readonly, getter=isRunning) BOOL running; -@property(nonatomic, readonly) NSUInteger port; // Only non-zero if running -@property(nonatomic, readonly) NSString* bonjourName; // Only non-nil if Bonjour registration is active + +/** + * 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 in 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) NSString* bonjourName; + +/** + * This method is the designated initializer for the class. + */ - (instancetype)init; + +/** + * Adds a handler to the server to handle incoming HTTP requests. + * Handlers are called in a LIFO queue, so the latest added handler overrides + * any previously added ones. + * + * @warning Addling handlers while the GCDWebServer is running is not allowed. + */ - (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; + +/** + * Removes all handlers previously added to the server. + * + * @warning Removing handlers while the GCDWebServer is running is not allowed. + */ - (void)removeAllHandlers; -- (BOOL)start; // Default is port 8080 (OS X & iOS Simulator) or 80 (iOS) and computer / device name for Bonjour -- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name; // Pass nil name to disable Bonjour or empty string to use computer name +/** + * Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS) + * using the computer / device name for as the 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 computer / device name. + * + * Returns NO if the server failed to start. + */ +- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name; + +/** + * Starts the server with explicit options. This method is the designated way + * to start the server. + * + * Returns NO if the server failed to start. + */ - (BOOL)startWithOptions:(NSDictionary*)options; -- (void)stop; // Does not abort any currently opened connections + +/** + * Stops the server and prevents it to accepts new HTTP requests. + * + * @warning Stopping the server does not abort GCDWebServerConnection instances + * handling already received HTTP requests. These connections will continue to + * execute until the corresponding requests and responses are completed. + */ +- (void)stop; + @end @interface GCDWebServer (Extensions) -@property(nonatomic, readonly) NSURL* serverURL; // Only non-nil if server is running -@property(nonatomic, readonly) NSURL* bonjourServerURL; // Only non-nil if server is running and Bonjour registration is active + +/** + * Returns the server's URL. + * + * @warning This property is only valid if the server is running. + */ +@property(nonatomic, readonly) 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. + */ +@property(nonatomic, readonly) NSURL* bonjourServerURL; + #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:(NSString*)name; -- (BOOL)runWithOptions:(NSDictionary*)options; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only) + +/** + * Runs the server synchronously using -startWithOptions: 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)runWithOptions:(NSDictionary*)options; + #endif + @end @interface GCDWebServer (Handlers) + +/** + * Adds a default handler to the server to handle all incoming HTTP requests + * with a given HTTP method. + */ - (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; -- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Path is case-insensitive -- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive + +/** + * Adds a handler to the server to handle incoming HTTP requests with a given + * HTTP method and a specific case-insensitive path. + */ +- (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 path matching a case-insensitive regular expression. + */ +- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; + @end @interface GCDWebServer (GETHandlers) -- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge; // Path is case-insensitive -- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Path is case-insensitive -- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Base path is recursive and case-sensitive + +/** + * 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:(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:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; + @end @interface GCDWebServer (Logging) + #ifndef __GCDWEBSERVER_LOGGING_HEADER__ -+ (void)setLogLevel:(GCDWebServerLogLevel)level; // Default level is DEBUG or INFO if "NDEBUG" is defined when building (it can also be set at runtime with the "logLevel" environment variable) + +/** + * Sets the current log level below which logged messages are discarded. + * + * The default level is either DEBUG or INFO if "NDEBUG" is defined at build-time. + * It can also be set at runtime with the "logLevel" environment variable. + */ ++ (void)setLogLevel:(GCDWebServerLogLevel)level; + #endif + +/** + * Logs a message with the kGCDWebServerLogLevel_Verbose level. + */ - (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); + +/** + * Logs a message with the kGCDWebServerLogLevel_Info level. + */ - (void)logInfo:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); + +/** + * Logs a message with the kGCDWebServerLogLevel_Warning level. + */ - (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); + +/** + * Logs a message with the kGCDWebServerLogLevel_Error level. + */ - (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); + @end #ifdef __GCDWEBSERVER_ENABLE_TESTING__ @interface GCDWebServer (Testing) -@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled; // Creates files in the current directory containing the raw data for all requests and responses (directory most NOT contain prior recordings) -- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path; // Returns number of failed tests or -1 if server failed to start + +/** + * 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:(NSDictionary*)options inDirectory:(NSString*)path; + @end #endif diff --git a/GCDWebServer/Core/GCDWebServerConnection.h b/GCDWebServer/Core/GCDWebServerConnection.h index 8f73494..7aa75a1 100644 --- a/GCDWebServer/Core/GCDWebServerConnection.h +++ b/GCDWebServer/Core/GCDWebServerConnection.h @@ -29,24 +29,132 @@ @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; -@property(nonatomic, readonly) NSData* localAddressData; // struct sockaddr + +/** + * 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 dotted string. + */ @property(nonatomic, readonly) NSString* localAddressString; -@property(nonatomic, readonly) NSData* remoteAddressData; // struct sockaddr + +/** + * 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 dotted 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 -// These methods can be called from any thread +/** + * 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) -- (BOOL)open; // Return NO to reject connection e.g. after validating local or remote addresses -- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length; // Called after data has been read from the connection -- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length; // Called after data has been written to the connection -- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request; // Called before request is processed to return an override response bypassing processing or nil to continue - Default implementation checks authentication if applicable -- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; // Only called if the request can be processed -- (GCDWebServerResponse*)overrideResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request; // Default implementation replaces any response matching the "ETag" or "Last-Modified-Date" header of the request by a barebone "Not-Modified" (304) one -- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; // If request headers were malformed, "request" will be nil + +/** + * 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). + */ +- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length; + +/** + * This method is called whenever data has been sent + * to the remote peer (i.e. client). + */ +- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length; + +/** + * Assuming a valid 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. + */ +- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request; + +/** + * Assuming a valid request was received and -preflightRequest: returned nil, + * this method is called to process the request. + */ +- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; + +/** + * Assuming a valid request was received and either -preflightRequest: + * or -processRequest:withBlock: 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 different 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 no GCDWebServerResponse is generated. + * + * @warning If the request was invalid (e.g. the HTTP headers were malformed), + * the "request" argument will be nil. + */ +- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; + +/** + * Called when the connection is closed. + */ - (void)close; + @end diff --git a/GCDWebServer/Core/GCDWebServerFunctions.h b/GCDWebServer/Core/GCDWebServerFunctions.h index 8c896b0..201d886 100644 --- a/GCDWebServer/Core/GCDWebServerFunctions.h +++ b/GCDWebServer/Core/GCDWebServerFunctions.h @@ -31,14 +31,68 @@ extern "C" { #endif +/** + * Converts a file extension to the corresponding MIME type. + * If there is no match, "application/octet-stream" is returned. + */ NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension); + +/** + * 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* GCDWebServerEscapeURLString(NSString* string); + +/** + * Unescapes a URL percent-encoded string. + */ NSString* GCDWebServerUnescapeURLString(NSString* string); + +/** + * Extracts the unescaped names and values + * from a "application/x-www-form-urlencoded" form. + * http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 + */ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form); -NSString* GCDWebServerGetPrimaryIPv4Address(); // Returns IPv4 address of primary connected service on OS X or of WiFi interface on iOS if connected + +/** + * OS X: Returns the IPv4 address as a dotted string of the primary connected + * service or nil if not available. + * iOS: Returns the IPv4 address as a dotted string of the WiFi interface + * if connected or nil otherwise. + */ +NSString* GCDWebServerGetPrimaryIPv4Address(); + +/** + * 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 are not supported at this time. + */ NSDate* 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 + * are not supported either. + */ NSDate* GCDWebServerParseISO8601(NSString* string); #ifdef __cplusplus diff --git a/GCDWebServer/Core/GCDWebServerFunctions.m b/GCDWebServer/Core/GCDWebServerFunctions.m index 59451ab..ba82264 100644 --- a/GCDWebServer/Core/GCDWebServerFunctions.m +++ b/GCDWebServer/Core/GCDWebServerFunctions.m @@ -43,8 +43,7 @@ static NSDateFormatter* _dateFormatterRFC822 = nil; static NSDateFormatter* _dateFormatterISO8601 = nil; static dispatch_queue_t _dateFormatterQueue = NULL; -// HTTP/1.1 server must use RFC822 -// TODO: Handle RFC 850 and ANSI C's asctime() format (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3) +// TODO: Handle RFC 850 and ANSI C's asctime() format void GCDWebServerInitializeFunctions() { DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread if (_dateFormatterRFC822 == nil) { @@ -187,7 +186,6 @@ NSString* GCDWebServerUnescapeURLString(NSString* string) { return ARC_BRIDGE_RELEASE(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8)); } -// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) { NSMutableDictionary* parameters = [NSMutableDictionary dictionary]; NSScanner* scanner = [[NSScanner alloc] initWithString:form]; diff --git a/GCDWebServer/Core/GCDWebServerHTTPStatusCodes.h b/GCDWebServer/Core/GCDWebServerHTTPStatusCodes.h index d64558d..7af51a2 100644 --- a/GCDWebServer/Core/GCDWebServerHTTPStatusCodes.h +++ b/GCDWebServer/Core/GCDWebServerHTTPStatusCodes.h @@ -30,12 +30,18 @@ #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, @@ -48,6 +54,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) { kGCDWebServerHTTPStatusCode_AlreadyReported = 208 }; +/** + * Convenience constants for "redirection" HTTP status codes. + */ typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) { kGCDWebServerHTTPStatusCode_MultipleChoices = 300, kGCDWebServerHTTPStatusCode_MovedPermanently = 301, @@ -59,6 +68,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) { kGCDWebServerHTTPStatusCode_PermanentRedirect = 308 }; +/** + * Convenience constants for "client error" HTTP status codes. + */ typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) { kGCDWebServerHTTPStatusCode_BadRequest = 400, kGCDWebServerHTTPStatusCode_Unauthorized = 401, @@ -87,6 +99,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) { kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431 }; +/** + * Convenience constants for "server error" HTTP status codes. + */ typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) { kGCDWebServerHTTPStatusCode_InternalServerError = 500, kGCDWebServerHTTPStatusCode_NotImplemented = 501, diff --git a/GCDWebServer/Core/GCDWebServerRequest.h b/GCDWebServer/Core/GCDWebServerRequest.h index 8e039a0..bcbbf39 100644 --- a/GCDWebServer/Core/GCDWebServerRequest.h +++ b/GCDWebServer/Core/GCDWebServerRequest.h @@ -27,25 +27,140 @@ #import +/** + * 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 -- (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL) -- (BOOL)writeData:(NSData*)data error:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL) -- (BOOL)close:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL) + +/** + * 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; -@property(nonatomic, readonly) NSDictionary* query; // May be nil -@property(nonatomic, readonly) NSString* contentType; // Automatically parsed from headers (nil if request has no body or set to "application/octet-stream" if a body is present without a "Content-Type" header) -@property(nonatomic, readonly) NSUInteger contentLength; // Automatically parsed from headers (NSNotFound if request has no "Content-Length" header) -@property(nonatomic, readonly) NSDate* ifModifiedSince; // Automatically parsed from headers (nil if request has no "If-Modified-Since" header or it is malformatted) -@property(nonatomic, readonly) NSString* ifNoneMatch; // Automatically parsed from headers (nil if request has no "If-None-Match" header) -@property(nonatomic, readonly) NSRange byteRange; // Automatically parsed from headers ([NSNotFound, 0] if request has no "Range" header, [offset, length] for byte range from beginning or [NSNotFound, -length] from end) -@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding; // Automatically parsed from headers + +/** + * 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) NSDictionary* query; + +/** + * Returns the content type for the body of the request (this property is + * automatically parsed from the HTTP headers). + * + * 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) NSString* contentType; + +/** + * Returns the content length for the body of the request (this property is + * automatically parsed from the HTTP headers). + * + * This property will be set to "NSNotFound" 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 of malformed. + */ +@property(nonatomic, readonly) NSDate* ifModifiedSince; + +/** + * Returns the parsed "If-None-Match" header or nil if absent of malformed. + */ +@property(nonatomic, readonly) NSString* ifNoneMatch; + +/** + * Returns the parsed "Range" header or (NSNotFound, 0) if absent or malformed. + * The range will be set to (offset, length) if expressed from the beginning + * of the body, or (NSNotFound, -length) if expressed from the end of the body. + */ +@property(nonatomic, readonly) NSRange byteRange; + +/** + * Indicates if the client supports gzip content encoding (this property is + * automatically parsed from the HTTP headers). + */ +@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding; + +/** + * This method is the designated initializer for the class. + */ - (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query; -- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil -- (BOOL)hasByteRange; // Convenience method that checks "byteRange" + +/** + * Convenience method that checks if the contentType property is defined. + */ +- (BOOL)hasBody; + +/** + * Convenience method that checks if the byteRange property is defined. + */ +- (BOOL)hasByteRange; + @end diff --git a/GCDWebServer/Core/GCDWebServerResponse.h b/GCDWebServer/Core/GCDWebServerResponse.h index f0cf708..80e34a2 100644 --- a/GCDWebServer/Core/GCDWebServerResponse.h +++ b/GCDWebServer/Core/GCDWebServerResponse.h @@ -27,29 +27,162 @@ #import +/** + * This protocol is used by the GCDWebServerConnection to communicate with + * the GCDWebServerResponse and read the sent HTTP body data. + * + * 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 -- (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL) -- (NSData*)readData:(NSError**)error; // Must return nil on error or empty NSData if at end ("error" is guaranteed to be non-NULL) + +/** + * 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 ready to be 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. + */ +- (NSData*)readData:(NSError**)error; + +/** + * This method is called after all body data has been sent. + */ - (void)close; + @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 retrieve 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 -@property(nonatomic, copy) NSString* contentType; // Default is nil i.e. no body (must be set if a body is present) -@property(nonatomic) NSUInteger contentLength; // Default is NSNotFound i.e. undefined (if a body is present but length is undefined, chunked transfer encoding will be enabled) -@property(nonatomic) NSInteger statusCode; // Default is 200 -@property(nonatomic) NSUInteger cacheControlMaxAge; // Default is 0 seconds i.e. "Cache-Control: no-cache" -@property(nonatomic, retain) NSDate* lastModifiedDate; // Default is nil i.e. no "Last-Modified" header -@property(nonatomic, copy) NSString* eTag; // Default is nil i.e. no "ETag" header -@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; // Default is disabled + +/** + * Sets the content type for the body of the response. + * This property must be set if a body is present. + * + * The default value is nil i.e. the response has no body. + */ +@property(nonatomic, copy) NSString* contentType; + +/** + * Sets the content length for the body of the response. If a body is present + * but this property is set to "NSNotFound", this means the length of the body + * cannot be known ahead of time and chunked transfer encoding will be + * automatically enabled by the GCDWebServerConnection to comply with HTTP/1.1 + * specifications. + * + * The default value is "NSNotFound" 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, retain) NSDate* lastModifiedDate; + +/** + * Sets the ETag for the response using the "ETag" header. + * + * The default value is nil. + */ +@property(nonatomic, copy) 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 a default response. + */ + (instancetype)response; + +/** + * This method is the designated initializer for the class. + */ - (instancetype)init; -- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; // Pass nil value to remove header -- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil + +/** + * 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 e.g. "Content-Type" or "ETag". + */ +- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; + +/** + * Convenience method that checks if the contentType property is defined. + */ +- (BOOL)hasBody; + @end @interface GCDWebServerResponse (Extensions) + +/** + * Creates a default 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 a default 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 diff --git a/GCDWebServer/Core/GCDWebServerResponse.m b/GCDWebServer/Core/GCDWebServerResponse.m index ce68cc7..0182ee2 100644 --- a/GCDWebServer/Core/GCDWebServerResponse.m +++ b/GCDWebServer/Core/GCDWebServerResponse.m @@ -81,7 +81,7 @@ - (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)reader { if ((self = [super initWithResponse:response reader:reader])) { - response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set since we don't know it (client will determine body length when connection is closed) + response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set since we don't know it [response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"]; } return self;