From 23bddb2b5ce89d14521fc795a77cb803953d202e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Sun, 30 Dec 2012 09:42:19 -0800 Subject: [PATCH] Moved GCDWebServerConnection to its own source files --- CGDWebServer/GCDWebServer.h | 28 -- CGDWebServer/GCDWebServer.m | 486 +------------------------ CGDWebServer/GCDWebServerConnection.h | 56 +++ CGDWebServer/GCDWebServerConnection.m | 485 ++++++++++++++++++++++++ CGDWebServer/GCDWebServerPrivate.h | 21 +- GCDWebServer.xcodeproj/project.pbxproj | 6 + 6 files changed, 570 insertions(+), 512 deletions(-) create mode 100644 CGDWebServer/GCDWebServerConnection.h create mode 100644 CGDWebServer/GCDWebServerConnection.m diff --git a/CGDWebServer/GCDWebServer.h b/CGDWebServer/GCDWebServer.h index 76ba685..2b21ecd 100644 --- a/CGDWebServer/GCDWebServer.h +++ b/CGDWebServer/GCDWebServer.h @@ -31,34 +31,6 @@ typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery); typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request); -@class GCDWebServer, GCDWebServerHandler; - -@interface GCDWebServerConnection : NSObject { -@private - GCDWebServer* _server; - NSData* _address; - CFSocketNativeHandle _socket; - NSUInteger _bytesRead; - NSUInteger _bytesWritten; - - CFHTTPMessageRef _requestMessage; - GCDWebServerRequest* _request; - GCDWebServerHandler* _handler; - CFHTTPMessageRef _responseMessage; - GCDWebServerResponse* _response; -} -@property(nonatomic, readonly) GCDWebServer* server; -@property(nonatomic, readonly) NSData* address; // struct sockaddr -@property(nonatomic, readonly) NSUInteger totalBytesRead; -@property(nonatomic, readonly) NSUInteger totalBytesWritten; -@end - -@interface GCDWebServerConnection (Subclassing) -- (void) open; -- (GCDWebServerResponse*) processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; -- (void) close; -@end - @interface GCDWebServer : NSObject { @private NSMutableArray* _handlers; diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index b8a6a90..39baba3 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -25,56 +25,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#import -#if TARGET_OS_IPHONE -#import -#import -#else -#import -#import -#endif -#import -#import #import #import "GCDWebServerPrivate.h" -#define kReadWriteQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) -#define kHeadersReadBuffer 1024 -#define kBodyWriteBufferSize (32 * 1024) - -typedef void (^ReadBufferCompletionBlock)(dispatch_data_t buffer); -typedef void (^ReadDataCompletionBlock)(NSData* data); -typedef void (^ReadHeadersCompletionBlock)(NSData* extraData); -typedef void (^ReadBodyCompletionBlock)(BOOL success); - -typedef void (^WriteBufferCompletionBlock)(BOOL success); -typedef void (^WriteDataCompletionBlock)(BOOL success); -typedef void (^WriteHeadersCompletionBlock)(BOOL success); -typedef void (^WriteBodyCompletionBlock)(BOOL success); - -@interface GCDWebServerHandler : NSObject { -@private - GCDWebServerMatchBlock _matchBlock; - GCDWebServerProcessBlock _processBlock; -} -@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock; -@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock; -- (id) initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; -@end - -@interface GCDWebServerConnection () -- (id) initWithServer:(GCDWebServer*)server address:(NSData*)address socket:(CFSocketNativeHandle)socket; -@end - -@interface GCDWebServer () -@property(nonatomic, readonly) NSArray* handlers; -@end - -static NSData* _separatorData = nil; -static NSData* _continueData = nil; -static NSDateFormatter* _dateFormatter = nil; -static dispatch_queue_t _formatterQueue = NULL; static BOOL _run; NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) { @@ -99,7 +53,7 @@ NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) { return mimeType; } -static NSString* _UnescapeURLString(NSString* string) { +NSString* GCDWebServerUnescapeURLString(NSString* string) { return [(id)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8) autorelease]; } @@ -122,7 +76,7 @@ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) { key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "]; value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; - [parameters setObject:_UnescapeURLString(value) forKey:_UnescapeURLString(key)]; + [parameters setObject:GCDWebServerUnescapeURLString(value) forKey:GCDWebServerUnescapeURLString(key)]; if ([scanner isAtEnd]) { break; @@ -159,446 +113,12 @@ static void _SignalHandler(int signal) { @end -@implementation GCDWebServerConnection (Read) - -- (void) _readBufferWithLength:(NSUInteger)length completionBlock:(ReadBufferCompletionBlock)block { - dispatch_read(_socket, length, kReadWriteQueue, ^(dispatch_data_t buffer, int error) { - - @autoreleasepool { - if (error == 0) { - size_t size = dispatch_data_get_size(buffer); - if (size > 0) { - LOG_DEBUG(@"Connection received %i bytes on socket %i", size, _socket); - _bytesRead += size; - block(buffer); - } else { - if (_bytesRead > 0) { - LOG_ERROR(@"No more data available on socket %i", _socket); - } else { - LOG_WARNING(@"No data received from socket %i", _socket); - } - block(NULL); - } - } else { - LOG_ERROR(@"Error while reading from socket %i: %s (%i)", _socket, strerror(error), error); - block(NULL); - } - } - - }); -} - -- (void) _readDataWithCompletionBlock:(ReadDataCompletionBlock)block { - [self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) { - - if (buffer) { - NSMutableData* data = [[NSMutableData alloc] initWithCapacity:dispatch_data_get_size(buffer)]; - dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) { - [data appendBytes:buffer length:size]; - return true; - }); - block(data); - [data release]; - } else { - block(nil); - } - - }]; -} - -- (void) _readHeadersWithCompletionBlock:(ReadHeadersCompletionBlock)block { - DCHECK(_requestMessage); - NSMutableData* data = [NSMutableData dataWithCapacity:kHeadersReadBuffer]; - [self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) { - - if (buffer) { - dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) { - [data appendBytes:buffer length:size]; - return true; - }); - NSRange range = [data rangeOfData:_separatorData options:0 range:NSMakeRange(0, data.length)]; - if (range.location == NSNotFound) { - [self _readHeadersWithCompletionBlock:block]; - } else { - NSUInteger length = range.location + range.length; - if (CFHTTPMessageAppendBytes(_requestMessage, data.bytes, length)) { - if (CFHTTPMessageIsHeaderComplete(_requestMessage)) { - block([data subdataWithRange:NSMakeRange(length, data.length - length)]); - } else { - LOG_ERROR(@"Failed parsing request headers from socket %i", _socket); - block(nil); - } - } else { - LOG_ERROR(@"Failed appending request headers data from socket %i", _socket); - block(nil); - } - } - } else { - block(nil); - } - - }]; -} - -- (void) _readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block { - DCHECK([_request hasBody]); - [self _readBufferWithLength:length completionBlock:^(dispatch_data_t buffer) { - - if (buffer) { - NSInteger remainingLength = length - dispatch_data_get_size(buffer); - if (remainingLength >= 0) { - bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) { - NSInteger result = [_request write:buffer maxLength:size]; - if (result != size) { - LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result); - return false; - } - return true; - }); - if (success) { - if (remainingLength > 0) { - [self _readBodyWithRemainingLength:remainingLength completionBlock:block]; - } else { - block(YES); - } - } else { - block(NO); - } - } else { - DNOT_REACHED(); - block(NO); - } - } else { - block(NO); - } - - }]; -} - -@end - -@implementation GCDWebServerConnection (Write) - -- (void) _writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block { - size_t size = dispatch_data_get_size(buffer); - dispatch_write(_socket, buffer, kReadWriteQueue, ^(dispatch_data_t data, int error) { - - @autoreleasepool { - if (error == 0) { - DCHECK(data == NULL); - LOG_DEBUG(@"Connection sent %i bytes on socket %i", size, _socket); - _bytesWritten += size; - block(YES); - } else { - LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error); - block(NO); - } - } - - }); -} - -- (void) _writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block { - [data retain]; - dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_current_queue(), ^{ - [data release]; - }); - [self _writeBuffer:buffer withCompletionBlock:block]; - dispatch_release(buffer); -} - -- (void) _writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block { - DCHECK(_responseMessage); - CFDataRef message = CFHTTPMessageCopySerializedMessage(_responseMessage); - [self _writeData:(NSData*)message withCompletionBlock:block]; - CFRelease(message); -} - -- (void) _writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block { - DCHECK([_response hasBody]); - void* buffer = malloc(kBodyWriteBufferSize); - NSInteger result = [_response read:buffer maxLength:kBodyWriteBufferSize]; - if (result > 0) { - dispatch_data_t wrapper = dispatch_data_create(buffer, result, NULL, DISPATCH_DATA_DESTRUCTOR_FREE); - [self _writeBuffer:wrapper withCompletionBlock:^(BOOL success) { - - if (success) { - [self _writeBodyWithCompletionBlock:block]; - } else { - block(NO); - } - - }]; - dispatch_release(wrapper); - } else if (result < 0) { - LOG_ERROR(@"Failed reading response body on socket %i (error %i)", _socket, (int)result); - block(NO); - free(buffer); - } else { - block(YES); - free(buffer); - } -} - -@end - -@implementation GCDWebServerConnection - -@synthesize server=_server, address=_address, totalBytesRead=_bytesRead, totalBytesWritten=_bytesWritten; - -- (void) _initializeResponseHeadersWithStatusCode:(NSInteger)statusCode { - _responseMessage = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_1); - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Connection"), CFSTR("Close")); - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Server"), (CFStringRef)[[_server class] serverName]); - dispatch_sync(_formatterQueue, ^{ - NSString* date = [_dateFormatter stringFromDate:[NSDate date]]; - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (CFStringRef)date); - }); -} - -- (void) _abortWithStatusCode:(NSUInteger)statusCode { - DCHECK(_responseMessage == NULL); - DCHECK((statusCode >= 400) && (statusCode < 600)); - [self _initializeResponseHeadersWithStatusCode:statusCode]; - [self _writeHeadersWithCompletionBlock:^(BOOL success) { - ; // Nothing more to do - }]; - LOG_DEBUG(@"Connection aborted with status code %i on socket %i", statusCode, _socket); -} - -// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html -- (void) _processRequest { - DCHECK(_responseMessage == NULL); - - GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock]; - if (![response hasBody] || [response open]) { - _response = [response retain]; - } - - if (_response) { - [self _initializeResponseHeadersWithStatusCode:_response.statusCode]; - NSUInteger maxAge = _response.cacheControlMaxAge; - if (maxAge > 0) { - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)maxAge]); - } else { - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache")); - } - [_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) { - CFHTTPMessageSetHeaderFieldValue(_responseMessage, (CFStringRef)key, (CFStringRef)obj); - }]; - if ([_response hasBody]) { - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (CFStringRef)_response.contentType); - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (CFStringRef)[NSString stringWithFormat:@"%i", (int)_response.contentLength]); - } - [self _writeHeadersWithCompletionBlock:^(BOOL success) { - - if (success) { - if ([_response hasBody]) { - [self _writeBodyWithCompletionBlock:^(BOOL success) { - - [_response close]; // Can't do anything with result anyway - - }]; - } - } else if ([_response hasBody]) { - [_response close]; // Can't do anything with result anyway - } - - }]; - } else { - [self _abortWithStatusCode:500]; - } - -} - -- (void) _readRequestBody:(NSData*)initialData { - if ([_request open]) { - NSInteger length = _request.contentLength; - if (initialData.length) { - NSInteger result = [_request write:initialData.bytes maxLength:initialData.length]; - if (result == initialData.length) { - length -= initialData.length; - DCHECK(length >= 0); - } else { - LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result); - length = -1; - } - } - if (length > 0) { - [self _readBodyWithRemainingLength:length completionBlock:^(BOOL success) { - - if (![_request close]) { - success = NO; - } - if (success) { - [self _processRequest]; - } else { - [self _abortWithStatusCode:500]; - } - - }]; - } else if (length == 0) { - if ([_request close]) { - [self _processRequest]; - } else { - [self _abortWithStatusCode:500]; - } - } else { - [_request close]; // Can't do anything with result anyway - [self _abortWithStatusCode:500]; - } - } else { - [self _abortWithStatusCode:500]; - } -} - -- (void) _readRequestHeaders { - _requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true); - [self _readHeadersWithCompletionBlock:^(NSData* extraData) { - - if (extraData) { - NSString* requestMethod = [[(id)CFHTTPMessageCopyRequestMethod(_requestMessage) autorelease] uppercaseString]; - DCHECK(requestMethod); - NSURL* requestURL = [(id)CFHTTPMessageCopyRequestURL(_requestMessage) autorelease]; - DCHECK(requestURL); - NSString* requestPath = _UnescapeURLString([(id)CFURLCopyPath((CFURLRef)requestURL) autorelease]); // Don't use -[NSURL path] which strips the ending slash - DCHECK(requestPath); - NSDictionary* requestQuery = nil; - NSString* queryString = [(id)CFURLCopyQueryString((CFURLRef)requestURL, NULL) autorelease]; // Don't use -[NSURL query] to make sure query is not unescaped; - if (queryString.length) { - requestQuery = GCDWebServerParseURLEncodedForm(queryString); - DCHECK(requestQuery); - } - NSDictionary* requestHeaders = [(id)CFHTTPMessageCopyAllHeaderFields(_requestMessage) autorelease]; - DCHECK(requestHeaders); - for (_handler in _server.handlers) { - _request = [_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery) retain]; - if (_request) { - break; - } - } - if (_request) { - if (_request.hasBody) { - if (extraData.length <= _request.contentLength) { - NSString* expectHeader = [(id)CFHTTPMessageCopyHeaderFieldValue(_requestMessage, CFSTR("Expect")) autorelease]; - if (expectHeader) { - if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) { - [self _writeData:_continueData withCompletionBlock:^(BOOL success) { - - if (success) { - [self _readRequestBody:extraData]; - } - - }]; - } else { - LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket); - [self _abortWithStatusCode:417]; - } - } else { - [self _readRequestBody:extraData]; - } - } else { - LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket); - [self _abortWithStatusCode:400]; - } - } else { - [self _processRequest]; - } - } else { - [self _abortWithStatusCode:405]; - } - } else { - [self _abortWithStatusCode:500]; - } - - }]; -} - -- (id) initWithServer:(GCDWebServer*)server address:(NSData*)address socket:(CFSocketNativeHandle)socket { - if ((self = [super init])) { - _server = [server retain]; - _address = [address retain]; - _socket = socket; - - [self open]; - } - return self; -} - -- (void) dealloc { - [self close]; - - [_server release]; - [_address release]; - - if (_requestMessage) { - CFRelease(_requestMessage); - } - [_request release]; - - if (_responseMessage) { - CFRelease(_responseMessage); - } - [_response release]; - - [super dealloc]; -} - -@end - -@implementation GCDWebServerConnection (Subclassing) - -- (void) open { - LOG_DEBUG(@"Did open connection on socket %i", _socket); - [self _readRequestHeaders]; -} - -- (GCDWebServerResponse*) processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block { - LOG_DEBUG(@"Connection on socket %i processing %@ request for \"%@\" (%i bytes body)", _socket, _request.method, _request.path, _request.contentLength); - GCDWebServerResponse* response = nil; - @try { - response = block(request); - } - @catch (NSException* exception) { - LOG_EXCEPTION(exception); - } - return response; -} - -- (void) close { - close(_socket); - LOG_DEBUG(@"Did close connection on socket %i", _socket); -} - -@end - @implementation GCDWebServer @synthesize handlers=_handlers, port=_port; + (void) initialize { - DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread - if (_separatorData == nil) { - _separatorData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; - DCHECK(_separatorData); - } - if (_continueData == nil) { - CFHTTPMessageRef message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, NULL, kCFHTTPVersion1_1); - _continueData = (NSData*)CFHTTPMessageCopySerializedMessage(message); - CFRelease(message); - DCHECK(_continueData); - } - if (_dateFormatter == nil) { - _dateFormatter = [[NSDateFormatter alloc] init]; - _dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; - _dateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"; - _dateFormatter.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease]; - DCHECK(_dateFormatter); - } - if (_formatterQueue == NULL) { - _formatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); - DCHECK(_formatterQueue); - } + [GCDWebServerConnection class]; // Initialize class immediately to make sure it happens on the main thread } - (id) init { diff --git a/CGDWebServer/GCDWebServerConnection.h b/CGDWebServer/GCDWebServerConnection.h new file mode 100644 index 0000000..db79e68 --- /dev/null +++ b/CGDWebServer/GCDWebServerConnection.h @@ -0,0 +1,56 @@ +/* + Copyright (c) 2012-2013, 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. + * Neither the name of the nor the + names of its contributors may 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 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" + +@class GCDWebServerHandler; + +@interface GCDWebServerConnection : NSObject { +@private + GCDWebServer* _server; + NSData* _address; + CFSocketNativeHandle _socket; + NSUInteger _bytesRead; + NSUInteger _bytesWritten; + + CFHTTPMessageRef _requestMessage; + GCDWebServerRequest* _request; + GCDWebServerHandler* _handler; + CFHTTPMessageRef _responseMessage; + GCDWebServerResponse* _response; +} +@property(nonatomic, readonly) GCDWebServer* server; +@property(nonatomic, readonly) NSData* address; // struct sockaddr +@property(nonatomic, readonly) NSUInteger totalBytesRead; +@property(nonatomic, readonly) NSUInteger totalBytesWritten; +@end + +@interface GCDWebServerConnection (Subclassing) +- (void) open; +- (GCDWebServerResponse*) processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; +- (void) close; +@end diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m new file mode 100644 index 0000000..87ed6fc --- /dev/null +++ b/CGDWebServer/GCDWebServerConnection.m @@ -0,0 +1,485 @@ +/* + Copyright (c) 2012-2013, 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. + * Neither the name of the nor the + names of its contributors may 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 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 "GCDWebServerPrivate.h" + +#define kReadWriteQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) +#define kHeadersReadBuffer 1024 +#define kBodyWriteBufferSize (32 * 1024) + +typedef void (^ReadBufferCompletionBlock)(dispatch_data_t buffer); +typedef void (^ReadDataCompletionBlock)(NSData* data); +typedef void (^ReadHeadersCompletionBlock)(NSData* extraData); +typedef void (^ReadBodyCompletionBlock)(BOOL success); + +typedef void (^WriteBufferCompletionBlock)(BOOL success); +typedef void (^WriteDataCompletionBlock)(BOOL success); +typedef void (^WriteHeadersCompletionBlock)(BOOL success); +typedef void (^WriteBodyCompletionBlock)(BOOL success); + +static NSData* _separatorData = nil; +static NSData* _continueData = nil; +static NSDateFormatter* _dateFormatter = nil; +static dispatch_queue_t _formatterQueue = NULL; + +@implementation GCDWebServerConnection (Read) + +- (void) _readBufferWithLength:(NSUInteger)length completionBlock:(ReadBufferCompletionBlock)block { + dispatch_read(_socket, length, kReadWriteQueue, ^(dispatch_data_t buffer, int error) { + + @autoreleasepool { + if (error == 0) { + size_t size = dispatch_data_get_size(buffer); + if (size > 0) { + LOG_DEBUG(@"Connection received %i bytes on socket %i", size, _socket); + _bytesRead += size; + block(buffer); + } else { + if (_bytesRead > 0) { + LOG_ERROR(@"No more data available on socket %i", _socket); + } else { + LOG_WARNING(@"No data received from socket %i", _socket); + } + block(NULL); + } + } else { + LOG_ERROR(@"Error while reading from socket %i: %s (%i)", _socket, strerror(error), error); + block(NULL); + } + } + + }); +} + +- (void) _readDataWithCompletionBlock:(ReadDataCompletionBlock)block { + [self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) { + + if (buffer) { + NSMutableData* data = [[NSMutableData alloc] initWithCapacity:dispatch_data_get_size(buffer)]; + dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) { + [data appendBytes:buffer length:size]; + return true; + }); + block(data); + [data release]; + } else { + block(nil); + } + + }]; +} + +- (void) _readHeadersWithCompletionBlock:(ReadHeadersCompletionBlock)block { + DCHECK(_requestMessage); + NSMutableData* data = [NSMutableData dataWithCapacity:kHeadersReadBuffer]; + [self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) { + + if (buffer) { + dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) { + [data appendBytes:buffer length:size]; + return true; + }); + NSRange range = [data rangeOfData:_separatorData options:0 range:NSMakeRange(0, data.length)]; + if (range.location == NSNotFound) { + [self _readHeadersWithCompletionBlock:block]; + } else { + NSUInteger length = range.location + range.length; + if (CFHTTPMessageAppendBytes(_requestMessage, data.bytes, length)) { + if (CFHTTPMessageIsHeaderComplete(_requestMessage)) { + block([data subdataWithRange:NSMakeRange(length, data.length - length)]); + } else { + LOG_ERROR(@"Failed parsing request headers from socket %i", _socket); + block(nil); + } + } else { + LOG_ERROR(@"Failed appending request headers data from socket %i", _socket); + block(nil); + } + } + } else { + block(nil); + } + + }]; +} + +- (void) _readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block { + DCHECK([_request hasBody]); + [self _readBufferWithLength:length completionBlock:^(dispatch_data_t buffer) { + + if (buffer) { + NSInteger remainingLength = length - dispatch_data_get_size(buffer); + if (remainingLength >= 0) { + bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) { + NSInteger result = [_request write:buffer maxLength:size]; + if (result != size) { + LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result); + return false; + } + return true; + }); + if (success) { + if (remainingLength > 0) { + [self _readBodyWithRemainingLength:remainingLength completionBlock:block]; + } else { + block(YES); + } + } else { + block(NO); + } + } else { + DNOT_REACHED(); + block(NO); + } + } else { + block(NO); + } + + }]; +} + +@end + +@implementation GCDWebServerConnection (Write) + +- (void) _writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block { + size_t size = dispatch_data_get_size(buffer); + dispatch_write(_socket, buffer, kReadWriteQueue, ^(dispatch_data_t data, int error) { + + @autoreleasepool { + if (error == 0) { + DCHECK(data == NULL); + LOG_DEBUG(@"Connection sent %i bytes on socket %i", size, _socket); + _bytesWritten += size; + block(YES); + } else { + LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error); + block(NO); + } + } + + }); +} + +- (void) _writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block { + [data retain]; + dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_current_queue(), ^{ + [data release]; + }); + [self _writeBuffer:buffer withCompletionBlock:block]; + dispatch_release(buffer); +} + +- (void) _writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block { + DCHECK(_responseMessage); + CFDataRef message = CFHTTPMessageCopySerializedMessage(_responseMessage); + [self _writeData:(NSData*)message withCompletionBlock:block]; + CFRelease(message); +} + +- (void) _writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block { + DCHECK([_response hasBody]); + void* buffer = malloc(kBodyWriteBufferSize); + NSInteger result = [_response read:buffer maxLength:kBodyWriteBufferSize]; + if (result > 0) { + dispatch_data_t wrapper = dispatch_data_create(buffer, result, NULL, DISPATCH_DATA_DESTRUCTOR_FREE); + [self _writeBuffer:wrapper withCompletionBlock:^(BOOL success) { + + if (success) { + [self _writeBodyWithCompletionBlock:block]; + } else { + block(NO); + } + + }]; + dispatch_release(wrapper); + } else if (result < 0) { + LOG_ERROR(@"Failed reading response body on socket %i (error %i)", _socket, (int)result); + block(NO); + free(buffer); + } else { + block(YES); + free(buffer); + } +} + +@end + +@implementation GCDWebServerConnection + +@synthesize server=_server, address=_address, totalBytesRead=_bytesRead, totalBytesWritten=_bytesWritten; + ++ (void) initialize { + DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread + if (_separatorData == nil) { + _separatorData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; + DCHECK(_separatorData); + } + if (_continueData == nil) { + CFHTTPMessageRef message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, NULL, kCFHTTPVersion1_1); + _continueData = (NSData*)CFHTTPMessageCopySerializedMessage(message); + CFRelease(message); + DCHECK(_continueData); + } + if (_dateFormatter == nil) { + _dateFormatter = [[NSDateFormatter alloc] init]; + _dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; + _dateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"; + _dateFormatter.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease]; + DCHECK(_dateFormatter); + } + if (_formatterQueue == NULL) { + _formatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + DCHECK(_formatterQueue); + } +} + +- (void) _initializeResponseHeadersWithStatusCode:(NSInteger)statusCode { + _responseMessage = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_1); + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Connection"), CFSTR("Close")); + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Server"), (CFStringRef)[[_server class] serverName]); + dispatch_sync(_formatterQueue, ^{ + NSString* date = [_dateFormatter stringFromDate:[NSDate date]]; + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (CFStringRef)date); + }); +} + +- (void) _abortWithStatusCode:(NSUInteger)statusCode { + DCHECK(_responseMessage == NULL); + DCHECK((statusCode >= 400) && (statusCode < 600)); + [self _initializeResponseHeadersWithStatusCode:statusCode]; + [self _writeHeadersWithCompletionBlock:^(BOOL success) { + ; // Nothing more to do + }]; + LOG_DEBUG(@"Connection aborted with status code %i on socket %i", statusCode, _socket); +} + +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html +- (void) _processRequest { + DCHECK(_responseMessage == NULL); + + GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock]; + if (![response hasBody] || [response open]) { + _response = [response retain]; + } + + if (_response) { + [self _initializeResponseHeadersWithStatusCode:_response.statusCode]; + NSUInteger maxAge = _response.cacheControlMaxAge; + if (maxAge > 0) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)maxAge]); + } else { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache")); + } + [_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, (CFStringRef)key, (CFStringRef)obj); + }]; + if ([_response hasBody]) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (CFStringRef)_response.contentType); + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (CFStringRef)[NSString stringWithFormat:@"%i", (int)_response.contentLength]); + } + [self _writeHeadersWithCompletionBlock:^(BOOL success) { + + if (success) { + if ([_response hasBody]) { + [self _writeBodyWithCompletionBlock:^(BOOL success) { + + [_response close]; // Can't do anything with result anyway + + }]; + } + } else if ([_response hasBody]) { + [_response close]; // Can't do anything with result anyway + } + + }]; + } else { + [self _abortWithStatusCode:500]; + } + +} + +- (void) _readRequestBody:(NSData*)initialData { + if ([_request open]) { + NSInteger length = _request.contentLength; + if (initialData.length) { + NSInteger result = [_request write:initialData.bytes maxLength:initialData.length]; + if (result == initialData.length) { + length -= initialData.length; + DCHECK(length >= 0); + } else { + LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result); + length = -1; + } + } + if (length > 0) { + [self _readBodyWithRemainingLength:length completionBlock:^(BOOL success) { + + if (![_request close]) { + success = NO; + } + if (success) { + [self _processRequest]; + } else { + [self _abortWithStatusCode:500]; + } + + }]; + } else if (length == 0) { + if ([_request close]) { + [self _processRequest]; + } else { + [self _abortWithStatusCode:500]; + } + } else { + [_request close]; // Can't do anything with result anyway + [self _abortWithStatusCode:500]; + } + } else { + [self _abortWithStatusCode:500]; + } +} + +- (void) _readRequestHeaders { + _requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true); + [self _readHeadersWithCompletionBlock:^(NSData* extraData) { + + if (extraData) { + NSString* requestMethod = [[(id)CFHTTPMessageCopyRequestMethod(_requestMessage) autorelease] uppercaseString]; + DCHECK(requestMethod); + NSURL* requestURL = [(id)CFHTTPMessageCopyRequestURL(_requestMessage) autorelease]; + DCHECK(requestURL); + NSString* requestPath = GCDWebServerUnescapeURLString([(id)CFURLCopyPath((CFURLRef)requestURL) autorelease]); // Don't use -[NSURL path] which strips the ending slash + DCHECK(requestPath); + NSDictionary* requestQuery = nil; + NSString* queryString = [(id)CFURLCopyQueryString((CFURLRef)requestURL, NULL) autorelease]; // Don't use -[NSURL query] to make sure query is not unescaped; + if (queryString.length) { + requestQuery = GCDWebServerParseURLEncodedForm(queryString); + DCHECK(requestQuery); + } + NSDictionary* requestHeaders = [(id)CFHTTPMessageCopyAllHeaderFields(_requestMessage) autorelease]; + DCHECK(requestHeaders); + for (_handler in _server.handlers) { + _request = [_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery) retain]; + if (_request) { + break; + } + } + if (_request) { + if (_request.hasBody) { + if (extraData.length <= _request.contentLength) { + NSString* expectHeader = [(id)CFHTTPMessageCopyHeaderFieldValue(_requestMessage, CFSTR("Expect")) autorelease]; + if (expectHeader) { + if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) { + [self _writeData:_continueData withCompletionBlock:^(BOOL success) { + + if (success) { + [self _readRequestBody:extraData]; + } + + }]; + } else { + LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket); + [self _abortWithStatusCode:417]; + } + } else { + [self _readRequestBody:extraData]; + } + } else { + LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket); + [self _abortWithStatusCode:400]; + } + } else { + [self _processRequest]; + } + } else { + [self _abortWithStatusCode:405]; + } + } else { + [self _abortWithStatusCode:500]; + } + + }]; +} + +- (id) initWithServer:(GCDWebServer*)server address:(NSData*)address socket:(CFSocketNativeHandle)socket { + if ((self = [super init])) { + _server = [server retain]; + _address = [address retain]; + _socket = socket; + + [self open]; + } + return self; +} + +- (void) dealloc { + [self close]; + + [_server release]; + [_address release]; + + if (_requestMessage) { + CFRelease(_requestMessage); + } + [_request release]; + + if (_responseMessage) { + CFRelease(_responseMessage); + } + [_response release]; + + [super dealloc]; +} + +@end + +@implementation GCDWebServerConnection (Subclassing) + +- (void) open { + LOG_DEBUG(@"Did open connection on socket %i", _socket); + [self _readRequestHeaders]; +} + +- (GCDWebServerResponse*) processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block { + LOG_DEBUG(@"Connection on socket %i processing %@ request for \"%@\" (%i bytes body)", _socket, _request.method, _request.path, _request.contentLength); + GCDWebServerResponse* response = nil; + @try { + response = block(request); + } + @catch (NSException* exception) { + LOG_EXCEPTION(exception); + } + return response; +} + +- (void) close { + close(_socket); + LOG_DEBUG(@"Did close connection on socket %i", _socket); +} + +@end diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 926157d..d537bd0 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -25,7 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#import "GCDWebServer.h" +#import "GCDWebServerConnection.h" #ifdef __LOGGING_HEADER__ @@ -85,8 +85,27 @@ extern "C" { #endif NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension); +NSString* GCDWebServerUnescapeURLString(NSString* string); NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form); #ifdef __cplusplus } #endif + +@interface GCDWebServerConnection () +- (id) initWithServer:(GCDWebServer*)server address:(NSData*)address socket:(CFSocketNativeHandle)socket; +@end + +@interface GCDWebServer () +@property(nonatomic, readonly) NSArray* handlers; +@end + +@interface GCDWebServerHandler : NSObject { +@private + GCDWebServerMatchBlock _matchBlock; + GCDWebServerProcessBlock _processBlock; +} +@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock; +@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock; +- (id) initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; +@end diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 421f034..3f6b133 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ E209F812169005AB00FF3062 /* GCDWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E209F80D169005AB00FF3062 /* GCDWebServer.m */; }; E209F813169005AB00FF3062 /* GCDWebServerRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E209F80F169005AB00FF3062 /* GCDWebServerRequest.m */; }; E209F814169005AB00FF3062 /* GCDWebServerResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E209F811169005AB00FF3062 /* GCDWebServerResponse.m */; }; + E22112441690B1D70048D2B2 /* GCDWebServerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E22112431690B1D70048D2B2 /* GCDWebServerConnection.m */; }; E2EE638D147DAE630004D40B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E2EE638C147DAE630004D40B /* main.m */; }; /* End PBXBuildFile section */ @@ -27,6 +28,8 @@ E209F80F169005AB00FF3062 /* GCDWebServerRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerRequest.m; sourceTree = ""; }; E209F810169005AB00FF3062 /* GCDWebServerResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerResponse.h; sourceTree = ""; }; E209F811169005AB00FF3062 /* GCDWebServerResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerResponse.m; sourceTree = ""; }; + E22112421690B1D70048D2B2 /* GCDWebServerConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerConnection.h; sourceTree = ""; }; + E22112431690B1D70048D2B2 /* GCDWebServerConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerConnection.m; sourceTree = ""; }; E2448DF616900A550069FA25 /* GCDWebServerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerPrivate.h; sourceTree = ""; }; E2EE638C147DAE630004D40B /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -77,6 +80,8 @@ children = ( E209F80C169005AB00FF3062 /* GCDWebServer.h */, E209F80D169005AB00FF3062 /* GCDWebServer.m */, + E22112421690B1D70048D2B2 /* GCDWebServerConnection.h */, + E22112431690B1D70048D2B2 /* GCDWebServerConnection.m */, E2448DF616900A550069FA25 /* GCDWebServerPrivate.h */, E209F80E169005AB00FF3062 /* GCDWebServerRequest.h */, E209F80F169005AB00FF3062 /* GCDWebServerRequest.m */, @@ -149,6 +154,7 @@ E209F812169005AB00FF3062 /* GCDWebServer.m in Sources */, E209F813169005AB00FF3062 /* GCDWebServerRequest.m in Sources */, E209F814169005AB00FF3062 /* GCDWebServerResponse.m in Sources */, + E22112441690B1D70048D2B2 /* GCDWebServerConnection.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };