浏览代码

Merge branch 'v2'

Pierre-Olivier Latour 11 年之前
父节点
当前提交
a3996f3fbf
共有 33 个文件被更改,包括 3637 次插入1210 次删除
  1. 9 0
      CGDWebServer/GCDWebServer.h
  2. 137 4
      CGDWebServer/GCDWebServer.m
  3. 3 1
      CGDWebServer/GCDWebServerConnection.h
  4. 297 115
      CGDWebServer/GCDWebServerConnection.m
  5. 37 0
      CGDWebServer/GCDWebServerDataRequest.h
  6. 109 0
      CGDWebServer/GCDWebServerDataRequest.m
  7. 46 0
      CGDWebServer/GCDWebServerDataResponse.h
  8. 150 0
      CGDWebServer/GCDWebServerDataResponse.m
  9. 41 0
      CGDWebServer/GCDWebServerErrorResponse.h
  10. 125 0
      CGDWebServer/GCDWebServerErrorResponse.m
  11. 32 0
      CGDWebServer/GCDWebServerFileRequest.h
  12. 90 0
      CGDWebServer/GCDWebServerFileRequest.m
  13. 39 0
      CGDWebServer/GCDWebServerFileResponse.h
  14. 179 0
      CGDWebServer/GCDWebServerFileResponse.m
  15. 101 0
      CGDWebServer/GCDWebServerHTTPStatusCodes.h
  16. 49 0
      CGDWebServer/GCDWebServerMultiPartFormRequest.h
  17. 388 0
      CGDWebServer/GCDWebServerMultiPartFormRequest.m
  18. 42 0
      CGDWebServer/GCDWebServerPrivate.h
  19. 15 45
      CGDWebServer/GCDWebServerRequest.h
  20. 171 470
      CGDWebServer/GCDWebServerRequest.m
  21. 20 54
      CGDWebServer/GCDWebServerResponse.h
  22. 183 335
      CGDWebServer/GCDWebServerResponse.m
  23. 35 0
      CGDWebServer/GCDWebServerStreamingResponse.h
  24. 67 0
      CGDWebServer/GCDWebServerStreamingResponse.m
  25. 33 0
      CGDWebServer/GCDWebServerURLEncodedFormRequest.h
  26. 73 0
      CGDWebServer/GCDWebServerURLEncodedFormRequest.m
  27. 58 0
      GCDWebDAVServer/GCDWebDAVServer.h
  28. 689 0
      GCDWebDAVServer/GCDWebDAVServer.m
  29. 106 1
      GCDWebServer.xcodeproj/project.pbxproj
  30. 3 1
      GCDWebUploader/GCDWebUploader.h
  31. 225 174
      GCDWebUploader/GCDWebUploader.m
  32. 40 2
      Mac/main.m
  33. 45 8
      README.md

+ 9 - 0
CGDWebServer/GCDWebServer.h

@@ -51,6 +51,7 @@ NSString* GCDWebServerGetPrimaryIPv4Address();  // Returns IPv4 address of prima
 @property(nonatomic, readonly, getter=isRunning) BOOL running;
 @property(nonatomic, readonly) NSUInteger port;
 @property(nonatomic, readonly) NSString* bonjourName;  // Only non-nil if Bonjour registration is active
+- (instancetype)init;
 - (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
 - (void)removeAllHandlers;
 
@@ -62,6 +63,7 @@ NSString* GCDWebServerGetPrimaryIPv4Address();  // Returns IPv4 address of prima
 @interface GCDWebServer (Subclassing)
 + (Class)connectionClass;
 + (NSString*)serverName;  // Default is class name
++ (BOOL)shouldAutomaticallyMapHEADToGET;  // Default is YES which means HEAD requests are mapped to GET requests with the response body being discarded
 @end
 
 @interface GCDWebServer (Extensions)
@@ -83,3 +85,10 @@ NSString* GCDWebServerGetPrimaryIPv4Address();  // Returns IPv4 address of prima
 - (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests;  // Path is case-insensitive
 - (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests;  // Base path is recursive and case-sensitive
 @end
+
+@interface GCDWebServer (Logging)
+- (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
+- (void)logInfo:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
+- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
+- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
+@end

+ 137 - 4
CGDWebServer/GCDWebServer.m

@@ -63,6 +63,8 @@
 }
 @end
 
+static NSDateFormatter* _dateFormatterRFC822 = nil;
+static dispatch_queue_t _dateFormatterQueue = NULL;
 #if !TARGET_OS_IPHONE
 static BOOL _run;
 #endif
@@ -88,6 +90,78 @@ void GCDLogMessage(long level, NSString* format, ...) {
 
 #endif
 
+NSString* GCDWebServerNormalizeHeaderValue(NSString* value) {
+  if (value) {
+    NSRange range = [value rangeOfString:@";"];  // Assume part before ";" separator is case-insensitive
+    if (range.location != NSNotFound) {
+      value = [[[value substringToIndex:range.location] lowercaseString] stringByAppendingString:[value substringFromIndex:range.location]];
+    } else {
+      value = [value lowercaseString];
+    }
+  }
+  return value;
+}
+
+NSString* GCDWebServerTruncateHeaderValue(NSString* value) {
+  DCHECK([value isEqualToString:GCDWebServerNormalizeHeaderValue(value)]);
+  NSRange range = [value rangeOfString:@";"];
+  return range.location != NSNotFound ? [value substringToIndex:range.location] : value;
+}
+
+NSString* GCDWebServerExtractHeaderValueParameter(NSString* value, NSString* name) {
+  DCHECK([value isEqualToString:GCDWebServerNormalizeHeaderValue(value)]);
+  NSString* parameter = nil;
+  NSScanner* scanner = [[NSScanner alloc] initWithString:value];
+  [scanner setCaseSensitive:NO];  // Assume parameter names are case-insensitive
+  NSString* string = [NSString stringWithFormat:@"%@=", name];
+  if ([scanner scanUpToString:string intoString:NULL]) {
+    [scanner scanString:string intoString:NULL];
+    if ([scanner scanString:@"\"" intoString:NULL]) {
+      [scanner scanUpToString:@"\"" intoString:&parameter];
+    } else {
+      [scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:&parameter];
+    }
+  }
+  ARC_RELEASE(scanner);
+  return parameter;
+}
+
+// http://www.w3schools.com/tags/ref_charactersets.asp
+NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset) {
+  NSStringEncoding encoding = kCFStringEncodingInvalidId;
+  if (charset) {
+    encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)charset));
+  }
+  return (encoding != kCFStringEncodingInvalidId ? encoding : NSUTF8StringEncoding);
+}
+
+NSString* GCDWebServerFormatHTTPDate(NSDate* date) {
+  __block NSString* string;
+  dispatch_sync(_dateFormatterQueue, ^{
+    string = [_dateFormatterRFC822 stringFromDate:date];  // HTTP/1.1 server must use RFC822
+  });
+  return string;
+}
+
+NSDate* GCDWebServerParseHTTPDate(NSString* string) {
+  __block NSDate* date;
+  dispatch_sync(_dateFormatterQueue, ^{
+    date = [_dateFormatterRFC822 dateFromString:string];  // TODO: Handle RFC 850 and ANSI C's asctime() format (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3)
+  });
+  return date;
+}
+
+NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType) {
+  if ([contentType hasPrefix:@"text/"] || [contentType isEqualToString:@"application/json"] || [contentType isEqualToString:@"application/xml"]) {
+    NSString* charset = GCDWebServerExtractHeaderValueParameter(contentType, @"charset");
+    NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)];
+    if (string) {
+      return ARC_AUTORELEASE(string);
+    }
+  }
+  return [NSString stringWithFormat:@"<%lu bytes>", (unsigned long)data.length];
+}
+
 NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) {
   static NSDictionary* _overrides = nil;
   if (_overrides == nil) {
@@ -118,6 +192,7 @@ NSString* GCDWebServerUnescapeURLString(NSString* string) {
   return ARC_BRIDGE_RELEASE(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
 }
 
+// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
 NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
   NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
   NSScanner* scanner = [[NSScanner alloc] initWithString:form];
@@ -231,10 +306,21 @@ static void _SignalHandler(int signal) {
 @synthesize handlers=_handlers, port=_port;
 
 + (void)initialize {
-  [GCDWebServerConnection class];  // Initialize class immediately to make sure it happens on the main thread
+  if (_dateFormatterRFC822 == nil) {
+    DCHECK([NSThread isMainThread]);  // NSDateFormatter should be initialized on main thread
+    _dateFormatterRFC822 = [[NSDateFormatter alloc] init];
+    _dateFormatterRFC822.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
+    _dateFormatterRFC822.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
+    _dateFormatterRFC822.locale = ARC_AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]);
+    DCHECK(_dateFormatterRFC822);
+  }
+  if (_dateFormatterQueue == NULL) {
+    _dateFormatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
+    DCHECK(_dateFormatterQueue);
+  }
 }
 
-- (id)init {
+- (instancetype)init {
   if ((self = [super init])) {
     _handlers = [[NSMutableArray alloc] init];
   }
@@ -424,6 +510,10 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
   return NSStringFromClass(self);
 }
 
++ (BOOL)shouldAutomaticallyMapHEADToGET {
+  return YES;
+}
+
 @end
 
 @implementation GCDWebServer (Extensions)
@@ -484,6 +574,9 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
 - (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
   [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
     
+    if (![requestMethod isEqualToString:method]) {
+      return nil;
+    }
     return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
     
   } processBlock:block];
@@ -586,7 +679,7 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
 - (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests {
   if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) {
 #if __has_feature(objc_arc)
-    __unsafe_unretained GCDWebServer* server = self;
+    GCDWebServer* __unsafe_unretained server = self;
 #else
     __block GCDWebServer* server = self;
 #endif
@@ -626,7 +719,7 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
       if (response) {
         response.cacheControlMaxAge = cacheAge;
       } else {
-        response = [GCDWebServerResponse responseWithStatusCode:404];
+        response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NotFound];
       }
       return response;
       
@@ -637,3 +730,43 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
 }
 
 @end
+
+@implementation GCDWebServer (Logging)
+
+- (void)logVerbose:(NSString*)format, ... {
+  va_list arguments;
+  va_start(arguments, format);
+  NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
+  va_end(arguments);
+  LOG_VERBOSE(@"%@", message);
+  ARC_RELEASE(message);
+}
+
+- (void)logInfo:(NSString*)format, ... {
+  va_list arguments;
+  va_start(arguments, format);
+  NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
+  va_end(arguments);
+  LOG_INFO(@"%@", message);
+  ARC_RELEASE(message);
+}
+
+- (void)logWarning:(NSString*)format, ... {
+  va_list arguments;
+  va_start(arguments, format);
+  NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
+  va_end(arguments);
+  LOG_WARNING(@"%@", message);
+  ARC_RELEASE(message);
+}
+
+- (void)logError:(NSString*)format, ... {
+  va_list arguments;
+  va_start(arguments, format);
+  NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
+  va_end(arguments);
+  LOG_ERROR(@"%@", message);
+  ARC_RELEASE(message);
+}
+
+@end

+ 3 - 1
CGDWebServer/GCDWebServerConnection.h

@@ -43,6 +43,8 @@
 - (void)open;
 - (void)didUpdateBytesRead;  // Called from arbitrary thread after @totalBytesRead is updated - Default implementation does nothing
 - (void)didUpdateBytesWritten;  // Called from arbitrary thread after @totalBytesWritten is updated - Default implementation does nothing
-- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block;
+- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block;  // Only called if the request can be processed
+- (GCDWebServerResponse*)replaceResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request;  // Default implementation replaces any response matching the "ETag" or "Last-Modified-Date" header of the request by a barebone "Not-Modified" (304) one
+- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode;  // If request headers was malformed, "request" will be nil
 - (void)close;
 @end

+ 297 - 115
CGDWebServer/GCDWebServerConnection.m

@@ -30,7 +30,6 @@
 #import "GCDWebServerPrivate.h"
 
 #define kHeadersReadBuffer 1024
-#define kBodyWriteBufferSize (32 * 1024)
 
 typedef void (^ReadBufferCompletionBlock)(dispatch_data_t buffer);
 typedef void (^ReadDataCompletionBlock)(NSData* data);
@@ -42,10 +41,10 @@ typedef void (^WriteDataCompletionBlock)(BOOL success);
 typedef void (^WriteHeadersCompletionBlock)(BOOL success);
 typedef void (^WriteBodyCompletionBlock)(BOOL success);
 
-static NSData* _separatorData = nil;
+static NSData* _CRLFData = nil;
+static NSData* _CRLFCRLFData = nil;
 static NSData* _continueData = nil;
-static NSDateFormatter* _dateFormatter = nil;
-static dispatch_queue_t _formatterQueue = NULL;
+static NSData* _lastChunkData = nil;
 
 @interface GCDWebServerConnection () {
 @private
@@ -55,12 +54,14 @@ static dispatch_queue_t _formatterQueue = NULL;
   CFSocketNativeHandle _socket;
   NSUInteger _bytesRead;
   NSUInteger _bytesWritten;
+  BOOL _virtualHEAD;
   
   CFHTTPMessageRef _requestMessage;
   GCDWebServerRequest* _request;
   GCDWebServerHandler* _handler;
   CFHTTPMessageRef _responseMessage;
   GCDWebServerResponse* _response;
+  NSInteger _statusCode;
 }
 @end
 
@@ -122,7 +123,7 @@ static dispatch_queue_t _formatterQueue = NULL;
         [data appendBytes:bufferChunk length:size];
         return true;
       });
-      NSRange range = [data rangeOfData:_separatorData options:0 range:NSMakeRange(0, data.length)];
+      NSRange range = [data rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, data.length)];
       if (range.location == NSNotFound) {
         if (CFHTTPMessageAppendBytes(_requestMessage, data.bytes, data.length)) {
           [self _readHeadersWithCompletionBlock:block];
@@ -152,22 +153,23 @@ static dispatch_queue_t _formatterQueue = NULL;
 }
 
 - (void)_readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block {
-  DCHECK([_request hasBody]);
+  DCHECK([_request hasBody] && ![_request usesChunkedTransferEncoding]);
   [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* bufferChunk, size_t size) {
-          NSInteger result = [_request write:bufferChunk maxLength:size];
-          if (result != (NSInteger)size) {
-            LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result);
+      if (dispatch_data_get_size(buffer) <= length) {
+        bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* chunkBytes, size_t chunkSize) {
+          NSData* data = [NSData dataWithBytesNoCopy:(void*)chunkBytes length:chunkSize freeWhenDone:NO];
+          NSError* error = nil;
+          if (![_request performWriteData:data error:&error]) {
+            LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
             return false;
           }
           return true;
         });
         if (success) {
-          if (remainingLength > 0) {
+          NSUInteger remainingLength = length - dispatch_data_get_size(buffer);
+          if (remainingLength) {
             [self _readBodyWithRemainingLength:remainingLength completionBlock:block];
           } else {
             block(YES);
@@ -176,6 +178,7 @@ static dispatch_queue_t _formatterQueue = NULL;
           block(NO);
         }
       } else {
+        LOG_ERROR(@"Unexpected extra content reading request body on socket %i", _socket);
         block(NO);
         DNOT_REACHED();
       }
@@ -186,6 +189,74 @@ static dispatch_queue_t _formatterQueue = NULL;
   }];
 }
 
+static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
+  char buffer[size + 1];
+  bcopy(bytes, buffer, size);
+  buffer[size] = 0;
+  char* end = NULL;
+  long result = strtol(buffer, &end, 16);
+  return ((end != NULL) && (*end == 0) && (result >= 0) ? result : NSNotFound);
+}
+
+- (void)_readNextBodyChunk:(NSMutableData*)chunkData completionBlock:(ReadBodyCompletionBlock)block {
+  DCHECK([_request hasBody] && [_request usesChunkedTransferEncoding]);
+  
+  while (1) {
+    NSRange range = [chunkData rangeOfData:_CRLFData options:0 range:NSMakeRange(0, chunkData.length)];
+    if (range.location == NSNotFound) {
+      break;
+    }
+    NSRange extensionRange = [chunkData rangeOfData:[NSData dataWithBytes:";" length:1] options:0 range:NSMakeRange(0, range.location)];  // Ignore chunk extensions
+    NSUInteger length = _ScanHexNumber((char*)chunkData.bytes, extensionRange.location != NSNotFound ? extensionRange.location : range.location);
+    if (length != NSNotFound) {
+      if (length) {
+        if (chunkData.length < range.location + range.length + length + 2) {
+          break;
+        }
+        const char* ptr = (char*)chunkData.bytes + range.location + range.length + length;
+        if ((*ptr == '\r') && (*(ptr + 1) == '\n')) {
+          NSError* error = nil;
+          if ([_request performWriteData:[chunkData subdataWithRange:NSMakeRange(range.location + range.length, length)] error:&error]) {
+            [chunkData replaceBytesInRange:NSMakeRange(0, range.location + range.length + length + 2) withBytes:NULL length:0];
+          } else {
+            LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
+            block(NO);
+            return;
+          }
+        } else {
+          LOG_ERROR(@"Missing terminating CRLF sequence for chunk reading request body on socket %i", _socket);
+          block(NO);
+          return;
+        }
+      } else {
+        NSRange trailerRange = [chunkData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(range.location, chunkData.length - range.location)];  // Ignore trailers
+        if (trailerRange.location != NSNotFound) {
+          block(YES);
+          return;
+        }
+      }
+    } else {
+      LOG_ERROR(@"Invalid chunk length reading request body on socket %i", _socket);
+      block(NO);
+      return;
+    }
+  }
+  
+  [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* chunkBytes, size_t chunkSize) {
+        [chunkData appendBytes:chunkBytes length:chunkSize];
+        return true;
+      });
+      [self _readNextBodyChunk:chunkData completionBlock:block];
+    } else {
+      block(NO);
+    }
+    
+  }];
+}
+
 @end
 
 @implementation GCDWebServerConnection (Write)
@@ -234,27 +305,53 @@ static dispatch_queue_t _formatterQueue = NULL;
 
 - (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];
+  NSError* error = nil;
+  NSData* data = [_response performReadData:&error];
+  if (data) {
+    if (data.length) {
+      if (_response.usesChunkedTransferEncoding) {
+        const char* hexString = [[NSString stringWithFormat:@"%lx", (unsigned long)data.length] UTF8String];
+        size_t hexLength = strlen(hexString);
+        NSData* chunk = [NSMutableData dataWithLength:(hexLength + 2 + data.length + 2)];
+        if (chunk == nil) {
+          LOG_ERROR(@"Failed allocating memory for response body chunk for socket %i: %@", _socket, error);
+          block(NO);
+          return;
+        }
+        char* ptr = (char*)[(NSMutableData*)chunk mutableBytes];
+        bcopy(hexString, ptr, hexLength);
+        ptr += hexLength;
+        *ptr++ = '\r';
+        *ptr++ = '\n';
+        bcopy(data.bytes, ptr, data.length);
+        ptr += data.length;
+        *ptr++ = '\r';
+        *ptr = '\n';
+        data = chunk;
+      }
+      [self _writeData:data withCompletionBlock:^(BOOL success) {
+        
+        if (success) {
+          [self _writeBodyWithCompletionBlock:block];
+        } else {
+          block(NO);
+        }
+        
+      }];
+    } else {
+      if (_response.usesChunkedTransferEncoding) {
+        [self _writeData:_lastChunkData withCompletionBlock:^(BOOL success) {
+          
+          block(success);
+          
+        }];
       } else {
-        block(NO);
+        block(YES);
       }
-      
-    }];
-    ARC_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);
+    LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error);
+    block(NO);
   }
 }
 
@@ -265,9 +362,13 @@ static dispatch_queue_t _formatterQueue = NULL;
 @synthesize server=_server, localAddressData=_localAddress, remoteAddressData=_remoteAddress, totalBytesRead=_bytesRead, totalBytesWritten=_bytesWritten;
 
 + (void)initialize {
-  if (_separatorData == nil) {
-    _separatorData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
-    DCHECK(_separatorData);
+  if (_CRLFData == nil) {
+    _CRLFData = [[NSData alloc] initWithBytes:"\r\n" length:2];
+    DCHECK(_CRLFData);
+  }
+  if (_CRLFCRLFData == nil) {
+    _CRLFCRLFData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
+    DCHECK(_CRLFCRLFData);
   }
   if (_continueData == nil) {
     CFHTTPMessageRef message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, NULL, kCFHTTPVersion1_1);
@@ -279,134 +380,165 @@ static dispatch_queue_t _formatterQueue = NULL;
     CFRelease(message);
     DCHECK(_continueData);
   }
-  if (_dateFormatter == nil) {
-    DCHECK([NSThread isMainThread]);  // NSDateFormatter should be initialized on main thread
-    _dateFormatter = [[NSDateFormatter alloc] init];
-    _dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
-    _dateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
-    _dateFormatter.locale = ARC_AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]);
-    DCHECK(_dateFormatter);
-  }
-  if (_formatterQueue == NULL) {
-    _formatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
-    DCHECK(_formatterQueue);
+  if (_lastChunkData == nil) {
+    _lastChunkData = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5];
   }
 }
 
 - (void)_initializeResponseHeadersWithStatusCode:(NSInteger)statusCode {
+  _statusCode = statusCode;
   _responseMessage = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_1);
   CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Connection"), CFSTR("Close"));
   CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Server"), (ARC_BRIDGE CFStringRef)[[_server class] serverName]);
-  dispatch_sync(_formatterQueue, ^{
-    NSString* date = [_dateFormatter stringFromDate:[NSDate date]];
-    CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE 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", (int)statusCode, _socket);
+  CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatHTTPDate([NSDate date]));
 }
 
 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
 - (void)_processRequest {
   DCHECK(_responseMessage == NULL);
+  BOOL hasBody = NO;
   
   GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock];
-  if (![response hasBody] || [response open]) {
-    _response = ARC_RETAIN(response);
+  if (response) {
+    response = [self replaceResponse:response forRequest:_request];
+    if (response) {
+      if ([response hasBody]) {
+        [response prepareForReading];
+        hasBody = !_virtualHEAD;
+      }
+      NSError* error = nil;
+      if (hasBody && ![response performOpen:&error]) {
+        LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error);
+      } else {
+        _response = ARC_RETAIN(response);
+      }
+    }
   }
   
   if (_response) {
     [self _initializeResponseHeadersWithStatusCode:_response.statusCode];
-    if (_response.cacheControlMaxAge > 0) {
-      CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)_response.cacheControlMaxAge]);
-    } else {
-      CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache"));
+    if (_response.lastModifiedDate) {
+      CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Last-Modified"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatHTTPDate(_response.lastModifiedDate));
+    }
+    if (_response.eTag) {
+      CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("ETag"), (ARC_BRIDGE CFStringRef)_response.eTag);
+    }
+    if ((_response.statusCode >= 200) && (_response.statusCode < 300)) {
+      if (_response.cacheControlMaxAge > 0) {
+        CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)_response.cacheControlMaxAge]);
+      } else {
+        CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache"));
+      }
     }
     if (_response.contentType != nil) {
-      CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)_response.contentType);
+      CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType));
     }
     if (_response.contentLength != NSNotFound) {
       CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
     }
+    if (_response.usesChunkedTransferEncoding) {
+      CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Transfer-Encoding"), CFSTR("chunked"));
+    }
     [_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
       CFHTTPMessageSetHeaderFieldValue(_responseMessage, (ARC_BRIDGE CFStringRef)key, (ARC_BRIDGE CFStringRef)obj);
     }];
     [self _writeHeadersWithCompletionBlock:^(BOOL success) {
       
       if (success) {
-        if ([_response hasBody]) {
+        if (hasBody) {
           [self _writeBodyWithCompletionBlock:^(BOOL successInner) {
             
-            [_response close];  // Can't do anything with result anyway
+            [_response performClose];  // TODO: There's nothing we can do on failure as headers have already been sent
             
           }];
         }
-      } else if ([_response hasBody]) {
-        [_response close];  // Can't do anything with result anyway
+      } else if (hasBody) {
+        [_response performClose];
       }
       
     }];
   } else {
-    [self _abortWithStatusCode:500];
+    [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
   }
   
 }
 
-- (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 == (NSInteger)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;
+- (void)_readBodyWithLength:(NSUInteger)length initialData:(NSData*)initialData {
+  NSError* error = nil;
+  if (![_request performOpen:&error]) {
+    LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
+    [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
+    return;
+  }
+  
+  if (initialData.length) {
+    if (![_request performWriteData:initialData error:&error]) {
+      LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
+      if (![_request performClose:&error]) {
+        LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
       }
+      [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
+      return;
     }
-    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]) {
+    length -= initialData.length;
+  }
+  
+  if (length) {
+    [self _readBodyWithRemainingLength:length completionBlock:^(BOOL success) {
+      
+      NSError* localError = nil;
+      if ([_request performClose:&localError]) {
         [self _processRequest];
       } else {
-        [self _abortWithStatusCode:500];
+        LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
+        [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
       }
+      
+    }];
+  } else {
+    if ([_request performClose:&error]) {
+      [self _processRequest];
     } else {
-      [_request close];  // Can't do anything with result anyway
-      [self _abortWithStatusCode:500];
+      LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
+      [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
     }
-  } else {
-    [self _abortWithStatusCode:500];
   }
 }
 
+- (void)_readChunkedBodyWithInitialData:(NSData*)initialData {
+  NSError* error = nil;
+  if (![_request performOpen:&error]) {
+    LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
+    [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
+    return;
+  }
+  
+  NSMutableData* chunkData = [[NSMutableData alloc] initWithData:initialData];
+  [self _readNextBodyChunk:chunkData completionBlock:^(BOOL success) {
+  
+    NSError* localError = nil;
+    if ([_request performClose:&localError]) {
+      [self _processRequest];
+    } else {
+      LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
+      [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
+    }
+    
+  }];
+  ARC_RELEASE(chunkData);
+}
+
 - (void)_readRequestHeaders {
   _requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true);
   [self _readHeadersWithCompletionBlock:^(NSData* extraData) {
     
     if (extraData) {
-      NSString* requestMethod = [ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(_requestMessage)) uppercaseString];
+      NSString* requestMethod = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(_requestMessage));  // Method verbs are case-sensitive and uppercase
       DCHECK(requestMethod);
+      if ([[_server class] shouldAutomaticallyMapHEADToGET] && [requestMethod isEqualToString:@"HEAD"]) {
+        requestMethod = @"GET";
+        _virtualHEAD = YES;
+      }
       NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(_requestMessage));
       DCHECK(requestURL);
       NSString* requestPath = GCDWebServerUnescapeURLString(ARC_BRIDGE_RELEASE(CFURLCopyPath((CFURLRef)requestURL)));  // Don't use -[NSURL path] which strips the ending slash
@@ -417,7 +549,7 @@ static dispatch_queue_t _formatterQueue = NULL;
         requestQuery = GCDWebServerParseURLEncodedForm(queryString);
         DCHECK(requestQuery);
       }
-      NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage));
+      NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage));  // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
       DCHECK(requestHeaders);
       for (_handler in _server.handlers) {
         _request = ARC_RETAIN(_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery));
@@ -426,37 +558,48 @@ static dispatch_queue_t _formatterQueue = NULL;
         }
       }
       if (_request) {
-        if (_request.hasBody) {
-          if (extraData.length <= _request.contentLength) {
+        if ([_request hasBody]) {
+          [_request prepareForWriting];
+          if (_request.usesChunkedTransferEncoding || (extraData.length <= _request.contentLength)) {
             NSString* expectHeader = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(_requestMessage, CFSTR("Expect")));
             if (expectHeader) {
               if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) {
                 [self _writeData:_continueData withCompletionBlock:^(BOOL success) {
                   
                   if (success) {
-                    [self _readRequestBody:extraData];
+                    if (_request.usesChunkedTransferEncoding) {
+                      [self _readChunkedBodyWithInitialData:extraData];
+                    } else {
+                      [self _readBodyWithLength:_request.contentLength initialData:extraData];
+                    }
                   }
                   
                 }];
               } else {
                 LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket);
-                [self _abortWithStatusCode:417];
+                [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_ExpectationFailed];
               }
             } else {
-              [self _readRequestBody:extraData];
+              if (_request.usesChunkedTransferEncoding) {
+                [self _readChunkedBodyWithInitialData:extraData];
+              } else {
+                [self _readBodyWithLength:_request.contentLength initialData:extraData];
+              }
             }
           } else {
             LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket);
-            [self _abortWithStatusCode:400];
+            [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest];
           }
         } else {
           [self _processRequest];
         }
       } else {
-        [self _abortWithStatusCode:405];
+        _request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery];
+        DCHECK(_request);
+        [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_MethodNotAllowed];
       }
     } else {
-      [self _abortWithStatusCode:500];
+      [self abortRequest:nil withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
     }
     
   }];
@@ -533,11 +676,10 @@ static NSString* _StringFromAddressData(NSData* data) {
 }
 
 - (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block {
-  LOG_DEBUG(@"Connection on socket %i processing %@ request for \"%@\" (%lu bytes body)", _socket, _request.method, _request.path, (unsigned long)_request.contentLength);
+  LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
   GCDWebServerResponse* response = nil;
   @try {
     response = block(request);
-    LOG_VERBOSE(@"%@ | %@ \"%@ %@\" %i %lu", self.localAddressString, self.remoteAddressString, _request.method, _request.path, (int)response.statusCode, (unsigned long)(response.contentLength != NSNotFound ? response.contentLength : 0));
   }
   @catch (NSException* exception) {
     LOG_EXCEPTION(exception);
@@ -545,12 +687,52 @@ static NSString* _StringFromAddressData(NSData* data) {
   return response;
 }
 
+// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
+static inline BOOL _CompareResources(NSString* responseETag, NSString* requestETag, NSDate* responseLastModified, NSDate* requestLastModified) {
+  if ([requestETag isEqualToString:@"*"] && (!responseLastModified || !requestLastModified || ([responseLastModified compare:requestLastModified] != NSOrderedDescending))) {
+    return YES;
+  } else {
+    if ([responseETag isEqualToString:requestETag]) {
+      return YES;
+    }
+    if (responseLastModified && requestLastModified && ([responseLastModified compare:requestLastModified] != NSOrderedDescending)) {
+      return YES;
+    }
+  }
+  return NO;
+}
+
+- (GCDWebServerResponse*)replaceResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request {
+  if ((response.statusCode >= 200) && (response.statusCode < 300) && _CompareResources(response.eTag, request.ifNoneMatch, response.lastModifiedDate, request.ifModifiedSince)) {
+    NSInteger code = [request.method isEqualToString:@"HEAD"] || [request.method isEqualToString:@"GET"] ? kGCDWebServerHTTPStatusCode_NotModified : kGCDWebServerHTTPStatusCode_PreconditionFailed;
+    GCDWebServerResponse* newResponse = [GCDWebServerResponse responseWithStatusCode:code];
+    newResponse.cacheControlMaxAge = response.cacheControlMaxAge;
+    newResponse.lastModifiedDate = response.lastModifiedDate;
+    newResponse.eTag = response.eTag;
+    DCHECK(newResponse);
+    return newResponse;
+  }
+  return response;
+}
+
+- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)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", (int)statusCode, _socket);
+}
+
 - (void)close {
   int result = close(_socket);
   if (result != 0) {
     LOG_ERROR(@"Failed closing socket %i for connection (%i): %s", _socket, errno, strerror(errno));
+  } else {
+    LOG_DEBUG(@"Did close connection on socket %i", _socket);
   }
-  LOG_DEBUG(@"Did close connection on socket %i", _socket);
+  LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten);
 }
 
 @end

+ 37 - 0
CGDWebServer/GCDWebServerDataRequest.h

@@ -0,0 +1,37 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerRequest.h"
+
+@interface GCDWebServerDataRequest : GCDWebServerRequest
+@property(nonatomic, readonly) NSData* data;
+@end
+
+@interface GCDWebServerDataRequest (Extensions)
+@property(nonatomic, readonly) NSString* text;  // Text encoding is extracted from Content-Type or defaults to UTF-8 - Returns nil on error
+@property(nonatomic, readonly) id jsonObject;  // Returns nil on error
+@end

+ 109 - 0
CGDWebServer/GCDWebServerDataRequest.m

@@ -0,0 +1,109 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerPrivate.h"
+
+@interface GCDWebServerDataRequest () {
+@private
+  NSMutableData* _data;
+  
+  NSString* _text;
+  id _jsonObject;
+}
+@end
+
+@implementation GCDWebServerDataRequest
+
+@synthesize data=_data;
+
+- (void)dealloc {
+  ARC_RELEASE(_data);
+  ARC_RELEASE(_text);
+  ARC_RELEASE(_jsonObject);
+  
+  ARC_DEALLOC(super);
+}
+
+- (BOOL)open:(NSError**)error {
+  if (self.contentLength != NSNotFound) {
+    _data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
+  } else {
+    _data = [[NSMutableData alloc] init];
+  }
+  if (_data == nil) {
+    *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed allocating memory"}];
+    return NO;
+  }
+  return YES;
+}
+
+- (BOOL)writeData:(NSData*)data error:(NSError**)error {
+  [_data appendData:data];
+  return YES;
+}
+
+- (BOOL)close:(NSError**)error {
+  return YES;
+}
+
+- (NSString*)description {
+  NSMutableString* description = [NSMutableString stringWithString:[super description]];
+  if (_data) {
+    [description appendString:@"\n\n"];
+    [description appendString:GCDWebServerDescribeData(_data, self.contentType)];
+  }
+  return description;
+}
+
+@end
+
+@implementation GCDWebServerDataRequest (Extensions)
+
+- (NSString*)text {
+  if (_text == nil) {
+    if ([self.contentType hasPrefix:@"text/"]) {
+      NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
+      _text = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)];
+    } else {
+      DNOT_REACHED();
+    }
+  }
+  return _text;
+}
+
+- (id)jsonObject {
+  if (_jsonObject == nil) {
+    if ([self.contentType isEqualToString:@"application/json"] || [self.contentType isEqualToString:@"text/json"] || [self.contentType isEqualToString:@"text/javascript"]) {
+      _jsonObject = ARC_RETAIN([NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL]);
+    } else {
+      DNOT_REACHED();
+    }
+  }
+  return _jsonObject;
+}
+
+@end

+ 46 - 0
CGDWebServer/GCDWebServerDataResponse.h

@@ -0,0 +1,46 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerResponse.h"
+
+@interface GCDWebServerDataResponse : GCDWebServerResponse
++ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type;
+- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type;
+@end
+
+@interface GCDWebServerDataResponse (Extensions)
++ (instancetype)responseWithText:(NSString*)text;
++ (instancetype)responseWithHTML:(NSString*)html;
++ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;
++ (instancetype)responseWithJSONObject:(id)object;
++ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type;
+- (instancetype)initWithText:(NSString*)text;  // Encodes using UTF-8
+- (instancetype)initWithHTML:(NSString*)html;  // Encodes using UTF-8
+- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;  // Simple template system that replaces all occurences of "%variable%" with corresponding value (encodes using UTF-8)
+- (instancetype)initWithJSONObject:(id)object;
+- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type;
+@end

+ 150 - 0
CGDWebServer/GCDWebServerDataResponse.m

@@ -0,0 +1,150 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerPrivate.h"
+
+@interface GCDWebServerDataResponse () {
+@private
+  NSData* _data;
+  BOOL _done;
+}
+@end
+
+@implementation GCDWebServerDataResponse
+
++ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type {
+  return ARC_AUTORELEASE([[[self class] alloc] initWithData:data contentType:type]);
+}
+
+- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type {
+  if (data == nil) {
+    DNOT_REACHED();
+    ARC_RELEASE(self);
+    return nil;
+  }
+  
+  if ((self = [super init])) {
+    _data = ARC_RETAIN(data);
+    
+    self.contentType = type;
+    self.contentLength = data.length;
+  }
+  return self;
+}
+
+- (void)dealloc {
+  ARC_RELEASE(_data);
+  
+  ARC_DEALLOC(super);
+}
+
+- (NSData*)readData:(NSError**)error {
+  NSData* data;
+  if (_done) {
+    data = [NSData data];
+  } else {
+    data = _data;
+    _done = YES;
+  }
+  return data;
+}
+
+- (NSString*)description {
+  NSMutableString* description = [NSMutableString stringWithString:[super description]];
+  [description appendString:@"\n\n"];
+  [description appendString:GCDWebServerDescribeData(_data, self.contentType)];
+  return description;
+}
+
+@end
+
+@implementation GCDWebServerDataResponse (Extensions)
+
++ (instancetype)responseWithText:(NSString*)text {
+  return ARC_AUTORELEASE([[self alloc] initWithText:text]);
+}
+
++ (instancetype)responseWithHTML:(NSString*)html {
+  return ARC_AUTORELEASE([[self alloc] initWithHTML:html]);
+}
+
++ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
+  return ARC_AUTORELEASE([[self alloc] initWithHTMLTemplate:path variables:variables]);
+}
+
++ (instancetype)responseWithJSONObject:(id)object {
+  return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object]);
+}
+
++ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type {
+  return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object contentType:type]);
+}
+
+- (instancetype)initWithText:(NSString*)text {
+  NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding];
+  if (data == nil) {
+    DNOT_REACHED();
+    ARC_RELEASE(self);
+    return nil;
+  }
+  return [self initWithData:data contentType:@"text/plain; charset=utf-8"];
+}
+
+- (instancetype)initWithHTML:(NSString*)html {
+  NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding];
+  if (data == nil) {
+    DNOT_REACHED();
+    ARC_RELEASE(self);
+    return nil;
+  }
+  return [self initWithData:data contentType:@"text/html; charset=utf-8"];
+}
+
+- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
+  NSMutableString* html = [[NSMutableString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
+  [variables enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL* stop) {
+    [html replaceOccurrencesOfString:[NSString stringWithFormat:@"%%%@%%", key] withString:value options:0 range:NSMakeRange(0, html.length)];
+  }];
+  id response = [self initWithHTML:html];
+  ARC_RELEASE(html);
+  return response;
+}
+
+- (instancetype)initWithJSONObject:(id)object {
+  return [self initWithJSONObject:object contentType:@"application/json"];
+}
+
+- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type {
+  NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL];
+  if (data == nil) {
+    ARC_RELEASE(self);
+    return nil;
+  }
+  return [self initWithData:data contentType:type];
+}
+
+@end

+ 41 - 0
CGDWebServer/GCDWebServerErrorResponse.h

@@ -0,0 +1,41 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerDataResponse.h"
+#import "GCDWebServerHTTPStatusCodes.h"
+
+// Returns responses with an HTML body containing the error message
+@interface GCDWebServerErrorResponse : GCDWebServerDataResponse
++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
+- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
+- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
+- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
+- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
+@end

+ 125 - 0
CGDWebServer/GCDWebServerErrorResponse.m

@@ -0,0 +1,125 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerPrivate.h"
+
+@interface GCDWebServerErrorResponse ()
+- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments;
+@end
+
+@implementation GCDWebServerErrorResponse
+
++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
+  DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
+  va_list arguments;
+  va_start(arguments, format);
+  GCDWebServerErrorResponse* response = ARC_AUTORELEASE([[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments]);
+  va_end(arguments);
+  return response;
+}
+
++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
+  DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
+  va_list arguments;
+  va_start(arguments, format);
+  GCDWebServerErrorResponse* response = ARC_AUTORELEASE([[self alloc] initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments]);
+  va_end(arguments);
+  return response;
+}
+
++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
+  DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
+  va_list arguments;
+  va_start(arguments, format);
+  GCDWebServerErrorResponse* response = ARC_AUTORELEASE([[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments]);
+  va_end(arguments);
+  return response;
+}
+
++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
+  DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
+  va_list arguments;
+  va_start(arguments, format);
+  GCDWebServerErrorResponse* response = ARC_AUTORELEASE([[self alloc] initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments]);
+  va_end(arguments);
+  return response;
+}
+
+static inline NSString* _EscapeHTMLString(NSString* string) {
+  return [string stringByReplacingOccurrencesOfString:@"\"" withString:@"&quot;"];
+}
+
+- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments {
+  NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
+  NSString* title = [NSString stringWithFormat:@"HTTP Error %i", (int)statusCode];
+  NSString* error = underlyingError ? [NSString stringWithFormat:@"[%@] %@ (%li)", underlyingError.domain, _EscapeHTMLString(underlyingError.localizedDescription), (long)underlyingError.code] : @"";
+  NSString* html = [NSString stringWithFormat:@"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>%@</title></head><body><h1>%@: %@</h1><h3>%@</h3></body></html>",
+                                              title, title, _EscapeHTMLString(message), error];
+  if ((self = [self initWithHTML:html])) {
+    self.statusCode = statusCode;
+  }
+  ARC_RELEASE(message);
+  return self;
+}
+
+- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
+  DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
+  va_list arguments;
+  va_start(arguments, format);
+  self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
+  va_end(arguments);
+  return self;
+}
+
+- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... {
+  DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
+  va_list arguments;
+  va_start(arguments, format);
+  self = [self initWithStatusCode:errorCode underlyingError:nil messageFormat:format arguments:arguments];
+  va_end(arguments);
+  return self;
+}
+
+- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
+  DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500));
+  va_list arguments;
+  va_start(arguments, format);
+  self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
+  va_end(arguments);
+  return self;
+}
+
+- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... {
+  DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600));
+  va_list arguments;
+  va_start(arguments, format);
+  self = [self initWithStatusCode:errorCode underlyingError:underlyingError messageFormat:format arguments:arguments];
+  va_end(arguments);
+  return self;
+}
+
+@end

+ 32 - 0
CGDWebServer/GCDWebServerFileRequest.h

@@ -0,0 +1,32 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerRequest.h"
+
+@interface GCDWebServerFileRequest : GCDWebServerRequest
+@property(nonatomic, readonly) NSString* temporaryPath;
+@end

+ 90 - 0
CGDWebServer/GCDWebServerFileRequest.m

@@ -0,0 +1,90 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerPrivate.h"
+
+@interface GCDWebServerFileRequest () {
+@private
+  NSString* _temporaryPath;
+  int _file;
+}
+@end
+
+static inline NSError* _MakePosixError(int code) {
+  return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
+}
+
+@implementation GCDWebServerFileRequest
+
+@synthesize temporaryPath=_temporaryPath;
+
+- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
+  if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
+    _temporaryPath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
+  }
+  return self;
+}
+
+- (void)dealloc {
+  unlink([_temporaryPath fileSystemRepresentation]);
+  ARC_RELEASE(_temporaryPath);
+  
+  ARC_DEALLOC(super);
+}
+
+- (BOOL)open:(NSError**)error {
+  _file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+  if (_file <= 0) {
+    *error = _MakePosixError(errno);
+    return NO;
+  }
+  return YES;
+}
+
+- (BOOL)writeData:(NSData*)data error:(NSError**)error {
+  if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
+    *error = _MakePosixError(errno);
+    return NO;
+  }
+  return YES;
+}
+
+- (BOOL)close:(NSError**)error {
+  if (close(_file) < 0) {
+    *error = _MakePosixError(errno);
+    return NO;
+  }
+  return YES;
+}
+
+- (NSString*)description {
+  NSMutableString* description = [NSMutableString stringWithString:[super description]];
+  [description appendFormat:@"\n\n{%@}", _temporaryPath];
+  return description;
+}
+
+@end

+ 39 - 0
CGDWebServer/GCDWebServerFileResponse.h

@@ -0,0 +1,39 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerResponse.h"
+
+@interface GCDWebServerFileResponse : GCDWebServerResponse
++ (instancetype)responseWithFile:(NSString*)path;
++ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
++ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range;
++ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
+- (instancetype)initWithFile:(NSString*)path;
+- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment;
+- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range;  // Pass [NSNotFound, 0] to disable byte range entirely, [offset, length] to enable byte range from beginning of file or [NSNotFound, -bytes] from end of file
+- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
+@end

+ 179 - 0
CGDWebServer/GCDWebServerFileResponse.m

@@ -0,0 +1,179 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <sys/stat.h>
+
+#import "GCDWebServerPrivate.h"
+
+#define kFileReadBufferSize (32 * 1024)
+
+@interface GCDWebServerFileResponse () {
+@private
+  NSString* _path;
+  NSUInteger _offset;
+  NSUInteger _size;
+  int _file;
+}
+@end
+
+static inline NSError* _MakePosixError(int code) {
+  return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
+}
+
+@implementation GCDWebServerFileResponse
+
++ (instancetype)responseWithFile:(NSString*)path {
+  return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path]);
+}
+
++ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment {
+  return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path isAttachment:attachment]);
+}
+
++ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range {
+  return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range]);
+}
+
++ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
+  return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range isAttachment:attachment]);
+}
+
+- (instancetype)initWithFile:(NSString*)path {
+  return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO];
+}
+
+- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
+  return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment];
+}
+
+- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
+  return [self initWithFile:path byteRange:range isAttachment:NO];
+}
+
+static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
+  return [NSDate dateWithTimeIntervalSince1970:((NSTimeInterval)t->tv_sec + (NSTimeInterval)t->tv_nsec / 1000000000.0)];
+}
+
+- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
+  struct stat info;
+  if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
+    DNOT_REACHED();
+    ARC_RELEASE(self);
+    return nil;
+  }
+  if (GCDWebServerIsValidByteRange(range)) {
+    if (range.location != NSNotFound) {
+      range.location = MIN(range.location, (NSUInteger)info.st_size);
+      range.length = MIN(range.length, (NSUInteger)info.st_size - range.location);
+    } else {
+      range.length = MIN(range.length, (NSUInteger)info.st_size);
+      range.location = (NSUInteger)info.st_size - range.length;
+    }
+    if (range.length == 0) {
+      ARC_RELEASE(self);
+      return nil;  // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
+    }
+  }
+  
+  if ((self = [super init])) {
+    _path = [path copy];
+    if (range.location != NSNotFound) {
+      _offset = range.location;
+      _size = range.length;
+      [self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent];
+      [self setValue:[NSString stringWithFormat:@"bytes %i-%i/%i", (int)range.location, (int)(range.location + range.length - 1), (int)info.st_size] forAdditionalHeader:@"Content-Range"];
+      LOG_DEBUG(@"Using content bytes range [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 1), path);
+    } else {
+      _offset = 0;
+      _size = (NSUInteger)info.st_size;
+    }
+    
+    if (attachment) {  // TODO: Use http://tools.ietf.org/html/rfc5987 to encode file names with special characters instead of using lossy conversion to ISO 8859-1
+      NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
+      NSString* fileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
+      if (fileName) {
+        [self setValue:[NSString stringWithFormat:@"attachment; filename=\"%@\"", fileName] forAdditionalHeader:@"Content-Disposition"];
+        ARC_RELEASE(fileName);
+      } else {
+        DNOT_REACHED();
+      }
+    }
+    
+    self.contentType = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
+    self.contentLength = (range.location != NSNotFound ? range.length : (NSUInteger)info.st_size);
+    self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec);
+    self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec];
+  }
+  return self;
+}
+
+- (void)dealloc {
+  ARC_RELEASE(_path);
+  
+  ARC_DEALLOC(super);
+}
+
+- (BOOL)open:(NSError**)error {
+  _file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
+  if (_file <= 0) {
+    *error = _MakePosixError(errno);
+    return NO;
+  }
+  if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
+    *error = _MakePosixError(errno);
+    close(_file);
+    return NO;
+  }
+  return YES;
+}
+
+- (NSData*)readData:(NSError**)error {
+  size_t length = MIN((NSUInteger)kFileReadBufferSize, _size);
+  NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
+  ssize_t result = read(_file, data.mutableBytes, length);
+  if (result < 0) {
+    *error = _MakePosixError(errno);
+    return nil;
+  }
+  if (result > 0) {
+    [data setLength:result];
+    _size -= result;
+  }
+  return ARC_AUTORELEASE(data);
+}
+
+- (void)close {
+  close(_file);
+}
+
+- (NSString*)description {
+  NSMutableString* description = [NSMutableString stringWithString:[super description]];
+  [description appendFormat:@"\n\n{%@}", _path];
+  return description;
+}
+
+@end

+ 101 - 0
CGDWebServer/GCDWebServerHTTPStatusCodes.h

@@ -0,0 +1,101 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+// http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) {
+  kGCDWebServerHTTPStatusCode_Continue = 100,
+  kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
+  kGCDWebServerHTTPStatusCode_Processing = 102
+};
+
+typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
+  kGCDWebServerHTTPStatusCode_OK = 200,
+  kGCDWebServerHTTPStatusCode_Created = 201,
+  kGCDWebServerHTTPStatusCode_Accepted = 202,
+  kGCDWebServerHTTPStatusCode_NonAuthoritativeInformation = 203,
+  kGCDWebServerHTTPStatusCode_NoContent = 204,
+  kGCDWebServerHTTPStatusCode_ResetContent = 205,
+  kGCDWebServerHTTPStatusCode_PartialContent = 206,
+  kGCDWebServerHTTPStatusCode_MultiStatus = 207,
+  kGCDWebServerHTTPStatusCode_AlreadyReported = 208
+};
+
+typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
+  kGCDWebServerHTTPStatusCode_MultipleChoices = 300,
+  kGCDWebServerHTTPStatusCode_MovedPermanently = 301,
+  kGCDWebServerHTTPStatusCode_Found = 302,
+  kGCDWebServerHTTPStatusCode_SeeOther = 303,
+  kGCDWebServerHTTPStatusCode_NotModified = 304,
+  kGCDWebServerHTTPStatusCode_UseProxy = 305,
+  kGCDWebServerHTTPStatusCode_TemporaryRedirect = 307,
+  kGCDWebServerHTTPStatusCode_PermanentRedirect = 308
+};
+
+typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
+  kGCDWebServerHTTPStatusCode_BadRequest = 400,
+  kGCDWebServerHTTPStatusCode_Unauthorized = 401,
+  kGCDWebServerHTTPStatusCode_PaymentRequired = 402,
+  kGCDWebServerHTTPStatusCode_Forbidden = 403,
+  kGCDWebServerHTTPStatusCode_NotFound = 404,
+  kGCDWebServerHTTPStatusCode_MethodNotAllowed = 405,
+  kGCDWebServerHTTPStatusCode_NotAcceptable = 406,
+  kGCDWebServerHTTPStatusCode_ProxyAuthenticationRequired = 407,
+  kGCDWebServerHTTPStatusCode_RequestTimeout = 408,
+  kGCDWebServerHTTPStatusCode_Conflict = 409,
+  kGCDWebServerHTTPStatusCode_Gone = 410,
+  kGCDWebServerHTTPStatusCode_LengthRequired = 411,
+  kGCDWebServerHTTPStatusCode_PreconditionFailed = 412,
+  kGCDWebServerHTTPStatusCode_RequestEntityTooLarge = 413,
+  kGCDWebServerHTTPStatusCode_RequestURITooLong = 414,
+  kGCDWebServerHTTPStatusCode_UnsupportedMediaType = 415,
+  kGCDWebServerHTTPStatusCode_RequestedRangeNotSatisfiable = 416,
+  kGCDWebServerHTTPStatusCode_ExpectationFailed = 417,
+  kGCDWebServerHTTPStatusCode_UnprocessableEntity = 422,
+  kGCDWebServerHTTPStatusCode_Locked = 423,
+  kGCDWebServerHTTPStatusCode_FailedDependency = 424,
+  kGCDWebServerHTTPStatusCode_UpgradeRequired = 426,
+  kGCDWebServerHTTPStatusCode_PreconditionRequired = 428,
+  kGCDWebServerHTTPStatusCode_TooManyRequests = 429,
+  kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
+};
+
+typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) {
+  kGCDWebServerHTTPStatusCode_InternalServerError = 500,
+  kGCDWebServerHTTPStatusCode_NotImplemented = 501,
+  kGCDWebServerHTTPStatusCode_BadGateway = 502,
+  kGCDWebServerHTTPStatusCode_ServiceUnavailable = 503,
+  kGCDWebServerHTTPStatusCode_GatewayTimeout = 504,
+  kGCDWebServerHTTPStatusCode_HTTPVersionNotSupported = 505,
+  kGCDWebServerHTTPStatusCode_InsufficientStorage = 507,
+  kGCDWebServerHTTPStatusCode_LoopDetected = 508,
+  kGCDWebServerHTTPStatusCode_NotExtended = 510,
+  kGCDWebServerHTTPStatusCode_NetworkAuthenticationRequired = 511
+};

+ 49 - 0
CGDWebServer/GCDWebServerMultiPartFormRequest.h

@@ -0,0 +1,49 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerRequest.h"
+
+@interface GCDWebServerMultiPart : NSObject
+@property(nonatomic, readonly) NSString* contentType;  // Defaults to "text/plain" per specifications if undefined
+@property(nonatomic, readonly) NSString* mimeType;
+@end
+
+@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart
+@property(nonatomic, readonly) NSData* data;
+@property(nonatomic, readonly) NSString* string;  // May be nil (only valid for text mime types)
+@end
+
+@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart
+@property(nonatomic, readonly) NSString* fileName;  // May be nil
+@property(nonatomic, readonly) NSString* temporaryPath;
+@end
+
+@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest
+@property(nonatomic, readonly) NSDictionary* arguments;
+@property(nonatomic, readonly) NSDictionary* files;
++ (NSString*)mimeType;
+@end

+ 388 - 0
CGDWebServer/GCDWebServerMultiPartFormRequest.m

@@ -0,0 +1,388 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "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;
+
+@interface GCDWebServerMultiPart () {
+@private
+  NSString* _contentType;
+  NSString* _mimeType;
+}
+@end
+
+@implementation GCDWebServerMultiPart
+
+@synthesize contentType=_contentType, mimeType=_mimeType;
+
+- (id)initWithContentType:(NSString*)contentType {
+  if ((self = [super init])) {
+    _contentType = [contentType copy];
+    _mimeType = ARC_RETAIN(GCDWebServerTruncateHeaderValue(_contentType));
+  }
+  return self;
+}
+
+- (void)dealloc {
+  ARC_RELEASE(_contentType);
+  ARC_RELEASE(_mimeType);
+  
+  ARC_DEALLOC(super);
+}
+
+@end
+
+@interface GCDWebServerMultiPartArgument () {
+@private
+  NSData* _data;
+  NSString* _string;
+}
+@end
+
+@implementation GCDWebServerMultiPartArgument
+
+@synthesize data=_data, string=_string;
+
+- (id)initWithContentType:(NSString*)contentType data:(NSData*)data {
+  if ((self = [super initWithContentType:contentType])) {
+    _data = ARC_RETAIN(data);
+    
+    if ([self.contentType hasPrefix:@"text/"]) {
+      NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
+      _string = [[NSString alloc] initWithData:_data encoding:GCDWebServerStringEncodingFromCharset(charset)];
+    }
+  }
+  return self;
+}
+
+- (void)dealloc {
+  ARC_RELEASE(_data);
+  ARC_RELEASE(_string);
+  
+  ARC_DEALLOC(super);
+}
+
+- (NSString*)description {
+  return [NSString stringWithFormat:@"<%@ | '%@' | %lu bytes>", [self class], self.mimeType, (unsigned long)_data.length];
+}
+
+@end
+
+@interface GCDWebServerMultiPartFile () {
+@private
+  NSString* _fileName;
+  NSString* _temporaryPath;
+}
+@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]);
+  
+  ARC_RELEASE(_fileName);
+  ARC_RELEASE(_temporaryPath);
+  
+  ARC_DEALLOC(super);
+}
+
+- (NSString*)description {
+  return [NSString stringWithFormat:@"<%@ | '%@' | '%@>'", [self class], self.mimeType, _fileName];
+}
+
+@end
+
+@interface GCDWebServerMultiPartFormRequest () {
+@private
+  NSData* _boundary;
+  
+  NSUInteger _parserState;
+  NSMutableData* _parserData;
+  NSString* _controlName;
+  NSString* _fileName;
+  NSString* _contentType;
+  NSString* _tmpPath;
+  int _tmpFile;
+  
+  NSMutableDictionary* _arguments;
+  NSMutableDictionary* _files;
+}
+@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";
+}
+
+- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
+  if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
+    NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
+    if (boundary) {
+      NSData* data = [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding];
+      _boundary = ARC_RETAIN(data);
+    }
+    if (_boundary == nil) {
+      DNOT_REACHED();
+      ARC_RELEASE(self);
+      return nil;
+    }
+    
+    _arguments = [[NSMutableDictionary alloc] init];
+    _files = [[NSMutableDictionary alloc] init];
+  }
+  return self;
+}
+
+- (void)dealloc {
+  ARC_RELEASE(_arguments);
+  ARC_RELEASE(_files);
+  ARC_RELEASE(_boundary);
+  
+  ARC_DEALLOC(super);
+}
+
+- (BOOL)open:(NSError**)error {
+  _parserData = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
+  _parserState = kParserState_Start;
+  return YES;
+}
+
+// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
+- (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) {
+      
+      ARC_RELEASE(_controlName);
+      _controlName = nil;
+      ARC_RELEASE(_fileName);
+      _fileName = nil;
+      ARC_RELEASE(_contentType);
+      _contentType = nil;
+      ARC_RELEASE(_tmpPath);
+      _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 = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(message));
+        NSString* contentDisposition = GCDWebServerNormalizeHeaderValue([headers objectForKey:@"Content-Disposition"]);
+        if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
+          controlName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name");
+          fileName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename");
+        }
+        _controlName = [controlName copy];
+        _fileName = [fileName copy];
+        _contentType = ARC_RETAIN(GCDWebServerNormalizeHeaderValue([headers objectForKey:@"Content-Type"]));
+        if (_contentType == nil) {
+          _contentType = @"text/plain";
+        }
+      }
+      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) {
+            ssize_t result = write(_tmpFile, dataBytes, dataLength);
+            if (result == (ssize_t)dataLength) {
+              if (close(_tmpFile) == 0) {
+                _tmpFile = 0;
+                GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithContentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
+                [_files setObject:file forKey:_controlName];
+                ARC_RELEASE(file);
+              } else {
+                DNOT_REACHED();
+                success = NO;
+              }
+            } else {
+              DNOT_REACHED();
+              success = NO;
+            }
+            ARC_RELEASE(_tmpPath);
+            _tmpPath = nil;
+          } else {
+            NSData* data = [[NSData alloc] initWithBytes:(void*)dataBytes length:dataLength];
+            GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithContentType:_contentType data:data];
+            [_arguments setObject:argument forKey:_controlName];
+            ARC_RELEASE(argument);
+            ARC_RELEASE(data);
+          }
+        }
+        
+        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;
+        ssize_t result = write(_tmpFile, _parserData.bytes, length);
+        if (result == (ssize_t)length) {
+          [_parserData replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
+        } else {
+          DNOT_REACHED();
+          success = NO;
+        }
+      }
+    }
+  }
+  return success;
+}
+
+- (BOOL)writeData:(NSData*)data error:(NSError**)error {
+  [_parserData appendBytes:data.bytes length:data.length];
+  if (![self _parseData]) {
+    *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
+    return NO;
+  }
+  return YES;
+}
+
+- (BOOL)close:(NSError**)error {
+  ARC_RELEASE(_parserData);
+  _parserData = nil;
+  ARC_RELEASE(_controlName);
+  _controlName = nil;
+  ARC_RELEASE(_fileName);
+  _fileName = nil;
+  ARC_RELEASE(_contentType);
+  _contentType = nil;
+  if (_tmpFile > 0) {
+    close(_tmpFile);
+    unlink([_tmpPath fileSystemRepresentation]);
+    _tmpFile = 0;
+  }
+  ARC_RELEASE(_tmpPath);
+  _tmpPath = nil;
+  if (_parserState != kParserState_End) {
+    *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
+    return NO;
+  }
+  return YES;
+}
+
+- (NSString*)description {
+  NSMutableString* description = [NSMutableString stringWithString:[super description]];
+  if (_arguments.count) {
+    [description appendString:@"\n"];
+    for (NSString* key in [[_arguments allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
+      GCDWebServerMultiPartArgument* argument = [_arguments objectForKey:key];
+      [description appendFormat:@"\n%@ (%@)\n", key, argument.contentType];
+      [description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)];
+    }
+  }
+  if (_files.count) {
+    [description appendString:@"\n"];
+    for (NSString* key in [[_files allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
+      GCDWebServerMultiPartFile* file = [_files objectForKey:key];
+      [description appendFormat:@"\n%@ (%@): %@\n{%@}", key, file.contentType, file.fileName, file.temporaryPath];
+    }
+  }
+  return description;
+}
+
+@end

+ 42 - 0
CGDWebServer/GCDWebServerPrivate.h

@@ -50,8 +50,20 @@
 #define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
 #endif
 
+#import "GCDWebServerHTTPStatusCodes.h"
+
 #import "GCDWebServerConnection.h"
 
+#import "GCDWebServerDataRequest.h"
+#import "GCDWebServerFileRequest.h"
+#import "GCDWebServerMultiPartFormRequest.h"
+#import "GCDWebServerURLEncodedFormRequest.h"
+
+#import "GCDWebServerDataResponse.h"
+#import "GCDWebServerErrorResponse.h"
+#import "GCDWebServerFileResponse.h"
+#import "GCDWebServerStreamingResponse.h"
+
 #ifdef __GCDWEBSERVER_LOGGING_HEADER__
 
 // Define __GCDWEBSERVER_LOGGING_HEADER__ as a preprocessor constant to redirect GCDWebServer logging to your own system
@@ -90,6 +102,19 @@ extern void GCDLogMessage(long level, NSString* format, ...) NS_FORMAT_FUNCTION(
 
 #define kGCDWebServerDefaultMimeType @"application/octet-stream"
 #define kGCDWebServerGCDQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
+#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
+
+static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
+  return ((range.location != NSNotFound) || (range.length > 0));
+}
+
+extern NSString* GCDWebServerNormalizeHeaderValue(NSString* value);
+extern NSString* GCDWebServerTruncateHeaderValue(NSString* value);
+extern NSString* GCDWebServerExtractHeaderValueParameter(NSString* header, NSString* attribute);
+extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset);
+extern NSString* GCDWebServerFormatHTTPDate(NSDate* date);
+extern NSDate* GCDWebServerParseHTTPDate(NSString* string);
+extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType);
 
 @interface GCDWebServerConnection ()
 - (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket;
@@ -104,3 +129,20 @@ extern void GCDLogMessage(long level, NSString* format, ...) NS_FORMAT_FUNCTION(
 @property(nonatomic, readonly) GCDWebServerProcessBlock processBlock;
 - (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
 @end
+
+@interface GCDWebServerRequest ()
+@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
+- (void)prepareForWriting;
+- (BOOL)performOpen:(NSError**)error;
+- (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
+- (BOOL)performClose:(NSError**)error;
+@end
+
+@interface GCDWebServerResponse ()
+@property(nonatomic, readonly) NSDictionary* additionalHeaders;
+@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
+- (void)prepareForReading;
+- (BOOL)performOpen:(NSError**)error;
+- (NSData*)performReadData:(NSError**)error;
+- (void)performClose;
+@end

+ 15 - 45
CGDWebServer/GCDWebServerRequest.h

@@ -27,55 +27,25 @@
 
 #import <Foundation/Foundation.h>
 
-@interface GCDWebServerRequest : NSObject
+@protocol GCDWebServerBodyWriter <NSObject>
+- (BOOL)open:(NSError**)error;  // Return NO on error ("error" is guaranteed to be non-NULL)
+- (BOOL)writeData:(NSData*)data error:(NSError**)error;  // Return NO on error ("error" is guaranteed to be non-NULL)
+- (BOOL)close:(NSError**)error;  // Return NO on error ("error" is guaranteed to be non-NULL)
+@end
+
+@interface GCDWebServerRequest : NSObject <GCDWebServerBodyWriter>
 @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
+@property(nonatomic, readonly) NSString* contentType;  // Automatically parsed from headers (nil if request has no body or set to "application/octet-stream" if a body is present without a "Content-Type" header)
+@property(nonatomic, readonly) NSUInteger contentLength;  // Automatically parsed from headers (NSNotFound if request has no "Content-Length" header)
+@property(nonatomic, readonly) NSDate* ifModifiedSince;  // Automatically parsed from headers (nil if request has no "If-Modified-Since" header or it is malformatted)
+@property(nonatomic, readonly) NSString* ifNoneMatch;  // Automatically parsed from headers (nil if request has no "If-None-Match" header)
 @property(nonatomic, readonly) NSRange byteRange;  // Automatically parsed from headers ([NSNotFound, 0] if request has no "Range" header, [offset, length] for byte range from beginning or [NSNotFound, -bytes] from end)
-- (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
-@property(nonatomic, readonly) NSData* data;  // Only valid after open / write / close sequence
-@end
-
-@interface GCDWebServerFileRequest : GCDWebServerRequest
-@property(nonatomic, readonly) NSString* filePath;  // Only valid after open / write / close sequence
-@end
-
-@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest
-@property(nonatomic, readonly) NSDictionary* arguments;  // Only valid after open / write / close sequence
-+ (NSString*)mimeType;
-@end
-
-@interface GCDWebServerMultiPart : NSObject
-@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
-@property(nonatomic, readonly) NSData* data;
-@property(nonatomic, readonly) NSString* string;  // May be nil (only valid for text mime types)
-@end
-
-@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart
-@property(nonatomic, readonly) NSString* fileName;  // May be nil
-@property(nonatomic, readonly) NSString* temporaryPath;
-@end
-
-@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest
-@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;
+@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding;
+- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query;
+- (BOOL)hasBody;  // Convenience method that checks if "contentType" is not nil
+- (BOOL)hasByteRange;  // Convenience method that checks "byteRange"
 @end

+ 171 - 470
CGDWebServer/GCDWebServerRequest.m

@@ -25,124 +25,143 @@
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+#import <zlib.h>
+
 #import "GCDWebServerPrivate.h"
 
-#define kMultiPartBufferSize (256 * 1024)
+#define kZlibErrorDomain @"ZlibErrorDomain"
+#define kGZipInitialBufferSize (256 * 1024)
 
-enum {
-  kParserState_Undefined = 0,
-  kParserState_Start,
-  kParserState_Headers,
-  kParserState_Content,
-  kParserState_End
-};
+@interface GCDWebServerBodyDecoder : NSObject <GCDWebServerBodyWriter>
+- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id<GCDWebServerBodyWriter>)writer;
+@end
 
-@interface GCDWebServerRequest () {
-@private
-  NSString* _method;
-  NSURL* _url;
-  NSDictionary* _headers;
-  NSString* _path;
-  NSDictionary* _query;
-  NSString* _type;
-  NSUInteger _length;
-  NSRange _range;
-}
+@interface GCDWebServerGZipDecoder : GCDWebServerBodyDecoder
 @end
 
-@interface GCDWebServerDataRequest () {
+@interface GCDWebServerBodyDecoder () {
 @private
-  NSMutableData* _data;
+  GCDWebServerRequest* __unsafe_unretained _request;
+  id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
 }
 @end
 
-@interface GCDWebServerFileRequest () {
-@private
-  NSString* _filePath;
-  int _file;
+@implementation GCDWebServerBodyDecoder
+
+- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id<GCDWebServerBodyWriter>)writer {
+  if ((self = [super init])) {
+    _request = request;
+    _writer = writer;
+  }
+  return self;
 }
-@end
 
-@interface GCDWebServerURLEncodedFormRequest () {
-@private
-  NSDictionary* _arguments;
+- (BOOL)open:(NSError**)error {
+  return [_writer open:error];
 }
-@end
 
-@interface GCDWebServerMultiPart () {
-@private
-  NSString* _contentType;
-  NSString* _mimeType;
+- (BOOL)writeData:(NSData*)data error:(NSError**)error {
+  return [_writer writeData:data error:error];
 }
-@end
 
-@interface GCDWebServerMultiPartArgument () {
-@private
-  NSData* _data;
-  NSString* _string;
+- (BOOL)close:(NSError**)error {
+  return [_writer close:error];
 }
+
 @end
 
-@interface GCDWebServerMultiPartFile () {
+@interface GCDWebServerGZipDecoder () {
 @private
-  NSString* _fileName;
-  NSString* _temporaryPath;
+  z_stream _stream;
+  BOOL _finished;
 }
 @end
 
-@interface GCDWebServerMultiPartFormRequest () {
-@private
-  NSData* _boundary;
-  
-  NSUInteger _parserState;
-  NSMutableData* _parserData;
-  NSString* _controlName;
-  NSString* _fileName;
-  NSString* _contentType;
-  NSString* _tmpPath;
-  int _tmpFile;
-  
-  NSMutableDictionary* _arguments;
-  NSMutableDictionary* _files;
+@implementation GCDWebServerGZipDecoder
+
+- (BOOL)open:(NSError**)error {
+  int result = inflateInit2(&_stream, 15 + 16);
+  if (result != Z_OK) {
+    *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
+    return NO;
+  }
+  if (![super open:error]) {
+    deflateEnd(&_stream);
+    return NO;
+  }
+  return YES;
 }
-@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];
+- (BOOL)writeData:(NSData*)data error:(NSError**)error {
+  DCHECK(!_finished);
+  _stream.next_in = (Bytef*)data.bytes;
+  _stream.avail_in = (uInt)data.length;
+  NSMutableData* decodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
+  if (decodedData == nil) {
+    DNOT_REACHED();
+    return NO;
+  }
+  NSUInteger length = 0;
+  while (1) {
+    NSUInteger maxLength = decodedData.length - length;
+    _stream.next_out = (Bytef*)((char*)decodedData.mutableBytes + length);
+    _stream.avail_out = (uInt)maxLength;
+    int result = inflate(&_stream, Z_NO_FLUSH);
+    if ((result != Z_OK) && (result != Z_STREAM_END)) {
+      ARC_RELEASE(decodedData);
+      *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
+      return NO;
+    }
+    length += maxLength - _stream.avail_out;
+    if (_stream.avail_out > 0) {
+      if (result == Z_STREAM_END) {
+        _finished = YES;
       }
+      break;
     }
-    ARC_RELEASE(scanner);
+    decodedData.length = 2 * decodedData.length;  // zlib has used all the output buffer so resize it and try again in case more data is available
   }
-  return value;
+  decodedData.length = length;
+  BOOL success = length ? [super writeData:decodedData error:error] : YES;  // No need to call writer if we have no data yet
+  ARC_RELEASE(decodedData);
+  return success;
 }
 
-// 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);
+- (BOOL)close:(NSError**)error {
+  DCHECK(_finished);
+  inflateEnd(&_stream);
+  return [super close:error];
+}
+
+@end
+
+@interface GCDWebServerRequest () {
+@private
+  NSString* _method;
+  NSURL* _url;
+  NSDictionary* _headers;
+  NSString* _path;
+  NSDictionary* _query;
+  NSString* _type;
+  BOOL _chunked;
+  NSUInteger _length;
+  NSDate* _modifiedSince;
+  NSString* _noneMatch;
+  NSRange _range;
+  BOOL _gzipAccepted;
+  
+  BOOL _opened;
+  NSMutableArray* _decoders;
+  id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
 }
+@end
 
 @implementation GCDWebServerRequest : NSObject
 
-@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, byteRange=_range;
+@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, ifModifiedSince=_modifiedSince, ifNoneMatch=_noneMatch,
+            byteRange=_range, acceptsGzipContentEncoding=_gzipAccepted, usesChunkedTransferEncoding=_chunked;
 
-- (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
+- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
   if ((self = [super init])) {
     _method = [method copy];
     _url = ARC_RETAIN(url);
@@ -150,24 +169,42 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
     _path = [path copy];
     _query = ARC_RETAIN(query);
     
-    _type = ARC_RETAIN([_headers objectForKey:@"Content-Type"]);
+    _type = ARC_RETAIN(GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]));
+    _chunked = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"];
     NSString* lengthHeader = [_headers objectForKey:@"Content-Length"];
-    if (_type) {
+    if (lengthHeader) {
       NSInteger length = [lengthHeader integerValue];
-      if ((lengthHeader == nil) || (length < 0)) {
+      if (_chunked || (length < 0)) {
         DNOT_REACHED();
         ARC_RELEASE(self);
         return nil;
       }
       _length = length;
-    } else if (lengthHeader) {
-      DNOT_REACHED();
-      ARC_RELEASE(self);
-      return nil;
+      if (_type == nil) {
+        _type = kGCDWebServerDefaultMimeType;
+      }
+    } else if (_chunked) {
+      if (_type == nil) {
+        _type = kGCDWebServerDefaultMimeType;
+      }
+      _length = NSNotFound;
+    } else {
+      if (_type) {
+        DNOT_REACHED();
+        ARC_RELEASE(self);
+        return nil;
+      }
+      _length = NSNotFound;
     }
     
+    NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
+    if (modifiedHeader) {
+      _modifiedSince = [GCDWebServerParseHTTPDate(modifiedHeader) copy];
+    }
+    _noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]);
+    
     _range = NSMakeRange(NSNotFound, 0);
-    NSString* rangeHeader = [[_headers objectForKey:@"Range"] lowercaseString];
+    NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
     if (rangeHeader) {
       if ([rangeHeader hasPrefix:@"bytes="]) {
         NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","];
@@ -195,6 +232,12 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
         LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
       }
     }
+    
+    if ([[_headers objectForKey:@"Accept-Encoding"] rangeOfString:@"gzip"].location != NSNotFound) {
+      _gzipAccepted = YES;
+    }
+    
+    _decoders = [[NSMutableArray alloc] init];
   }
   return self;
 }
@@ -206,6 +249,9 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
   ARC_RELEASE(_path);
   ARC_RELEASE(_query);
   ARC_RELEASE(_type);
+  ARC_RELEASE(_modifiedSince);
+  ARC_RELEASE(_noneMatch);
+  ARC_RELEASE(_decoders);
   
   ARC_DEALLOC(super);
 }
@@ -214,408 +260,63 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
   return _type ? YES : NO;
 }
 
-@end
-
-@implementation GCDWebServerRequest (Subclassing)
-
-- (BOOL)open {
-  [self doesNotRecognizeSelector:_cmd];
-  return NO;
+- (BOOL)hasByteRange {
+  return GCDWebServerIsValidByteRange(_range);
 }
 
-- (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);
-  ARC_RELEASE(_data);
-  
-  ARC_DEALLOC(super);
-}
-
-- (BOOL)open {
-  DCHECK(_data == nil);
-  _data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
-  return _data ? YES : NO;
+- (BOOL)open:(NSError**)error {
+  return YES;
 }
 
-- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length {
-  DCHECK(_data != nil);
-  [_data appendBytes:buffer length:length];
-  return length;
+- (BOOL)writeData:(NSData*)data error:(NSError**)error {
+  return YES;
 }
 
-- (BOOL)close {
-  DCHECK(_data != nil);
+- (BOOL)close:(NSError**)error {
   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 = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
+- (void)prepareForWriting {
+  _writer = self;
+  if ([GCDWebServerNormalizeHeaderValue([self.headers objectForKey:@"Content-Encoding"]) isEqualToString:@"gzip"]) {
+    GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer];
+    [_decoders addObject:decoder];
+    ARC_RELEASE(decoder);
+    _writer = decoder;
   }
-  return self;
-}
-
-- (void)dealloc {
-  DCHECK(_file < 0);
-  unlink([_filePath fileSystemRepresentation]);
-  ARC_RELEASE(_filePath);
-  
-  ARC_DEALLOC(super);
-}
-
-- (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 {
-  ARC_RELEASE(_arguments);
-  
-  ARC_DEALLOC(super);
-}
-
-- (BOOL)close {
-  if (![super close]) {
+- (BOOL)performOpen:(NSError**)error {
+  DCHECK(_type);
+  DCHECK(_writer);
+  if (_opened) {
+    DNOT_REACHED();
     return NO;
   }
-  
-  NSString* charset = _ExtractHeaderParameter(self.contentType, @"charset");
-  NSString* string = [[NSString alloc] initWithData:self.data encoding:_StringEncodingFromCharset(charset)];
-  _arguments = ARC_RETAIN(GCDWebServerParseURLEncodedForm(string));
-  ARC_RELEASE(string);
-  
-  return (_arguments ? YES : NO);
+  _opened = YES;
+  return [_writer open:error];
 }
 
-@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 = ARC_RETAIN([[components objectAtIndex:0] lowercaseString]);
-    }
-    if (_mimeType == nil) {
-      _mimeType = @"text/plain";
-    }
-  }
-  return self;
+- (BOOL)performWriteData:(NSData*)data error:(NSError**)error {
+  DCHECK(_opened);
+  return [_writer writeData:data error:error];
 }
 
-- (void)dealloc {
-  ARC_RELEASE(_contentType);
-  ARC_RELEASE(_mimeType);
-  
-  ARC_DEALLOC(super);
-}
-
-@end
-
-@implementation GCDWebServerMultiPartArgument
-
-@synthesize data=_data, string=_string;
-
-- (id)initWithContentType:(NSString*)contentType data:(NSData*)data {
-  if ((self = [super initWithContentType:contentType])) {
-    _data = ARC_RETAIN(data);
-    
-    if ([self.mimeType hasPrefix:@"text/"]) {
-      NSString* charset = _ExtractHeaderParameter(self.contentType, @"charset");
-      _string = [[NSString alloc] initWithData:_data encoding:_StringEncodingFromCharset(charset)];
-    }
-  }
-  return self;
-}
-
-- (void)dealloc {
-  ARC_RELEASE(_data);
-  ARC_RELEASE(_string);
-  
-  ARC_DEALLOC(super);
+- (BOOL)performClose:(NSError**)error {
+  DCHECK(_opened);
+  return [_writer close:error];
 }
 
 - (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];
+  NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path];
+  for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
+    [description appendFormat:@"\n  %@ = %@", argument, [_query objectForKey:argument]];
   }
-  return self;
-}
-
-- (void)dealloc {
-  unlink([_temporaryPath fileSystemRepresentation]);
-  
-  ARC_RELEASE(_fileName);
-  ARC_RELEASE(_temporaryPath);
-  
-  ARC_DEALLOC(super);
-}
-
-- (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) {
-      NSData* data = [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding];
-      _boundary = ARC_RETAIN(data);
-    }
-    if (_boundary == nil) {
-      DNOT_REACHED();
-      ARC_RELEASE(self);
-      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) {
-      
-      ARC_RELEASE(_controlName);
-      _controlName = nil;
-      ARC_RELEASE(_fileName);
-      _fileName = nil;
-      ARC_RELEASE(_contentType);
-      _contentType = nil;
-      ARC_RELEASE(_tmpPath);
-      _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 = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(message));
-        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 = ARC_RETAIN([headers objectForKey:@"Content-Type"]);
-      }
-      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) {
-            ssize_t result = write(_tmpFile, dataBytes, dataLength);
-            if (result == (ssize_t)dataLength) {
-              if (close(_tmpFile) == 0) {
-                _tmpFile = 0;
-                GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithContentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
-                [_files setObject:file forKey:_controlName];
-                ARC_RELEASE(file);
-              } else {
-                DNOT_REACHED();
-                success = NO;
-              }
-            } else {
-              DNOT_REACHED();
-              success = NO;
-            }
-            ARC_RELEASE(_tmpPath);
-            _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];
-            ARC_RELEASE(argument);
-            ARC_RELEASE(data);
-          }
-        }
-        
-        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;
-        ssize_t result = write(_tmpFile, _parserData.bytes, length);
-        if (result == (ssize_t)length) {
-          [_parserData replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
-        } else {
-          DNOT_REACHED();
-          success = NO;
-        }
-      }
-    }
+  [description appendString:@"\n"];
+  for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
+    [description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]];
   }
-  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);
-  ARC_RELEASE(_parserData);
-  _parserData = nil;
-  ARC_RELEASE(_controlName);
-  _controlName = nil;
-  ARC_RELEASE(_fileName);
-  _fileName = nil;
-  ARC_RELEASE(_contentType);
-  _contentType = nil;
-  if (_tmpFile > 0) {
-    close(_tmpFile);
-    unlink([_tmpPath fileSystemRepresentation]);
-    _tmpFile = 0;
-  }
-  ARC_RELEASE(_tmpPath);
-  _tmpPath = nil;
-  return (_parserState == kParserState_End ? YES : NO);
-}
-
-- (void)dealloc {
-  DCHECK(_parserData == nil);
-  ARC_RELEASE(_arguments);
-  ARC_RELEASE(_files);
-  ARC_RELEASE(_boundary);
-  
-  ARC_DEALLOC(super);
+  return description;
 }
 
 @end

+ 20 - 54
CGDWebServer/GCDWebServerResponse.h

@@ -27,63 +27,29 @@
 
 #import <Foundation/Foundation.h>
 
-typedef NSData* (^GCDWebServerChunkBlock)();
-
-@interface GCDWebServerResponse : NSObject
-@property(nonatomic, copy) NSString* contentType;  // Default is nil i.e. no body
-@property(nonatomic) NSUInteger contentLength;  // Default is NSNotFound i.e. undefined
-@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;
-- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header;
-- (BOOL)hasBody;  // Convenience method
+@protocol GCDWebServerBodyReader <NSObject>
+- (BOOL)open:(NSError**)error;  // Return NO on error ("error" is guaranteed to be non-NULL)
+- (NSData*)readData:(NSError**)error;  // Must return nil on error or empty NSData if at end ("error" is guaranteed to be non-NULL)
+- (void)close;
 @end
 
-@interface GCDWebServerResponse (Subclassing)
-- (BOOL)open;  // Implementation required
-- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length;  // Implementation required
-- (BOOL)close;  // Implementation required
+@interface GCDWebServerResponse : NSObject <GCDWebServerBodyReader>
+@property(nonatomic, copy) NSString* contentType;  // Default is nil i.e. no body (must be set if a body is present)
+@property(nonatomic) NSUInteger contentLength;  // Default is NSNotFound i.e. undefined (if a body is present but length is undefined, chunked transfer encoding will be enabled)
+@property(nonatomic) NSInteger statusCode;  // Default is 200
+@property(nonatomic) NSUInteger cacheControlMaxAge;  // Default is 0 seconds i.e. "Cache-Control: no-cache"
+@property(nonatomic, retain) NSDate* lastModifiedDate;  // Default is nil i.e. no "Last-Modified" header
+@property(nonatomic, copy) NSString* eTag;  // Default is nil i.e. no "ETag" header
+@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled;  // Default is disabled
++ (instancetype)response;
+- (instancetype)init;
+- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header;  // Pass nil value to remove header
+- (BOOL)hasBody;  // Convenience method that checks if "contentType" is not nil
 @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
-+ (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;
-+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object;
-+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object contentType:(NSString*)type;
-- (id)initWithText:(NSString*)text;  // Encodes using UTF-8
-- (id)initWithHTML:(NSString*)html;  // Encodes using UTF-8
-- (id)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;  // Simple template system that replaces all occurences of "%variable%" with corresponding value (encodes using UTF-8)
-- (id)initWithJSONObject:(id)object;
-- (id)initWithJSONObject:(id)object contentType:(NSString*)type;
-@end
-
-@interface GCDWebServerFileResponse : GCDWebServerResponse
-+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path;
-+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
-+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range;
-+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
-- (id)initWithFile:(NSString*)path;
-- (id)initWithFile:(NSString*)path isAttachment:(BOOL)attachment;
-- (id)initWithFile:(NSString*)path byteRange:(NSRange)range;  // Pass [NSNotFound, 0] to disable byte range entirely, [offset, length] to enable byte range from beginning of file or [NSNotFound, -bytes] from end of file
-- (id)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
-@end
-
-@interface GCDWebServerChunkedResponse : GCDWebServerResponse  // Use chunked transfer encoding
-+ (GCDWebServerChunkedResponse*)responseWithContentType:(NSString*)type chunkBlock:(GCDWebServerChunkBlock)block;
-- (id)initWithContentType:(NSString*)type chunkBlock:(GCDWebServerChunkBlock)block;  // Return nil when done
++ (instancetype)responseWithStatusCode:(NSInteger)statusCode;
++ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
+- (instancetype)initWithStatusCode:(NSInteger)statusCode;
+- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
 @end

+ 183 - 335
CGDWebServer/GCDWebServerResponse.m

@@ -25,436 +25,284 @@
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#import <sys/stat.h>
+#import <zlib.h>
 
 #import "GCDWebServerPrivate.h"
 
-@interface GCDWebServerResponse () {
-@private
-  NSString* _type;
-  NSUInteger _length;
-  NSInteger _status;
-  NSUInteger _maxAge;
-  NSMutableDictionary* _headers;
-}
-@end
+#define kZlibErrorDomain @"ZlibErrorDomain"
+#define kGZipInitialBufferSize (256 * 1024)
 
-@interface GCDWebServerDataResponse () {
-@private
-  NSData* _data;
-  NSInteger _offset;
-}
+@interface GCDWebServerBodyEncoder : NSObject <GCDWebServerBodyReader>
+- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader;
 @end
 
-@interface GCDWebServerFileResponse () {
-@private
-  NSString* _path;
-  NSUInteger _offset;
-  NSUInteger _size;
-  int _file;
-}
+@interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder
 @end
 
-@interface GCDWebServerChunkedResponse () {
+@interface GCDWebServerBodyEncoder () {
 @private
-  GCDWebServerChunkBlock _block;
-  NSData* _chunk;
-  NSUInteger _offset;
-  BOOL _terminated;
+  GCDWebServerResponse* __unsafe_unretained _response;
+  id<GCDWebServerBodyReader> __unsafe_unretained _reader;
 }
 @end
 
-@implementation GCDWebServerResponse
-
-@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, additionalHeaders=_headers;
-
-+ (GCDWebServerResponse*)response {
-  return ARC_AUTORELEASE([[[self class] alloc] init]);
-}
+@implementation GCDWebServerBodyEncoder
 
-- (id)init {
+- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
   if ((self = [super init])) {
-    _type = nil;
-    _length = NSNotFound;
-    _status = 200;
-    _maxAge = 0;
-    _headers = [[NSMutableDictionary alloc] init];
+    _response = response;
+    _reader = reader;
   }
   return self;
 }
 
-- (void)dealloc {
-  ARC_RELEASE(_type);
-  ARC_RELEASE(_headers);
-  
-  ARC_DEALLOC(super);
+- (BOOL)open:(NSError**)error {
+  return [_reader open:error];
 }
 
-- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header {
-  [_headers setValue:value forKey:header];
+- (NSData*)readData:(NSError**)error {
+  return [_reader readData:error];
 }
 
-- (BOOL)hasBody {
-  return _type ? YES : NO;
+- (void)close {
+  [_reader close];
 }
 
 @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;
+@interface GCDWebServerGZipEncoder () {
+@private
+  z_stream _stream;
+  BOOL _finished;
 }
-
 @end
 
-@implementation GCDWebServerResponse (Extensions)
+@implementation GCDWebServerGZipEncoder
 
-+ (GCDWebServerResponse*)responseWithStatusCode:(NSInteger)statusCode {
-  return ARC_AUTORELEASE([[self alloc] initWithStatusCode:statusCode]);
+- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
+  if ((self = [super initWithResponse:response reader:reader])) {
+    response.contentLength = NSNotFound;  // Make sure "Content-Length" header is not set since we don't know it (client will determine body length when connection is closed)
+    [response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
+  }
+  return self;
 }
 
-+ (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
-  return ARC_AUTORELEASE([[self alloc] initWithRedirect:location permanent:permanent]);
+- (BOOL)open:(NSError**)error {
+  int result = deflateInit2(&_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
+  if (result != Z_OK) {
+    *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
+    return NO;
+  }
+  if (![super open:error]) {
+    deflateEnd(&_stream);
+    return NO;
+  }
+  return YES;
 }
 
-- (id)initWithStatusCode:(NSInteger)statusCode {
-  if ((self = [self init])) {
-    self.statusCode = statusCode;
+- (NSData*)readData:(NSError**)error {
+  NSMutableData* encodedData;
+  if (_finished) {
+    encodedData = [[NSMutableData alloc] init];
+  } else {
+    encodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
+    if (encodedData == nil) {
+      DNOT_REACHED();
+      return nil;
+    }
+    NSUInteger length = 0;
+    do {
+      NSData* data = [super readData:error];
+      if (data == nil) {
+        return nil;
+      }
+      _stream.next_in = (Bytef*)data.bytes;
+      _stream.avail_in = (uInt)data.length;
+      while (1) {
+        NSUInteger maxLength = encodedData.length - length;
+        _stream.next_out = (Bytef*)((char*)encodedData.mutableBytes + length);
+        _stream.avail_out = (uInt)maxLength;
+        int result = deflate(&_stream, data.length ? Z_NO_FLUSH : Z_FINISH);
+        if (result == Z_STREAM_END) {
+          _finished = YES;
+        } else if (result != Z_OK) {
+          ARC_RELEASE(encodedData);
+          *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
+          return nil;
+        }
+        length += maxLength - _stream.avail_out;
+        if (_stream.avail_out > 0) {
+          break;
+        }
+        encodedData.length = 2 * encodedData.length;  // zlib has used all the output buffer so resize it and try again in case more data is available
+      }
+      DCHECK(_stream.avail_in == 0);
+    } while (length == 0);  // Make sure we don't return an empty NSData if not in finished state
+    encodedData.length = length;
   }
-  return self;
+  return ARC_AUTORELEASE(encodedData);
 }
 
-- (id)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
-  if ((self = [self init])) {
-    self.statusCode = permanent ? 301 : 307;
-    [self setValue:[location absoluteString] forAdditionalHeader:@"Location"];
-  }
-  return self;
+- (void)close {
+  deflateEnd(&_stream);
+  [super close];
 }
 
 @end
 
-@implementation GCDWebServerDataResponse
+@interface GCDWebServerResponse () {
+@private
+  NSString* _type;
+  NSUInteger _length;
+  NSInteger _status;
+  NSUInteger _maxAge;
+  NSDate* _lastModified;
+  NSString* _eTag;
+  NSMutableDictionary* _headers;
+  BOOL _chunked;
+  BOOL _gzipped;
+  
+  BOOL _opened;
+  NSMutableArray* _encoders;
+  id<GCDWebServerBodyReader> __unsafe_unretained _reader;
+}
+@end
 
-+ (GCDWebServerDataResponse*)responseWithData:(NSData*)data contentType:(NSString*)type {
-  return ARC_AUTORELEASE([[[self class] alloc] initWithData:data contentType:type]);
+@implementation GCDWebServerResponse
+
+@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, lastModifiedDate=_lastModified, eTag=_eTag,
+            gzipContentEncodingEnabled=_gzipped, additionalHeaders=_headers;
+
++ (instancetype)response {
+  return ARC_AUTORELEASE([[[self class] alloc] init]);
 }
 
-- (id)initWithData:(NSData*)data contentType:(NSString*)type {
-  if (data == nil) {
-    DNOT_REACHED();
-    ARC_RELEASE(self);
-    return nil;
-  }
-  
+- (instancetype)init {
   if ((self = [super init])) {
-    _data = ARC_RETAIN(data);
-    _offset = -1;
-    
-    self.contentType = type;
-    self.contentLength = data.length;
+    _type = nil;
+    _length = NSNotFound;
+    _status = kGCDWebServerHTTPStatusCode_OK;
+    _maxAge = 0;
+    _headers = [[NSMutableDictionary alloc] init];
+    _encoders = [[NSMutableArray alloc] init];
   }
   return self;
 }
 
 - (void)dealloc {
-  DCHECK(_offset < 0);
-  ARC_RELEASE(_data);
+  ARC_RELEASE(_type);
+  ARC_RELEASE(_lastModified);
+  ARC_RELEASE(_eTag);
+  ARC_RELEASE(_headers);
+  ARC_RELEASE(_encoders);
   
   ARC_DEALLOC(super);
 }
 
-- (BOOL)open {
-  DCHECK(_offset < 0);
-  _offset = 0;
-  return YES;
-}
-
-- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
-  DCHECK(_offset >= 0);
-  NSInteger size = 0;
-  if (_offset < (NSInteger)_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;
+- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header {
+  [_headers setValue:value forKey:header];
 }
 
-@end
-
-@implementation GCDWebServerDataResponse (Extensions)
-
-+ (GCDWebServerDataResponse*)responseWithText:(NSString*)text {
-  return ARC_AUTORELEASE([[self alloc] initWithText:text]);
+- (BOOL)hasBody {
+  return _type ? YES : NO;
 }
 
-+ (GCDWebServerDataResponse*)responseWithHTML:(NSString*)html {
-  return ARC_AUTORELEASE([[self alloc] initWithHTML:html]);
+- (BOOL)usesChunkedTransferEncoding {
+  return (_type != nil) && (_length == NSNotFound);
 }
 
-+ (GCDWebServerDataResponse*)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
-  return ARC_AUTORELEASE([[self alloc] initWithHTMLTemplate:path variables:variables]);
+- (BOOL)open:(NSError**)error {
+  return YES;
 }
 
-+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object {
-  return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object]);
+- (NSData*)readData:(NSError**)error {
+  return [NSData data];
 }
 
-+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object contentType:(NSString*)type {
-  return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object contentType:type]);
+- (void)close {
+  ;
 }
 
-- (id)initWithText:(NSString*)text {
-  NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding];
-  if (data == nil) {
-    DNOT_REACHED();
-    ARC_RELEASE(self);
-    return nil;
+- (void)prepareForReading {
+  _reader = self;
+  if (_gzipped) {
+    GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader];
+    [_encoders addObject:encoder];
+    ARC_RELEASE(encoder);
+    _reader = encoder;
   }
-  return [self initWithData:data contentType:@"text/plain; charset=utf-8"];
 }
 
-- (id)initWithHTML:(NSString*)html {
-  NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding];
-  if (data == nil) {
+- (BOOL)performOpen:(NSError**)error {
+  DCHECK(_type);
+  DCHECK(_reader);
+  if (_opened) {
     DNOT_REACHED();
-    ARC_RELEASE(self);
-    return nil;
+    return NO;
   }
-  return [self initWithData:data contentType:@"text/html; charset=utf-8"];
+  _opened = YES;
+  return [_reader open:error];
 }
 
-- (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];
-  ARC_RELEASE(html);
-  return response;
+- (NSData*)performReadData:(NSError**)error {
+  DCHECK(_opened);
+  return [_reader readData:error];
 }
 
-- (id)initWithJSONObject:(id)object {
-  return [self initWithJSONObject:object contentType:@"application/json"];
+- (void)performClose {
+  DCHECK(_opened);
+  [_reader close];
 }
 
-- (id)initWithJSONObject:(id)object contentType:(NSString*)type {
-  NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL];
-  if (data == nil) {
-    ARC_RELEASE(self);
-    return nil;
+- (NSString*)description {
+  NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_status];
+  if (_type) {
+    [description appendFormat:@"\nContent Type = %@", _type];
   }
-  return [self initWithData:data contentType:type];
-}
-
-@end
-
-@implementation GCDWebServerFileResponse
-
-+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path {
-  return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path]);
-}
-
-+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment {
-  return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path isAttachment:attachment]);
-}
-
-+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range {
-  return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range]);
-}
-
-+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
-  return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range isAttachment:attachment]);
-}
-
-- (id)initWithFile:(NSString*)path {
-  return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO];
-}
-
-- (id)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
-  return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment];
-}
-
-- (id)initWithFile:(NSString*)path byteRange:(NSRange)range {
-  return [self initWithFile:path byteRange:range isAttachment:NO];
-}
-
-- (id)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
-  struct stat info;
-  if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
-    DNOT_REACHED();
-    ARC_RELEASE(self);
-    return nil;
+  if (_length != NSNotFound) {
+    [description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
   }
-  if ((range.location != NSNotFound) || (range.length > 0)) {
-    if (range.location != NSNotFound) {
-      range.location = MIN(range.location, (NSUInteger)info.st_size);
-      range.length = MIN(range.length, (NSUInteger)info.st_size - range.location);
-    } else {
-      range.length = MIN(range.length, (NSUInteger)info.st_size);
-      range.location = (NSUInteger)info.st_size - range.length;
-    }
-    if (range.length == 0) {
-      ARC_RELEASE(self);
-      return nil;  // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
-    }
+  [description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];
+  if (_lastModified) {
+    [description appendFormat:@"\nLast Modified Date = %@", _lastModified];
   }
-  
-  if ((self = [super init])) {
-    _path = [path copy];
-    if (range.location != NSNotFound) {
-      _offset = range.location;
-      _size = range.length;
-      [self setStatusCode:206];
-      [self setValue:[NSString stringWithFormat:@"bytes %i-%i/%i", (int)range.location, (int)(range.location + range.length - 1), (int)info.st_size] forAdditionalHeader:@"Content-Range"];
-      LOG_DEBUG(@"Using content bytes range [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 1), path);
-    } else {
-      _offset = 0;
-      _size = (NSUInteger)info.st_size;
-    }
-    
-    if (attachment) {  // TODO: Use http://tools.ietf.org/html/rfc5987 to encode file names with special characters instead of using lossy conversion to ISO 8859-1
-      NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
-      NSString* fileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
-      if (fileName) {
-        [self setValue:[NSString stringWithFormat:@"attachment; filename=\"%@\"", fileName] forAdditionalHeader:@"Content-Disposition"];
-        ARC_RELEASE(fileName);
-      } else {
-        DNOT_REACHED();
-      }
+  if (_eTag) {
+    [description appendFormat:@"\nETag = %@", _eTag];
+  }
+  if (_headers.count) {
+    [description appendString:@"\n"];
+    for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
+      [description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]];
     }
-    
-    self.contentType = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
-    self.contentLength = (range.location != NSNotFound ? range.length : (NSUInteger)info.st_size);
   }
-  return self;
-}
-
-- (void)dealloc {
-  DCHECK(_file <= 0);
-  ARC_RELEASE(_path);
-  
-  ARC_DEALLOC(super);
+  return description;
 }
 
-- (BOOL)open {
-  DCHECK(_file <= 0);
-  _file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
-  if (_file <= 0) {
-    return NO;
-  }
-  if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
-    close(_file);
-    _file = 0;
-    return NO;
-  }
-  return YES;
-}
+@end
 
-- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
-  DCHECK(_file > 0);
-  ssize_t result = read(_file, buffer, MIN(length, _size));
-  if (result > 0) {
-    _size -= result;
-  }
-  return result;
-}
+@implementation GCDWebServerResponse (Extensions)
 
-- (BOOL)close {
-  DCHECK(_file > 0);
-  int result = close(_file);
-  _file = 0;
-  return (result == 0 ? YES : NO);
++ (instancetype)responseWithStatusCode:(NSInteger)statusCode {
+  return ARC_AUTORELEASE([[self alloc] initWithStatusCode:statusCode]);
 }
 
-@end
-
-@implementation GCDWebServerChunkedResponse
-
-+ (GCDWebServerChunkedResponse*)responseWithContentType:(NSString*)type chunkBlock:(GCDWebServerChunkBlock)block {
-  return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type chunkBlock:block]);
++ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
+  return ARC_AUTORELEASE([[self alloc] initWithRedirect:location permanent:permanent]);
 }
 
-- (id)initWithContentType:(NSString*)type chunkBlock:(GCDWebServerChunkBlock)block {
-  if ((self = [super init])) {
-    _block = [block copy];
-    
-    self.contentType = type;
-    [self setValue:@"chunked" forAdditionalHeader:@"Transfer-Encoding"];
+- (instancetype)initWithStatusCode:(NSInteger)statusCode {
+  if ((self = [self init])) {
+    self.statusCode = statusCode;
   }
   return self;
 }
 
-- (BOOL)open {
-  DCHECK(_chunk == nil);
-  return YES;
-}
-
-- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
-  if (_offset >= _chunk.length) {
-    ARC_RELEASE(_chunk);
-    _chunk = nil;
-  }
-  if (_chunk == nil) {
-    if (_terminated) {
-      return 0;
-    }
-    NSData* data = _block();
-    if (data.length > 0) {
-      const char* hexString = [[NSString stringWithFormat:@"%lx", (unsigned long)data.length] UTF8String];
-      size_t hexLength = strlen(hexString);
-      _chunk = [[NSMutableData alloc] initWithLength:(hexLength + 2 + data.length + 2)];
-      char* ptr = (char*)_chunk.bytes;
-      bcopy(hexString, ptr, hexLength);
-      ptr += hexLength;
-      *ptr++ = '\r';
-      *ptr++ = '\n';
-      bcopy(data.bytes, ptr, data.length);
-      ptr += data.length;
-      *ptr++ = '\r';
-      *ptr = '\n';
-    } else {
-      _chunk = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5];
-      _terminated = YES;
-    }
-    _offset = 0;
+- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
+  if ((self = [self init])) {
+    self.statusCode = permanent ? kGCDWebServerHTTPStatusCode_MovedPermanently : kGCDWebServerHTTPStatusCode_TemporaryRedirect;
+    [self setValue:[location absoluteString] forAdditionalHeader:@"Location"];
   }
-  NSInteger size = MIN(_chunk.length - _offset, length);
-  bcopy((char*)_chunk.bytes + _offset, buffer, size);
-  _offset += size;
-  return size;
-}
-
-- (BOOL)close {
-  ARC_RELEASE(_chunk);
-  _chunk = nil;
-  return YES;
-}
-
-- (void)dealloc {
-  DCHECK(_chunk == nil);
-  ARC_RELEASE(_chunk);
-  
-  ARC_DEALLOC(super);
+  return self;
 }
 
 @end

+ 35 - 0
CGDWebServer/GCDWebServerStreamingResponse.h

@@ -0,0 +1,35 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerStreamingResponse.h"
+
+typedef NSData* (^GCDWebServerStreamBlock)(NSError** error);
+
+@interface GCDWebServerStreamingResponse : GCDWebServerResponse  // Automatically enables chunked transfer encoding
++ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
+- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;  // Block must return empty NSData when done or nil on error
+@end

+ 67 - 0
CGDWebServer/GCDWebServerStreamingResponse.m

@@ -0,0 +1,67 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerPrivate.h"
+
+@interface GCDWebServerStreamingResponse () {
+@private
+  GCDWebServerStreamBlock _block;
+}
+@end
+
+@implementation GCDWebServerStreamingResponse
+
++ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
+  return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]);
+}
+
+- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
+  if ((self = [super init])) {
+    _block = [block copy];
+    
+    self.contentType = type;
+  }
+  return self;
+}
+
+- (void)dealloc {
+  ARC_RELEASE(_block);
+  
+  ARC_DEALLOC(super);
+}
+
+- (NSData*)readData:(NSError**)error {
+  return _block(error);
+}
+
+- (NSString*)description {
+  NSMutableString* description = [NSMutableString stringWithString:[super description]];
+  [description appendString:@"\n\n<STREAM>"];
+  return description;
+}
+
+@end

+ 33 - 0
CGDWebServer/GCDWebServerURLEncodedFormRequest.h

@@ -0,0 +1,33 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerDataRequest.h"
+
+@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest
+@property(nonatomic, readonly) NSDictionary* arguments;  // Text encoding is extracted from Content-Type or defaults to UTF-8
++ (NSString*)mimeType;
+@end

+ 73 - 0
CGDWebServer/GCDWebServerURLEncodedFormRequest.m

@@ -0,0 +1,73 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "GCDWebServerPrivate.h"
+
+@interface GCDWebServerURLEncodedFormRequest () {
+@private
+  NSDictionary* _arguments;
+}
+@end
+
+@implementation GCDWebServerURLEncodedFormRequest
+
+@synthesize arguments=_arguments;
+
++ (NSString*)mimeType {
+  return @"application/x-www-form-urlencoded";
+}
+
+- (void)dealloc {
+  ARC_RELEASE(_arguments);
+  
+  ARC_DEALLOC(super);
+}
+
+- (BOOL)close:(NSError**)error {
+  if (![super close:error]) {
+    return NO;
+  }
+  
+  NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset");
+  NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)];
+  _arguments = ARC_RETAIN(GCDWebServerParseURLEncodedForm(string));
+  DCHECK(_arguments);
+  ARC_RELEASE(string);
+  
+  return YES;
+}
+
+- (NSString*)description {
+  NSMutableString* description = [NSMutableString stringWithString:[super description]];
+  [description appendString:@"\n"];
+  for (NSString* argument in [[_arguments allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
+    [description appendFormat:@"\n%@ = %@", argument, [_arguments objectForKey:argument]];
+  }
+  return description;
+}
+
+@end

+ 58 - 0
GCDWebDAVServer/GCDWebDAVServer.h

@@ -0,0 +1,58 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Requires HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2" in Xcode build settings
+
+#import "GCDWebServer.h"
+
+@class GCDWebDAVServer;
+
+@protocol GCDWebDAVServerDelegate <NSObject>
+@optional
+- (void)davServer:(GCDWebDAVServer*)uploader didDownloadFileAtPath:(NSString*)path;
+- (void)davServer:(GCDWebDAVServer*)uploader didUploadFileAtPath:(NSString*)path;
+- (void)davServer:(GCDWebDAVServer*)uploader didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
+- (void)davServer:(GCDWebDAVServer*)uploader didCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
+- (void)davServer:(GCDWebDAVServer*)uploader didDeleteItemAtPath:(NSString*)path;
+- (void)davServer:(GCDWebDAVServer*)uploader didCreateDirectoryAtPath:(NSString*)path;
+@end
+
+@interface GCDWebDAVServer : GCDWebServer
+@property(nonatomic, readonly) NSString* uploadDirectory;
+@property(nonatomic, assign) id<GCDWebDAVServerDelegate> delegate;
+@property(nonatomic, copy) NSArray* allowedFileExtensions;  // Default is nil i.e. all file extensions are allowed
+@property(nonatomic) BOOL showHiddenFiles;  // Default is NO
+- (instancetype)initWithUploadDirectory:(NSString*)path;
+@end
+
+@interface GCDWebDAVServer (Subclassing)
+- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath;  // Default implementation returns YES
+- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;  // Default implementation returns YES
+- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;  // Default implementation returns YES
+- (BOOL)shouldDeleteItemAtPath:(NSString*)path;  // Default implementation returns YES
+- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path;  // Default implementation returns YES
+@end

+ 689 - 0
GCDWebDAVServer/GCDWebDAVServer.m

@@ -0,0 +1,689 @@
+/*
+ Copyright (c) 2012-2014, Pierre-Olivier Latour
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * The name of Pierre-Olivier Latour may not be used to endorse
+ or promote products derived from this software without specific
+ prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// WebDAV specifications: http://webdav.org/specs/rfc4918.html
+
+#import <libxml/parser.h>
+
+#import "GCDWebDAVServer.h"
+
+#import "GCDWebServerDataRequest.h"
+#import "GCDWebServerFileRequest.h"
+
+#import "GCDWebServerDataResponse.h"
+#import "GCDWebServerErrorResponse.h"
+#import "GCDWebServerFileResponse.h"
+
+#define kXMLParseOptions (XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT | XML_PARSE_NOWARNING | XML_PARSE_NOERROR)
+
+typedef NS_ENUM(NSInteger, DAVProperties) {
+  kDAVProperty_ResourceType = (1 << 0),
+  kDAVProperty_CreationDate = (1 << 1),
+  kDAVProperty_LastModified = (1 << 2),
+  kDAVProperty_ContentLength = (1 << 3),
+  kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
+};
+
+@interface GCDWebDAVServer () {
+@private
+  NSString* _uploadDirectory;
+  id<GCDWebDAVServerDelegate> __unsafe_unretained _delegate;
+  NSArray* _allowedExtensions;
+  BOOL _showHidden;
+}
+@end
+
+@implementation GCDWebDAVServer (Methods)
+
+- (BOOL)_checkFileExtension:(NSString*)fileName {
+  if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
+    return NO;
+  }
+  return YES;
+}
+
+static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
+  NSString* userAgentHeader = [request.headers objectForKey:@"User-Agent"];
+  return ([userAgentHeader hasPrefix:@"WebDAVFS/"] || [userAgentHeader hasPrefix:@"WebDAVLib/"]);  // OS X WebDAV client
+}
+
+- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
+  GCDWebServerResponse* response = [GCDWebServerResponse response];
+  if (_IsMacFinder(request)) {
+    [response setValue:@"1, 2" forAdditionalHeader:@"DAV"];  // Classes 1 and 2
+  } else {
+    [response setValue:@"1" forAdditionalHeader:@"DAV"];  // Class 1
+  }
+  return response;
+}
+
+- (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request {
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory = NO;
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSString* itemName = [absolutePath lastPathComponent];
+  if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading item name \"%@\" is not allowed", itemName];
+  }
+  
+  // Because HEAD requests are mapped to GET ones, we need to handle directories but it's OK to return nothing per http://webdav.org/specs/rfc4918.html#rfc.section.9.4
+  if (isDirectory) {
+    return [GCDWebServerResponse response];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(davServer:didDownloadFileAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate davServer:self didDownloadFileAtPath:absolutePath];
+    });
+  }
+  return [GCDWebServerFileResponse responseWithFile:absolutePath];
+}
+
+- (GCDWebServerResponse*)performPUT:(GCDWebServerFileRequest*)request {
+  if ([request hasByteRange]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Range uploads not supported"];
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  BOOL isDirectory;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
+  }
+  
+  BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory];
+  if (existing && isDirectory) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"PUT not allowed on existing collection \"%@\"", relativePath];
+  }
+  
+  NSString* fileName = [absolutePath lastPathComponent];
+  if (([fileName hasPrefix:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName];
+  }
+  
+  if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.temporaryPath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not permitted", relativePath];
+  }
+  
+  [[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL];
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] moveItemAtPath:request.temporaryPath toPath:absolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(davServer:didUploadFileAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate davServer:self didUploadFileAtPath:absolutePath];
+    });
+  }
+  return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
+}
+
+- (GCDWebServerResponse*)performDELETE:(GCDWebServerRequest*)request {
+  NSString* depthHeader = [request.headers objectForKey:@"Depth"];
+  if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory = NO;
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSString* itemName = [absolutePath lastPathComponent];
+  if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
+  }
+  
+  if (![self shouldDeleteItemAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
+  }
+  
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(davServer:didDeleteItemAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate davServer:self didDeleteItemAtPath:absolutePath];
+    });
+  }
+  return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
+}
+
+- (GCDWebServerResponse*)performMKCOL:(GCDWebServerDataRequest*)request {
+  if ([request hasBody] && (request.contentLength > 0)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_UnsupportedMediaType message:@"Unexpected request body for MKCOL method"];
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  BOOL isDirectory;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
+  }
+  
+  NSString* directoryName = [absolutePath lastPathComponent];
+  if (!_showHidden && [directoryName hasPrefix:@"."]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
+  }
+  
+  if (![self shouldCreateDirectoryAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
+  }
+  
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(davServer:didCreateDirectoryAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate davServer:self didCreateDirectoryAtPath:absolutePath];
+    });
+  }
+  return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Created];
+}
+
+- (GCDWebServerResponse*)performCOPY:(GCDWebServerRequest*)request isMove:(BOOL)isMove {
+  if (!isMove) {
+    NSString* depthHeader = [request.headers objectForKey:@"Depth"];  // TODO: Support "Depth: 0"
+    if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
+    }
+  }
+  
+  NSString* srcRelativePath = request.path;
+  NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath];
+  if (![srcAbsolutePath hasPrefix:_uploadDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
+  }
+  
+  NSString* dstRelativePath = [request.headers objectForKey:@"Destination"];
+  NSRange range = [dstRelativePath rangeOfString:[request.headers objectForKey:@"Host"]];
+  if ((dstRelativePath == nil) || (range.location == NSNotFound)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Malformed 'Destination' header: %@", dstRelativePath];
+  }
+  dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+  NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
+  if (![dstAbsolutePath hasPrefix:_uploadDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
+  }
+  
+  BOOL isDirectory;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:[dstAbsolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Invalid destination \"%@\"", dstRelativePath];
+  }
+  
+  NSString* itemName = [dstAbsolutePath lastPathComponent];
+  if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", itemName];
+  }
+  
+  NSString* overwriteHeader = [request.headers objectForKey:@"Overwrite"];
+  BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:dstAbsolutePath];
+  if (existing && ((isMove && ![overwriteHeader isEqualToString:@"T"]) || (!isMove && [overwriteHeader isEqualToString:@"F"]))) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_PreconditionFailed message:@"Destination \"%@\" already exists", dstRelativePath];
+  }
+  
+  if (isMove) {
+    if (![self shouldMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
+    }
+  } else {
+    if (![self shouldCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
+    }
+  }
+  
+  NSError* error = nil;
+  if (isMove) {
+    [[NSFileManager defaultManager] removeItemAtPath:dstAbsolutePath error:NULL];
+    if (![[NSFileManager defaultManager] moveItemAtPath:srcAbsolutePath toPath:dstAbsolutePath error:&error]) {
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
+    }
+  } else {
+    if (![[NSFileManager defaultManager] copyItemAtPath:srcAbsolutePath toPath:dstAbsolutePath error:&error]) {
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
+    }
+  }
+  
+  if (isMove) {
+    if ([_delegate respondsToSelector:@selector(davServer:didMoveItemFromPath:toPath:)]) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        [_delegate davServer:self didMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
+      });
+    }
+  } else {
+    if ([_delegate respondsToSelector:@selector(davServer:didCopyItemFromPath:toPath:)]) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        [_delegate davServer:self didCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
+      });
+    }
+  }
+  
+  return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
+}
+
+static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name) {
+  while (child) {
+    if ((child->type == XML_ELEMENT_NODE) && !xmlStrcmp(child->name, name)) {
+      return child;
+    }
+    child = child->next;
+  }
+  return NULL;
+}
+
+- (void)_addPropertyResponseForItem:(NSString*)itemPath resource:(NSString*)resourcePath properties:(DAVProperties)properties xmlString:(NSMutableString*)xmlString {
+  CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8);
+  if (escapedPath) {
+    NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
+    NSString* type = [attributes objectForKey:NSFileType];
+    BOOL isFile = [type isEqualToString:NSFileTypeRegular];
+    BOOL isDirectory = [type isEqualToString:NSFileTypeDirectory];
+    if ((isFile && [self _checkFileExtension:itemPath]) || isDirectory) {
+      [xmlString appendString:@"<D:response>"];
+      [xmlString appendFormat:@"<D:href>%@</D:href>", escapedPath];
+      [xmlString appendString:@"<D:propstat>"];
+      [xmlString appendString:@"<D:prop>"];
+      
+      if (properties & kDAVProperty_ResourceType) {
+        if (isDirectory) {
+          [xmlString appendString:@"<D:resourcetype><D:collection/></D:resourcetype>"];
+        } else {
+          [xmlString appendString:@"<D:resourcetype/>"];
+        }
+      }
+      
+      if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) {
+        NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
+        formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
+        formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
+        formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
+        [xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", [formatter stringFromDate:[attributes fileCreationDate]]];
+      }
+      
+      if ((properties & kDAVProperty_LastModified) && [attributes objectForKey:NSFileModificationDate]) {
+        NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
+        formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
+        formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
+        formatter.dateFormat = @"EEE', 'd' 'MMM' 'yyyy' 'HH:mm:ss' GMT'";
+        [xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", [formatter stringFromDate:[attributes fileModificationDate]]];
+      }
+      
+      if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) {
+        [xmlString appendFormat:@"<D:getcontentlength>%llu</D:getcontentlength>", [attributes fileSize]];
+      }
+      
+      [xmlString appendString:@"</D:prop>"];
+      [xmlString appendString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
+      [xmlString appendString:@"</D:propstat>"];
+      [xmlString appendString:@"</D:response>\n"];
+    }
+    CFRelease(escapedPath);
+  }
+}
+
+- (GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request {
+  NSInteger depth;
+  NSString* depthHeader = [request.headers objectForKey:@"Depth"];
+  if ([depthHeader isEqualToString:@"0"]) {
+    depth = 0;
+  } else if ([depthHeader isEqualToString:@"1"]) {
+    depth = 1;
+  } else {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];  // TODO: Return 403 / propfind-finite-depth for "infinity" depth
+  }
+  
+  DAVProperties properties = 0;
+  if (request.data.length) {
+    BOOL success = YES;
+    xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
+    if (document) {
+      xmlNodePtr rootNode = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
+      xmlNodePtr allNode = rootNode ? _XMLChildWithName(rootNode->children, (const xmlChar*)"allprop") : NULL;
+      xmlNodePtr propNode = rootNode ? _XMLChildWithName(rootNode->children, (const xmlChar*)"prop") : NULL;
+      if (allNode) {
+        properties = kDAVAllProperties;
+      } else if (propNode) {
+        xmlNodePtr node = propNode->children;
+        while (node) {
+          if (!xmlStrcmp(node->name, (const xmlChar*)"resourcetype")) {
+            properties |= kDAVProperty_ResourceType;
+          } else if (!xmlStrcmp(node->name, (const xmlChar*)"creationdate")) {
+            properties |= kDAVProperty_CreationDate;
+          } else if (!xmlStrcmp(node->name, (const xmlChar*)"getlastmodified")) {
+            properties |= kDAVProperty_LastModified;
+          } else if (!xmlStrcmp(node->name, (const xmlChar*)"getcontentlength")) {
+            properties |= kDAVProperty_ContentLength;
+          } else {
+            [self logWarning:@"Unknown DAV property requested \"%s\"", node->name];
+          }
+          node = node->next;
+        }
+      } else {
+        success = NO;
+      }
+      xmlFreeDoc(document);
+    } else {
+      success = NO;
+    }
+    if (!success) {
+      NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
+#if !__has_feature(objc_arc)
+      [string release];
+#endif
+    }
+  } else {
+    properties = kDAVAllProperties;
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory = NO;
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSString* itemName = [absolutePath lastPathComponent];
+  if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Retrieving properties for item name \"%@\" is not allowed", itemName];
+  }
+  
+  NSArray* items = nil;
+  if (isDirectory) {
+    NSError* error = nil;
+    items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
+    if (items == nil) {
+      return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
+    }
+  }
+  
+  NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
+  [xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
+  if (![relativePath hasPrefix:@"/"]) {
+    relativePath = [@"/" stringByAppendingString:relativePath];
+  }
+  [self _addPropertyResponseForItem:absolutePath resource:relativePath properties:properties xmlString:xmlString];
+  if (depth == 1) {
+    if (![relativePath hasSuffix:@"/"]) {
+      relativePath = [relativePath stringByAppendingString:@"/"];
+    }
+    for (NSString* item in items) {
+      if (_showHidden || ![item hasPrefix:@"."]) {
+        [self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
+      }
+    }
+  }
+  [xmlString appendString:@"</D:multistatus>"];
+  
+  GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]
+                                                                      contentType:@"application/xml; charset=\"utf-8\""];
+  response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus;
+  return response;
+}
+
+- (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request {
+  if (!_IsMacFinder(request)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory = NO;
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSString* depthHeader = [request.headers objectForKey:@"Depth"];
+  NSString* timeoutHeader = [request.headers objectForKey:@"Timeout"];
+  NSString* scope = nil;
+  NSString* type = nil;
+  NSString* owner = nil;
+  NSString* token = nil;
+  BOOL success = YES;
+  xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
+  if (document) {
+    xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"lockinfo");
+    if (node) {
+      xmlNodePtr scopeNode = _XMLChildWithName(node->children, (const xmlChar*)"lockscope");
+      if (scopeNode && scopeNode->children && scopeNode->children->name) {
+        scope = [NSString stringWithUTF8String:(const char*)scopeNode->children->name];
+      }
+      xmlNodePtr typeNode = _XMLChildWithName(node->children, (const xmlChar*)"locktype");
+      if (typeNode && typeNode->children && typeNode->children->name) {
+        type = [NSString stringWithUTF8String:(const char*)typeNode->children->name];
+      }
+      xmlNodePtr ownerNode = _XMLChildWithName(node->children, (const xmlChar*)"owner");
+      if (ownerNode) {
+        ownerNode = _XMLChildWithName(ownerNode->children, (const xmlChar*)"href");
+        if (ownerNode && ownerNode->children && ownerNode->children->content) {
+          owner = [NSString stringWithUTF8String:(const char*)ownerNode->children->content];
+        }
+      }
+    } else {
+      success = NO;
+    }
+    xmlFreeDoc(document);
+  } else {
+    success = NO;
+  }
+  if (!success) {
+    NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
+#if !__has_feature(objc_arc)
+    [string release];
+#endif
+  }
+  
+  if (![scope isEqualToString:@"exclusive"] || ![type isEqualToString:@"write"] || ![depthHeader isEqualToString:@"0"]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depthHeader, relativePath];
+  }
+  
+  NSString* itemName = [absolutePath lastPathComponent];
+  if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking item name \"%@\" is not allowed", itemName];
+  }
+  
+  if (!token) {
+    CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
+    CFStringRef string = CFUUIDCreateString(kCFAllocatorDefault, uuid);
+    token = [NSString stringWithFormat:@"urn:uuid:%@", (__bridge NSString*)string];
+    CFRelease(string);
+    CFRelease(uuid);
+  }
+  
+  NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
+  [xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
+  [xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
+  [xmlString appendFormat:@"<D:locktype><D:%@/></D:locktype>\n", type];
+  [xmlString appendFormat:@"<D:lockscope><D:%@/></D:lockscope>\n", scope];
+  [xmlString appendFormat:@"<D:depth>%@</D:depth>\n", depthHeader];
+  if (owner) {
+    [xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
+  }
+  if (timeoutHeader) {
+    [xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeoutHeader];
+  }
+  [xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
+  NSString* lockroot = [@"http://" stringByAppendingString:[[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]];
+  [xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
+  [xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
+  [xmlString appendString:@"</D:prop>"];
+  
+  [self logVerbose:@"WebDAV pretending to lock \"%@\"", relativePath];
+  GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]
+                                                                      contentType:@"application/xml; charset=\"utf-8\""];
+  return response;
+}
+
+- (GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request {
+  if (!_IsMacFinder(request)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory = NO;
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSString* tokenHeader = [request.headers objectForKey:@"Lock-Token"];
+  if (!tokenHeader.length) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Missing 'Lock-Token' header"];
+  }
+  
+  NSString* itemName = [absolutePath lastPathComponent];
+  if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Unlocking item name \"%@\" is not allowed", itemName];
+  }
+  
+  [self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
+  return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
+}
+
+@end
+
+@implementation GCDWebDAVServer
+
+@synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden;
+
+- (instancetype)initWithUploadDirectory:(NSString*)path {
+  if ((self = [super init])) {
+    _uploadDirectory = [[path stringByStandardizingPath] copy];
+#if __has_feature(objc_arc)
+    GCDWebDAVServer* __unsafe_unretained server = self;
+#else
+    __block GCDWebDAVServer* server = self;
+#endif
+    
+    // 9.1 PROPFIND method
+    [self addDefaultHandlerForMethod:@"PROPFIND" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performPROPFIND:(GCDWebServerDataRequest*)request];
+    }];
+    
+    // 9.3 MKCOL Method
+    [self addDefaultHandlerForMethod:@"MKCOL" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performMKCOL:(GCDWebServerDataRequest*)request];
+    }];
+    
+    // 9.4 GET & HEAD methods
+    [self addDefaultHandlerForMethod:@"GET" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performGET:request];
+    }];
+    
+    // 9.6 DELETE method
+    [self addDefaultHandlerForMethod:@"DELETE" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performDELETE:request];
+    }];
+    
+    // 9.7 PUT method
+    [self addDefaultHandlerForMethod:@"PUT" requestClass:[GCDWebServerFileRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performPUT:(GCDWebServerFileRequest*)request];
+    }];
+    
+    // 9.8 COPY method
+    [self addDefaultHandlerForMethod:@"COPY" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performCOPY:request isMove:NO];
+    }];
+    
+    // 9.9 MOVE method
+    [self addDefaultHandlerForMethod:@"MOVE" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performCOPY:request isMove:YES];
+    }];
+    
+    // 9.10 LOCK method
+    [self addDefaultHandlerForMethod:@"LOCK" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performLOCK:(GCDWebServerDataRequest*)request];
+    }];
+    
+    // 9.11 UNLOCK method
+    [self addDefaultHandlerForMethod:@"UNLOCK" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performUNLOCK:request];
+    }];
+    
+    // 10.1 OPTIONS method / DAV Header
+    [self addDefaultHandlerForMethod:@"OPTIONS" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performOPTIONS:request];
+    }];
+    
+  }
+  return self;
+}
+
+#if !__has_feature(objc_arc)
+
+- (void)dealloc {
+  [_uploadDirectory release];
+  [_allowedExtensions release];
+  
+  [super dealloc];
+}
+
+#endif
+
+@end
+
+@implementation GCDWebDAVServer (Subclassing)
+
+- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath {
+  return YES;
+}
+
+- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
+  return YES;
+}
+
+- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
+  return YES;
+}
+
+- (BOOL)shouldDeleteItemAtPath:(NSString*)path {
+  return YES;
+}
+
+- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path {
+  return YES;
+}
+
+@end

+ 106 - 1
GCDWebServer.xcodeproj/project.pbxproj

@@ -38,6 +38,28 @@
 		E22112991690B7AA0048D2B2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E22112981690B7AA0048D2B2 /* CFNetwork.framework */; };
 		E221129B1690B7B10048D2B2 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E221129A1690B7B10048D2B2 /* UIKit.framework */; };
 		E221129D1690B7BA0048D2B2 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */; };
+		E276647C18F3BC2100A034BA /* GCDWebServerErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E276647B18F3BC2100A034BA /* GCDWebServerErrorResponse.m */; };
+		E276647D18F3BC2100A034BA /* GCDWebServerErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E276647B18F3BC2100A034BA /* GCDWebServerErrorResponse.m */; };
+		E2A0E7ED18F1D03700C580B1 /* GCDWebServerDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */; };
+		E2A0E7EE18F1D03700C580B1 /* GCDWebServerDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */; };
+		E2A0E7F118F1D12E00C580B1 /* GCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F018F1D12E00C580B1 /* GCDWebServerFileResponse.m */; };
+		E2A0E7F218F1D12E00C580B1 /* GCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F018F1D12E00C580B1 /* GCDWebServerFileResponse.m */; };
+		E2A0E7F518F1D1E500C580B1 /* GCDWebServerStreamingResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamingResponse.m */; };
+		E2A0E7F618F1D1E500C580B1 /* GCDWebServerStreamingResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamingResponse.m */; };
+		E2A0E7F918F1D24700C580B1 /* GCDWebServerDataRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F818F1D24700C580B1 /* GCDWebServerDataRequest.m */; };
+		E2A0E7FA18F1D24700C580B1 /* GCDWebServerDataRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F818F1D24700C580B1 /* GCDWebServerDataRequest.m */; };
+		E2A0E7FD18F1D36C00C580B1 /* GCDWebServerFileRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7FC18F1D36C00C580B1 /* GCDWebServerFileRequest.m */; };
+		E2A0E7FE18F1D36C00C580B1 /* GCDWebServerFileRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7FC18F1D36C00C580B1 /* GCDWebServerFileRequest.m */; };
+		E2A0E80118F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */; };
+		E2A0E80218F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */; };
+		E2A0E80518F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */; };
+		E2A0E80618F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */; };
+		E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
+		E2A0E80B18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
+		E2A0E80D18F35C9A00C580B1 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */; };
+		E2A0E80F18F35CA300C580B1 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A0E80E18F35CA300C580B1 /* libxml2.dylib */; };
+		E2B0D4A718F13495009A7927 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2B0D4A618F13495009A7927 /* libz.dylib */; };
+		E2B0D4A918F134A8009A7927 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2B0D4A818F134A8009A7927 /* libz.dylib */; };
 		E2BE850A18E77ECA0061360B /* GCDWebUploader.bundle in Resources */ = {isa = PBXBuildFile; fileRef = E2BE850718E77ECA0061360B /* GCDWebUploader.bundle */; };
 		E2BE850B18E77ECA0061360B /* GCDWebUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = E2BE850918E77ECA0061360B /* GCDWebUploader.m */; };
 		E2BE850C18E785940061360B /* GCDWebUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = E2BE850918E77ECA0061360B /* GCDWebUploader.m */; };
@@ -97,6 +119,29 @@
 		E22112981690B7AA0048D2B2 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; };
 		E221129A1690B7B10048D2B2 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
 		E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/MobileCoreServices.framework; sourceTree = DEVELOPER_DIR; };
+		E276647A18F3BC2100A034BA /* GCDWebServerErrorResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerErrorResponse.h; sourceTree = "<group>"; };
+		E276647B18F3BC2100A034BA /* GCDWebServerErrorResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerErrorResponse.m; sourceTree = "<group>"; };
+		E2A0E7EB18F1D03700C580B1 /* GCDWebServerDataResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerDataResponse.h; sourceTree = "<group>"; };
+		E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataResponse.m; sourceTree = "<group>"; };
+		E2A0E7EF18F1D12E00C580B1 /* GCDWebServerFileResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileResponse.h; sourceTree = "<group>"; };
+		E2A0E7F018F1D12E00C580B1 /* GCDWebServerFileResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFileResponse.m; sourceTree = "<group>"; };
+		E2A0E7F318F1D1E500C580B1 /* GCDWebServerStreamingResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerStreamingResponse.h; sourceTree = "<group>"; };
+		E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamingResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerStreamingResponse.m; sourceTree = "<group>"; };
+		E2A0E7F718F1D24700C580B1 /* GCDWebServerDataRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerDataRequest.h; sourceTree = "<group>"; };
+		E2A0E7F818F1D24700C580B1 /* GCDWebServerDataRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataRequest.m; sourceTree = "<group>"; };
+		E2A0E7FB18F1D36C00C580B1 /* GCDWebServerFileRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileRequest.h; sourceTree = "<group>"; };
+		E2A0E7FC18F1D36C00C580B1 /* GCDWebServerFileRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFileRequest.m; sourceTree = "<group>"; };
+		E2A0E7FF18F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerMultiPartFormRequest.h; sourceTree = "<group>"; };
+		E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerMultiPartFormRequest.m; sourceTree = "<group>"; };
+		E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerURLEncodedFormRequest.h; sourceTree = "<group>"; };
+		E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerURLEncodedFormRequest.m; sourceTree = "<group>"; };
+		E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebDAVServer.h; sourceTree = "<group>"; };
+		E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebDAVServer.m; sourceTree = "<group>"; };
+		E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.1.sdk/usr/lib/libxml2.dylib; sourceTree = DEVELOPER_DIR; };
+		E2A0E80E18F35CA300C580B1 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; };
+		E2A0E81018F3737B00C580B1 /* GCDWebServerHTTPStatusCodes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GCDWebServerHTTPStatusCodes.h; sourceTree = "<group>"; };
+		E2B0D4A618F13495009A7927 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
+		E2B0D4A818F134A8009A7927 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.1.sdk/usr/lib/libz.dylib; sourceTree = DEVELOPER_DIR; };
 		E2BE850718E77ECA0061360B /* GCDWebUploader.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = GCDWebUploader.bundle; sourceTree = "<group>"; };
 		E2BE850818E77ECA0061360B /* GCDWebUploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebUploader.h; sourceTree = "<group>"; };
 		E2BE850918E77ECA0061360B /* GCDWebUploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebUploader.m; sourceTree = "<group>"; };
@@ -111,6 +156,8 @@
 				E2BE851118E79DAF0061360B /* SystemConfiguration.framework in Frameworks */,
 				E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */,
 				E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */,
+				E2A0E80F18F35CA300C580B1 /* libxml2.dylib in Frameworks */,
+				E2B0D4A718F13495009A7927 /* libz.dylib in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -121,6 +168,8 @@
 				E221129D1690B7BA0048D2B2 /* MobileCoreServices.framework in Frameworks */,
 				E221129B1690B7B10048D2B2 /* UIKit.framework in Frameworks */,
 				E22112991690B7AA0048D2B2 /* CFNetwork.framework in Frameworks */,
+				E2A0E80D18F35C9A00C580B1 /* libxml2.dylib in Frameworks */,
+				E2B0D4A918F134A8009A7927 /* libz.dylib in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -131,6 +180,7 @@
 			isa = PBXGroup;
 			children = (
 				E221127B1690B63A0048D2B2 /* CGDWebServer */,
+				E2A0E80718F3432600C580B1 /* GCDWebDAVServer */,
 				E2BE850618E77ECA0061360B /* GCDWebUploader */,
 				E221128D1690B6470048D2B2 /* Mac */,
 				E22112901690B64F0048D2B2 /* iOS */,
@@ -157,11 +207,28 @@
 				E221127D1690B63A0048D2B2 /* GCDWebServer.m */,
 				E221127E1690B63A0048D2B2 /* GCDWebServerConnection.h */,
 				E221127F1690B63A0048D2B2 /* GCDWebServerConnection.m */,
+				E2A0E7F718F1D24700C580B1 /* GCDWebServerDataRequest.h */,
+				E2A0E7F818F1D24700C580B1 /* GCDWebServerDataRequest.m */,
+				E2A0E7EB18F1D03700C580B1 /* GCDWebServerDataResponse.h */,
+				E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */,
+				E276647A18F3BC2100A034BA /* GCDWebServerErrorResponse.h */,
+				E276647B18F3BC2100A034BA /* GCDWebServerErrorResponse.m */,
+				E2A0E7FB18F1D36C00C580B1 /* GCDWebServerFileRequest.h */,
+				E2A0E7FC18F1D36C00C580B1 /* GCDWebServerFileRequest.m */,
+				E2A0E7EF18F1D12E00C580B1 /* GCDWebServerFileResponse.h */,
+				E2A0E7F018F1D12E00C580B1 /* GCDWebServerFileResponse.m */,
+				E2A0E81018F3737B00C580B1 /* GCDWebServerHTTPStatusCodes.h */,
+				E2A0E7FF18F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.h */,
+				E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */,
 				E22112801690B63A0048D2B2 /* GCDWebServerPrivate.h */,
 				E22112811690B63A0048D2B2 /* GCDWebServerRequest.h */,
 				E22112821690B63A0048D2B2 /* GCDWebServerRequest.m */,
 				E22112831690B63A0048D2B2 /* GCDWebServerResponse.h */,
 				E22112841690B63A0048D2B2 /* GCDWebServerResponse.m */,
+				E2A0E7F318F1D1E500C580B1 /* GCDWebServerStreamingResponse.h */,
+				E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamingResponse.m */,
+				E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */,
+				E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */,
 			);
 			path = CGDWebServer;
 			sourceTree = "<group>";
@@ -191,6 +258,8 @@
 				E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */,
 				E221129A1690B7B10048D2B2 /* UIKit.framework */,
 				E22112981690B7AA0048D2B2 /* CFNetwork.framework */,
+				E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */,
+				E2B0D4A818F134A8009A7927 /* libz.dylib */,
 			);
 			name = "iOS Frameworks and Libraries";
 			sourceTree = "<group>";
@@ -201,10 +270,21 @@
 				E2BE851018E79DAF0061360B /* SystemConfiguration.framework */,
 				E208D1B2167BB17E00500836 /* CoreServices.framework */,
 				E208D148167B76B700500836 /* CFNetwork.framework */,
+				E2A0E80E18F35CA300C580B1 /* libxml2.dylib */,
+				E2B0D4A618F13495009A7927 /* libz.dylib */,
 			);
 			name = "Mac Frameworks and Libraries";
 			sourceTree = "<group>";
 		};
+		E2A0E80718F3432600C580B1 /* GCDWebDAVServer */ = {
+			isa = PBXGroup;
+			children = (
+				E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */,
+				E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */,
+			);
+			path = GCDWebDAVServer;
+			sourceTree = "<group>";
+		};
 		E2BE850618E77ECA0061360B /* GCDWebUploader */ = {
 			isa = PBXGroup;
 			children = (
@@ -300,11 +380,20 @@
 			buildActionMask = 2147483647;
 			files = (
 				E22112851690B63A0048D2B2 /* GCDWebServer.m in Sources */,
+				E2A0E7FD18F1D36C00C580B1 /* GCDWebServerFileRequest.m in Sources */,
 				E22112871690B63A0048D2B2 /* GCDWebServerConnection.m in Sources */,
+				E2A0E80518F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m in Sources */,
+				E2A0E7F918F1D24700C580B1 /* GCDWebServerDataRequest.m in Sources */,
 				E22112891690B63A0048D2B2 /* GCDWebServerRequest.m in Sources */,
+				E276647C18F3BC2100A034BA /* GCDWebServerErrorResponse.m in Sources */,
+				E2A0E7ED18F1D03700C580B1 /* GCDWebServerDataResponse.m in Sources */,
+				E2A0E7F518F1D1E500C580B1 /* GCDWebServerStreamingResponse.m in Sources */,
+				E2A0E80118F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m in Sources */,
 				E221128B1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */,
+				E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
 				E2BE850C18E785940061360B /* GCDWebUploader.m in Sources */,
 				E221128F1690B6470048D2B2 /* main.m in Sources */,
+				E2A0E7F118F1D12E00C580B1 /* GCDWebServerFileResponse.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -312,12 +401,21 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				E2A0E80B18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
 				E22112861690B63A0048D2B2 /* GCDWebServer.m in Sources */,
+				E276647D18F3BC2100A034BA /* GCDWebServerErrorResponse.m in Sources */,
+				E2A0E7EE18F1D03700C580B1 /* GCDWebServerDataResponse.m in Sources */,
+				E2A0E80218F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m in Sources */,
 				E22112881690B63A0048D2B2 /* GCDWebServerConnection.m in Sources */,
 				E221128A1690B63A0048D2B2 /* GCDWebServerRequest.m in Sources */,
 				E221128C1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */,
+				E2A0E7FA18F1D24700C580B1 /* GCDWebServerDataRequest.m in Sources */,
+				E2A0E80618F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m in Sources */,
 				E2BE850B18E77ECA0061360B /* GCDWebUploader.m in Sources */,
 				E22112951690B64F0048D2B2 /* AppDelegate.m in Sources */,
+				E2A0E7FE18F1D36C00C580B1 /* GCDWebServerFileRequest.m in Sources */,
+				E2A0E7F618F1D1E500C580B1 /* GCDWebServerStreamingResponse.m in Sources */,
+				E2A0E7F218F1D12E00C580B1 /* GCDWebServerFileResponse.m in Sources */,
 				E22112971690B64F0048D2B2 /* main.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -363,18 +461,24 @@
 			buildSettings = {
 				CLANG_ENABLE_OBJC_ARC = YES;
 				GCC_OPTIMIZATION_LEVEL = 0;
+				HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
 				ONLY_ACTIVE_ARCH = YES;
 				WARNING_CFLAGS = (
 					"-Wall",
-					"-Wshadow",
 					"-Weverything",
+					"-Wshadow",
 					"-Wshorten-64-to-32",
+					"-Wno-vla",
+					"-Wno-explicit-ownership-type",
 					"-Wno-gnu-statement-expression",
 					"-Wno-direct-ivar-access",
 					"-Wno-implicit-retain-self",
 					"-Wno-assign-enum",
 					"-Wno-format-nonliteral",
 					"-Wno-cast-align",
+					"-Wno-padded",
+					"-Wno-documentation",
+					"-Wno-documentation-unknown-command",
 				);
 			};
 			name = Debug;
@@ -385,6 +489,7 @@
 				CLANG_ENABLE_OBJC_ARC = YES;
 				GCC_PREPROCESSOR_DEFINITIONS = NDEBUG;
 				GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+				HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
 				WARNING_CFLAGS = "-Wall";
 			};
 			name = Release;

+ 3 - 1
GCDWebUploader/GCDWebUploader.h

@@ -48,10 +48,12 @@
 @property(nonatomic, copy) NSString* prologue;  // Default is mini help (must be raw HTML)
 @property(nonatomic, copy) NSString* epilogue;  // Default is nothing (must be raw HTML)
 @property(nonatomic, copy) NSString* footer;  // Default is application name and version (must be HTML escaped)
-- (id)initWithUploadDirectory:(NSString*)path;
+- (instancetype)initWithUploadDirectory:(NSString*)path;
 @end
 
 @interface GCDWebUploader (Subclassing)
 - (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath;  // Default implementation returns YES
 - (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;  // Default implementation returns YES
+- (BOOL)shouldDeleteItemAtPath:(NSString*)path;  // Default implementation returns YES
+- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path;  // Default implementation returns YES
 @end

+ 225 - 174
GCDWebUploader/GCDWebUploader.m

@@ -34,10 +34,18 @@
 
 #import "GCDWebUploader.h"
 
+#import "GCDWebServerDataRequest.h"
+#import "GCDWebServerMultiPartFormRequest.h"
+#import "GCDWebServerURLEncodedFormRequest.h"
+
+#import "GCDWebServerDataResponse.h"
+#import "GCDWebServerErrorResponse.h"
+#import "GCDWebServerFileResponse.h"
+
 @interface GCDWebUploader () {
 @private
   NSString* _uploadDirectory;
-  id<GCDWebUploaderDelegate> delegate;
+  id<GCDWebUploaderDelegate> __unsafe_unretained _delegate;
   NSArray* _allowedExtensions;
   BOOL _showHidden;
   NSString* _title;
@@ -48,10 +56,7 @@
 }
 @end
 
-@implementation GCDWebUploader
-
-@synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden,
-            title=_title, header=_header, prologue=_prologue, epilogue=_epilogue, footer=_footer;
+@implementation GCDWebUploader (Methods)
 
 - (BOOL)_checkFileExtension:(NSString*)fileName {
   if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
@@ -78,7 +83,199 @@
   return path;
 }
 
-- (id)initWithUploadDirectory:(NSString*)path {
+- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
+  NSString* relativePath = [[request query] objectForKey:@"path"];
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  if (!isDirectory) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a directory", relativePath];
+  }
+  
+  NSString* directoryName = [absolutePath lastPathComponent];
+  if (!_showHidden && [directoryName hasPrefix:@"."]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Listing directory name \"%@\" is not allowed", directoryName];
+  }
+  
+  NSError* error = nil;
+  NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
+  if (contents == nil) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
+  }
+  
+  NSMutableArray* array = [NSMutableArray array];
+  for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) {
+    if (_showHidden || ![item hasPrefix:@"."]) {
+      NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL];
+      NSString* type = [attributes objectForKey:NSFileType];
+      if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) {
+        [array addObject:@{
+                           @"path": [relativePath stringByAppendingPathComponent:item],
+                           @"name": item,
+                           @"size": [attributes objectForKey:NSFileSize]
+                           }];
+      } else if ([type isEqualToString:NSFileTypeDirectory]) {
+        [array addObject:@{
+                           @"path": [[relativePath stringByAppendingPathComponent:item] stringByAppendingString:@"/"],
+                           @"name": item
+                           }];
+      }
+    }
+  }
+  return [GCDWebServerDataResponse responseWithJSONObject:array];
+}
+
+- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
+  NSString* relativePath = [[request query] objectForKey:@"path"];
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  if (isDirectory) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is a directory", relativePath];
+  }
+  
+  NSString* fileName = [absolutePath lastPathComponent];
+  if (([fileName hasPrefix:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading file name \"%@\" is not allowed", fileName];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath:  )]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate webUploader:self didDownloadFileAtPath:absolutePath];
+    });
+  }
+  return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES];
+}
+
+- (GCDWebServerResponse*)uploadFile:(GCDWebServerMultiPartFormRequest*)request {
+  NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch];
+  NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8");  // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
+  
+  GCDWebServerMultiPartFile* file = [request.files objectForKey:@"files[]"];
+  if ((!_showHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName];
+  }
+  NSString* relativePath = [(GCDWebServerMultiPartArgument*)[request.arguments objectForKey:@"path"] string];
+  NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]];
+  
+  if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
+  }
+  
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate webUploader:self didUploadFileAtPath:absolutePath];
+    });
+  }
+  return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType];
+}
+
+- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
+  NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
+  NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath];
+  BOOL isDirectory;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
+  }
+  
+  NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
+  NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]];
+  
+  NSString* itemName = [newAbsolutePath lastPathComponent];
+  if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", itemName];
+  }
+  
+  if (![self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", oldRelativePath, newRelativePath];
+  }
+  
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate webUploader:self didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath];
+    });
+  }
+  return [GCDWebServerDataResponse responseWithJSONObject:@{}];
+}
+
+- (GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request {
+  NSString* relativePath = [request.arguments objectForKey:@"path"];
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory = NO;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSString* itemName = [absolutePath lastPathComponent];
+  if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
+  }
+  
+  if (![self shouldDeleteItemAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
+  }
+  
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate webUploader:self didDeleteItemAtPath:absolutePath];
+    });
+  }
+  return [GCDWebServerDataResponse responseWithJSONObject:@{}];
+}
+
+- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
+  NSString* relativePath = [request.arguments objectForKey:@"path"];
+  NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]];
+  
+  NSString* directoryName = [absolutePath lastPathComponent];
+  if (!_showHidden && [directoryName hasPrefix:@"."]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
+  }
+  
+  if (![self shouldCreateDirectoryAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
+  }
+  
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate webUploader:self didCreateDirectoryAtPath:absolutePath];
+    });
+  }
+  return [GCDWebServerDataResponse responseWithJSONObject:@{}];
+}
+
+@end
+
+@implementation GCDWebUploader
+
+@synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden,
+            title=_title, header=_header, prologue=_prologue, epilogue=_epilogue, footer=_footer;
+
+- (instancetype)initWithUploadDirectory:(NSString*)path {
   if ((self = [super init])) {
     NSBundle* siteBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"GCDWebUploader" ofType:@"bundle"]];
     if (siteBundle == nil) {
@@ -89,9 +286,9 @@
     }
     _uploadDirectory = [[path stringByStandardizingPath] copy];
 #if __has_feature(objc_arc)
-    __unsafe_unretained GCDWebUploader* uploader = self;
+    GCDWebUploader* __unsafe_unretained server = self;
 #else
-    __block GCDWebUploader* uploader = self;
+    __block GCDWebUploader* server = self;
 #endif
     
     // Resource files
@@ -109,7 +306,7 @@
       NSString* device = [(id)SCDynamicStoreCopyComputerName(NULL, NULL) autorelease];
 #endif
 #endif
-      NSString* title = uploader.title;
+      NSString* title = server.title;
       if (title == nil) {
         title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
 #if !TARGET_OS_IPHONE
@@ -118,19 +315,19 @@
         }
 #endif
       }
-      NSString* header = uploader.header;
+      NSString* header = server.header;
       if (header == nil) {
         header = title;
       }
-      NSString* prologue = uploader.prologue;
+      NSString* prologue = server.prologue;
       if (prologue == nil) {
         prologue = [siteBundle localizedStringForKey:@"PROLOGUE" value:@"" table:nil];
       }
-      NSString* epilogue = uploader.epilogue;
+      NSString* epilogue = server.epilogue;
       if (epilogue == nil) {
         epilogue = [siteBundle localizedStringForKey:@"EPILOGUE" value:@"" table:nil];
       }
-      NSString* footer = uploader.footer;
+      NSString* footer = server.footer;
       if (footer == nil) {
         NSString* name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
         NSString* version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
@@ -156,186 +353,32 @@
     
     // File listing
     [self addHandlerForMethod:@"GET" path:@"/list" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
-      
-      NSString* relativePath = [[request query] objectForKey:@"path"];
-      NSString* absolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:relativePath];
-      BOOL isDirectory;
-      if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
-        if (isDirectory) {
-          BOOL showHidden = uploader.showHiddenFiles;
-          NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:NULL];
-          if (contents) {
-            NSMutableArray* array = [NSMutableArray array];
-            for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) {
-              if (showHidden || ![item hasPrefix:@"."]) {
-                NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL];
-                NSString* type = [attributes objectForKey:NSFileType];
-                if ([type isEqualToString:NSFileTypeRegular] && [uploader _checkFileExtension:item]) {
-                  [array addObject:@{
-                                     @"path": [relativePath stringByAppendingPathComponent:item],
-                                     @"name": item,
-                                     @"size": [attributes objectForKey:NSFileSize]
-                                     }];
-                } else if ([type isEqualToString:NSFileTypeDirectory]) {
-                  [array addObject:@{
-                                     @"path": [[relativePath stringByAppendingPathComponent:item] stringByAppendingString:@"/"],
-                                     @"name": item
-                                     }];
-                }
-              }
-            }
-            return [GCDWebServerDataResponse responseWithJSONObject:array];
-          } else {
-            return [GCDWebServerResponse responseWithStatusCode:500];
-          }
-        } else {
-          return [GCDWebServerResponse responseWithStatusCode:400];
-        }
-      } else {
-        return [GCDWebServerResponse responseWithStatusCode:404];
-      }
-      
+      return [server listDirectory:request];
     }];
     
     // File download
     [self addHandlerForMethod:@"GET" path:@"/download" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
-      
-      NSString* relativePath = [[request query] objectForKey:@"path"];
-      NSString* absolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:relativePath];
-      BOOL isDirectory;
-      if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
-        if (isDirectory) {
-          return [GCDWebServerResponse responseWithStatusCode:400];
-        } else {
-          if ([uploader.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath:  )]) {
-            dispatch_async(dispatch_get_main_queue(), ^{
-              [uploader.delegate webUploader:uploader didDownloadFileAtPath:absolutePath];
-            });
-          }
-          return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES];
-        }
-      } else {
-        return [GCDWebServerResponse responseWithStatusCode:404];
-      }
-      
+      return [server downloadFile:request];
     }];
     
     // File upload
     [self addHandlerForMethod:@"POST" path:@"/upload" requestClass:[GCDWebServerMultiPartFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
-      
-      // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
-      NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch];
-      NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8");
-      
-      GCDWebServerMultiPartFile* file = [[(GCDWebServerMultiPartFormRequest*)request files] objectForKey:@"files[]"];
-      if ((![file.fileName hasPrefix:@"."] || uploader.showHiddenFiles) && [uploader _checkFileExtension:file.fileName]) {
-        NSString* relativePath = [(GCDWebServerMultiPartArgument*)[[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"path"] string];
-        NSString* absolutePath = [uploader _uniquePathForPath:[[uploader.uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]];
-        if ([uploader shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
-          NSError* error = nil;
-          if ([[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) {
-            if ([uploader.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) {
-              dispatch_async(dispatch_get_main_queue(), ^{
-                [uploader.delegate webUploader:uploader didUploadFileAtPath:absolutePath];
-              });
-            }
-            return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType];
-          } else {
-            return [GCDWebServerResponse responseWithStatusCode:500];
-          }
-        } else {
-          return [GCDWebServerResponse responseWithStatusCode:403];
-        }
-      } else {
-        return [GCDWebServerResponse responseWithStatusCode:400];
-      }
-      
+      return [server uploadFile:(GCDWebServerMultiPartFormRequest*)request];
     }];
     
     // File and folder moving
     [self addHandlerForMethod:@"POST" path:@"/move" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
-      
-      NSString* oldRelativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"oldPath"];
-      NSString* oldAbsolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:oldRelativePath];
-      BOOL isDirectory;
-      if ([[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
-        NSString* newRelativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"newPath"];
-        if (!uploader.showHiddenFiles) {
-          for (NSString* component in [newRelativePath pathComponents]) {
-            if ([component hasPrefix:@"."]) {
-              return [GCDWebServerResponse responseWithStatusCode:400];
-            }
-          }
-        }
-        if (!isDirectory && ![uploader _checkFileExtension:newRelativePath]) {
-          return [GCDWebServerResponse responseWithStatusCode:400];
-        }
-        NSString* newAbsolutePath = [uploader _uniquePathForPath:[uploader.uploadDirectory stringByAppendingPathComponent:newRelativePath]];
-        if ([uploader shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) {
-          if ([[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:NULL]) {
-            if ([uploader.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) {
-              dispatch_async(dispatch_get_main_queue(), ^{
-                [uploader.delegate webUploader:uploader didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath];
-              });
-            }
-            return [GCDWebServerDataResponse responseWithJSONObject:@{}];
-          } else {
-            return [GCDWebServerResponse responseWithStatusCode:500];
-          }
-        } else {
-          return [GCDWebServerResponse responseWithStatusCode:403];
-        }
-      } else {
-        return [GCDWebServerResponse responseWithStatusCode:404];
-      }
-      
+      return [server moveItem:(GCDWebServerURLEncodedFormRequest*)request];
     }];
     
-    // File deletion
+    // File and folder deletion
     [self addHandlerForMethod:@"POST" path:@"/delete" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
-      
-      NSString* relativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"path"];
-      NSString* absolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:relativePath];
-      if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
-        if ([[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL]) {
-          if ([uploader.delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) {
-            dispatch_async(dispatch_get_main_queue(), ^{
-              [uploader.delegate webUploader:uploader didDeleteItemAtPath:absolutePath];
-            });
-          }
-          return [GCDWebServerDataResponse responseWithJSONObject:@{}];
-        } else {
-          return [GCDWebServerResponse responseWithStatusCode:500];
-        }
-      } else {
-        return [GCDWebServerResponse responseWithStatusCode:404];
-      }
-      
+      return [server deleteItem:(GCDWebServerURLEncodedFormRequest*)request];
     }];
     
     // Directory creation
     [self addHandlerForMethod:@"POST" path:@"/create" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
-      
-      NSString* relativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"path"];
-      if (!uploader.showHiddenFiles) {
-        for (NSString* component in [relativePath pathComponents]) {
-          if ([component hasPrefix:@"."]) {
-            return [GCDWebServerResponse responseWithStatusCode:400];
-          }
-        }
-      }
-      NSString* absolutePath = [uploader _uniquePathForPath:[uploader.uploadDirectory stringByAppendingPathComponent:relativePath]];
-      if ([[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:NULL]) {
-        if ([uploader.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) {
-          dispatch_async(dispatch_get_main_queue(), ^{
-            [uploader.delegate webUploader:uploader didCreateDirectoryAtPath:absolutePath];
-          });
-        }
-        return [GCDWebServerDataResponse responseWithJSONObject:@{}];
-      } else {
-        return [GCDWebServerResponse responseWithStatusCode:500];
-      }
-      
+      return [server createDirectory:(GCDWebServerURLEncodedFormRequest*)request];
     }];
     
   }
@@ -370,4 +413,12 @@
   return YES;
 }
 
+- (BOOL)shouldDeleteItemAtPath:(NSString*)path {
+  return YES;
+}
+
+- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path {
+  return YES;
+}
+
 @end

+ 40 - 2
Mac/main.m

@@ -25,11 +25,21 @@
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+#import "GCDWebServer.h"
+
+#import "GCDWebServerDataRequest.h"
+#import "GCDWebServerURLEncodedFormRequest.h"
+
+#import "GCDWebServerDataResponse.h"
+#import "GCDWebServerStreamingResponse.h"
+
+#import "GCDWebDAVServer.h"
+
 #import "GCDWebUploader.h"
 
 int main(int argc, const char* argv[]) {
   BOOL success = NO;
-  int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 3) : 0);
+  int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 5) : 0);
   @autoreleasepool {
     GCDWebServer* webServer = nil;
     switch (mode) {
@@ -87,7 +97,35 @@ int main(int argc, const char* argv[]) {
       }
       
       case 3: {
-        webServer = [[GCDWebUploader alloc] initWithUploadDirectory:@"/tmp"];
+        webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]];
+        break;
+      }
+      
+      case 4: {
+        webServer = [[GCDWebUploader alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]];
+        break;
+      }
+      
+      case 5: {
+        webServer = [[GCDWebServer alloc] init];
+        [webServer addHandlerForMethod:@"GET"
+                                  path:@"/"
+                          requestClass:[GCDWebServerRequest class]
+                          processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+          
+          __block int countDown = 10;
+          return [GCDWebServerStreamingResponse responseWithContentType:@"text/plain" streamBlock:^NSData *(NSError** error) {
+            
+            usleep(100 * 1000);
+            if (countDown) {
+              return [[NSString stringWithFormat:@"%i\n", countDown--] dataUsingEncoding:NSUTF8StringEncoding];
+            } else {
+              return [NSData data];
+            }
+            
+          }];
+          
+        }];
         break;
       }
       

+ 45 - 8
README.md

@@ -2,16 +2,22 @@ Overview
 ========
 
 GCDWebServer is a lightweight GCD based HTTP 1.1 server designed to be embedded in Mac & iOS apps. It was written from scratch with the following goals in mind:
-* Easy to use and understand: only 4 main classes and less than 10 source code files
+* Easy to use and understand architecture with only 4 core classes: server, connection, request and response
 * Well designed API for easy integration and customization
-* Entirely built with an event-driven design using [Grand Central Dispatch](http://en.wikipedia.org/wiki/Grand_Central_Dispatch) for maximum performance and concurrency
-* Support for streaming large HTTP bodies for requests and responses to minimize memory usage
-* Built-in parser for web forms submitted using "application/x-www-form-urlencoded" or "multipart/form-data" encodings (including file uploads)
+* Entirely built with an event-driven design using [Grand Central Dispatch](http://en.wikipedia.org/wiki/Grand_Central_Dispatch) for maximal performance and concurrency
 * No dependencies on third-party source code
 * Available under a friendly [New BSD License](LICENSE)
 
+Extra built-in features:
+* Minimize memory usage with disk streaming of large HTTP request or response bodies
+* Parser for [web forms](http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4) submitted using "application/x-www-form-urlencoded" or "multipart/form-data" encodings (including file uploads)
+* [JSON](http://www.json.org/) parsing and serialization for request and response HTTP bodies
+* [Chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) for request and response HTTP bodies
+* [HTTP compression](https://en.wikipedia.org/wiki/HTTP_compression) with gzip for request and response HTTP bodies
+
 Included extensions:
 * [GCDWebUploader](GCDWebUploader/GCDWebUploader.h): subclass of GCDWebServer that implements an interface for uploading and downloading files from an iOS app's sandbox using a web browser
+* [GCDWebDAVServer](GCDWebDAVServer/GCDWebDAVServer.h): subclass of GCDWebServer that implements a class 1 [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server (with partial class 2 support for OS X Finder)
 
 What's not available out of the box but can be implemented on top of the API:
 * Authentication like [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
@@ -25,6 +31,16 @@ Requirements:
 * OS X 10.7 or later (x86_64)
 * iOS 5.0 or later (armv7, armv7s or arm64)
 
+Getting Started
+===============
+
+Download or checkout the source for GCDWebServer then add the entire "GCDWebServer" subfolder to your Xcode project. If you intend to use one of the extensions like GCDWebDAVServer or GCDWebUploader, add these subfolders as well.
+
+Alternatively, you can install GCDWebServer using [CocoaPods](http://cocoapods.org/) by simply adding this line to your Xcode project's Podfile:
+```
+pod "GCDWebServer", "~> 2.0"
+```
+
 Hello World
 ===========
 
@@ -59,10 +75,10 @@ int main(int argc, const char* argv[]) {
 }
 ```
 
-Adding File Upload to iOS Apps
-==============================
+Web Based Uploads in iOS Apps
+=============================
 
-GCDWebUploader is a subclass of GCDWebServer that provides a ready-to-use HTML 5 file uploader & downloader. This lets users upload, download and delete files from a directory inside your iOS app's sandbox using a clean user interface in their web browser.
+GCDWebUploader is a subclass of GCDWebServer that provides a ready-to-use HTML 5 file uploader & downloader. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using a clean user interface in their web browser.
 
 Simply instantiate and run a GCDWebUploader instance then visit http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ from your web browser:
 
@@ -78,6 +94,27 @@ Simply instantiate and run a GCDWebUploader instance then visit http://{YOUR-IOS
 }
 ```
 
+WebDAV Server in iOS Apps
+=========================
+
+GCDWebDAVServer is a subclass of GCDWebServer that provides a class 1 compliant [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using any WebDAV client like [Transmit](https://panic.com/transmit/) (Mac), [ForkLift](http://binarynights.com/forklift/) (Mac) or [CyberDuck](http://cyberduck.io/) (Mac / Windows).
+
+GCDWebDAVServer should also work with the [OS X Finder](http://support.apple.com/kb/PH13859) as it is partially class 2 compliant (but only when the client is the OS X WebDAV implementation).
+
+Simply instantiate and run a GCDWebDAVServer instance then connect to http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ using a WebDAV client:
+
+```objectivec
+#import "GCDWebDAVServer.h"
+
+- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
+  NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
+  GCDWebDAVServer* davServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:documentsPath];
+  [davServer start];
+  NSLog(@"Visit %@ in your WebDAV client", davServer.serverURL);
+  return YES;
+}
+```
+
 Serving a Static Website
 ========================
 
@@ -224,6 +261,6 @@ NSString* websitePath = [[NSBundle mainBundle] pathForResource:@"Website" ofType
 Final Example: File Downloads and Uploads From iOS App
 ======================================================
 
-GCDWebServer was originally written for the [ComicFlow](http://itunes.apple.com/us/app/comicflow/id409290355?mt=8) comic reader app for iPad. It uses it to provide a web server for people to upload and download comic files directly over WiFi to and from the app.
+GCDWebServer was originally written for the [ComicFlow](http://itunes.apple.com/us/app/comicflow/id409290355?mt=8) comic reader app for iPad. It allow users to connect to their iPad with their web browser over WiFi and then upload, download and organize comic files inside the app.
 
 ComicFlow is [entirely open-source](https://github.com/swisspol/ComicFlow) and you can see how it uses GCDWebUploader in the [WebServer.m](https://github.com/swisspol/ComicFlow/blob/master/Classes/WebServer.m) file.