diff --git a/CGDWebServer/GCDWebServer.h b/CGDWebServer/GCDWebServer.h new file mode 100644 index 0000000..76ba685 --- /dev/null +++ b/CGDWebServer/GCDWebServer.h @@ -0,0 +1,95 @@ +/* + 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 "GCDWebServerRequest.h" +#import "GCDWebServerResponse.h" + +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; + + NSUInteger _port; + NSRunLoop* _runLoop; + CFSocketRef _socket; + CFNetServiceRef _service; +} +@property(nonatomic, readonly, getter=isRunning) BOOL running; +@property(nonatomic, readonly) NSUInteger port; +- (void) addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; +- (void) removeAllHandlers; + +- (BOOL) start; // Default is main runloop, 8080 port and computer name +- (BOOL) startWithRunloop:(NSRunLoop*)runloop port:(NSUInteger)port bonjourName:(NSString*)name; // Pass nil name to disable Bonjour or empty string to use computer name +- (void) stop; +@end + +@interface GCDWebServer (Subclassing) ++ (Class) connectionClass; ++ (NSString*) serverName; // Default is class name +@end + +@interface GCDWebServer (Extensions) +- (BOOL) runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only) +@end + +@interface GCDWebServer (Handlers) +- (void) addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block; +- (void) addHandlerForBasePath:(NSString*)basePath localPath:(NSString*)localPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge; // Base path is recursive and case-sensitive +- (void) addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block; // Path is case-insensitive +- (void) addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive +@end diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m new file mode 100644 index 0000000..b8a6a90 --- /dev/null +++ b/CGDWebServer/GCDWebServer.m @@ -0,0 +1,888 @@ +/* + 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 +#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) { + static NSDictionary* _overrides = nil; + if (_overrides == nil) { + _overrides = [[NSDictionary alloc] initWithObjectsAndKeys: + @"text/css", @"css", + nil]; + } + NSString* mimeType = nil; + extension = [extension lowercaseString]; + if (extension.length) { + mimeType = [_overrides objectForKey:extension]; + if (mimeType == nil) { + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)extension, NULL); + if (uti) { + mimeType = [(id)UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType) autorelease]; + CFRelease(uti); + } + } + } + return mimeType; +} + +static NSString* _UnescapeURLString(NSString* string) { + return [(id)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), + kCFStringEncodingUTF8) autorelease]; +} + +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; + if (![scanner scanUpToString:@"&" intoString:&value]) { + break; + } + + key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + [parameters setObject:_UnescapeURLString(value) forKey:_UnescapeURLString(key)]; + + if ([scanner isAtEnd]) { + break; + } + [scanner setScanLocation:([scanner scanLocation] + 1)]; + } + [scanner release]; + return parameters; +} + +static void _SignalHandler(int signal) { + _run = NO; + printf("\n"); +} + +@implementation GCDWebServerHandler + +@synthesize matchBlock=_matchBlock, processBlock=_processBlock; + +- (id) initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock { + if ((self = [super init])) { + _matchBlock = Block_copy(matchBlock); + _processBlock = Block_copy(processBlock); + } + return self; +} + +- (void) dealloc { + Block_release(_matchBlock); + Block_release(_processBlock); + + [super dealloc]; +} + +@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); + } +} + +- (id) init { + if ((self = [super init])) { + _handlers = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void) dealloc { + if (_runLoop) { + [self stop]; + } + + [_handlers release]; + + [super dealloc]; +} + +- (void) addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock { + DCHECK(_runLoop == nil); + GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock]; + [_handlers insertObject:handler atIndex:0]; + [handler release]; +} + +- (void) removeAllHandlers { + DCHECK(_runLoop == nil); + [_handlers removeAllObjects]; +} + +- (BOOL) start { + return [self startWithRunloop:[NSRunLoop mainRunLoop] port:8080 bonjourName:@""]; +} + +static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) { + @autoreleasepool { + if (error->error) { + LOG_ERROR(@"Bonjour error %i (domain %i)", error->error, (int)error->domain); + } else { + LOG_VERBOSE(@"Registered Bonjour service \"%@\" with type '%@' on port %i", CFNetServiceGetName(service), CFNetServiceGetType(service), CFNetServiceGetPortNumber(service)); + } + } +} + +static void _SocketCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void* data, void* info) { + if (type == kCFSocketAcceptCallBack) { + CFSocketNativeHandle handle = *(CFSocketNativeHandle*)data; + int set = 1; + setsockopt(handle, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int)); // Make sure this socket cannot generate SIG_PIPE + @autoreleasepool { + Class class = [[(GCDWebServer*)info class] connectionClass]; + GCDWebServerConnection* connection = [[class alloc] initWithServer:(GCDWebServer*)info address:(NSData*)address socket:handle]; + [connection release]; // Connection will automatically retain itself while opened + } + } else { + DNOT_REACHED(); + } +} + +- (BOOL) startWithRunloop:(NSRunLoop*)runloop port:(NSUInteger)port bonjourName:(NSString*)name { + DCHECK(runloop); + DCHECK(port); + DCHECK(_runLoop == nil); + CFSocketContext context = {0, self, NULL, NULL, NULL}; + _socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, _SocketCallBack, &context); + if (_socket) { + int yes = 1; + setsockopt(CFSocketGetNative(_socket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + + 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 = htonl(INADDR_ANY); + if (CFSocketSetAddress(_socket, (CFDataRef)[NSData dataWithBytes:&addr4 length:sizeof(addr4)]) == kCFSocketSuccess) { + CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0); + CFRunLoopAddSource([runloop getCFRunLoop], source, kCFRunLoopCommonModes); + CFRelease(source); + + if (name) { + _service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (CFStringRef)name, port); + if (_service) { + CFNetServiceClientContext context = {0, self, NULL, NULL, NULL}; + CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context); + CFNetServiceScheduleWithRunLoop(_service, [runloop getCFRunLoop], kCFRunLoopCommonModes); + CFStreamError error = {0}; + CFNetServiceRegisterWithOptions(_service, 0, &error); + } else { + LOG_ERROR(@"Failed creating CFNetService"); + } + } + + _port = port; + _runLoop = [runloop retain]; + LOG_VERBOSE(@"%@ started on port %i", [self class], (int)port); + } else { + LOG_ERROR(@"Failed binding socket"); + CFRelease(_socket); + _socket = NULL; + } + } else { + LOG_ERROR(@"Failed creating CFSocket"); + } + return (_runLoop != nil ? YES : NO); +} + +- (BOOL) isRunning { + return (_runLoop != nil ? YES : NO); +} + +- (void) stop { + DCHECK(_runLoop != nil); + if (_socket) { + if (_service) { + CFNetServiceUnscheduleFromRunLoop(_service, [_runLoop getCFRunLoop], kCFRunLoopCommonModes); + CFNetServiceSetClient(_service, NULL, NULL); + CFRelease(_service); + } + + CFSocketInvalidate(_socket); + CFRelease(_socket); + _socket = NULL; + LOG_VERBOSE(@"%@ stopped", [self class]); + } + [_runLoop release]; + _runLoop = nil; + _port = 0; +} + +@end + +@implementation GCDWebServer (Subclassing) + ++ (Class) connectionClass { + return [GCDWebServerConnection class]; +} + ++ (NSString*) serverName { + return NSStringFromClass(self); +} + +@end + +@implementation GCDWebServer (Extensions) + +- (BOOL) runWithPort:(NSUInteger)port { + BOOL success = NO; + _run = YES; + void* handler = signal(SIGINT, _SignalHandler); + if (handler != SIG_ERR) { + if ([self startWithRunloop:[NSRunLoop currentRunLoop] port:port bonjourName:@""]) { + while (_run) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + } + [self stop]; + success = YES; + } + signal(SIGINT, handler); + } + return success; +} + +@end + +@implementation GCDWebServer (Handlers) + +- (void) addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block { + [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { + + return [[[class alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery] autorelease]; + + } processBlock:block]; +} + +- (GCDWebServerResponse*) _responseWithContentsOfFile:(NSString*)path { + return [GCDWebServerFileResponse responseWithFile:path]; +} + +- (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"]; + for (NSString* file in enumerator) { + if (![file hasPrefix:@"."]) { + NSString* type = [[enumerator fileAttributes] objectForKey:NSFileType]; + NSString* escapedFile = [file stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + 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) addHandlerForBasePath:(NSString*)basePath localPath:(NSString*)localPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge { + if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) { + [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] autorelease]; + + } processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) { + + GCDWebServerResponse* response = nil; + NSString* filePath = [localPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]]; + BOOL isDirectory; + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) { + if (isDirectory) { + if (indexFilename) { + NSString* indexPath = [filePath stringByAppendingPathComponent:indexFilename]; + if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath isDirectory:&isDirectory] && !isDirectory) { + return [self _responseWithContentsOfFile:indexPath]; + } + } + response = [self _responseWithContentsOfDirectory:filePath]; + } else { + response = [self _responseWithContentsOfFile:filePath]; + } + } + if (response) { + response.cacheControlMaxAge = cacheAge; + } else { + response = [GCDWebServerResponse responseWithStatusCode:404]; + } + return response; + + }]; + } else { + DNOT_REACHED(); + } +} + +- (void) addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block { + if ([path hasPrefix:@"/"] && [class 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 [[[class alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery] autorelease]; + + } processBlock:block]; + } else { + DNOT_REACHED(); + } +} + +- (void) addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block { + NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL]; + if (expression && [class isSubclassOfClass:[GCDWebServerRequest class]]) { + [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { + + if (![requestMethod isEqualToString:method]) { + return nil; + } + if ([expression firstMatchInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)] == nil) { + return nil; + } + return [[[class alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery] autorelease]; + + } processBlock:block]; + } else { + DNOT_REACHED(); + } +} + +@end diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h new file mode 100644 index 0000000..60a6641 --- /dev/null +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -0,0 +1,91 @@ +/* + 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" + +#ifdef __LOGGING_HEADER__ + +#import __LOGGING_HEADER__ + +#else + +static inline void __LogMessage(long level, NSString* format, ...) { + static const char* levelNames[] = {"DEBUG", "VERBOSE", "INFO", "WARNING", "ERROR", "EXCEPTION"}; + static long minLevel = -1; + if (minLevel < 0) { + const char* logLevel = getenv("logLevel"); + minLevel = logLevel ? atoi(logLevel) : 0; + } + if (level >= minLevel) { + va_list arguments; + va_start(arguments, format); + NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; + va_end(arguments); + printf("[%s] %s\n", levelNames[level], [message UTF8String]); + [message release]; + } +} + +#define LOG_VERBOSE(...) __LogMessage(1, __VA_ARGS__) +#define LOG_INFO(...) __LogMessage(2, __VA_ARGS__) +#define LOG_WARNING(...) __LogMessage(3, __VA_ARGS__) +#define LOG_ERROR(...) __LogMessage(4, __VA_ARGS__) +#define LOG_EXCEPTION(__EXCEPTION__) __LogMessage(5, @"%@", __EXCEPTION__) + +#ifdef NDEBUG + +#define DCHECK(__CONDITION__) +#define DNOT_REACHED() +#define LOG_DEBUG(...) + +#else + +#define DCHECK(__CONDITION__) \ + do { \ + if (!(__CONDITION__)) { \ + abort(); \ + } \ + } while (0) +#define DNOT_REACHED() abort() +#define LOG_DEBUG(...) __LogMessage(0, __VA_ARGS__) + +#endif + +#endif + +#define kGCDWebServerDefaultMimeType @"application/octet-stream" + +#ifdef __cplusplus +extern "C" { +#endif + +NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension); +NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form); + +#ifdef __cplusplus +} +#endif diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h new file mode 100644 index 0000000..c28e236 --- /dev/null +++ b/CGDWebServer/GCDWebServerRequest.h @@ -0,0 +1,125 @@ +/* + 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 + +@interface GCDWebServerRequest : NSObject { +@private + NSString* _method; + NSURL* _url; + NSDictionary* _headers; + NSString* _path; + NSDictionary* _query; + NSString* _type; + NSUInteger _length; +} +@property(nonatomic, readonly) NSString* method; +@property(nonatomic, readonly) NSURL* URL; +@property(nonatomic, readonly) NSDictionary* headers; +@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) +@property(nonatomic, readonly) NSUInteger contentLength; // Automatically parsed from headers +- (id) initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query; +- (BOOL) hasBody; // Convenience method +@end + +@interface GCDWebServerRequest (Subclassing) +- (BOOL) open; // Implementation required +- (NSInteger) write:(const void*)buffer maxLength:(NSUInteger)length; // Implementation required +- (BOOL) close; // Implementation required +@end + +@interface GCDWebServerDataRequest : GCDWebServerRequest { +@private + NSMutableData* _data; +} +@property(nonatomic, readonly) NSData* data; // Only valid after open / write / close sequence +@end + +@interface GCDWebServerFileRequest : GCDWebServerRequest { +@private + NSString* _filePath; + int _file; +} +@property(nonatomic, readonly) NSString* filePath; // Only valid after open / write / close sequence +@end + +@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest { +@private + NSDictionary* _arguments; +} +@property(nonatomic, readonly) NSDictionary* arguments; // Only valid after open / write / close sequence ++ (NSString*) mimeType; +@end + +@interface GCDWebServerMultiPart : NSObject { +@private + NSString* _contentType; + NSString* _mimeType; +} +@property(nonatomic, readonly) NSString* contentType; // May be nil +@property(nonatomic, readonly) NSString* mimeType; // Defaults to "text/plain" per specifications if undefined +@end + +@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart { +@private + NSData* _data; + NSString* _string; +} +@property(nonatomic, readonly) NSData* data; +@property(nonatomic, readonly) NSString* string; // May be nil (only valid for text mime types +@end + +@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart { +@private + NSString* _fileName; + NSString* _temporaryPath; +} +@property(nonatomic, readonly) NSString* fileName; // May be nil +@property(nonatomic, readonly) NSString* temporaryPath; +@end + +@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest { +@private + NSData* _boundary; + + NSUInteger _parserState; + NSMutableData* _parserData; + NSString* _controlName; + NSString* _fileName; + NSString* _contentType; + NSString* _tmpPath; + int _tmpFile; + + NSMutableDictionary* _arguments; + NSMutableDictionary* _files; +} +@property(nonatomic, readonly) NSDictionary* arguments; // Only valid after open / write / close sequence +@property(nonatomic, readonly) NSDictionary* files; // Only valid after open / write / close sequence ++ (NSString*) mimeType; +@end diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m new file mode 100644 index 0000000..cdc5bf0 --- /dev/null +++ b/CGDWebServer/GCDWebServerRequest.m @@ -0,0 +1,512 @@ +/* + 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 kMultiPartBufferSize (256 * 1024) + +enum { + kParserState_Undefined = 0, + kParserState_Start, + kParserState_Headers, + kParserState_Content, + kParserState_End +}; + +static NSData* _newlineData = nil; +static NSData* _newlinesData = nil; +static NSData* _dashNewlineData = nil; + +static NSString* _ExtractHeaderParameter(NSString* header, NSString* attribute) { + NSString* value = nil; + if (header) { + NSScanner* scanner = [[NSScanner alloc] initWithString:header]; + NSString* string = [NSString stringWithFormat:@"%@=", attribute]; + if ([scanner scanUpToString:string intoString:NULL]) { + [scanner scanString:string intoString:NULL]; + if ([scanner scanString:@"\"" intoString:NULL]) { + [scanner scanUpToString:@"\"" intoString:&value]; + } else { + [scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:&value]; + } + } + [scanner release]; + } + return value; +} + +// http://www.w3schools.com/tags/ref_charactersets.asp +static NSStringEncoding _StringEncodingFromCharset(NSString* charset) { + NSStringEncoding encoding = kCFStringEncodingInvalidId; + if (charset) { + encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)charset)); + } + return (encoding != kCFStringEncodingInvalidId ? encoding : NSUTF8StringEncoding); +} + +@implementation GCDWebServerRequest : NSObject + +@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length; + +- (id) initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { + if ((self = [super init])) { + _method = [method copy]; + _url = [url retain]; + _headers = [headers retain]; + _path = [path copy]; + _query = [query retain]; + + _type = [[_headers objectForKey:@"Content-Type"] retain]; + NSInteger length = [[_headers objectForKey:@"Content-Length"] integerValue]; + if (length < 0) { + DNOT_REACHED(); + [self release]; + return nil; + } + _length = length; + + if ((_length > 0) && (_type == nil)) { + _type = [kGCDWebServerDefaultMimeType copy]; + } + } + return self; +} + +- (void) dealloc { + [_method release]; + [_url release]; + [_headers release]; + [_path release]; + [_query release]; + [_type release]; + + [super dealloc]; +} + +- (BOOL) hasBody { + return _type ? YES : NO; +} + +@end + +@implementation GCDWebServerRequest (Subclassing) + +- (BOOL) open { + [self doesNotRecognizeSelector:_cmd]; + return NO; +} + +- (NSInteger) write:(const void*)buffer maxLength:(NSUInteger)length { + [self doesNotRecognizeSelector:_cmd]; + return -1; +} + +- (BOOL) close { + [self doesNotRecognizeSelector:_cmd]; + return NO; +} + +@end + +@implementation GCDWebServerDataRequest + +@synthesize data=_data; + +- (void) dealloc { + DCHECK(_data != nil); + [_data release]; + + [super dealloc]; +} + +- (BOOL) open { + DCHECK(_data == nil); + _data = [[NSMutableData alloc] initWithCapacity:self.contentLength]; + return _data ? YES : NO; +} + +- (NSInteger) write:(const void*)buffer maxLength:(NSUInteger)length { + DCHECK(_data != nil); + [_data appendBytes:buffer length:length]; + return length; +} + +- (BOOL) close { + DCHECK(_data != nil); + return YES; +} + +@end + +@implementation GCDWebServerFileRequest + +@synthesize filePath=_filePath; + +- (id) 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])) { + _filePath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]] retain]; + } + return self; +} + +- (void) dealloc { + DCHECK(_file < 0); + unlink([_filePath fileSystemRepresentation]); + [_filePath release]; + + [super dealloc]; +} + +- (BOOL) open { + DCHECK(_file == 0); + _file = open([_filePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + return (_file > 0 ? YES : NO); +} + +- (NSInteger) write:(const void*)buffer maxLength:(NSUInteger)length { + DCHECK(_file > 0); + return write(_file, buffer, length); +} + +- (BOOL) close { + DCHECK(_file > 0); + int result = close(_file); + _file = -1; + return (result == 0 ? YES : NO); +} + +@end + +@implementation GCDWebServerURLEncodedFormRequest + +@synthesize arguments=_arguments; + ++ (NSString*) mimeType { + return @"application/x-www-form-urlencoded"; +} + +- (void) dealloc { + [_arguments release]; + + [super dealloc]; +} + +- (BOOL) close { + if (![super close]) { + return NO; + } + + NSString* charset = _ExtractHeaderParameter(self.contentType, @"charset"); + NSString* string = [[NSString alloc] initWithData:self.data encoding:_StringEncodingFromCharset(charset)]; + _arguments = [GCDWebServerParseURLEncodedForm(string) retain]; + [string release]; + + return (_arguments ? YES : NO); +} + +@end + +@implementation GCDWebServerMultiPart + +@synthesize contentType=_contentType, mimeType=_mimeType; + +- (id) initWithContentType:(NSString*)contentType { + if ((self = [super init])) { + _contentType = [contentType copy]; + NSArray* components = [_contentType componentsSeparatedByString:@";"]; + if (components.count) { + _mimeType = [[[components objectAtIndex:0] lowercaseString] retain]; + } + if (_mimeType == nil) { + _mimeType = @"text/plain"; + } + } + return self; +} + +- (void) dealloc { + [_contentType release]; + [_mimeType release]; + + [super dealloc]; +} + +@end + +@implementation GCDWebServerMultiPartArgument + +@synthesize data=_data, string=_string; + +- (id) initWithContentType:(NSString*)contentType data:(NSData*)data { + if ((self = [super initWithContentType:contentType])) { + _data = [data retain]; + + if ([self.mimeType hasPrefix:@"text/"]) { + NSString* charset = _ExtractHeaderParameter(self.contentType, @"charset"); + _string = [[NSString alloc] initWithData:_data encoding:_StringEncodingFromCharset(charset)]; + } + } + return self; +} + +- (void) dealloc { + [_data release]; + [_string release]; + + [super dealloc]; +} + +- (NSString*) description { + return [NSString stringWithFormat:@"<%@ | '%@' | %i bytes>", [self class], self.mimeType, (int)_data.length]; +} + +@end + +@implementation GCDWebServerMultiPartFile + +@synthesize fileName=_fileName, temporaryPath=_temporaryPath; + +- (id) initWithContentType:(NSString*)contentType fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath { + if ((self = [super initWithContentType:contentType])) { + _fileName = [fileName copy]; + _temporaryPath = [temporaryPath copy]; + } + return self; +} + +- (void) dealloc { + unlink([_temporaryPath fileSystemRepresentation]); + + [_fileName release]; + [_temporaryPath release]; + + [super dealloc]; +} + +- (NSString*) description { + return [NSString stringWithFormat:@"<%@ | '%@' | '%@>'", [self class], self.mimeType, _fileName]; +} + +@end + +@implementation GCDWebServerMultiPartFormRequest + +@synthesize arguments=_arguments, files=_files; + ++ (void) initialize { + if (_newlineData == nil) { + _newlineData = [[NSData alloc] initWithBytes:"\r\n" length:2]; + DCHECK(_newlineData); + } + if (_newlinesData == nil) { + _newlinesData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; + DCHECK(_newlinesData); + } + if (_dashNewlineData == nil) { + _dashNewlineData = [[NSData alloc] initWithBytes:"--\r\n" length:4]; + DCHECK(_dashNewlineData); + } +} + ++ (NSString*) mimeType { + return @"multipart/form-data"; +} + +- (id) 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])) { + NSString* boundary = _ExtractHeaderParameter(self.contentType, @"boundary"); + if (boundary) { + _boundary = [[[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding] retain]; + } + if (_boundary == nil) { + DNOT_REACHED(); + [self release]; + return nil; + } + + _arguments = [[NSMutableDictionary alloc] init]; + _files = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL) open { + DCHECK(_parserData == nil); + _parserData = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize]; + _parserState = kParserState_Start; + return YES; +} + +// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4 +- (BOOL) _parseData { + BOOL success = YES; + + if (_parserState == kParserState_Headers) { + NSRange range = [_parserData rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _parserData.length)]; + if (range.location != NSNotFound) { + + [_controlName release]; + _controlName = nil; + [_fileName release]; + _fileName = nil; + [_contentType release]; + _contentType = nil; + [_tmpPath release]; + _tmpPath = nil; + CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true); + const char* temp = "GET / HTTP/1.0\r\n"; + CFHTTPMessageAppendBytes(message, (const UInt8*)temp, strlen(temp)); + CFHTTPMessageAppendBytes(message, _parserData.bytes, range.location + range.length); + if (CFHTTPMessageIsHeaderComplete(message)) { + NSString* controlName = nil; + NSString* fileName = nil; + NSDictionary* headers = [(id)CFHTTPMessageCopyAllHeaderFields(message) autorelease]; + NSString* contentDisposition = [headers objectForKey:@"Content-Disposition"]; + if ([[contentDisposition lowercaseString] hasPrefix:@"form-data;"]) { + controlName = _ExtractHeaderParameter(contentDisposition, @"name"); + fileName = _ExtractHeaderParameter(contentDisposition, @"filename"); + } + _controlName = [controlName copy]; + _fileName = [fileName copy]; + _contentType = [[headers objectForKey:@"Content-Type"] retain]; + } + CFRelease(message); + if (_controlName) { + 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 { + DNOT_REACHED(); + success = NO; + } + } + } else { + DNOT_REACHED(); + success = NO; + } + + [_parserData replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0]; + _parserState = kParserState_Content; + } + } + + if ((_parserState == kParserState_Start) || (_parserState == kParserState_Content)) { + NSRange range = [_parserData rangeOfData:_boundary options:0 range:NSMakeRange(0, _parserData.length)]; + if (range.location != NSNotFound) { + NSRange subRange = NSMakeRange(range.location + range.length, _parserData.length - range.location - range.length); + NSRange subRange1 = [_parserData rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange]; + NSRange subRange2 = [_parserData rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange]; + if ((subRange1.location != NSNotFound) || (subRange2.location != NSNotFound)) { + + if (_parserState == kParserState_Content) { + const void* dataBytes = _parserData.bytes; + NSUInteger dataLength = range.location - 2; + if (_tmpPath) { + int result = write(_tmpFile, dataBytes, dataLength); + if (result == dataLength) { + if (close(_tmpFile) == 0) { + _tmpFile = 0; + GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithContentType:_contentType fileName:_fileName temporaryPath:_tmpPath]; + [_files setObject:file forKey:_controlName]; + [file release]; + } else { + DNOT_REACHED(); + success = NO; + } + } else { + DNOT_REACHED(); + success = NO; + } + [_tmpPath release]; + _tmpPath = nil; + } else { + NSData* data = [[NSData alloc] initWithBytesNoCopy:(void*)dataBytes length:dataLength freeWhenDone:NO]; + GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithContentType:_contentType data:data]; + [_arguments setObject:argument forKey:_controlName]; + [argument release]; + [data release]; + } + } + + if (subRange1.location != NSNotFound) { + [_parserData replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0]; + _parserState = kParserState_Headers; + success = [self _parseData]; + } else { + _parserState = kParserState_End; + } + } + } else { + NSUInteger margin = 2 * _boundary.length; + if (_tmpPath && (_parserData.length > margin)) { + NSUInteger length = _parserData.length - margin; + int result = write(_tmpFile, _parserData.bytes, length); + if (result == length) { + [_parserData replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0]; + } else { + DNOT_REACHED(); + success = NO; + } + } + } + } + return success; +} + +- (NSInteger) write:(const void*)buffer maxLength:(NSUInteger)length { + DCHECK(_parserData != nil); + [_parserData appendBytes:buffer length:length]; + return ([self _parseData] ? length : -1); +} + +- (BOOL) close { + DCHECK(_parserData != nil); + [_parserData release]; + _parserData = nil; + [_controlName release]; + [_fileName release]; + [_contentType release]; + if (_tmpFile > 0) { + close(_tmpFile); + unlink([_tmpPath fileSystemRepresentation]); + } + [_tmpPath release]; + return (_parserState == kParserState_End ? YES : NO); +} + +- (void) dealloc { + DCHECK(_parserData == nil); + [_arguments release]; + [_files release]; + [_boundary release]; + + [super dealloc]; +} + +@end diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h new file mode 100644 index 0000000..7536948 --- /dev/null +++ b/CGDWebServer/GCDWebServerResponse.h @@ -0,0 +1,90 @@ +/* + 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 + +@interface GCDWebServerResponse : NSObject { +@private + NSString* _type; + NSUInteger _length; + NSInteger _status; + NSUInteger _maxAge; + NSMutableDictionary* _headers; +} +@property(nonatomic, readonly) NSString* contentType; +@property(nonatomic, readonly) NSUInteger contentLength; +@property(nonatomic) NSInteger statusCode; // Default is 200 +@property(nonatomic) NSUInteger cacheControlMaxAge; // Default is 0 seconds i.e. "no-cache" +@property(nonatomic, readonly) NSDictionary* additionalHeaders; ++ (GCDWebServerResponse*) response; +- (id) init; +- (id) initWithContentType:(NSString*)type contentLength:(NSUInteger)length; // Pass nil contentType to indicate empty body +- (void) setValue:(NSString*)value forAdditionalHeader:(NSString*)header; +- (BOOL) hasBody; // Convenience method +@end + +@interface GCDWebServerResponse (Subclassing) +- (BOOL) open; // Implementation required +- (NSInteger) read:(void*)buffer maxLength:(NSUInteger)length; // Implementation required +- (BOOL) close; // Implementation required +@end + +@interface GCDWebServerResponse (Extensions) ++ (GCDWebServerResponse*) responseWithStatusCode:(NSInteger)statusCode; ++ (GCDWebServerResponse*) responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent; +- (id) initWithStatusCode:(NSInteger)statusCode; +- (id) initWithRedirect:(NSURL*)location permanent:(BOOL)permanent; +@end + +@interface GCDWebServerDataResponse : GCDWebServerResponse { +@private + NSData* _data; + NSInteger _offset; +} ++ (GCDWebServerDataResponse*) responseWithData:(NSData*)data contentType:(NSString*)type; +- (id) initWithData:(NSData*)data contentType:(NSString*)type; +@end + +@interface GCDWebServerDataResponse (Extensions) ++ (GCDWebServerDataResponse*) responseWithText:(NSString*)text; ++ (GCDWebServerDataResponse*) responseWithHTML:(NSString*)html; ++ (GCDWebServerDataResponse*) responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; // Simple template system that replaces all occurences of "%variable%" with corresponding value (encodes using UTF-8) +- (id) initWithText:(NSString*)text; // Encodes using UTF-8 +- (id) initWithHTML:(NSString*)html; // Encodes using UTF-8 +- (id) initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; +@end + +@interface GCDWebServerFileResponse : GCDWebServerResponse { +@private + NSString* _path; + int _file; +} ++ (GCDWebServerFileResponse*) responseWithFile:(NSString*)path; ++ (GCDWebServerFileResponse*) responseWithFile:(NSString*)path isAttachment:(BOOL)attachment; +- (id) initWithFile:(NSString*)path; +- (id) initWithFile:(NSString*)path isAttachment:(BOOL)attachment; +@end diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m new file mode 100644 index 0000000..4fde312 --- /dev/null +++ b/CGDWebServer/GCDWebServerResponse.m @@ -0,0 +1,286 @@ +/* + 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 + +#import "GCDWebServerPrivate.h" + +@implementation GCDWebServerResponse + +@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, additionalHeaders=_headers; + ++ (GCDWebServerResponse*) response { + return [[[[self class] alloc] init] autorelease]; +} + +- (id) init { + return [self initWithContentType:nil contentLength:0]; +} + +- (id) initWithContentType:(NSString*)type contentLength:(NSUInteger)length { + if ((self = [super init])) { + _type = [type copy]; + _length = length; + _status = 200; + _maxAge = 0; + _headers = [[NSMutableDictionary alloc] init]; + + if ((_length > 0) && (_type == nil)) { + _type = [kGCDWebServerDefaultMimeType copy]; + } + } + return self; +} + +- (void) dealloc { + [_type release]; + [_headers release]; + + [super dealloc]; +} + +- (void) setValue:(NSString*)value forAdditionalHeader:(NSString*)header { + [_headers setValue:value forKey:header]; +} + +- (BOOL) hasBody { + return _type ? YES : NO; +} + +@end + +@implementation GCDWebServerResponse (Subclassing) + +- (BOOL) open { + [self doesNotRecognizeSelector:_cmd]; + return NO; +} + +- (NSInteger) read:(void*)buffer maxLength:(NSUInteger)length { + [self doesNotRecognizeSelector:_cmd]; + return -1; +} + +- (BOOL) close { + [self doesNotRecognizeSelector:_cmd]; + return NO; +} + +@end + +@implementation GCDWebServerResponse (Extensions) + ++ (GCDWebServerResponse*) responseWithStatusCode:(NSInteger)statusCode { + return [[[self alloc] initWithStatusCode:statusCode] autorelease]; +} + ++ (GCDWebServerResponse*) responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent { + return [[[self alloc] initWithRedirect:location permanent:permanent] autorelease]; +} + +- (id) initWithStatusCode:(NSInteger)statusCode { + if ((self = [self initWithContentType:nil contentLength:0])) { + self.statusCode = statusCode; + } + return self; +} + +- (id) initWithRedirect:(NSURL*)location permanent:(BOOL)permanent { + if ((self = [self initWithContentType:nil contentLength:0])) { + self.statusCode = permanent ? 301 : 307; + [self setValue:[location absoluteString] forAdditionalHeader:@"Location"]; + } + return self; +} + +@end + +@implementation GCDWebServerDataResponse + ++ (GCDWebServerDataResponse*) responseWithData:(NSData*)data contentType:(NSString*)type { + return [[[[self class] alloc] initWithData:data contentType:type] autorelease]; +} + +- (id) initWithData:(NSData*)data contentType:(NSString*)type { + if (data == nil) { + DNOT_REACHED(); + [self release]; + return nil; + } + + if ((self = [super initWithContentType:type contentLength:data.length])) { + _data = [data retain]; + _offset = -1; + } + return self; +} + +- (void) dealloc { + DCHECK(_offset < 0); + [_data release]; + + [super dealloc]; +} + +- (BOOL) open { + DCHECK(_offset < 0); + _offset = 0; + return YES; +} + +- (NSInteger) read:(void*)buffer maxLength:(NSUInteger)length { + DCHECK(_offset >= 0); + NSInteger size = 0; + if (_offset < _data.length) { + size = MIN(_data.length - _offset, length); + bcopy((char*)_data.bytes + _offset, buffer, size); + _offset += size; + } + return size; +} + +- (BOOL) close { + DCHECK(_offset >= 0); + _offset = -1; + return YES; +} + +@end + +@implementation GCDWebServerDataResponse (Extensions) + ++ (GCDWebServerDataResponse*) responseWithText:(NSString*)text { + return [[[self alloc] initWithText:text] autorelease]; +} + ++ (GCDWebServerDataResponse*) responseWithHTML:(NSString*)html { + return [[[self alloc] initWithHTML:html] autorelease]; +} + ++ (GCDWebServerDataResponse*) responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables { + return [[[self alloc] initWithHTMLTemplate:path variables:variables] autorelease]; +} + +- (id) initWithText:(NSString*)text { + NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding]; + if (data == nil) { + DNOT_REACHED(); + [self release]; + return nil; + } + return [self initWithData:data contentType:@"text/plain; charset=utf-8"]; +} + +- (id) initWithHTML:(NSString*)html { + NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding]; + if (data == nil) { + DNOT_REACHED(); + [self release]; + return nil; + } + return [self initWithData:data contentType:@"text/html; charset=utf-8"]; +} + +- (id) 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)]; + }]; + id response = [self initWithHTML:html]; + [html release]; + return response; +} + +@end + +@implementation GCDWebServerFileResponse + ++ (GCDWebServerFileResponse*) responseWithFile:(NSString*)path { + return [[[[self class] alloc] initWithFile:path] autorelease]; +} + ++ (GCDWebServerFileResponse*) responseWithFile:(NSString*)path isAttachment:(BOOL)attachment { + return [[[[self class] alloc] initWithFile:path isAttachment:attachment] autorelease]; +} + +- (id) initWithFile:(NSString*)path { + return [self initWithFile:path isAttachment:NO]; +} + +- (id) initWithFile:(NSString*)path isAttachment:(BOOL)attachment { + struct stat info; + if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) { + DNOT_REACHED(); + [self release]; + return nil; + } + NSString* type = GCDWebServerGetMimeTypeForExtension([path pathExtension]); + if (type == nil) { + type = kGCDWebServerDefaultMimeType; + } + + if ((self = [super initWithContentType:type contentLength:info.st_size])) { + _path = [path copy]; + if (attachment) { + NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES]; // ISO 8859-1 + NSString* fileName = data ? [[[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] autorelease] : nil; + if (fileName) { + [self setValue:[NSString stringWithFormat:@"attachment; filename=\"%@\"", fileName] forAdditionalHeader:@"Content-Disposition"]; // TODO: Use http://tools.ietf.org/html/rfc5987 + } else { + DNOT_REACHED(); + } + } + } + return self; +} + +- (void) dealloc { + DCHECK(_file <= 0); + [_path release]; + + [super dealloc]; +} + +- (BOOL) open { + DCHECK(_file <= 0); + _file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY); + return (_file > 0 ? YES : NO); +} + +- (NSInteger) read:(void*)buffer maxLength:(NSUInteger)length { + DCHECK(_file > 0); + return read(_file, buffer, length); +} + +- (BOOL) close { + DCHECK(_file > 0); + int result = close(_file); + _file = 0; + return (result == 0 ? YES : NO); +} + +@end diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj new file mode 100644 index 0000000..421f034 --- /dev/null +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -0,0 +1,222 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + E208D143167B723200500836 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E208D142167B723200500836 /* libsqlite3.dylib */; }; + E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E208D148167B76B700500836 /* CFNetwork.framework */; }; + E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E208D1B2167BB17E00500836 /* CoreServices.framework */; }; + 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 */; }; + E2EE638D147DAE630004D40B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E2EE638C147DAE630004D40B /* main.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8DD76FB20486AB0100D96B5E /* GCDWebServer */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = GCDWebServer; sourceTree = BUILT_PRODUCTS_DIR; }; + E208D142167B723200500836 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; + E208D148167B76B700500836 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + E208D1B2167BB17E00500836 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; + E209F80C169005AB00FF3062 /* GCDWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServer.h; sourceTree = ""; }; + E209F80D169005AB00FF3062 /* GCDWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServer.m; sourceTree = ""; }; + E209F80E169005AB00FF3062 /* GCDWebServerRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerRequest.h; sourceTree = ""; }; + 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 = ""; }; + 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 */ + +/* Begin PBXFrameworksBuildPhase section */ + 8DD76FAD0486AB0100D96B5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */, + E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */, + E208D143167B723200500836 /* libsqlite3.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 08FB7794FE84155DC02AAC07 /* LittleCMS */ = { + isa = PBXGroup; + children = ( + 08FB7795FE84155DC02AAC07 /* Source */, + E282F1A7150FF0630004D7C0 /* Frameworks and Libraries */, + 1AB674ADFE9D54B511CA2CBB /* Products */, + ); + name = LittleCMS; + sourceTree = ""; + }; + 08FB7795FE84155DC02AAC07 /* Source */ = { + isa = PBXGroup; + children = ( + E2EE638C147DAE630004D40B /* main.m */, + E209F80B169005AB00FF3062 /* CGDWebServer */, + ); + name = Source; + sourceTree = ""; + }; + 1AB674ADFE9D54B511CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8DD76FB20486AB0100D96B5E /* GCDWebServer */, + ); + name = Products; + sourceTree = ""; + }; + E209F80B169005AB00FF3062 /* CGDWebServer */ = { + isa = PBXGroup; + children = ( + E209F80C169005AB00FF3062 /* GCDWebServer.h */, + E209F80D169005AB00FF3062 /* GCDWebServer.m */, + E2448DF616900A550069FA25 /* GCDWebServerPrivate.h */, + E209F80E169005AB00FF3062 /* GCDWebServerRequest.h */, + E209F80F169005AB00FF3062 /* GCDWebServerRequest.m */, + E209F810169005AB00FF3062 /* GCDWebServerResponse.h */, + E209F811169005AB00FF3062 /* GCDWebServerResponse.m */, + ); + path = CGDWebServer; + sourceTree = ""; + }; + E282F1A7150FF0630004D7C0 /* Frameworks and Libraries */ = { + isa = PBXGroup; + children = ( + E208D1B2167BB17E00500836 /* CoreServices.framework */, + E208D148167B76B700500836 /* CFNetwork.framework */, + E208D142167B723200500836 /* libsqlite3.dylib */, + ); + name = "Frameworks and Libraries"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8DD76FA90486AB0100D96B5E /* GCDWebServer */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB928508733DD80010E9CD /* Build configuration list for PBXNativeTarget "GCDWebServer" */; + buildPhases = ( + 8DD76FAB0486AB0100D96B5E /* Sources */, + 8DD76FAD0486AB0100D96B5E /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GCDWebServer; + productInstallPath = "$(HOME)/bin"; + productName = LittleCMS; + productReference = 8DD76FB20486AB0100D96B5E /* GCDWebServer */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 08FB7793FE84155DC02AAC07 /* Project object */ = { + isa = PBXProject; + buildConfigurationList = 1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 1; + knownRegions = ( + English, + Japanese, + French, + German, + ); + mainGroup = 08FB7794FE84155DC02AAC07 /* LittleCMS */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8DD76FA90486AB0100D96B5E /* GCDWebServer */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 8DD76FAB0486AB0100D96B5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E2EE638D147DAE630004D40B /* main.m in Sources */, + E209F812169005AB00FF3062 /* GCDWebServer.m in Sources */, + E209F813169005AB00FF3062 /* GCDWebServerRequest.m in Sources */, + E209F814169005AB00FF3062 /* GCDWebServerResponse.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1DEB928608733DD80010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = GCDWebServer; + }; + name = Debug; + }; + 1DEB928708733DD80010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = GCDWebServer; + }; + name = Release; + }; + 1DEB928A08733DD80010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; + GCC_OPTIMIZATION_LEVEL = 0; + MACOSX_DEPLOYMENT_TARGET = 10.7; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + WARNING_CFLAGS = "-Wall"; + }; + name = Debug; + }; + 1DEB928B08733DD80010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; + GCC_PREPROCESSOR_DEFINITIONS = ( + NDEBUG, + NS_BLOCK_ASSERTIONS, + ); + MACOSX_DEPLOYMENT_TARGET = 10.7; + SDKROOT = macosx; + WARNING_CFLAGS = "-Wall"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1DEB928508733DD80010E9CD /* Build configuration list for PBXNativeTarget "GCDWebServer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB928608733DD80010E9CD /* Debug */, + 1DEB928708733DD80010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB928A08733DD80010E9CD /* Debug */, + 1DEB928B08733DD80010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 08FB7793FE84155DC02AAC07 /* Project object */; +} diff --git a/main.m b/main.m new file mode 100644 index 0000000..96f3813 --- /dev/null +++ b/main.m @@ -0,0 +1,39 @@ +/* + 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" + +int main(int argc, const char* argv[]) { + BOOL success = NO; + @autoreleasepool { + GCDWebServer* webServer = [[GCDWebServer alloc] init]; + [webServer addHandlerForBasePath:@"/" localPath:NSHomeDirectory() indexFilename:nil cacheAge:0]; + success = [webServer runWithPort:8080]; + [webServer release]; + } + return success ? 0 : -1; +}