Bläddra i källkod

Initial import

Pierre-Olivier Latour 12 år sedan
förälder
incheckning
9219c52be8

+ 95 - 0
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 <organization> 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 <COPYRIGHT HOLDER> 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

+ 888 - 0
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 <organization> 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 <COPYRIGHT HOLDER> 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 <TargetConditionals.h>
+#if TARGET_OS_IPHONE
+#import <CFNetwork/CFNetwork.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+#else
+#import <ApplicationServices/ApplicationServices.h>
+#import <CoreServices/CoreServices.h>
+#endif
+#import <sys/fcntl.h>
+#import <sys/stat.h>
+#import <netinet/in.h>
+
+#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:@"<html><body>\n"];
+  [html appendString:@"<ul>\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:@"<li><a href=\"%@\">%@</a></li>\n", escapedFile, file];
+      } else if ([type isEqualToString:NSFileTypeDirectory]) {
+        [html appendFormat:@"<li><a href=\"%@/\">%@/</a></li>\n", escapedFile, file];
+      }
+    }
+    [enumerator skipDescendents];
+  }
+  [html appendString:@"</ul>\n"];
+  [html appendString:@"</body></html>\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

+ 91 - 0
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 <organization> 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 <COPYRIGHT HOLDER> 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

+ 125 - 0
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 <organization> 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 <COPYRIGHT HOLDER> 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 <Foundation/Foundation.h>
+
+@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

+ 512 - 0
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 <organization> 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 <COPYRIGHT HOLDER> 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

+ 90 - 0
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 <organization> 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 <COPYRIGHT HOLDER> 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 <Foundation/Foundation.h>
+
+@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

+ 286 - 0
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 <organization> 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 <COPYRIGHT HOLDER> 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 <sys/stat.h>
+
+#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

+ 222 - 0
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 = "<group>"; };
+		E209F80D169005AB00FF3062 /* GCDWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServer.m; sourceTree = "<group>"; };
+		E209F80E169005AB00FF3062 /* GCDWebServerRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerRequest.h; sourceTree = "<group>"; };
+		E209F80F169005AB00FF3062 /* GCDWebServerRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerRequest.m; sourceTree = "<group>"; };
+		E209F810169005AB00FF3062 /* GCDWebServerResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerResponse.h; sourceTree = "<group>"; };
+		E209F811169005AB00FF3062 /* GCDWebServerResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerResponse.m; sourceTree = "<group>"; };
+		E2448DF616900A550069FA25 /* GCDWebServerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerPrivate.h; sourceTree = "<group>"; };
+		E2EE638C147DAE630004D40B /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+/* 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 = "<group>";
+		};
+		08FB7795FE84155DC02AAC07 /* Source */ = {
+			isa = PBXGroup;
+			children = (
+				E2EE638C147DAE630004D40B /* main.m */,
+				E209F80B169005AB00FF3062 /* CGDWebServer */,
+			);
+			name = Source;
+			sourceTree = "<group>";
+		};
+		1AB674ADFE9D54B511CA2CBB /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				8DD76FB20486AB0100D96B5E /* GCDWebServer */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		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 = "<group>";
+		};
+		E282F1A7150FF0630004D7C0 /* Frameworks and Libraries */ = {
+			isa = PBXGroup;
+			children = (
+				E208D1B2167BB17E00500836 /* CoreServices.framework */,
+				E208D148167B76B700500836 /* CFNetwork.framework */,
+				E208D142167B723200500836 /* libsqlite3.dylib */,
+			);
+			name = "Frameworks and Libraries";
+			sourceTree = "<group>";
+		};
+/* 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 */;
+}

+ 39 - 0
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 <organization> 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 <COPYRIGHT HOLDER> 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;
+}