Эх сурвалжийг харах

#17 Added support for chunked transfer encoding in request bodies

Pierre-Olivier Latour 11 жил өмнө
parent
commit
7af258eb6b

+ 145 - 42
CGDWebServer/GCDWebServerConnection.m

@@ -41,7 +41,8 @@ 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;
@@ -121,7 +122,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];
@@ -151,14 +152,13 @@ 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) {
+      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 dataWithBytes:chunkBytes length: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);
@@ -167,7 +167,8 @@ static dispatch_queue_t _formatterQueue = NULL;
           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 +177,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 +188,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)
@@ -263,9 +333,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);
@@ -363,48 +437,69 @@ static dispatch_queue_t _formatterQueue = NULL;
   
 }
 
-- (void)_readRequestBody:(NSData*)initialData {
+- (void)_readBodyWithLength:(NSUInteger)length initialData:(NSData*)initialData {
   NSError* error = nil;
-  if ([_request performOpen:&error]) {
-    NSInteger length = _request.contentLength;
-    if (initialData.length) {
-      if ([_request performWriteData:initialData error:&error]) {
-        length -= initialData.length;
-        DCHECK(length >= 0);
-      } else {
-        LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
-        length = -1;
+  if (![_request performOpen:&error]) {
+    LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
+    [self _abortWithStatusCode:500];
+    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 _abortWithStatusCode:500];
+      return;
     }
-    if (length > 0) {
-      [self _readBodyWithRemainingLength:length 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 _abortWithStatusCode:500];
-        }
-        
-      }];
-    } else if (length == 0) {
-      if ([_request performClose:&error]) {
+    length -= initialData.length;
+  }
+  
+  if (length) {
+    [self _readBodyWithRemainingLength:length 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 _abortWithStatusCode:500];
       }
+      
+    }];
+  } else {
+    if ([_request performClose:&error]) {
+      [self _processRequest];
     } else {
-      if (![_request performClose:&error]) {
-        LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
-      }
+      LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
       [self _abortWithStatusCode:500];
     }
-  } else {
+  }
+}
+
+- (void)_readChunkedBodyWithInitialData:(NSData*)initialData {
+  NSError* error = nil;
+  if (![_request performOpen:&error]) {
     LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
     [self _abortWithStatusCode:500];
+    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 _abortWithStatusCode:500];
+    }
+    
+  }];
+  ARC_RELEASE(chunkData);
 }
 
 - (void)_readRequestHeaders {
@@ -434,14 +529,18 @@ static dispatch_queue_t _formatterQueue = NULL;
       }
       if (_request) {
         if (_request.hasBody) {
-          if (extraData.length <= _request.contentLength) {
+          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];
+                    }
                   }
                   
                 }];
@@ -450,7 +549,11 @@ static dispatch_queue_t _formatterQueue = NULL;
                 [self _abortWithStatusCode:417];
               }
             } 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);
@@ -540,7 +643,7 @@ 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 for \"%@\" (%lu bytes body)", _socket, _request.method, _request.path, (unsigned long)_bytesRead);
   GCDWebServerResponse* response = nil;
   @try {
     response = block(request);

+ 5 - 1
CGDWebServer/GCDWebServerDataRequest.m

@@ -46,7 +46,11 @@
 
 - (BOOL)open:(NSError**)error {
   DCHECK(_data == nil);
-  _data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
+  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;

+ 1 - 0
CGDWebServer/GCDWebServerPrivate.h

@@ -119,6 +119,7 @@ extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset)
 @end
 
 @interface GCDWebServerRequest ()
+@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
 - (BOOL)performOpen:(NSError**)error;
 - (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
 - (BOOL)performClose:(NSError**)error;

+ 1 - 1
CGDWebServer/GCDWebServerRequest.h

@@ -40,7 +40,7 @@
 @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) NSUInteger contentLength;  // Automatically parsed from headers (NSNotFound if request has no "Content-Length" 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

+ 14 - 8
CGDWebServer/GCDWebServerRequest.m

@@ -96,7 +96,7 @@
   DCHECK(!_finished);
   _stream.next_in = (Bytef*)data.bytes;
   _stream.avail_in = (uInt)data.length;
-  NSMutableData* decodedData = [[NSMutableData alloc] initWithLength:1024]; // kGZipInitialBufferSize
+  NSMutableData* decodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
   if (decodedData == nil) {
     DNOT_REACHED();
     return NO;
@@ -143,6 +143,7 @@
   NSString* _path;
   NSDictionary* _query;
   NSString* _type;
+  BOOL _chunked;
   NSUInteger _length;
   NSRange _range;
   
@@ -154,7 +155,8 @@
 
 @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, byteRange=_range,
+            usesChunkedTransferEncoding=_chunked;
 
 - (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
   if ((self = [super init])) {
@@ -165,19 +167,23 @@
     _query = ARC_RETAIN(query);
     
     _type = ARC_RETAIN([_headers objectForKey:@"Content-Type"]);
+    _chunked = [[[_headers objectForKey:@"Transfer-Encoding"] lowercaseString] isEqualToString:@"chunked"];
     NSString* lengthHeader = [_headers objectForKey:@"Content-Length"];
-    if (_type) {
+    if (lengthHeader) {
       NSInteger length = [lengthHeader integerValue];
-      if ((lengthHeader == nil) || (length < 0)) {
+      if (_chunked || !_type || (length < 0)) {
         DNOT_REACHED();
         ARC_RELEASE(self);
         return nil;
       }
       _length = length;
-    } else if (lengthHeader) {
-      DNOT_REACHED();
-      ARC_RELEASE(self);
-      return nil;
+    } else {
+      if (_type && !_chunked) {
+        DNOT_REACHED();
+        ARC_RELEASE(self);
+        return nil;
+      }
+      _length = NSNotFound;
     }
     
     _range = NSMakeRange(NSNotFound, 0);

+ 2 - 2
CGDWebServer/GCDWebServerResponse.h

@@ -38,8 +38,8 @@
 @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, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled;
-@property(nonatomic, getter=isChunkedTransferEncodingEnabled) BOOL chunkedTransferEncodingEnabled;
+@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled;  // Default is disabled
+@property(nonatomic, getter=isChunkedTransferEncodingEnabled) BOOL chunkedTransferEncodingEnabled;  // Default is disabled
 + (GCDWebServerResponse*) response;
 - (id)init;
 - (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header;

+ 2 - 2
CGDWebServer/GCDWebServerResponse.m

@@ -83,7 +83,7 @@
 
 - (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
+    response.contentLength = NSNotFound;  // Make sure "Content-Length" header is not set since body length is determined by chunked transfer encoding
     [response setValue:@"chunked" forAdditionalHeader:@"Transfer-Encoding"];
   }
   return self;
@@ -137,7 +137,7 @@
 
 - (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
+    response.contentLength = NSNotFound;  // Make sure "Content-Length" header is not set since body length is determined by closing the connection
     [response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
   }
   return self;

+ 1 - 1
CGDWebServer/GCDWebServerStreamResponse.h

@@ -29,7 +29,7 @@
 
 typedef NSData* (^GCDWebServerStreamBlock)(NSError** error);
 
-@interface GCDWebServerStreamResponse : GCDWebServerResponse  // Forces chunked transfer encoding
+@interface GCDWebServerStreamResponse : GCDWebServerResponse  // Automatically enables chunked transfer encoding
 + (GCDWebServerStreamResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
 - (id)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;  // Block must return empty NSData when done or nil on error
 @end

+ 1 - 0
GCDWebServer.xcodeproj/project.pbxproj

@@ -433,6 +433,7 @@
 					"-Weverything",
 					"-Wshadow",
 					"-Wshorten-64-to-32",
+					"-Wno-vla",
 					"-Wno-explicit-ownership-type",
 					"-Wno-gnu-statement-expression",
 					"-Wno-direct-ivar-access",