From 7af258eb6b68dd602e3b9eea5b63f67b55545273 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 12:21:19 -0700 Subject: [PATCH] #17 Added support for chunked transfer encoding in request bodies --- CGDWebServer/GCDWebServerConnection.m | 187 +++++++++++++++++----- CGDWebServer/GCDWebServerDataRequest.m | 6 +- CGDWebServer/GCDWebServerPrivate.h | 1 + CGDWebServer/GCDWebServerRequest.h | 2 +- CGDWebServer/GCDWebServerRequest.m | 22 ++- CGDWebServer/GCDWebServerResponse.h | 4 +- CGDWebServer/GCDWebServerResponse.m | 4 +- CGDWebServer/GCDWebServerStreamResponse.h | 2 +- GCDWebServer.xcodeproj/project.pbxproj | 1 + 9 files changed, 172 insertions(+), 57 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 515821f..43cd4fc 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/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); diff --git a/CGDWebServer/GCDWebServerDataRequest.m b/CGDWebServer/GCDWebServerDataRequest.m index 801dbae..d010efd 100644 --- a/CGDWebServer/GCDWebServerDataRequest.m +++ b/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; diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 3dfdae9..374ee94 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/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; diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index 256c0c1..7c6d818 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/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 diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 1a2626b..449fa34 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/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); diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index 433cd08..e2b76e4 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/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; diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 1ef8a07..4358790 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -83,7 +83,7 @@ - (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)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)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; diff --git a/CGDWebServer/GCDWebServerStreamResponse.h b/CGDWebServer/GCDWebServerStreamResponse.h index 62ea9b0..205c461 100644 --- a/CGDWebServer/GCDWebServerStreamResponse.h +++ b/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 diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 92090a0..9d1e3e8 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/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",