From 7506f9c9a28b7a4d140893b6a4b44c9063fe3baa Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Sun, 6 Apr 2014 10:10:25 -0700 Subject: [PATCH 01/60] Fix --- CGDWebServer/GCDWebServer.m | 6 +----- GCDWebUploader/GCDWebUploader.m | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index f443cb5..53819fb 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -585,11 +585,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; -#else - __block GCDWebServer* server = self; -#endif + GCDWebServer* __unsafe_unretained server = self; [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { if (![requestMethod isEqualToString:@"GET"]) { diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index 94dfc16..0445d72 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -88,11 +88,7 @@ return nil; } _uploadDirectory = [[path stringByStandardizingPath] copy]; -#if __has_feature(objc_arc) - __unsafe_unretained GCDWebUploader* uploader = self; -#else - __block GCDWebUploader* uploader = self; -#endif + GCDWebUploader* __unsafe_unretained uploader = self; // Resource files [self addGETHandlerForBasePath:@"/" directoryPath:[siteBundle resourcePath] indexFilename:nil cacheAge:3600 allowRangeRequests:NO]; From 81638ad08628c095dfc7a5aab135eeb2bf664c30 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Sun, 6 Apr 2014 10:44:53 -0700 Subject: [PATCH 02/60] First pass at adding body encoders --- CGDWebServer/GCDWebServerConnection.m | 54 ++-- CGDWebServer/GCDWebServerPrivate.h | 7 + CGDWebServer/GCDWebServerResponse.h | 25 +- CGDWebServer/GCDWebServerResponse.m | 406 +++++++++++++++++-------- GCDWebServer.xcodeproj/project.pbxproj | 11 +- 5 files changed, 343 insertions(+), 160 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 8fb85b4..f140271 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/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); @@ -234,27 +233,25 @@ 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]; - } else { - block(NO); - } - - }]; - 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); + NSError* error = nil; + NSData* data = [_response performReadData:&error]; + if (data) { + if (data.length) { + [self _writeData:data withCompletionBlock:^(BOOL success) { + + if (success) { + [self _writeBodyWithCompletionBlock:block]; + } else { + block(NO); + } + + }]; + } else { + block(YES); + } } else { - block(YES); - free(buffer); + LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error); + block(NO); } } @@ -318,8 +315,13 @@ static dispatch_queue_t _formatterQueue = NULL; DCHECK(_responseMessage == NULL); GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock]; - if (![response hasBody] || [response open]) { - _response = ARC_RETAIN(response); + if (response) { + NSError* error = nil; + if ([response hasBody] && ![response performOpen:&error]) { + LOG_WARNING(@"Failed opening response body for socket %i: %@", _socket, error); + } else { + _response = ARC_RETAIN(response); + } } if (_response) { @@ -344,12 +346,13 @@ static dispatch_queue_t _formatterQueue = NULL; if ([_response hasBody]) { [self _writeBodyWithCompletionBlock:^(BOOL successInner) { - [_response close]; // Can't do anything with result anyway + [_response performClose]; + LOG_VERBOSE(@"%@ | %@ \"%@ %@\" %i %lu", self.localAddressString, self.remoteAddressString, _request.method, _request.path, (int)_response.statusCode, (unsigned long)_bytesWritten); }]; } } else if ([_response hasBody]) { - [_response close]; // Can't do anything with result anyway + [_response performClose]; } }]; @@ -537,7 +540,6 @@ static NSString* _StringFromAddressData(NSData* data) { 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); diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index dad7adc..ec8d823 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -104,3 +104,10 @@ extern void GCDLogMessage(long level, NSString* format, ...) NS_FORMAT_FUNCTION( @property(nonatomic, readonly) GCDWebServerProcessBlock processBlock; - (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; @end + +@interface GCDWebServerResponse () +@property(nonatomic, readonly) NSDictionary* additionalHeaders; +- (BOOL)performOpen:(NSError**)error; +- (NSData*)performReadData:(NSError**)error; +- (void)performClose; +@end diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index 432619d..0d89e43 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -27,26 +27,27 @@ #import -typedef NSData* (^GCDWebServerChunkBlock)(); +typedef NSData* (^GCDWebServerStreamBlock)(NSError** error); -@interface GCDWebServerResponse : NSObject +@protocol GCDWebServerBodyReader +- (BOOL)open:(NSError**)error; +- (NSData*)readData:(NSError**)error; // Return nil on error or empty NSData if at end +- (void)close; +@end + +@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; +@property(nonatomic) BOOL gzipContentEncoding; +@property(nonatomic) BOOL chunkedTransferEncoding; + (GCDWebServerResponse*) response; - (id)init; - (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; - (BOOL)hasBody; // Convenience method @end -@interface GCDWebServerResponse (Subclassing) -- (BOOL)open; // Implementation required -- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length; // Implementation required -- (BOOL)close; // Implementation required -@end - @interface GCDWebServerResponse (Extensions) + (GCDWebServerResponse*)responseWithStatusCode:(NSInteger)statusCode; + (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent; @@ -83,7 +84,7 @@ typedef NSData* (^GCDWebServerChunkBlock)(); - (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 +@interface GCDWebServerStreamResponse : GCDWebServerResponse // Forces 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/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index bb777b1..a78e17a 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -26,9 +26,188 @@ */ #import +#import #import "GCDWebServerPrivate.h" +#define kZlibErrorDomain @"ZlibErrorDomain" +#define kGZipInitialBufferSize (256 * 1024) +#define kFileReadBufferSize (32 * 1024) + +@interface GCDWebServerBodyEncoder : NSObject +- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)reader; +@end + +@interface GCDWebServerChunkEncoder : GCDWebServerBodyEncoder +@end + +@interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder +@end + +@interface GCDWebServerBodyEncoder () { +@private + GCDWebServerResponse* __unsafe_unretained _response; + id __unsafe_unretained _reader; +} +@end + +@implementation GCDWebServerBodyEncoder + +- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)reader { + if ((self = [super init])) { + _response = response; + _reader = reader; + } + return self; +} + +- (BOOL)open:(NSError**)error { + return [_reader open:error]; +} + +- (NSData*)readData:(NSError**)error { + return [_reader readData:error]; +} + +- (void)close { + [_reader close]; +} + +@end + +@interface GCDWebServerChunkEncoder () { +@private + BOOL _finished; +} +@end + +@implementation GCDWebServerChunkEncoder + +- (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 setValue:@"chunked" forAdditionalHeader:@"Transfer-Encoding"]; + } + return self; +} + +- (NSData*)readData:(NSError**)error { + NSData* chunk; + if (_finished) { + chunk = [[NSData alloc] init]; + } else { + NSData* data = [super readData:error]; + if (data == nil) { + return nil; + } + if (data.length) { + 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)]; + if (chunk == nil) { + DNOT_REACHED(); + return nil; + } + 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'; + } else { + chunk = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5]; + DCHECK(chunk); + _finished = YES; + } + } + return ARC_AUTORELEASE(chunk); +} + +@end + +@interface GCDWebServerGZipEncoder () { +@private + z_stream _stream; + BOOL _finished; +} +@end + +@implementation GCDWebServerGZipEncoder + +- (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 setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"]; + } + return self; +} + +- (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; +} + +- (NSData*)readData:(NSError**)error { + NSMutableData* gzipData; + if (_finished) { + gzipData = [[NSMutableData alloc] init]; + } else { + gzipData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize]; + if (gzipData == 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 = gzipData.length - length; + _stream.next_out = (Bytef*)((char*)gzipData.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(gzipData); + *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil]; + return nil; + } + length += maxLength - _stream.avail_out; + if (_stream.avail_out > 0) { + break; + } + gzipData.length = 2 * gzipData.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 + gzipData.length = length; + } + return ARC_AUTORELEASE(gzipData); +} + +- (void)close { + deflateEnd(&_stream); + [super close]; +} + +@end + @interface GCDWebServerResponse () { @private NSString* _type; @@ -36,37 +215,19 @@ NSInteger _status; NSUInteger _maxAge; NSMutableDictionary* _headers; -} -@end - -@interface GCDWebServerDataResponse () { -@private - NSData* _data; - NSInteger _offset; -} -@end - -@interface GCDWebServerFileResponse () { -@private - NSString* _path; - NSUInteger _offset; - NSUInteger _size; - int _file; -} -@end - -@interface GCDWebServerChunkedResponse () { -@private - GCDWebServerChunkBlock _block; - NSData* _chunk; - NSUInteger _offset; - BOOL _terminated; + BOOL _gzipped; + BOOL _chunked; + + BOOL _opened; + NSMutableArray* _encoders; + id __unsafe_unretained _reader; } @end @implementation GCDWebServerResponse -@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, additionalHeaders=_headers; +@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, + gzipContentEncoding=_gzipped, chunkedTransferEncoding=_chunked, additionalHeaders=_headers; + (GCDWebServerResponse*)response { return ARC_AUTORELEASE([[[self class] alloc] init]); @@ -79,6 +240,7 @@ _status = 200; _maxAge = 0; _headers = [[NSMutableDictionary alloc] init]; + _encoders = [[NSMutableArray alloc] init]; } return self; } @@ -86,6 +248,7 @@ - (void)dealloc { ARC_RELEASE(_type); ARC_RELEASE(_headers); + ARC_RELEASE(_encoders); ARC_DEALLOC(super); } @@ -98,23 +261,47 @@ return _type ? YES : NO; } -@end - -@implementation GCDWebServerResponse (Subclassing) - -- (BOOL)open { - [self doesNotRecognizeSelector:_cmd]; - return NO; +- (BOOL)open:(NSError**)error { + return YES; } -- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length { - [self doesNotRecognizeSelector:_cmd]; - return -1; +- (NSData*)readData:(NSError**)error { + return nil; } -- (BOOL)close { - [self doesNotRecognizeSelector:_cmd]; - return NO; +- (void)close { + ; +} + +- (BOOL)performOpen:(NSError**)error { + if (_opened) { + DNOT_REACHED(); + return NO; + } + _opened = YES; + + _reader = self; + if (_gzipped) { + GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader]; + [_encoders addObject:encoder]; + ARC_RELEASE(encoder); + _reader = encoder; + } + if (_chunked) { + GCDWebServerChunkEncoder* encoder = [[GCDWebServerChunkEncoder alloc] initWithResponse:self reader:_reader]; + [_encoders addObject:encoder]; + ARC_RELEASE(encoder); + _reader = encoder; + } + return [_reader open:error]; +} + +- (NSData*)performReadData:(NSError**)error { + return [_reader readData:error]; +} + +- (void)performClose { + [_reader close]; } @end @@ -146,6 +333,13 @@ @end +@interface GCDWebServerDataResponse () { +@private + NSData* _data; + BOOL _done; +} +@end + @implementation GCDWebServerDataResponse + (GCDWebServerDataResponse*)responseWithData:(NSData*)data contentType:(NSString*)type { @@ -161,7 +355,6 @@ if ((self = [super init])) { _data = ARC_RETAIN(data); - _offset = -1; self.contentType = type; self.contentLength = data.length; @@ -170,33 +363,20 @@ } - (void)dealloc { - DCHECK(_offset < 0); ARC_RELEASE(_data); 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; +- (NSData*)readData:(NSError**)error { + NSData* data; + if (_done) { + data = [NSData data]; + } else { + data = _data; + _done = YES; } - return size; -} - -- (BOOL)close { - DCHECK(_offset >= 0); - _offset = -1; - return YES; + return data; } @end @@ -268,6 +448,15 @@ @end +@interface GCDWebServerFileResponse () { +@private + NSString* _path; + NSUInteger _offset; + NSUInteger _size; + int _file; +} +@end + @implementation GCDWebServerFileResponse + (GCDWebServerFileResponse*)responseWithFile:(NSString*)path { @@ -356,13 +545,19 @@ ARC_DEALLOC(super); } -- (BOOL)open { +static inline NSError* _MakePosixError(int code) { + return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}]; +} + +- (BOOL)open:(NSError**)error { DCHECK(_file <= 0); _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); _file = 0; return NO; @@ -370,91 +565,60 @@ return YES; } -- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length { +- (NSData*)readData:(NSError**)error { DCHECK(_file > 0); - ssize_t result = read(_file, buffer, MIN(length, _size)); + 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 result; + return ARC_AUTORELEASE(data); } -- (BOOL)close { +- (void)close { DCHECK(_file > 0); - int result = close(_file); + close(_file); _file = 0; - return (result == 0 ? YES : NO); } @end -@implementation GCDWebServerChunkedResponse +@interface GCDWebServerStreamResponse () { +@private + GCDWebServerStreamBlock _block; +} +@end -+ (GCDWebServerChunkedResponse*)responseWithContentType:(NSString*)type chunkBlock:(GCDWebServerChunkBlock)block { - return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type chunkBlock:block]); +@implementation GCDWebServerStreamResponse + ++ (GCDWebServerStreamResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { + return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]); } -- (id)initWithContentType:(NSString*)type chunkBlock:(GCDWebServerChunkBlock)block { +- (id)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { if ((self = [super init])) { _block = [block copy]; self.contentType = type; - [self setValue:@"chunked" forAdditionalHeader:@"Transfer-Encoding"]; + self.chunkedTransferEncoding = YES; } 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; - } - 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_RELEASE(_block); ARC_DEALLOC(super); } +- (NSData*)readData:(NSError**)error { + return _block(error); +} + @end diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 739df1e..b076fcc 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ 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 */; }; + 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 +99,8 @@ 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; }; + 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 = ""; }; E2BE850818E77ECA0061360B /* GCDWebUploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebUploader.h; sourceTree = ""; }; E2BE850918E77ECA0061360B /* GCDWebUploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebUploader.m; sourceTree = ""; }; @@ -111,6 +115,7 @@ E2BE851118E79DAF0061360B /* SystemConfiguration.framework in Frameworks */, E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */, E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */, + E2B0D4A718F13495009A7927 /* libz.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -121,6 +126,7 @@ E221129D1690B7BA0048D2B2 /* MobileCoreServices.framework in Frameworks */, E221129B1690B7B10048D2B2 /* UIKit.framework in Frameworks */, E22112991690B7AA0048D2B2 /* CFNetwork.framework in Frameworks */, + E2B0D4A918F134A8009A7927 /* libz.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -191,6 +197,7 @@ E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */, E221129A1690B7B10048D2B2 /* UIKit.framework */, E22112981690B7AA0048D2B2 /* CFNetwork.framework */, + E2B0D4A818F134A8009A7927 /* libz.dylib */, ); name = "iOS Frameworks and Libraries"; sourceTree = ""; @@ -201,6 +208,7 @@ E2BE851018E79DAF0061360B /* SystemConfiguration.framework */, E208D1B2167BB17E00500836 /* CoreServices.framework */, E208D148167B76B700500836 /* CFNetwork.framework */, + E2B0D4A618F13495009A7927 /* libz.dylib */, ); name = "Mac Frameworks and Libraries"; sourceTree = ""; @@ -366,9 +374,10 @@ ONLY_ACTIVE_ARCH = YES; WARNING_CFLAGS = ( "-Wall", - "-Wshadow", "-Weverything", + "-Wshadow", "-Wshorten-64-to-32", + "-Wno-explicit-ownership-type", "-Wno-gnu-statement-expression", "-Wno-direct-ivar-access", "-Wno-implicit-retain-self", From 1f9a0d38d06e48da2fa39a3f38f06157bdd47a60 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Sun, 6 Apr 2014 11:32:07 -0700 Subject: [PATCH 03/60] Split class files --- CGDWebServer/GCDWebServer.m | 27 + CGDWebServer/GCDWebServerDataRequest.h | 32 ++ CGDWebServer/GCDWebServerDataRequest.m | 64 +++ CGDWebServer/GCDWebServerDataResponse.h | 46 ++ CGDWebServer/GCDWebServerDataResponse.m | 143 ++++++ CGDWebServer/GCDWebServerFileRequest.h | 32 ++ CGDWebServer/GCDWebServerFileRequest.m | 74 +++ CGDWebServer/GCDWebServerFileResponse.h | 39 ++ CGDWebServer/GCDWebServerFileResponse.m | 173 +++++++ .../GCDWebServerMultiPartFormRequest.h | 49 ++ .../GCDWebServerMultiPartFormRequest.m | 367 +++++++++++++ CGDWebServer/GCDWebServerPrivate.h | 12 + CGDWebServer/GCDWebServerRequest.h | 34 -- CGDWebServer/GCDWebServerRequest.m | 483 ------------------ CGDWebServer/GCDWebServerResponse.h | 36 -- CGDWebServer/GCDWebServerResponse.m | 292 ----------- CGDWebServer/GCDWebServerStreamResponse.h | 35 ++ CGDWebServer/GCDWebServerStreamResponse.m | 62 +++ .../GCDWebServerURLEncodedFormRequest.h | 33 ++ .../GCDWebServerURLEncodedFormRequest.m | 63 +++ GCDWebServer.xcodeproj/project.pbxproj | 56 ++ GCDWebUploader/GCDWebUploader.m | 5 + Mac/main.m | 3 + README.md | 2 +- 24 files changed, 1316 insertions(+), 846 deletions(-) create mode 100644 CGDWebServer/GCDWebServerDataRequest.h create mode 100644 CGDWebServer/GCDWebServerDataRequest.m create mode 100644 CGDWebServer/GCDWebServerDataResponse.h create mode 100644 CGDWebServer/GCDWebServerDataResponse.m create mode 100644 CGDWebServer/GCDWebServerFileRequest.h create mode 100644 CGDWebServer/GCDWebServerFileRequest.m create mode 100644 CGDWebServer/GCDWebServerFileResponse.h create mode 100644 CGDWebServer/GCDWebServerFileResponse.m create mode 100644 CGDWebServer/GCDWebServerMultiPartFormRequest.h create mode 100644 CGDWebServer/GCDWebServerMultiPartFormRequest.m create mode 100644 CGDWebServer/GCDWebServerStreamResponse.h create mode 100644 CGDWebServer/GCDWebServerStreamResponse.m create mode 100644 CGDWebServer/GCDWebServerURLEncodedFormRequest.h create mode 100644 CGDWebServer/GCDWebServerURLEncodedFormRequest.m diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 53819fb..a8cab82 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -88,6 +88,33 @@ void GCDLogMessage(long level, NSString* format, ...) { #endif +NSString* GCDWebServerExtractHeaderParameter(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]; + } + } + ARC_RELEASE(scanner); + } + return value; +} + +// 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* GCDWebServerGetMimeTypeForExtension(NSString* extension) { static NSDictionary* _overrides = nil; if (_overrides == nil) { diff --git a/CGDWebServer/GCDWebServerDataRequest.h b/CGDWebServer/GCDWebServerDataRequest.h new file mode 100644 index 0000000..2cb923f --- /dev/null +++ b/CGDWebServer/GCDWebServerDataRequest.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 GCDWebServerDataRequest : GCDWebServerRequest +@property(nonatomic, readonly) NSData* data; // Only valid after open / write / close sequence +@end diff --git a/CGDWebServer/GCDWebServerDataRequest.m b/CGDWebServer/GCDWebServerDataRequest.m new file mode 100644 index 0000000..1bfab58 --- /dev/null +++ b/CGDWebServer/GCDWebServerDataRequest.m @@ -0,0 +1,64 @@ +/* + 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; +} +@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; +} + +- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length { + DCHECK(_data != nil); + [_data appendBytes:buffer length:length]; + return length; +} + +- (BOOL)close { + DCHECK(_data != nil); + return YES; +} + +@end diff --git a/CGDWebServer/GCDWebServerDataResponse.h b/CGDWebServer/GCDWebServerDataResponse.h new file mode 100644 index 0000000..05ecdb5 --- /dev/null +++ b/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 ++ (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 diff --git a/CGDWebServer/GCDWebServerDataResponse.m b/CGDWebServer/GCDWebServerDataResponse.m new file mode 100644 index 0000000..b36d2df --- /dev/null +++ b/CGDWebServer/GCDWebServerDataResponse.m @@ -0,0 +1,143 @@ +/* + 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 + ++ (GCDWebServerDataResponse*)responseWithData:(NSData*)data contentType:(NSString*)type { + return ARC_AUTORELEASE([[[self class] alloc] initWithData:data contentType:type]); +} + +- (id)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; +} + +@end + +@implementation GCDWebServerDataResponse (Extensions) + ++ (GCDWebServerDataResponse*)responseWithText:(NSString*)text { + return ARC_AUTORELEASE([[self alloc] initWithText:text]); +} + ++ (GCDWebServerDataResponse*)responseWithHTML:(NSString*)html { + return ARC_AUTORELEASE([[self alloc] initWithHTML:html]); +} + ++ (GCDWebServerDataResponse*)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables { + return ARC_AUTORELEASE([[self alloc] initWithHTMLTemplate:path variables:variables]); +} + ++ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object { + return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object]); +} + ++ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object contentType:(NSString*)type { + return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object contentType:type]); +} + +- (id)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"]; +} + +- (id)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"]; +} + +- (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; +} + +- (id)initWithJSONObject:(id)object { + return [self initWithJSONObject:object contentType:@"application/json"]; +} + +- (id)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 diff --git a/CGDWebServer/GCDWebServerFileRequest.h b/CGDWebServer/GCDWebServerFileRequest.h new file mode 100644 index 0000000..7ef3ac2 --- /dev/null +++ b/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* filePath; // Only valid after open / write / close sequence +@end diff --git a/CGDWebServer/GCDWebServerFileRequest.m b/CGDWebServer/GCDWebServerFileRequest.m new file mode 100644 index 0000000..2a5c1c7 --- /dev/null +++ b/CGDWebServer/GCDWebServerFileRequest.m @@ -0,0 +1,74 @@ +/* + 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* _filePath; + int _file; +} +@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]]); + } + 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 diff --git a/CGDWebServer/GCDWebServerFileResponse.h b/CGDWebServer/GCDWebServerFileResponse.h new file mode 100644 index 0000000..ec296d7 --- /dev/null +++ b/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 ++ (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 diff --git a/CGDWebServer/GCDWebServerFileResponse.m b/CGDWebServer/GCDWebServerFileResponse.m new file mode 100644 index 0000000..2631c51 --- /dev/null +++ b/CGDWebServer/GCDWebServerFileResponse.m @@ -0,0 +1,173 @@ +/* + 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 + +#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 + ++ (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 ((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 + } + } + + 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(); + } + } + + 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); +} + +- (BOOL)open:(NSError**)error { + DCHECK(_file <= 0); + _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); + _file = 0; + return NO; + } + return YES; +} + +- (NSData*)readData:(NSError**)error { + DCHECK(_file > 0); + 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 { + DCHECK(_file > 0); + close(_file); + _file = 0; +} + +@end diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.h b/CGDWebServer/GCDWebServerMultiPartFormRequest.h new file mode 100644 index 0000000..a3ea6b7 --- /dev/null +++ b/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; // 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; +@end diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.m b/CGDWebServer/GCDWebServerMultiPartFormRequest.m new file mode 100644 index 0000000..52f5933 --- /dev/null +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.m @@ -0,0 +1,367 @@ +/* + 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]; + NSArray* components = [_contentType componentsSeparatedByString:@";"]; + if (components.count) { + _mimeType = ARC_RETAIN([[components objectAtIndex:0] lowercaseString]); + } + if (_mimeType == nil) { + _mimeType = @"text/plain"; + } + } + 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.mimeType hasPrefix:@"text/"]) { + NSString* charset = GCDWebServerExtractHeaderParameter(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:@"<%@ | '%@' | %i bytes>", [self class], self.mimeType, (int)_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"; +} + +- (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 = GCDWebServerExtractHeaderParameter(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 = GCDWebServerExtractHeaderParameter(contentDisposition, @"name"); + fileName = GCDWebServerExtractHeaderParameter(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; + } + } + } + } + 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); +} + +@end diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index ec8d823..4fe60ae 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -52,6 +52,15 @@ #import "GCDWebServerConnection.h" +#import "GCDWebServerDataRequest.h" +#import "GCDWebServerFileRequest.h" +#import "GCDWebServerMultiPartFormRequest.h" +#import "GCDWebServerURLEncodedFormRequest.h" + +#import "GCDWebServerDataResponse.h" +#import "GCDWebServerFileResponse.h" +#import "GCDWebServerStreamResponse.h" + #ifdef __GCDWEBSERVER_LOGGING_HEADER__ // Define __GCDWEBSERVER_LOGGING_HEADER__ as a preprocessor constant to redirect GCDWebServer logging to your own system @@ -91,6 +100,9 @@ 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) +extern NSString* GCDWebServerExtractHeaderParameter(NSString* header, NSString* attribute); +extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset); + @interface GCDWebServerConnection () - (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket; @end diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index fce7752..5dcc645 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/CGDWebServer/GCDWebServerRequest.h @@ -45,37 +45,3 @@ - (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; -@end diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index df77c55..9a03b5b 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -27,16 +27,6 @@ #import "GCDWebServerPrivate.h" -#define kMultiPartBufferSize (256 * 1024) - -enum { - kParserState_Undefined = 0, - kParserState_Start, - kParserState_Headers, - kParserState_Content, - kParserState_End -}; - @interface GCDWebServerRequest () { @private NSString* _method; @@ -50,94 +40,6 @@ enum { } @end -@interface GCDWebServerDataRequest () { -@private - NSMutableData* _data; -} -@end - -@interface GCDWebServerFileRequest () { -@private - NSString* _filePath; - int _file; -} -@end - -@interface GCDWebServerURLEncodedFormRequest () { -@private - NSDictionary* _arguments; -} -@end - -@interface GCDWebServerMultiPart () { -@private - NSString* _contentType; - NSString* _mimeType; -} -@end - -@interface GCDWebServerMultiPartArgument () { -@private - NSData* _data; - NSString* _string; -} -@end - -@interface GCDWebServerMultiPartFile () { -@private - NSString* _fileName; - NSString* _temporaryPath; -} -@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 - -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]; - } - } - ARC_RELEASE(scanner); - } - return value; -} - -// http://www.w3schools.com/tags/ref_charactersets.asp -static NSStringEncoding _StringEncodingFromCharset(NSString* charset) { - NSStringEncoding encoding = kCFStringEncodingInvalidId; - if (charset) { - encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)charset)); - } - return (encoding != kCFStringEncodingInvalidId ? encoding : NSUTF8StringEncoding); -} - @implementation GCDWebServerRequest : NSObject @synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, byteRange=_range; @@ -234,388 +136,3 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) { } @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; -} - -- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length { - DCHECK(_data != nil); - [_data appendBytes:buffer length:length]; - return length; -} - -- (BOOL)close { - DCHECK(_data != nil); - return YES; -} - -@end - -@implementation GCDWebServerFileRequest - -@synthesize filePath=_filePath; - -- (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { - if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) { - _filePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]); - } - 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]) { - 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); -} - -@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; -} - -- (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); -} - -- (NSString*)description { - return [NSString stringWithFormat:@"<%@ | '%@' | %i bytes>", [self class], self.mimeType, (int)_data.length]; -} - -@end - -@implementation GCDWebServerMultiPartFile - -@synthesize fileName=_fileName, temporaryPath=_temporaryPath; - -- (id)initWithContentType:(NSString*)contentType fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath { - if ((self = [super initWithContentType:contentType])) { - _fileName = [fileName copy]; - _temporaryPath = [temporaryPath copy]; - } - return self; -} - -- (void)dealloc { - unlink([_temporaryPath fileSystemRepresentation]); - - 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; - } - } - } - } - 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); -} - -@end diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index 0d89e43..e8469bc 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -27,8 +27,6 @@ #import -typedef NSData* (^GCDWebServerStreamBlock)(NSError** error); - @protocol GCDWebServerBodyReader - (BOOL)open:(NSError**)error; - (NSData*)readData:(NSError**)error; // Return nil on error or empty NSData if at end @@ -54,37 +52,3 @@ typedef NSData* (^GCDWebServerStreamBlock)(NSError** error); - (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 GCDWebServerStreamResponse : GCDWebServerResponse // Forces 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/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index a78e17a..770d839 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -25,14 +25,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#import #import #import "GCDWebServerPrivate.h" #define kZlibErrorDomain @"ZlibErrorDomain" #define kGZipInitialBufferSize (256 * 1024) -#define kFileReadBufferSize (32 * 1024) @interface GCDWebServerBodyEncoder : NSObject - (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)reader; @@ -332,293 +330,3 @@ } @end - -@interface GCDWebServerDataResponse () { -@private - NSData* _data; - BOOL _done; -} -@end - -@implementation GCDWebServerDataResponse - -+ (GCDWebServerDataResponse*)responseWithData:(NSData*)data contentType:(NSString*)type { - return ARC_AUTORELEASE([[[self class] alloc] initWithData:data contentType:type]); -} - -- (id)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; -} - -@end - -@implementation GCDWebServerDataResponse (Extensions) - -+ (GCDWebServerDataResponse*)responseWithText:(NSString*)text { - return ARC_AUTORELEASE([[self alloc] initWithText:text]); -} - -+ (GCDWebServerDataResponse*)responseWithHTML:(NSString*)html { - return ARC_AUTORELEASE([[self alloc] initWithHTML:html]); -} - -+ (GCDWebServerDataResponse*)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables { - return ARC_AUTORELEASE([[self alloc] initWithHTMLTemplate:path variables:variables]); -} - -+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object { - return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object]); -} - -+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object contentType:(NSString*)type { - return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object contentType:type]); -} - -- (id)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"]; -} - -- (id)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"]; -} - -- (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; -} - -- (id)initWithJSONObject:(id)object { - return [self initWithJSONObject:object contentType:@"application/json"]; -} - -- (id)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 - -@interface GCDWebServerFileResponse () { -@private - NSString* _path; - NSUInteger _offset; - NSUInteger _size; - int _file; -} -@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 ((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 - } - } - - 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(); - } - } - - 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); -} - -static inline NSError* _MakePosixError(int code) { - return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}]; -} - -- (BOOL)open:(NSError**)error { - DCHECK(_file <= 0); - _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); - _file = 0; - return NO; - } - return YES; -} - -- (NSData*)readData:(NSError**)error { - DCHECK(_file > 0); - 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 { - DCHECK(_file > 0); - close(_file); - _file = 0; -} - -@end - -@interface GCDWebServerStreamResponse () { -@private - GCDWebServerStreamBlock _block; -} -@end - -@implementation GCDWebServerStreamResponse - -+ (GCDWebServerStreamResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { - return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]); -} - -- (id)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { - if ((self = [super init])) { - _block = [block copy]; - - self.contentType = type; - self.chunkedTransferEncoding = YES; - } - return self; -} - -- (void)dealloc { - ARC_RELEASE(_block); - - ARC_DEALLOC(super); -} - -- (NSData*)readData:(NSError**)error { - return _block(error); -} - -@end diff --git a/CGDWebServer/GCDWebServerStreamResponse.h b/CGDWebServer/GCDWebServerStreamResponse.h new file mode 100644 index 0000000..62ea9b0 --- /dev/null +++ b/CGDWebServer/GCDWebServerStreamResponse.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 "GCDWebServerStreamResponse.h" + +typedef NSData* (^GCDWebServerStreamBlock)(NSError** error); + +@interface GCDWebServerStreamResponse : GCDWebServerResponse // Forces 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/CGDWebServer/GCDWebServerStreamResponse.m b/CGDWebServer/GCDWebServerStreamResponse.m new file mode 100644 index 0000000..9ab33e0 --- /dev/null +++ b/CGDWebServer/GCDWebServerStreamResponse.m @@ -0,0 +1,62 @@ +/* + 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 GCDWebServerStreamResponse () { +@private + GCDWebServerStreamBlock _block; +} +@end + +@implementation GCDWebServerStreamResponse + ++ (GCDWebServerStreamResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { + return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]); +} + +- (id)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { + if ((self = [super init])) { + _block = [block copy]; + + self.contentType = type; + self.chunkedTransferEncoding = YES; + } + return self; +} + +- (void)dealloc { + ARC_RELEASE(_block); + + ARC_DEALLOC(super); +} + +- (NSData*)readData:(NSError**)error { + return _block(error); +} + +@end diff --git a/CGDWebServer/GCDWebServerURLEncodedFormRequest.h b/CGDWebServer/GCDWebServerURLEncodedFormRequest.h new file mode 100644 index 0000000..c369da3 --- /dev/null +++ b/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; // Only valid after open / write / close sequence ++ (NSString*)mimeType; +@end diff --git a/CGDWebServer/GCDWebServerURLEncodedFormRequest.m b/CGDWebServer/GCDWebServerURLEncodedFormRequest.m new file mode 100644 index 0000000..e2fe12a --- /dev/null +++ b/CGDWebServer/GCDWebServerURLEncodedFormRequest.m @@ -0,0 +1,63 @@ +/* + 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 { + if (![super close]) { + return NO; + } + + NSString* charset = GCDWebServerExtractHeaderParameter(self.contentType, @"charset"); + NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)]; + _arguments = ARC_RETAIN(GCDWebServerParseURLEncodedForm(string)); + ARC_RELEASE(string); + + return (_arguments ? YES : NO); +} + +@end diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index b076fcc..92090a0 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -38,6 +38,20 @@ 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 */; }; + 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 /* GCDWebServerStreamResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamResponse.m */; }; + E2A0E7F618F1D1E500C580B1 /* GCDWebServerStreamResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamResponse.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 */; }; 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 */; }; @@ -99,6 +113,20 @@ 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; }; + E2A0E7EB18F1D03700C580B1 /* GCDWebServerDataResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerDataResponse.h; sourceTree = ""; }; + E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataResponse.m; sourceTree = ""; }; + E2A0E7EF18F1D12E00C580B1 /* GCDWebServerFileResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileResponse.h; sourceTree = ""; }; + E2A0E7F018F1D12E00C580B1 /* GCDWebServerFileResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFileResponse.m; sourceTree = ""; }; + E2A0E7F318F1D1E500C580B1 /* GCDWebServerStreamResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerStreamResponse.h; sourceTree = ""; }; + E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerStreamResponse.m; sourceTree = ""; }; + E2A0E7F718F1D24700C580B1 /* GCDWebServerDataRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerDataRequest.h; sourceTree = ""; }; + E2A0E7F818F1D24700C580B1 /* GCDWebServerDataRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataRequest.m; sourceTree = ""; }; + E2A0E7FB18F1D36C00C580B1 /* GCDWebServerFileRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileRequest.h; sourceTree = ""; }; + E2A0E7FC18F1D36C00C580B1 /* GCDWebServerFileRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFileRequest.m; sourceTree = ""; }; + E2A0E7FF18F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerMultiPartFormRequest.h; sourceTree = ""; }; + E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerMultiPartFormRequest.m; sourceTree = ""; }; + E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerURLEncodedFormRequest.h; sourceTree = ""; }; + E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerURLEncodedFormRequest.m; sourceTree = ""; }; 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 = ""; }; @@ -163,11 +191,25 @@ E221127D1690B63A0048D2B2 /* GCDWebServer.m */, E221127E1690B63A0048D2B2 /* GCDWebServerConnection.h */, E221127F1690B63A0048D2B2 /* GCDWebServerConnection.m */, + E2A0E7F718F1D24700C580B1 /* GCDWebServerDataRequest.h */, + E2A0E7F818F1D24700C580B1 /* GCDWebServerDataRequest.m */, + E2A0E7EB18F1D03700C580B1 /* GCDWebServerDataResponse.h */, + E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */, + E2A0E7FB18F1D36C00C580B1 /* GCDWebServerFileRequest.h */, + E2A0E7FC18F1D36C00C580B1 /* GCDWebServerFileRequest.m */, + E2A0E7EF18F1D12E00C580B1 /* GCDWebServerFileResponse.h */, + E2A0E7F018F1D12E00C580B1 /* GCDWebServerFileResponse.m */, + E2A0E7FF18F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.h */, + E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */, E22112801690B63A0048D2B2 /* GCDWebServerPrivate.h */, E22112811690B63A0048D2B2 /* GCDWebServerRequest.h */, E22112821690B63A0048D2B2 /* GCDWebServerRequest.m */, E22112831690B63A0048D2B2 /* GCDWebServerResponse.h */, E22112841690B63A0048D2B2 /* GCDWebServerResponse.m */, + E2A0E7F318F1D1E500C580B1 /* GCDWebServerStreamResponse.h */, + E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamResponse.m */, + E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */, + E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */, ); path = CGDWebServer; sourceTree = ""; @@ -308,11 +350,18 @@ 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 */, + E2A0E7ED18F1D03700C580B1 /* GCDWebServerDataResponse.m in Sources */, + E2A0E7F518F1D1E500C580B1 /* GCDWebServerStreamResponse.m in Sources */, + E2A0E80118F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m in Sources */, E221128B1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */, E2BE850C18E785940061360B /* GCDWebUploader.m in Sources */, E221128F1690B6470048D2B2 /* main.m in Sources */, + E2A0E7F118F1D12E00C580B1 /* GCDWebServerFileResponse.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -321,11 +370,18 @@ buildActionMask = 2147483647; files = ( E22112861690B63A0048D2B2 /* GCDWebServer.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 /* GCDWebServerStreamResponse.m in Sources */, + E2A0E7F218F1D12E00C580B1 /* GCDWebServerFileResponse.m in Sources */, E22112971690B64F0048D2B2 /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index 0445d72..ee7f377 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -33,6 +33,11 @@ #endif #import "GCDWebUploader.h" +#import "GCDWebServerDataRequest.h" +#import "GCDWebServerMultiPartFormRequest.h" +#import "GCDWebServerURLEncodedFormRequest.h" +#import "GCDWebServerDataResponse.h" +#import "GCDWebServerFileResponse.h" @interface GCDWebUploader () { @private diff --git a/Mac/main.m b/Mac/main.m index 983ac9c..b3862c5 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -26,6 +26,9 @@ */ #import "GCDWebUploader.h" +#import "GCDWebServerDataRequest.h" +#import "GCDWebServerURLEncodedFormRequest.h" +#import "GCDWebServerDataResponse.h" int main(int argc, const char* argv[]) { BOOL success = NO; diff --git a/README.md b/README.md index 5fd3839..e29bd28 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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: only 4 core classes to deal with * 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 From 63a66ff331ce31f548af180c5d4b216393a56e37 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Sun, 6 Apr 2014 12:09:44 -0700 Subject: [PATCH 04/60] Added GCDWebServerBodyWriter protocol --- CGDWebServer/GCDWebServerConnection.m | 34 ++++++++------- CGDWebServer/GCDWebServerDataRequest.m | 16 +++++--- CGDWebServer/GCDWebServerFileRequest.m | 28 ++++++++++--- .../GCDWebServerMultiPartFormRequest.m | 20 ++++++--- CGDWebServer/GCDWebServerPrivate.h | 7 ++++ CGDWebServer/GCDWebServerRequest.h | 14 +++---- CGDWebServer/GCDWebServerRequest.m | 41 ++++++++++++++----- CGDWebServer/GCDWebServerResponse.h | 8 ++-- CGDWebServer/GCDWebServerResponse.m | 23 ++++++----- CGDWebServer/GCDWebServerStreamResponse.m | 2 +- .../GCDWebServerURLEncodedFormRequest.m | 7 ++-- 11 files changed, 131 insertions(+), 69 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index f140271..515821f 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -157,10 +157,11 @@ static dispatch_queue_t _formatterQueue = NULL; 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); + 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]; + NSError* error = nil; + if (![_request performWriteData:data error:&error]) { + LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); return false; } return true; @@ -318,7 +319,7 @@ static dispatch_queue_t _formatterQueue = NULL; if (response) { NSError* error = nil; if ([response hasBody] && ![response performOpen:&error]) { - LOG_WARNING(@"Failed opening response body for socket %i: %@", _socket, error); + LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error); } else { _response = ARC_RETAIN(response); } @@ -363,42 +364,45 @@ static dispatch_queue_t _formatterQueue = NULL; } - (void)_readRequestBody:(NSData*)initialData { - if ([_request open]) { + NSError* error = nil; + if ([_request performOpen:&error]) { NSInteger length = _request.contentLength; if (initialData.length) { - NSInteger result = [_request write:initialData.bytes maxLength:initialData.length]; - if (result == (NSInteger)initialData.length) { + if ([_request performWriteData:initialData error:&error]) { length -= initialData.length; DCHECK(length >= 0); } else { - LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result); + LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); length = -1; } } if (length > 0) { [self _readBodyWithRemainingLength:length completionBlock:^(BOOL success) { - if (![_request close]) { - success = NO; - } - if (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 close]) { + if ([_request performClose:&error]) { [self _processRequest]; } else { + LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); [self _abortWithStatusCode:500]; } } else { - [_request close]; // Can't do anything with result anyway + if (![_request performClose:&error]) { + LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); + } [self _abortWithStatusCode:500]; } } else { + LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error); [self _abortWithStatusCode:500]; } } diff --git a/CGDWebServer/GCDWebServerDataRequest.m b/CGDWebServer/GCDWebServerDataRequest.m index 1bfab58..801dbae 100644 --- a/CGDWebServer/GCDWebServerDataRequest.m +++ b/CGDWebServer/GCDWebServerDataRequest.m @@ -44,19 +44,23 @@ ARC_DEALLOC(super); } -- (BOOL)open { +- (BOOL)open:(NSError**)error { DCHECK(_data == nil); _data = [[NSMutableData alloc] initWithCapacity:self.contentLength]; - return _data ? YES : NO; + if (_data == nil) { + *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed allocating memory"}]; + return NO; + } + return YES; } -- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length { +- (BOOL)writeData:(NSData*)data error:(NSError**)error { DCHECK(_data != nil); - [_data appendBytes:buffer length:length]; - return length; + [_data appendData:data]; + return YES; } -- (BOOL)close { +- (BOOL)close:(NSError**)error { DCHECK(_data != nil); return YES; } diff --git a/CGDWebServer/GCDWebServerFileRequest.m b/CGDWebServer/GCDWebServerFileRequest.m index 2a5c1c7..11db890 100644 --- a/CGDWebServer/GCDWebServerFileRequest.m +++ b/CGDWebServer/GCDWebServerFileRequest.m @@ -34,6 +34,10 @@ } @end +static inline NSError* _MakePosixError(int code) { + return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}]; +} + @implementation GCDWebServerFileRequest @synthesize filePath=_filePath; @@ -53,22 +57,34 @@ ARC_DEALLOC(super); } -- (BOOL)open { +- (BOOL)open:(NSError**)error { 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); + if (_file <= 0) { + *error = _MakePosixError(errno); + return NO; + } + return YES; } -- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length { +- (BOOL)writeData:(NSData*)data error:(NSError**)error { DCHECK(_file > 0); - return write(_file, buffer, length); + if (write(_file, data.bytes, data.length) != (ssize_t)data.length) { + *error = _MakePosixError(errno); + return NO; + } + return YES; } -- (BOOL)close { +- (BOOL)close:(NSError**)error { DCHECK(_file > 0); int result = close(_file); _file = -1; - return (result == 0 ? YES : NO); + if (result < 0) { + *error = _MakePosixError(errno); + return NO; + } + return YES; } @end diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.m b/CGDWebServer/GCDWebServerMultiPartFormRequest.m index 52f5933..0571c1f 100644 --- a/CGDWebServer/GCDWebServerMultiPartFormRequest.m +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.m @@ -204,7 +204,7 @@ static NSData* _dashNewlineData = nil; return self; } -- (BOOL)open { +- (BOOL)open:(NSError**)error { DCHECK(_parserData == nil); _parserData = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize]; _parserState = kParserState_Start; @@ -329,13 +329,17 @@ static NSData* _dashNewlineData = nil; return success; } -- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length { +- (BOOL)writeData:(NSData*)data error:(NSError**)error { DCHECK(_parserData != nil); - [_parserData appendBytes:buffer length:length]; - return ([self _parseData] ? length : -1); + [_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 { +- (BOOL)close:(NSError**)error { DCHECK(_parserData != nil); ARC_RELEASE(_parserData); _parserData = nil; @@ -352,7 +356,11 @@ static NSData* _dashNewlineData = nil; } ARC_RELEASE(_tmpPath); _tmpPath = nil; - return (_parserState == kParserState_End ? YES : NO); + if (_parserState != kParserState_End) { + *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}]; + return NO; + } + return YES; } - (void)dealloc { diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 4fe60ae..3dfdae9 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -99,6 +99,7 @@ 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" extern NSString* GCDWebServerExtractHeaderParameter(NSString* header, NSString* attribute); extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset); @@ -117,6 +118,12 @@ extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset) - (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; @end +@interface GCDWebServerRequest () +- (BOOL)performOpen:(NSError**)error; +- (BOOL)performWriteData:(NSData*)data error:(NSError**)error; +- (BOOL)performClose:(NSError**)error; +@end + @interface GCDWebServerResponse () @property(nonatomic, readonly) NSDictionary* additionalHeaders; - (BOOL)performOpen:(NSError**)error; diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index 5dcc645..256c0c1 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/CGDWebServer/GCDWebServerRequest.h @@ -27,7 +27,13 @@ #import -@interface GCDWebServerRequest : NSObject +@protocol GCDWebServerBodyWriter +- (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 @property(nonatomic, readonly) NSString* method; @property(nonatomic, readonly) NSURL* URL; @property(nonatomic, readonly) NSDictionary* headers; @@ -39,9 +45,3 @@ - (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 diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 9a03b5b..307b8d7 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -37,6 +37,10 @@ NSString* _type; NSUInteger _length; NSRange _range; + + BOOL _opened; + NSMutableArray* _decoders; + id __unsafe_unretained _writer; } @end @@ -97,6 +101,8 @@ LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url); } } + + _decoders = [[NSMutableArray alloc] init]; } return self; } @@ -108,6 +114,7 @@ ARC_RELEASE(_path); ARC_RELEASE(_query); ARC_RELEASE(_type); + ARC_RELEASE(_decoders); ARC_DEALLOC(super); } @@ -116,23 +123,37 @@ return _type ? YES : NO; } -@end +- (BOOL)open:(NSError**)error { + return YES; +} -@implementation GCDWebServerRequest (Subclassing) - -- (BOOL)open { +- (BOOL)writeData:(NSData*)data error:(NSError**)error { [self doesNotRecognizeSelector:_cmd]; return NO; } -- (NSInteger)write:(const void*)buffer maxLength:(NSUInteger)length { - [self doesNotRecognizeSelector:_cmd]; - return -1; +- (BOOL)close:(NSError**)error { + return YES; } -- (BOOL)close { - [self doesNotRecognizeSelector:_cmd]; - return NO; +- (BOOL)performOpen:(NSError**)error { + if (_opened) { + DNOT_REACHED(); + return NO; + } + _opened = YES; + + _writer = self; + // TODO: Inject decoders + return [_writer open:error]; +} + +- (BOOL)performWriteData:(NSData*)data error:(NSError**)error { + return [_writer writeData:data error:error]; +} + +- (BOOL)performClose:(NSError**)error { + return [_writer close:error]; } @end diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index e8469bc..433cd08 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -28,8 +28,8 @@ #import @protocol GCDWebServerBodyReader -- (BOOL)open:(NSError**)error; -- (NSData*)readData:(NSError**)error; // Return nil on error or empty NSData if at end +- (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 @@ -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) BOOL gzipContentEncoding; -@property(nonatomic) BOOL chunkedTransferEncoding; +@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; +@property(nonatomic, getter=isChunkedTransferEncodingEnabled) BOOL chunkedTransferEncodingEnabled; + (GCDWebServerResponse*) response; - (id)init; - (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 770d839..1ef8a07 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -157,12 +157,12 @@ } - (NSData*)readData:(NSError**)error { - NSMutableData* gzipData; + NSMutableData* encodedData; if (_finished) { - gzipData = [[NSMutableData alloc] init]; + encodedData = [[NSMutableData alloc] init]; } else { - gzipData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize]; - if (gzipData == nil) { + encodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize]; + if (encodedData == nil) { DNOT_REACHED(); return nil; } @@ -175,14 +175,14 @@ _stream.next_in = (Bytef*)data.bytes; _stream.avail_in = (uInt)data.length; while (1) { - NSUInteger maxLength = gzipData.length - length; - _stream.next_out = (Bytef*)((char*)gzipData.mutableBytes + length); + 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(gzipData); + ARC_RELEASE(encodedData); *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil]; return nil; } @@ -190,13 +190,13 @@ if (_stream.avail_out > 0) { break; } - gzipData.length = 2 * gzipData.length; // zlib has used all the output buffer so resize it and try again in case more data is available + 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 - gzipData.length = length; + encodedData.length = length; } - return ARC_AUTORELEASE(gzipData); + return ARC_AUTORELEASE(encodedData); } - (void)close { @@ -225,7 +225,7 @@ @implementation GCDWebServerResponse @synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, - gzipContentEncoding=_gzipped, chunkedTransferEncoding=_chunked, additionalHeaders=_headers; + gzipContentEncodingEnabled=_gzipped, chunkedTransferEncodingEnabled=_chunked, additionalHeaders=_headers; + (GCDWebServerResponse*)response { return ARC_AUTORELEASE([[[self class] alloc] init]); @@ -264,6 +264,7 @@ } - (NSData*)readData:(NSError**)error { + [self doesNotRecognizeSelector:_cmd]; return nil; } diff --git a/CGDWebServer/GCDWebServerStreamResponse.m b/CGDWebServer/GCDWebServerStreamResponse.m index 9ab33e0..0338019 100644 --- a/CGDWebServer/GCDWebServerStreamResponse.m +++ b/CGDWebServer/GCDWebServerStreamResponse.m @@ -44,7 +44,7 @@ _block = [block copy]; self.contentType = type; - self.chunkedTransferEncoding = YES; + self.chunkedTransferEncodingEnabled = YES; } return self; } diff --git a/CGDWebServer/GCDWebServerURLEncodedFormRequest.m b/CGDWebServer/GCDWebServerURLEncodedFormRequest.m index e2fe12a..7962e54 100644 --- a/CGDWebServer/GCDWebServerURLEncodedFormRequest.m +++ b/CGDWebServer/GCDWebServerURLEncodedFormRequest.m @@ -47,17 +47,18 @@ ARC_DEALLOC(super); } -- (BOOL)close { - if (![super close]) { +- (BOOL)close:(NSError**)error { + if (![super close:error]) { return NO; } NSString* charset = GCDWebServerExtractHeaderParameter(self.contentType, @"charset"); NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)]; _arguments = ARC_RETAIN(GCDWebServerParseURLEncodedForm(string)); + DCHECK(_arguments); ARC_RELEASE(string); - return (_arguments ? YES : NO); + return YES; } @end From 06630d32458e5758d64166d5b65fc61dc9d97db4 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Sun, 6 Apr 2014 18:03:24 -0700 Subject: [PATCH 05/60] Added support for gzip body encoding --- CGDWebServer/GCDWebServerRequest.m | 115 ++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 307b8d7..1a2626b 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -25,8 +25,116 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#import + #import "GCDWebServerPrivate.h" +#define kZlibErrorDomain @"ZlibErrorDomain" +#define kGZipInitialBufferSize (256 * 1024) + +@interface GCDWebServerBodyDecoder : NSObject +- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id)writer; +@end + +@interface GCDWebServerGZipDecoder : GCDWebServerBodyDecoder +@end + +@interface GCDWebServerBodyDecoder () { +@private + GCDWebServerRequest* __unsafe_unretained _request; + id __unsafe_unretained _writer; +} +@end + +@implementation GCDWebServerBodyDecoder + +- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id)writer { + if ((self = [super init])) { + _request = request; + _writer = writer; + } + return self; +} + +- (BOOL)open:(NSError**)error { + return [_writer open:error]; +} + +- (BOOL)writeData:(NSData*)data error:(NSError**)error { + return [_writer writeData:data error:error]; +} + +- (BOOL)close:(NSError**)error { + return [_writer close:error]; +} + +@end + +@interface GCDWebServerGZipDecoder () { +@private + z_stream _stream; + BOOL _finished; +} +@end + +@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; +} + +- (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:1024]; // 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 nil; + } + length += maxLength - _stream.avail_out; + if (_stream.avail_out > 0) { + if (result == Z_STREAM_END) { + _finished = YES; + } + break; + } + decodedData.length = 2 * decodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available + } + 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; +} + +- (BOOL)close:(NSError**)error { + DCHECK(_finished); + inflateEnd(&_stream); + return [super close:error]; +} + +@end + @interface GCDWebServerRequest () { @private NSString* _method; @@ -144,7 +252,12 @@ _opened = YES; _writer = self; - // TODO: Inject decoders + if ([[[self.headers objectForKey:@"Content-Encoding"] lowercaseString] isEqualToString:@"gzip"]) { + GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer]; + [_decoders addObject:decoder]; + ARC_RELEASE(decoder); + _writer = decoder; + } return [_writer open:error]; } From c213e167b48a13ae159d88e241bf5c6502c8be5a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 09:31:28 -0700 Subject: [PATCH 06/60] Fix --- CGDWebServer/GCDWebServerDataRequest.h | 2 +- CGDWebServer/GCDWebServerFileRequest.h | 2 +- CGDWebServer/GCDWebServerMultiPartFormRequest.h | 4 ++-- CGDWebServer/GCDWebServerURLEncodedFormRequest.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CGDWebServer/GCDWebServerDataRequest.h b/CGDWebServer/GCDWebServerDataRequest.h index 2cb923f..8a224a2 100644 --- a/CGDWebServer/GCDWebServerDataRequest.h +++ b/CGDWebServer/GCDWebServerDataRequest.h @@ -28,5 +28,5 @@ #import "GCDWebServerRequest.h" @interface GCDWebServerDataRequest : GCDWebServerRequest -@property(nonatomic, readonly) NSData* data; // Only valid after open / write / close sequence +@property(nonatomic, readonly) NSData* data; @end diff --git a/CGDWebServer/GCDWebServerFileRequest.h b/CGDWebServer/GCDWebServerFileRequest.h index 7ef3ac2..b511e2b 100644 --- a/CGDWebServer/GCDWebServerFileRequest.h +++ b/CGDWebServer/GCDWebServerFileRequest.h @@ -28,5 +28,5 @@ #import "GCDWebServerRequest.h" @interface GCDWebServerFileRequest : GCDWebServerRequest -@property(nonatomic, readonly) NSString* filePath; // Only valid after open / write / close sequence +@property(nonatomic, readonly) NSString* filePath; @end diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.h b/CGDWebServer/GCDWebServerMultiPartFormRequest.h index a3ea6b7..e318d67 100644 --- a/CGDWebServer/GCDWebServerMultiPartFormRequest.h +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.h @@ -43,7 +43,7 @@ @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 +@property(nonatomic, readonly) NSDictionary* arguments; +@property(nonatomic, readonly) NSDictionary* files; + (NSString*)mimeType; @end diff --git a/CGDWebServer/GCDWebServerURLEncodedFormRequest.h b/CGDWebServer/GCDWebServerURLEncodedFormRequest.h index c369da3..8fe6567 100644 --- a/CGDWebServer/GCDWebServerURLEncodedFormRequest.h +++ b/CGDWebServer/GCDWebServerURLEncodedFormRequest.h @@ -28,6 +28,6 @@ #import "GCDWebServerDataRequest.h" @interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest -@property(nonatomic, readonly) NSDictionary* arguments; // Only valid after open / write / close sequence +@property(nonatomic, readonly) NSDictionary* arguments; + (NSString*)mimeType; @end From 7af258eb6b68dd602e3b9eea5b63f67b55545273 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 12:21:19 -0700 Subject: [PATCH 07/60] #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", From c5d3764913d0bc6fbcc40e20630c3543e9f9dcfb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 13:09:03 -0700 Subject: [PATCH 08/60] Moved response body chunked transfer encoding to GCDWebServerConnection --- CGDWebServer/GCDWebServerConnection.m | 37 +++++++++++- CGDWebServer/GCDWebServerPrivate.h | 1 + CGDWebServer/GCDWebServerResponse.h | 1 - CGDWebServer/GCDWebServerResponse.m | 72 +++-------------------- CGDWebServer/GCDWebServerStreamResponse.m | 1 - 5 files changed, 44 insertions(+), 68 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 43cd4fc..4c01393 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -44,6 +44,7 @@ typedef void (^WriteBodyCompletionBlock)(BOOL success); static NSData* _CRLFData = nil; static NSData* _CRLFCRLFData = nil; static NSData* _continueData = nil; +static NSData* _lastChunkData = nil; static NSDateFormatter* _dateFormatter = nil; static dispatch_queue_t _formatterQueue = NULL; @@ -308,6 +309,26 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { 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) { @@ -318,7 +339,15 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { }]; } else { - block(YES); + if (_response.usesChunkedTransferEncoding) { + [self _writeData:_lastChunkData withCompletionBlock:^(BOOL success) { + + block(success); + + }]; + } else { + block(YES); + } } } else { LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error); @@ -351,6 +380,9 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { CFRelease(message); DCHECK(_continueData); } + if (_lastChunkData == nil) { + _lastChunkData = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5]; + } if (_dateFormatter == nil) { DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread _dateFormatter = [[NSDateFormatter alloc] init]; @@ -412,6 +444,9 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { 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); }]; diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 374ee94..a765179 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -127,6 +127,7 @@ extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset) @interface GCDWebServerResponse () @property(nonatomic, readonly) NSDictionary* additionalHeaders; +@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding; - (BOOL)performOpen:(NSError**)error; - (NSData*)performReadData:(NSError**)error; - (void)performClose; diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index e2b76e4..cee967e 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -39,7 +39,6 @@ @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; // 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 4358790..a12fe9b 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -36,9 +36,6 @@ - (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)reader; @end -@interface GCDWebServerChunkEncoder : GCDWebServerBodyEncoder -@end - @interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder @end @@ -73,59 +70,6 @@ @end -@interface GCDWebServerChunkEncoder () { -@private - BOOL _finished; -} -@end - -@implementation GCDWebServerChunkEncoder - -- (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 since body length is determined by chunked transfer encoding - [response setValue:@"chunked" forAdditionalHeader:@"Transfer-Encoding"]; - } - return self; -} - -- (NSData*)readData:(NSError**)error { - NSData* chunk; - if (_finished) { - chunk = [[NSData alloc] init]; - } else { - NSData* data = [super readData:error]; - if (data == nil) { - return nil; - } - if (data.length) { - 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)]; - if (chunk == nil) { - DNOT_REACHED(); - return nil; - } - 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'; - } else { - chunk = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5]; - DCHECK(chunk); - _finished = YES; - } - } - return ARC_AUTORELEASE(chunk); -} - -@end - @interface GCDWebServerGZipEncoder () { @private z_stream _stream; @@ -137,7 +81,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 since body length is determined by closing the connection + 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; @@ -213,8 +157,8 @@ NSInteger _status; NSUInteger _maxAge; NSMutableDictionary* _headers; - BOOL _gzipped; BOOL _chunked; + BOOL _gzipped; BOOL _opened; NSMutableArray* _encoders; @@ -225,7 +169,7 @@ @implementation GCDWebServerResponse @synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, - gzipContentEncodingEnabled=_gzipped, chunkedTransferEncodingEnabled=_chunked, additionalHeaders=_headers; + gzipContentEncodingEnabled=_gzipped, additionalHeaders=_headers; + (GCDWebServerResponse*)response { return ARC_AUTORELEASE([[[self class] alloc] init]); @@ -259,6 +203,10 @@ return _type ? YES : NO; } +- (BOOL)usesChunkedTransferEncoding { + return (_type != nil) && (_length == NSNotFound); +} + - (BOOL)open:(NSError**)error { return YES; } @@ -286,12 +234,6 @@ ARC_RELEASE(encoder); _reader = encoder; } - if (_chunked) { - GCDWebServerChunkEncoder* encoder = [[GCDWebServerChunkEncoder alloc] initWithResponse:self reader:_reader]; - [_encoders addObject:encoder]; - ARC_RELEASE(encoder); - _reader = encoder; - } return [_reader open:error]; } diff --git a/CGDWebServer/GCDWebServerStreamResponse.m b/CGDWebServer/GCDWebServerStreamResponse.m index 0338019..6d576cc 100644 --- a/CGDWebServer/GCDWebServerStreamResponse.m +++ b/CGDWebServer/GCDWebServerStreamResponse.m @@ -44,7 +44,6 @@ _block = [block copy]; self.contentType = type; - self.chunkedTransferEncodingEnabled = YES; } return self; } From 8f9c03991d7cf6739a6021fa09e0543d80e0152e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 13:10:17 -0700 Subject: [PATCH 09/60] Renamed GCDWebServerStreamResponse to GCDWebServerStreamingResponse --- CGDWebServer/GCDWebServerPrivate.h | 2 +- ...esponse.h => GCDWebServerStreamingResponse.h} | 6 +++--- ...esponse.m => GCDWebServerStreamingResponse.m} | 6 +++--- GCDWebServer.xcodeproj/project.pbxproj | 16 ++++++++-------- 4 files changed, 15 insertions(+), 15 deletions(-) rename CGDWebServer/{GCDWebServerStreamResponse.h => GCDWebServerStreamingResponse.h} (85%) rename CGDWebServer/{GCDWebServerStreamResponse.m => GCDWebServerStreamingResponse.m} (90%) diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index a765179..7e320ee 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -59,7 +59,7 @@ #import "GCDWebServerDataResponse.h" #import "GCDWebServerFileResponse.h" -#import "GCDWebServerStreamResponse.h" +#import "GCDWebServerStreamingResponse.h" #ifdef __GCDWEBSERVER_LOGGING_HEADER__ diff --git a/CGDWebServer/GCDWebServerStreamResponse.h b/CGDWebServer/GCDWebServerStreamingResponse.h similarity index 85% rename from CGDWebServer/GCDWebServerStreamResponse.h rename to CGDWebServer/GCDWebServerStreamingResponse.h index 205c461..f0500a7 100644 --- a/CGDWebServer/GCDWebServerStreamResponse.h +++ b/CGDWebServer/GCDWebServerStreamingResponse.h @@ -25,11 +25,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#import "GCDWebServerStreamResponse.h" +#import "GCDWebServerStreamingResponse.h" typedef NSData* (^GCDWebServerStreamBlock)(NSError** error); -@interface GCDWebServerStreamResponse : GCDWebServerResponse // Automatically enables chunked transfer encoding -+ (GCDWebServerStreamResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block; +@interface GCDWebServerStreamingResponse : GCDWebServerResponse // Automatically enables chunked transfer encoding ++ (GCDWebServerStreamingResponse*)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/CGDWebServer/GCDWebServerStreamResponse.m b/CGDWebServer/GCDWebServerStreamingResponse.m similarity index 90% rename from CGDWebServer/GCDWebServerStreamResponse.m rename to CGDWebServer/GCDWebServerStreamingResponse.m index 6d576cc..ab4ed6c 100644 --- a/CGDWebServer/GCDWebServerStreamResponse.m +++ b/CGDWebServer/GCDWebServerStreamingResponse.m @@ -27,15 +27,15 @@ #import "GCDWebServerPrivate.h" -@interface GCDWebServerStreamResponse () { +@interface GCDWebServerStreamingResponse () { @private GCDWebServerStreamBlock _block; } @end -@implementation GCDWebServerStreamResponse +@implementation GCDWebServerStreamingResponse -+ (GCDWebServerStreamResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { ++ (GCDWebServerStreamingResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]); } diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 9d1e3e8..7fcb148 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -42,8 +42,8 @@ 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 /* GCDWebServerStreamResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamResponse.m */; }; - E2A0E7F618F1D1E500C580B1 /* GCDWebServerStreamResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamResponse.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 */; }; @@ -117,8 +117,8 @@ E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataResponse.m; sourceTree = ""; }; E2A0E7EF18F1D12E00C580B1 /* GCDWebServerFileResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileResponse.h; sourceTree = ""; }; E2A0E7F018F1D12E00C580B1 /* GCDWebServerFileResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFileResponse.m; sourceTree = ""; }; - E2A0E7F318F1D1E500C580B1 /* GCDWebServerStreamResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerStreamResponse.h; sourceTree = ""; }; - E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerStreamResponse.m; sourceTree = ""; }; + E2A0E7F318F1D1E500C580B1 /* GCDWebServerStreamingResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerStreamingResponse.h; sourceTree = ""; }; + E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamingResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerStreamingResponse.m; sourceTree = ""; }; E2A0E7F718F1D24700C580B1 /* GCDWebServerDataRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerDataRequest.h; sourceTree = ""; }; E2A0E7F818F1D24700C580B1 /* GCDWebServerDataRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataRequest.m; sourceTree = ""; }; E2A0E7FB18F1D36C00C580B1 /* GCDWebServerFileRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileRequest.h; sourceTree = ""; }; @@ -206,8 +206,8 @@ E22112821690B63A0048D2B2 /* GCDWebServerRequest.m */, E22112831690B63A0048D2B2 /* GCDWebServerResponse.h */, E22112841690B63A0048D2B2 /* GCDWebServerResponse.m */, - E2A0E7F318F1D1E500C580B1 /* GCDWebServerStreamResponse.h */, - E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamResponse.m */, + E2A0E7F318F1D1E500C580B1 /* GCDWebServerStreamingResponse.h */, + E2A0E7F418F1D1E500C580B1 /* GCDWebServerStreamingResponse.m */, E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */, E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */, ); @@ -356,7 +356,7 @@ E2A0E7F918F1D24700C580B1 /* GCDWebServerDataRequest.m in Sources */, E22112891690B63A0048D2B2 /* GCDWebServerRequest.m in Sources */, E2A0E7ED18F1D03700C580B1 /* GCDWebServerDataResponse.m in Sources */, - E2A0E7F518F1D1E500C580B1 /* GCDWebServerStreamResponse.m in Sources */, + E2A0E7F518F1D1E500C580B1 /* GCDWebServerStreamingResponse.m in Sources */, E2A0E80118F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m in Sources */, E221128B1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */, E2BE850C18E785940061360B /* GCDWebUploader.m in Sources */, @@ -380,7 +380,7 @@ E2BE850B18E77ECA0061360B /* GCDWebUploader.m in Sources */, E22112951690B64F0048D2B2 /* AppDelegate.m in Sources */, E2A0E7FE18F1D36C00C580B1 /* GCDWebServerFileRequest.m in Sources */, - E2A0E7F618F1D1E500C580B1 /* GCDWebServerStreamResponse.m in Sources */, + E2A0E7F618F1D1E500C580B1 /* GCDWebServerStreamingResponse.m in Sources */, E2A0E7F218F1D12E00C580B1 /* GCDWebServerFileResponse.m in Sources */, E22112971690B64F0048D2B2 /* main.m in Sources */, ); From e85a0c9a618f141f74260e6b286cab50e011e834 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 13:14:28 -0700 Subject: [PATCH 10/60] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e29bd28..b752461 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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 core classes to deal with +* 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 @@ -10,6 +10,10 @@ GCDWebServer is a lightweight GCD based HTTP 1.1 server designed to be embedded * No dependencies on third-party source code * Available under a friendly [New BSD License](LICENSE) +Extra built-in features: +* [Chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) for both requests and responses +* [HTTP compression](https://en.wikipedia.org/wiki/HTTP_compression) with gzip for both requests and responses + 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 From f61ff832eadf2c19777394d1d609dafea4be4036 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 14:18:32 -0700 Subject: [PATCH 11/60] Added -abortRequest:withStatusCode: API --- CGDWebServer/GCDWebServerConnection.h | 3 +- CGDWebServer/GCDWebServerConnection.m | 48 +++++++++++++++------------ 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.h b/CGDWebServer/GCDWebServerConnection.h index 2f5f1aa..4eee105 100644 --- a/CGDWebServer/GCDWebServerConnection.h +++ b/CGDWebServer/GCDWebServerConnection.h @@ -43,6 +43,7 @@ - (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 +- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; // If request headers was malformed, "request" will be nil - (void)close; @end diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 4c01393..e1d39d5 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -62,6 +62,7 @@ static dispatch_queue_t _formatterQueue = NULL; GCDWebServerHandler* _handler; CFHTTPMessageRef _responseMessage; GCDWebServerResponse* _response; + NSInteger _statusCode; } @end @@ -398,6 +399,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } - (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]); @@ -407,16 +409,6 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { }); } -- (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); -} - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - (void)_processRequest { DCHECK(_responseMessage == NULL); @@ -457,7 +449,6 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _writeBodyWithCompletionBlock:^(BOOL successInner) { [_response performClose]; - LOG_VERBOSE(@"%@ | %@ \"%@ %@\" %i %lu", self.localAddressString, self.remoteAddressString, _request.method, _request.path, (int)_response.statusCode, (unsigned long)_bytesWritten); }]; } @@ -467,7 +458,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { }]; } else { - [self _abortWithStatusCode:500]; + [self abortRequest:_request withStatusCode:500]; } } @@ -476,7 +467,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { NSError* error = nil; if (![_request performOpen:&error]) { LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error); - [self _abortWithStatusCode:500]; + [self abortRequest:_request withStatusCode:500]; return; } @@ -486,7 +477,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (![_request performClose:&error]) { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); } - [self _abortWithStatusCode:500]; + [self abortRequest:_request withStatusCode:500]; return; } length -= initialData.length; @@ -500,7 +491,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _processRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); - [self _abortWithStatusCode:500]; + [self abortRequest:_request withStatusCode:500]; } }]; @@ -509,7 +500,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _processRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); - [self _abortWithStatusCode:500]; + [self abortRequest:_request withStatusCode:500]; } } } @@ -518,7 +509,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { NSError* error = nil; if (![_request performOpen:&error]) { LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error); - [self _abortWithStatusCode:500]; + [self abortRequest:_request withStatusCode:500]; return; } @@ -530,7 +521,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _processRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); - [self _abortWithStatusCode:500]; + [self abortRequest:_request withStatusCode:500]; } }]; @@ -581,7 +572,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { }]; } else { LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket); - [self _abortWithStatusCode:417]; + [self abortRequest:_request withStatusCode:417]; } } else { if (_request.usesChunkedTransferEncoding) { @@ -592,16 +583,18 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } } else { LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket); - [self _abortWithStatusCode:400]; + [self abortRequest:_request withStatusCode:400]; } } 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:405]; } } else { - [self _abortWithStatusCode:500]; + [self abortRequest:nil withStatusCode:500]; } }]; @@ -689,12 +682,23 @@ static NSString* _StringFromAddressData(NSData* data) { 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)); } LOG_DEBUG(@"Did close connection on socket %i", _socket); + LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten); } @end From dcbc0f96c563d45bfb65a6713e381584ce94f7f6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 14:28:53 -0700 Subject: [PATCH 12/60] Fixed addDefaultHandlerForMethod:requestClass:processBlock: ignoring method --- CGDWebServer/GCDWebServer.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index a8cab82..825903f 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -511,6 +511,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]; From 7ec8d5247a30ac3858c821cded71fd499e48b1e6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 19:25:16 -0700 Subject: [PATCH 13/60] Added logging APIs --- CGDWebServer/GCDWebServer.h | 2 ++ CGDWebServer/GCDWebServer.m | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/CGDWebServer/GCDWebServer.h b/CGDWebServer/GCDWebServer.h index 90e4bc7..ec44ad5 100644 --- a/CGDWebServer/GCDWebServer.h +++ b/CGDWebServer/GCDWebServer.h @@ -67,6 +67,8 @@ NSString* GCDWebServerGetPrimaryIPv4Address(); // Returns IPv4 address of prima @interface GCDWebServer (Extensions) @property(nonatomic, readonly) NSURL* serverURL; // Only non-nil if server is running @property(nonatomic, readonly) NSURL* bonjourServerURL; // Only non-nil if server is running and Bonjour registration is active +- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); +- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); #if !TARGET_OS_IPHONE - (BOOL)runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only) #endif diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 825903f..2370937 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -483,6 +483,24 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er return nil; } +- (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); +} + #if !TARGET_OS_IPHONE - (BOOL)runWithPort:(NSUInteger)port { From c51f9ad7d91184f11bc366f6d9b477c09a606b65 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 19:27:58 -0700 Subject: [PATCH 14/60] Added GCDWebServerHTTPStatusCodes.h --- CGDWebServer/GCDWebServer.m | 2 +- CGDWebServer/GCDWebServerConnection.m | 22 ++--- CGDWebServer/GCDWebServerFileResponse.m | 2 +- CGDWebServer/GCDWebServerHTTPStatusCodes.h | 101 +++++++++++++++++++++ CGDWebServer/GCDWebServerPrivate.h | 2 + CGDWebServer/GCDWebServerResponse.h | 6 +- CGDWebServer/GCDWebServerResponse.m | 22 ++++- GCDWebServer.xcodeproj/project.pbxproj | 2 + 8 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 CGDWebServer/GCDWebServerHTTPStatusCodes.h diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 2370937..31ac987 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -670,7 +670,7 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er if (response) { response.cacheControlMaxAge = cacheAge; } else { - response = [GCDWebServerResponse responseWithStatusCode:404]; + response = [GCDWebServerResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound]; } return response; diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index e1d39d5..43de7d1 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -458,7 +458,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { }]; } else { - [self abortRequest:_request withStatusCode:500]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; } } @@ -467,7 +467,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { NSError* error = nil; if (![_request performOpen:&error]) { LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error); - [self abortRequest:_request withStatusCode:500]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; return; } @@ -477,7 +477,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (![_request performClose:&error]) { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); } - [self abortRequest:_request withStatusCode:500]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; return; } length -= initialData.length; @@ -491,7 +491,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _processRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); - [self abortRequest:_request withStatusCode:500]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; } }]; @@ -500,7 +500,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _processRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); - [self abortRequest:_request withStatusCode:500]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; } } } @@ -509,7 +509,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { NSError* error = nil; if (![_request performOpen:&error]) { LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error); - [self abortRequest:_request withStatusCode:500]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; return; } @@ -521,7 +521,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _processRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); - [self abortRequest:_request withStatusCode:500]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; } }]; @@ -572,7 +572,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { }]; } else { LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket); - [self abortRequest:_request withStatusCode:417]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_ExpectationFailed]; } } else { if (_request.usesChunkedTransferEncoding) { @@ -583,7 +583,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } } else { LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket); - [self abortRequest:_request withStatusCode:400]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest]; } } else { [self _processRequest]; @@ -591,10 +591,10 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } else { _request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery]; DCHECK(_request); - [self abortRequest:_request withStatusCode:405]; + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_MethodNotAllowed]; } } else { - [self abortRequest:nil withStatusCode:500]; + [self abortRequest:nil withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; } }]; diff --git a/CGDWebServer/GCDWebServerFileResponse.m b/CGDWebServer/GCDWebServerFileResponse.m index 2631c51..60e1bf6 100644 --- a/CGDWebServer/GCDWebServerFileResponse.m +++ b/CGDWebServer/GCDWebServerFileResponse.m @@ -100,7 +100,7 @@ static inline NSError* _MakePosixError(int code) { if (range.location != NSNotFound) { _offset = range.location; _size = range.length; - [self setStatusCode:206]; + [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 { diff --git a/CGDWebServer/GCDWebServerHTTPStatusCodes.h b/CGDWebServer/GCDWebServerHTTPStatusCodes.h new file mode 100644 index 0000000..d64558d --- /dev/null +++ b/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 + +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 +}; diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 7e320ee..736828e 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -50,6 +50,8 @@ #define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__) #endif +#import "GCDWebServerHTTPStatusCodes.h" + #import "GCDWebServerConnection.h" #import "GCDWebServerDataRequest.h" diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index cee967e..4892535 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -25,7 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#import +#import "GCDWebServerHTTPStatusCodes.h" @protocol GCDWebServerBodyReader - (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL) @@ -47,7 +47,11 @@ @interface GCDWebServerResponse (Extensions) + (GCDWebServerResponse*)responseWithStatusCode:(NSInteger)statusCode; ++ (GCDWebServerResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)error; ++ (GCDWebServerResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)error; + (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent; - (id)initWithStatusCode:(NSInteger)statusCode; +- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)error; +- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)error; - (id)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent; @end diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index a12fe9b..71f65ab 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -179,7 +179,7 @@ if ((self = [super init])) { _type = nil; _length = NSNotFound; - _status = 200; + _status = kGCDWebServerHTTPStatusCode_OK; _maxAge = 0; _headers = [[NSMutableDictionary alloc] init]; _encoders = [[NSMutableArray alloc] init]; @@ -253,6 +253,14 @@ return ARC_AUTORELEASE([[self alloc] initWithStatusCode:statusCode]); } ++ (GCDWebServerResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)error { + return ARC_AUTORELEASE([[self alloc] initWithClientError:error]); +} + ++ (GCDWebServerResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)error { + return ARC_AUTORELEASE([[self alloc] initWithServerError:error]); +} + + (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent { return ARC_AUTORELEASE([[self alloc] initWithRedirect:location permanent:permanent]); } @@ -264,9 +272,19 @@ return self; } +- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)error { + DCHECK(((NSInteger)error >= 400) && ((NSInteger)error < 500)); + return [self initWithStatusCode:error]; +} + +- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)error { + DCHECK(((NSInteger)error >= 500) && ((NSInteger)error < 600)); + return [self initWithStatusCode:error]; +} + - (id)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent { if ((self = [self init])) { - self.statusCode = permanent ? 301 : 307; + self.statusCode = permanent ? kGCDWebServerHTTPStatusCode_MovedPermanently : kGCDWebServerHTTPStatusCode_TemporaryRedirect; [self setValue:[location absoluteString] forAdditionalHeader:@"Location"]; } return self; diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 7fcb148..8e9e4a9 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -127,6 +127,7 @@ E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerMultiPartFormRequest.m; sourceTree = ""; }; E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerURLEncodedFormRequest.h; sourceTree = ""; }; E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerURLEncodedFormRequest.m; sourceTree = ""; }; + E2A0E81018F3737B00C580B1 /* GCDWebServerHTTPStatusCodes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GCDWebServerHTTPStatusCodes.h; sourceTree = ""; }; 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 = ""; }; @@ -199,6 +200,7 @@ E2A0E7FC18F1D36C00C580B1 /* GCDWebServerFileRequest.m */, E2A0E7EF18F1D12E00C580B1 /* GCDWebServerFileResponse.h */, E2A0E7F018F1D12E00C580B1 /* GCDWebServerFileResponse.m */, + E2A0E81018F3737B00C580B1 /* GCDWebServerHTTPStatusCodes.h */, E2A0E7FF18F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.h */, E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */, E22112801690B63A0048D2B2 /* GCDWebServerPrivate.h */, From fb08e77c0cc041f05ff463d102079d81d5aed12d Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 19:29:04 -0700 Subject: [PATCH 15/60] Added -hasByteRange API --- CGDWebServer/GCDWebServerFileResponse.m | 2 +- CGDWebServer/GCDWebServerPrivate.h | 4 ++++ CGDWebServer/GCDWebServerRequest.h | 1 + CGDWebServer/GCDWebServerRequest.m | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerFileResponse.m b/CGDWebServer/GCDWebServerFileResponse.m index 60e1bf6..e93de27 100644 --- a/CGDWebServer/GCDWebServerFileResponse.m +++ b/CGDWebServer/GCDWebServerFileResponse.m @@ -81,7 +81,7 @@ static inline NSError* _MakePosixError(int code) { ARC_RELEASE(self); return nil; } - if ((range.location != NSNotFound) || (range.length > 0)) { + 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); diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 736828e..1915aa5 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -103,6 +103,10 @@ extern void GCDLogMessage(long level, NSString* format, ...) NS_FORMAT_FUNCTION( #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* GCDWebServerExtractHeaderParameter(NSString* header, NSString* attribute); extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset); diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index 7c6d818..49684b8 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/CGDWebServer/GCDWebServerRequest.h @@ -44,4 +44,5 @@ @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 +- (BOOL)hasByteRange; // Convenience method that checks "byteRange" @end diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 449fa34..b3b42ef 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -237,6 +237,10 @@ return _type ? YES : NO; } +- (BOOL)hasByteRange { + return GCDWebServerIsValidByteRange(_range); +} + - (BOOL)open:(NSError**)error { return YES; } From 794ab5f29393bec9403b11fb46459db0f3683102 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 19:29:33 -0700 Subject: [PATCH 16/60] Fixes --- CGDWebServer/GCDWebServerConnection.m | 2 +- CGDWebServer/GCDWebServerRequest.h | 4 ++-- CGDWebServer/GCDWebServerResponse.h | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 43de7d1..c9d7a47 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -448,7 +448,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if ([_response hasBody]) { [self _writeBodyWithCompletionBlock:^(BOOL successInner) { - [_response performClose]; + [_response performClose]; // TODO: There's nothing we can do on failure as headers have already been sent }]; } diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index 49684b8..7122223 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/CGDWebServer/GCDWebServerRequest.h @@ -39,10 +39,10 @@ @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) 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) 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 +- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil - (BOOL)hasByteRange; // Convenience method that checks "byteRange" @end diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index 4892535..18470a0 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -34,15 +34,15 @@ @end @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, 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. "no-cache" @property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; // Default is disabled + (GCDWebServerResponse*) response; - (id)init; -- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; -- (BOOL)hasBody; // Convenience method +- (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) From 4b46c95a788f7a0ebcdb428b385fda6ec50ad59c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 19:30:15 -0700 Subject: [PATCH 17/60] Allow requests with body but no Content-Type header --- CGDWebServer/GCDWebServerRequest.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index b3b42ef..8539bbd 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -171,12 +171,15 @@ NSString* lengthHeader = [_headers objectForKey:@"Content-Length"]; if (lengthHeader) { NSInteger length = [lengthHeader integerValue]; - if (_chunked || !_type || (length < 0)) { + if (_chunked || (length < 0)) { DNOT_REACHED(); ARC_RELEASE(self); return nil; } _length = length; + if (_type == nil) { + _type = kGCDWebServerDefaultMimeType; + } } else { if (_type && !_chunked) { DNOT_REACHED(); From 047a0604bfeb2341041fcc533d4caba1693ad64e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 19:30:57 -0700 Subject: [PATCH 18/60] Make default implementation for GCDWebServerRequest and GCDWebServerResponse ignore bodies --- CGDWebServer/GCDWebServerRequest.m | 3 +-- CGDWebServer/GCDWebServerResponse.m | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 8539bbd..d39ac95 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -249,8 +249,7 @@ } - (BOOL)writeData:(NSData*)data error:(NSError**)error { - [self doesNotRecognizeSelector:_cmd]; - return NO; + return YES; } - (BOOL)close:(NSError**)error { diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 71f65ab..e3e5085 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -212,8 +212,7 @@ } - (NSData*)readData:(NSError**)error { - [self doesNotRecognizeSelector:_cmd]; - return nil; + return [NSData data]; } - (void)close { From b942a9d2b82409852f54464dade775f6ebec65d6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 20:52:23 -0700 Subject: [PATCH 19/60] Fix --- GCDWebUploader/GCDWebUploader.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index ee7f377..a6962fa 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -42,7 +42,7 @@ @interface GCDWebUploader () { @private NSString* _uploadDirectory; - id delegate; + id __unsafe_unretained _delegate; NSArray* _allowedExtensions; BOOL _showHidden; NSString* _title; From 131810229f08979260449e904477ac4955fa08ad Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 21:12:02 -0700 Subject: [PATCH 20/60] Improved logging APIs --- CGDWebServer/GCDWebServer.h | 9 ++++-- CGDWebServer/GCDWebServer.m | 58 +++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/CGDWebServer/GCDWebServer.h b/CGDWebServer/GCDWebServer.h index ec44ad5..39b119b 100644 --- a/CGDWebServer/GCDWebServer.h +++ b/CGDWebServer/GCDWebServer.h @@ -67,8 +67,6 @@ NSString* GCDWebServerGetPrimaryIPv4Address(); // Returns IPv4 address of prima @interface GCDWebServer (Extensions) @property(nonatomic, readonly) NSURL* serverURL; // Only non-nil if server is running @property(nonatomic, readonly) NSURL* bonjourServerURL; // Only non-nil if server is running and Bonjour registration is active -- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); -- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); #if !TARGET_OS_IPHONE - (BOOL)runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only) #endif @@ -85,3 +83,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 diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 31ac987..552f6fe 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -483,24 +483,6 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er return nil; } -- (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); -} - #if !TARGET_OS_IPHONE - (BOOL)runWithPort:(NSUInteger)port { @@ -681,3 +663,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 From 5ece52fa1b1bc1383f08cfd07d69579219addfbb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 21:20:20 -0700 Subject: [PATCH 21/60] Fix --- CGDWebServer/GCDWebServerDataRequest.m | 1 - 1 file changed, 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerDataRequest.m b/CGDWebServer/GCDWebServerDataRequest.m index d010efd..cfb6aaa 100644 --- a/CGDWebServer/GCDWebServerDataRequest.m +++ b/CGDWebServer/GCDWebServerDataRequest.m @@ -38,7 +38,6 @@ @synthesize data=_data; - (void)dealloc { - DCHECK(_data != nil); ARC_RELEASE(_data); ARC_DEALLOC(super); From 35ce17832314418dd643655de221a55a9cb06162 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 23:00:19 -0700 Subject: [PATCH 22/60] Added GCDWebServerErrorResponse --- CGDWebServer/GCDWebServer.m | 2 +- CGDWebServer/GCDWebServerErrorResponse.h | 41 ++++++++ CGDWebServer/GCDWebServerErrorResponse.m | 125 +++++++++++++++++++++++ CGDWebServer/GCDWebServerPrivate.h | 1 + CGDWebServer/GCDWebServerResponse.h | 6 +- CGDWebServer/GCDWebServerResponse.m | 18 ---- GCDWebServer.xcodeproj/project.pbxproj | 8 ++ 7 files changed, 177 insertions(+), 24 deletions(-) create mode 100644 CGDWebServer/GCDWebServerErrorResponse.h create mode 100644 CGDWebServer/GCDWebServerErrorResponse.m diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 552f6fe..179bbbe 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -652,7 +652,7 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er if (response) { response.cacheControlMaxAge = cacheAge; } else { - response = [GCDWebServerResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound]; + response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NotFound]; } return response; diff --git a/CGDWebServer/GCDWebServerErrorResponse.h b/CGDWebServer/GCDWebServerErrorResponse.h new file mode 100644 index 0000000..9e51a45 --- /dev/null +++ b/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 ++ (GCDWebServerErrorResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3); ++ (GCDWebServerErrorResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3); ++ (GCDWebServerErrorResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4); ++ (GCDWebServerErrorResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4); +- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3); +- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3); +- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4); +- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4); +@end diff --git a/CGDWebServer/GCDWebServerErrorResponse.m b/CGDWebServer/GCDWebServerErrorResponse.m new file mode 100644 index 0000000..abc368a --- /dev/null +++ b/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 () +- (id)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments; +@end + +@implementation GCDWebServerErrorResponse + ++ (GCDWebServerErrorResponse*)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; +} + ++ (GCDWebServerErrorResponse*)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; +} + ++ (GCDWebServerErrorResponse*)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; +} + ++ (GCDWebServerErrorResponse*)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:@"""]; +} + +- (id)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:@"%@

%@: %@

%@

", + title, title, _EscapeHTMLString(message), error]; + if ((self = [self initWithHTML:html])) { + self.statusCode = statusCode; + } + ARC_RELEASE(message); + return self; +} + +- (id)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; +} + +- (id)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; +} + +- (id)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; +} + +- (id)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 diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 1915aa5..08662c2 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -60,6 +60,7 @@ #import "GCDWebServerURLEncodedFormRequest.h" #import "GCDWebServerDataResponse.h" +#import "GCDWebServerErrorResponse.h" #import "GCDWebServerFileResponse.h" #import "GCDWebServerStreamingResponse.h" diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index 18470a0..ead69e5 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -25,7 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#import "GCDWebServerHTTPStatusCodes.h" +#import @protocol GCDWebServerBodyReader - (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL) @@ -47,11 +47,7 @@ @interface GCDWebServerResponse (Extensions) + (GCDWebServerResponse*)responseWithStatusCode:(NSInteger)statusCode; -+ (GCDWebServerResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)error; -+ (GCDWebServerResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)error; + (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent; - (id)initWithStatusCode:(NSInteger)statusCode; -- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)error; -- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)error; - (id)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent; @end diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index e3e5085..4cc88e6 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -252,14 +252,6 @@ return ARC_AUTORELEASE([[self alloc] initWithStatusCode:statusCode]); } -+ (GCDWebServerResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)error { - return ARC_AUTORELEASE([[self alloc] initWithClientError:error]); -} - -+ (GCDWebServerResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)error { - return ARC_AUTORELEASE([[self alloc] initWithServerError:error]); -} - + (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent { return ARC_AUTORELEASE([[self alloc] initWithRedirect:location permanent:permanent]); } @@ -271,16 +263,6 @@ return self; } -- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)error { - DCHECK(((NSInteger)error >= 400) && ((NSInteger)error < 500)); - return [self initWithStatusCode:error]; -} - -- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)error { - DCHECK(((NSInteger)error >= 500) && ((NSInteger)error < 600)); - return [self initWithStatusCode:error]; -} - - (id)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent { if ((self = [self init])) { self.statusCode = permanent ? kGCDWebServerHTTPStatusCode_MovedPermanently : kGCDWebServerHTTPStatusCode_TemporaryRedirect; diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 8e9e4a9..79dd7ca 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ 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 */; }; @@ -113,6 +115,8 @@ 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 = ""; }; + E276647B18F3BC2100A034BA /* GCDWebServerErrorResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerErrorResponse.m; sourceTree = ""; }; E2A0E7EB18F1D03700C580B1 /* GCDWebServerDataResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerDataResponse.h; sourceTree = ""; }; E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerDataResponse.m; sourceTree = ""; }; E2A0E7EF18F1D12E00C580B1 /* GCDWebServerFileResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileResponse.h; sourceTree = ""; }; @@ -196,6 +200,8 @@ E2A0E7F818F1D24700C580B1 /* GCDWebServerDataRequest.m */, E2A0E7EB18F1D03700C580B1 /* GCDWebServerDataResponse.h */, E2A0E7EC18F1D03700C580B1 /* GCDWebServerDataResponse.m */, + E276647A18F3BC2100A034BA /* GCDWebServerErrorResponse.h */, + E276647B18F3BC2100A034BA /* GCDWebServerErrorResponse.m */, E2A0E7FB18F1D36C00C580B1 /* GCDWebServerFileRequest.h */, E2A0E7FC18F1D36C00C580B1 /* GCDWebServerFileRequest.m */, E2A0E7EF18F1D12E00C580B1 /* GCDWebServerFileResponse.h */, @@ -357,6 +363,7 @@ 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 */, @@ -372,6 +379,7 @@ buildActionMask = 2147483647; files = ( 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 */, From 31d51cf5c0634ff0a7c29b0a938eee29041bafeb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 23:03:08 -0700 Subject: [PATCH 23/60] Updated GCDWebUploader to latest APIs --- GCDWebUploader/GCDWebUploader.h | 2 + GCDWebUploader/GCDWebUploader.m | 140 +++++++++++++++++++------------- 2 files changed, 84 insertions(+), 58 deletions(-) diff --git a/GCDWebUploader/GCDWebUploader.h b/GCDWebUploader/GCDWebUploader.h index b83bf39..449ef41 100644 --- a/GCDWebUploader/GCDWebUploader.h +++ b/GCDWebUploader/GCDWebUploader.h @@ -54,4 +54,6 @@ @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 diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index a6962fa..a29502f 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -33,10 +33,13 @@ #endif #import "GCDWebUploader.h" + #import "GCDWebServerDataRequest.h" #import "GCDWebServerMultiPartFormRequest.h" #import "GCDWebServerURLEncodedFormRequest.h" + #import "GCDWebServerDataResponse.h" +#import "GCDWebServerErrorResponse.h" #import "GCDWebServerFileResponse.h" @interface GCDWebUploader () { @@ -93,7 +96,7 @@ return nil; } _uploadDirectory = [[path stringByStandardizingPath] copy]; - GCDWebUploader* __unsafe_unretained uploader = self; + GCDWebUploader* __unsafe_unretained server = self; // Resource files [self addGETHandlerForBasePath:@"/" directoryPath:[siteBundle resourcePath] indexFilename:nil cacheAge:3600 allowRangeRequests:NO]; @@ -110,7 +113,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 @@ -119,19 +122,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"]; @@ -159,19 +162,20 @@ [self addHandlerForMethod:@"GET" path:@"/list" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) { NSString* relativePath = [[request query] objectForKey:@"path"]; - NSString* absolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:relativePath]; + NSString* absolutePath = [server.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]; + BOOL showHidden = server.showHiddenFiles; + NSError* error = nil; + NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error]; 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]) { + if ([type isEqualToString:NSFileTypeRegular] && [server _checkFileExtension:item]) { [array addObject:@{ @"path": [relativePath stringByAppendingPathComponent:item], @"name": item, @@ -187,13 +191,13 @@ } return [GCDWebServerDataResponse responseWithJSONObject:array]; } else { - return [GCDWebServerResponse responseWithStatusCode:500]; + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath]; } } else { - return [GCDWebServerResponse responseWithStatusCode:400]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a directory", relativePath]; } } else { - return [GCDWebServerResponse responseWithStatusCode:404]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; } }]; @@ -202,21 +206,21 @@ [self addHandlerForMethod:@"GET" path:@"/download" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) { NSString* relativePath = [[request query] objectForKey:@"path"]; - NSString* absolutePath = [uploader.uploadDirectory stringByAppendingPathComponent:relativePath]; + NSString* absolutePath = [server.uploadDirectory stringByAppendingPathComponent:relativePath]; BOOL isDirectory; if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { if (isDirectory) { - return [GCDWebServerResponse responseWithStatusCode:400]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is a directory", relativePath]; } else { - if ([uploader.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath: )]) { + if ([server.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath: )]) { dispatch_async(dispatch_get_main_queue(), ^{ - [uploader.delegate webUploader:uploader didDownloadFileAtPath:absolutePath]; + [server.delegate webUploader:server didDownloadFileAtPath:absolutePath]; }); } return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES]; } } else { - return [GCDWebServerResponse responseWithStatusCode:404]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; } }]; @@ -229,26 +233,26 @@ 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]) { + if ((![file.fileName hasPrefix:@"."] || server.showHiddenFiles) && [server _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]) { + NSString* absolutePath = [server _uniquePathForPath:[[server.uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]]; + if ([server 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:)]) { + if ([server.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) { dispatch_async(dispatch_get_main_queue(), ^{ - [uploader.delegate webUploader:uploader didUploadFileAtPath:absolutePath]; + [server.delegate webUploader:server didUploadFileAtPath:absolutePath]; }); } return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType]; } else { - return [GCDWebServerResponse responseWithStatusCode:500]; + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath]; } } else { - return [GCDWebServerResponse responseWithStatusCode:403]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not allowed", file.fileName, relativePath]; } } else { - return [GCDWebServerResponse responseWithStatusCode:400]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName]; } }]; @@ -257,37 +261,39 @@ [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]; + NSString* oldAbsolutePath = [server.uploadDirectory stringByAppendingPathComponent:oldRelativePath]; BOOL isDirectory; if ([[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) { NSString* newRelativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"newPath"]; - if (!uploader.showHiddenFiles) { + if (!server.showHiddenFiles) { for (NSString* component in [newRelativePath pathComponents]) { if ([component hasPrefix:@"."]) { - return [GCDWebServerResponse responseWithStatusCode:400]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Item path \"%@\" is not allowed", newRelativePath]; } } } - if (!isDirectory && ![uploader _checkFileExtension:newRelativePath]) { - return [GCDWebServerResponse responseWithStatusCode:400]; + if (!isDirectory && ![server _checkFileExtension:newRelativePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Item path \"%@\" is not allowed", newRelativePath]; } - 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:)]) { + NSString* newAbsolutePath = [server _uniquePathForPath:[server.uploadDirectory stringByAppendingPathComponent:newRelativePath]]; + if ([server shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) { + [[NSFileManager defaultManager] createDirectoryAtPath:[newAbsolutePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; + NSError* error = nil; + if ([[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) { + if ([server.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) { dispatch_async(dispatch_get_main_queue(), ^{ - [uploader.delegate webUploader:uploader didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]; + [server.delegate webUploader:server didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]; }); } return [GCDWebServerDataResponse responseWithJSONObject:@{}]; } else { - return [GCDWebServerResponse responseWithStatusCode:500]; + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath]; } } else { - return [GCDWebServerResponse responseWithStatusCode:403]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not allowed", oldRelativePath, newRelativePath]; } } else { - return [GCDWebServerResponse responseWithStatusCode:404]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath]; } }]; @@ -296,20 +302,25 @@ [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]; + NSString* absolutePath = [server.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]; - }); + if ([server shouldDeleteItemAtPath:absolutePath]) { + NSError* error = nil; + if ([[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) { + if ([server.delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [server.delegate webUploader:server didDeleteItemAtPath:absolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{}]; + } else { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath]; } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; } else { - return [GCDWebServerResponse responseWithStatusCode:500]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not allowed", relativePath]; } } else { - return [GCDWebServerResponse responseWithStatusCode:404]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; } }]; @@ -318,23 +329,28 @@ [self addHandlerForMethod:@"POST" path:@"/create" requestClass:[GCDWebServerURLEncodedFormRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) { NSString* relativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"path"]; - if (!uploader.showHiddenFiles) { + if (!server.showHiddenFiles) { for (NSString* component in [relativePath pathComponents]) { if ([component hasPrefix:@"."]) { - return [GCDWebServerResponse responseWithStatusCode:400]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Directory path \"%@\" is not allowed", relativePath]; } } } - 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]; - }); + NSString* absolutePath = [server _uniquePathForPath:[server.uploadDirectory stringByAppendingPathComponent:relativePath]]; + if ([server shouldCreateDirectoryAtPath:absolutePath]) { + NSError* error = nil; + if ([[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:&error]) { + if ([server.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [server.delegate webUploader:server didCreateDirectoryAtPath:absolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{}]; + } else { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath]; } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; } else { - return [GCDWebServerResponse responseWithStatusCode:500]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not allowed", relativePath]; } }]; @@ -371,4 +387,12 @@ return YES; } +- (BOOL)shouldDeleteItemAtPath:(NSString*)path { + return YES; +} + +- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path { + return YES; +} + @end From 1d381c9fc65807409496db89d26ab623dd363ff2 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 23:12:18 -0700 Subject: [PATCH 24/60] Refactor GCDWebUploader --- GCDWebUploader/GCDWebUploader.m | 370 +++++++++++++++++--------------- 1 file changed, 192 insertions(+), 178 deletions(-) diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index a29502f..13f7bc4 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -56,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]]) { @@ -86,6 +83,190 @@ return 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]) { + if (isDirectory) { + NSError* error = nil; + NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error]; + 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] && [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]; + } else { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a directory", relativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; + } +} + +- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request { + NSString* relativePath = [[request query] objectForKey:@"path"]; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + BOOL isDirectory; + if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { + if (isDirectory) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is a directory", relativePath]; + } else { + if ([_delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath: )]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate webUploader:self didDownloadFileAtPath:absolutePath]; + }); + } + return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; + } +} + +- (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 ((![file.fileName hasPrefix:@"."] || _showHidden) && [self _checkFileExtension: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]) { + NSError* error = nil; + if ([[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) { + if ([_delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate webUploader:self didUploadFileAtPath:absolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType]; + } else { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not allowed", file.fileName, relativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName]; + } +} + +- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request { + NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"]; + NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath]; + BOOL isDirectory; + if ([[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) { + NSString* newRelativePath = [request.arguments objectForKey:@"newPath"]; + if (!_showHidden) { + for (NSString* component in [newRelativePath pathComponents]) { + if ([component hasPrefix:@"."]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Item path \"%@\" is not allowed", newRelativePath]; + } + } + } + if (!isDirectory && ![self _checkFileExtension:newRelativePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Item path \"%@\" is not allowed", newRelativePath]; + } + NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]]; + if ([self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) { + [[NSFileManager defaultManager] createDirectoryAtPath:[newAbsolutePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; + NSError* error = nil; + if ([[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) { + if ([_delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate webUploader:self didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{}]; + } else { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not allowed", oldRelativePath, newRelativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath]; + } +} + +- (GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request { + NSString* relativePath = [request.arguments objectForKey:@"path"]; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + if ([self shouldDeleteItemAtPath:absolutePath]) { + NSError* error = nil; + if ([[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) { + if ([_delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate webUploader:self didDeleteItemAtPath:absolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{}]; + } else { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not allowed", relativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; + } +} + +- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request { + NSString* relativePath = [request.arguments objectForKey:@"path"]; + if (!_showHidden) { + for (NSString* component in [relativePath pathComponents]) { + if ([component hasPrefix:@"."]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Directory path \"%@\" is not allowed", relativePath]; + } + } + } + NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]]; + if ([self shouldCreateDirectoryAtPath:absolutePath]) { + NSError* error = nil; + if ([[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:&error]) { + if ([_delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate webUploader:self didCreateDirectoryAtPath:absolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{}]; + } else { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath]; + } + } else { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not allowed", relativePath]; + } +} + +@end + +@implementation GCDWebUploader + +@synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden, + title=_title, header=_header, prologue=_prologue, epilogue=_epilogue, footer=_footer; + - (id)initWithUploadDirectory:(NSString*)path { if ((self = [super init])) { NSBundle* siteBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"GCDWebUploader" ofType:@"bundle"]]; @@ -160,199 +341,32 @@ // File listing [self addHandlerForMethod:@"GET" path:@"/list" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) { - - NSString* relativePath = [[request query] objectForKey:@"path"]; - NSString* absolutePath = [server.uploadDirectory stringByAppendingPathComponent:relativePath]; - BOOL isDirectory; - if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { - if (isDirectory) { - BOOL showHidden = server.showHiddenFiles; - NSError* error = nil; - NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error]; - 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] && [server _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 [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a directory", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - + 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 = [server.uploadDirectory stringByAppendingPathComponent:relativePath]; - BOOL isDirectory; - if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { - if (isDirectory) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is a directory", relativePath]; - } else { - if ([server.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath: )]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [server.delegate webUploader:server didDownloadFileAtPath:absolutePath]; - }); - } - return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - + 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:@"."] || server.showHiddenFiles) && [server _checkFileExtension:file.fileName]) { - NSString* relativePath = [(GCDWebServerMultiPartArgument*)[[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"path"] string]; - NSString* absolutePath = [server _uniquePathForPath:[[server.uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]]; - if ([server shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) { - NSError* error = nil; - if ([[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) { - if ([server.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [server.delegate webUploader:server didUploadFileAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not allowed", file.fileName, relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName]; - } - + 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 = [server.uploadDirectory stringByAppendingPathComponent:oldRelativePath]; - BOOL isDirectory; - if ([[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) { - NSString* newRelativePath = [[(GCDWebServerURLEncodedFormRequest*)request arguments] objectForKey:@"newPath"]; - if (!server.showHiddenFiles) { - for (NSString* component in [newRelativePath pathComponents]) { - if ([component hasPrefix:@"."]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Item path \"%@\" is not allowed", newRelativePath]; - } - } - } - if (!isDirectory && ![server _checkFileExtension:newRelativePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Item path \"%@\" is not allowed", newRelativePath]; - } - NSString* newAbsolutePath = [server _uniquePathForPath:[server.uploadDirectory stringByAppendingPathComponent:newRelativePath]]; - if ([server shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) { - [[NSFileManager defaultManager] createDirectoryAtPath:[newAbsolutePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; - NSError* error = nil; - if ([[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) { - if ([server.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [server.delegate webUploader:server didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not allowed", oldRelativePath, newRelativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath]; - } - + 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 = [server.uploadDirectory stringByAppendingPathComponent:relativePath]; - if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { - if ([server shouldDeleteItemAtPath:absolutePath]) { - NSError* error = nil; - if ([[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) { - if ([server.delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [server.delegate webUploader:server didDeleteItemAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not allowed", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - + 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 (!server.showHiddenFiles) { - for (NSString* component in [relativePath pathComponents]) { - if ([component hasPrefix:@"."]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Directory path \"%@\" is not allowed", relativePath]; - } - } - } - NSString* absolutePath = [server _uniquePathForPath:[server.uploadDirectory stringByAppendingPathComponent:relativePath]]; - if ([server shouldCreateDirectoryAtPath:absolutePath]) { - NSError* error = nil; - if ([[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:&error]) { - if ([server.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [server.delegate webUploader:server didCreateDirectoryAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not allowed", relativePath]; - } - + return [server createDirectory:(GCDWebServerURLEncodedFormRequest*)request]; }]; } From 93c6a9beceb225d55443734bb3e4785bff6b231f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 23:17:57 -0700 Subject: [PATCH 25/60] More refactoring of GCDWebUploader --- GCDWebUploader/GCDWebUploader.m | 248 ++++++++++++++++---------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index 13f7bc4..b887d93 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -87,60 +87,58 @@ NSString* relativePath = [[request query] objectForKey:@"path"]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; BOOL isDirectory; - if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { - if (isDirectory) { - NSError* error = nil; - NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error]; - 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] && [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]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a directory", relativePath]; - } - } else { + 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]; + } + + 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]) { - if (isDirectory) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is a directory", relativePath]; - } else { - if ([_delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath: )]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_delegate webUploader:self didDownloadFileAtPath:absolutePath]; - }); - } - return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES]; - } - } else { + 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]; + } + + 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 { @@ -148,93 +146,95 @@ 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 ((![file.fileName hasPrefix:@"."] || _showHidden) && [self _checkFileExtension: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]) { - NSError* error = nil; - if ([[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) { - if ([_delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_delegate webUploader:self didUploadFileAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not allowed", file.fileName, relativePath]; - } - } else { + 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 allowed", 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]) { - NSString* newRelativePath = [request.arguments objectForKey:@"newPath"]; - if (!_showHidden) { - for (NSString* component in [newRelativePath pathComponents]) { - if ([component hasPrefix:@"."]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Item path \"%@\" is not allowed", newRelativePath]; - } - } - } - if (!isDirectory && ![self _checkFileExtension:newRelativePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Item path \"%@\" is not allowed", newRelativePath]; - } - NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]]; - if ([self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) { - [[NSFileManager defaultManager] createDirectoryAtPath:[newAbsolutePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; - NSError* error = nil; - if ([[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) { - if ([_delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_delegate webUploader:self didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not allowed", oldRelativePath, newRelativePath]; - } - } else { + 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]]; + if (!_showHidden) { + for (NSString* component in [newRelativePath pathComponents]) { + if ([component hasPrefix:@"."]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Item path \"%@\" is not allowed", newRelativePath]; + } + } + } + if (!isDirectory && ![self _checkFileExtension:newRelativePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Item path \"%@\" is not allowed", newRelativePath]; + } + + if (![self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not allowed", oldRelativePath, newRelativePath]; + } + + [[NSFileManager defaultManager] createDirectoryAtPath:[newAbsolutePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; + 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]; - if ([[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { - if ([self shouldDeleteItemAtPath:absolutePath]) { - NSError* error = nil; - if ([[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) { - if ([_delegate respondsToSelector:@selector(webUploader:didDeleteItemAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_delegate webUploader:self didDeleteItemAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath]; - } - } else { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not allowed", relativePath]; - } - } else { + if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; } + + if (![self shouldDeleteItemAtPath:absolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not allowed", 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]]; if (!_showHidden) { for (NSString* component in [relativePath pathComponents]) { if ([component hasPrefix:@"."]) { @@ -242,22 +242,22 @@ } } } - NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]]; - if ([self shouldCreateDirectoryAtPath:absolutePath]) { - NSError* error = nil; - if ([[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:&error]) { - if ([_delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_delegate webUploader:self didCreateDirectoryAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; - } else { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath]; - } - } else { + + if (![self shouldCreateDirectoryAtPath:absolutePath]) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not allowed", relativePath]; } + + NSError* error = nil; + if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES 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 From efb1f9f4eb2b3c77fec68ca53f56448f45bf8ee1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 23:19:31 -0700 Subject: [PATCH 26/60] First pass at implementing class 1 WebDAV server --- GCDWebDAVServer/GCDWebDAVServer.h | 58 +++ GCDWebDAVServer/GCDWebDAVServer.m | 544 +++++++++++++++++++++++++ GCDWebServer.xcodeproj/project.pbxproj | 29 ++ Mac/main.m | 13 +- README.md | 1 + 5 files changed, 643 insertions(+), 2 deletions(-) create mode 100644 GCDWebDAVServer/GCDWebDAVServer.h create mode 100644 GCDWebDAVServer/GCDWebDAVServer.m diff --git a/GCDWebDAVServer/GCDWebDAVServer.h b/GCDWebDAVServer/GCDWebDAVServer.h new file mode 100644 index 0000000..9f96f4d --- /dev/null +++ b/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 +@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 delegate; +@property(nonatomic, copy) NSArray* allowedFileExtensions; // Default is nil i.e. all file extensions are allowed +@property(nonatomic) BOOL showHiddenFiles; // Default is NO +- (id)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 diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m new file mode 100644 index 0000000..bf57bbd --- /dev/null +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -0,0 +1,544 @@ +/* + 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 + +#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 __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; +} + +- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request { + GCDWebServerResponse* response = [GCDWebServerResponse response]; + [response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1 + return response; +} + +- (GCDWebServerResponse*)performHEAD:(GCDWebServerRequest*)request { + NSString* relativePath = request.path; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; + } + + NSError* error = nil; + NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:absolutePath error:&error]; + if (!attributes) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound underlyingError:error message:@"Failed retrieving attributes for \"%@\"", relativePath]; + } + + GCDWebServerResponse* response = [GCDWebServerResponse response]; + if ([[attributes fileType] isEqualToString:NSFileTypeRegular]) { + [response setValue:GCDWebServerGetMimeTypeForExtension([absolutePath pathExtension]) forAdditionalHeader:@"Content-Type"]; + [response setValue:[NSString stringWithFormat:@"%llu", [attributes fileSize]] forAdditionalHeader:@"Content-Length"]; + } + return response; +} + +- (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request { + NSString* relativePath = request.path; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + BOOL isDirectory = YES; + if (![absolutePath hasPrefix:_uploadDirectory] || ![[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 file", relativePath]; + } + + 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:@"Uploaded file name \"%@\" is not allowed", fileName]; + } + + if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.filePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not allowed", relativePath]; + } + + [[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL]; + NSError* error = nil; + if (![[NSFileManager defaultManager] moveItemAtPath:request.filePath 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]; + if (![absolutePath hasPrefix:_uploadDirectory]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; + } + + if (![self shouldDeleteItemAtPath:absolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not allowed", 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:@"Directory name \"%@\" is not allowed", directoryName]; + } + + if (![self shouldCreateDirectoryAtPath:absolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not allowed", 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* fileName = [dstAbsolutePath lastPathComponent]; + if ((!_showHidden && [fileName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:fileName])) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Destination name \"%@\" is not allowed", fileName]; + } + + 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 allowed", srcRelativePath, dstRelativePath]; + } + } else { + if (![self shouldCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not allowed", 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:@""]; + [xmlString appendFormat:@"%@", escapedPath]; + [xmlString appendString:@""]; + [xmlString appendString:@""]; + + if (properties & kDAVProperty_ResourceType) { + if (isDirectory) { + [xmlString appendString:@""]; + } else { + [xmlString appendString:@""]; + } + } + + 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:@"%@", [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:@"%@", [formatter stringFromDate:[attributes fileModificationDate]]]; + } + + if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) { + [xmlString appendFormat:@"%llu", [attributes fileSize]]; + } + + [xmlString appendString:@""]; + [xmlString appendString:@"HTTP/1.1 200 OK"]; + [xmlString appendString:@""]; + [xmlString appendString:@"\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) { + 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 { + NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding]; + [self logError:@"Invalid DAV properties\n%@", string]; +#if !__has_feature(objc_arc) + [string release]; +#endif + } + xmlFreeDoc(document); + } + } else { + properties = kDAVAllProperties; + } + + NSString* relativePath = request.path; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; + } + + NSError* error = nil; + NSArray* 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:@""]; + [xmlString appendString:@"\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:@""]; + + GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding] + contentType:@"application/xml; charset=\"utf-8\""]; + response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus; + return response; +} + +@end + +@implementation GCDWebDAVServer + +@synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden; + +- (id)initWithUploadDirectory:(NSString*)path { + if ((self = [super init])) { + _uploadDirectory = [[path stringByStandardizingPath] copy]; + GCDWebDAVServer* __unsafe_unretained server = self; + + // 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 HEAD method + [self addDefaultHandlerForMethod:@"HEAD" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) { + return [server performHEAD:request]; + }]; + + // 9.4 GET method + [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]; + }]; + + // 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 diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 79dd7ca..2cb44a8 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -54,6 +54,10 @@ 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 */; }; @@ -131,6 +135,10 @@ E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerMultiPartFormRequest.m; sourceTree = ""; }; E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerURLEncodedFormRequest.h; sourceTree = ""; }; E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerURLEncodedFormRequest.m; sourceTree = ""; }; + E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebDAVServer.h; sourceTree = ""; }; + E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebDAVServer.m; sourceTree = ""; }; + 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 = ""; }; 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; }; @@ -148,6 +156,7 @@ 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; @@ -159,6 +168,7 @@ 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; @@ -170,6 +180,7 @@ isa = PBXGroup; children = ( E221127B1690B63A0048D2B2 /* CGDWebServer */, + E2A0E80718F3432600C580B1 /* GCDWebDAVServer */, E2BE850618E77ECA0061360B /* GCDWebUploader */, E221128D1690B6470048D2B2 /* Mac */, E22112901690B64F0048D2B2 /* iOS */, @@ -247,6 +258,7 @@ E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */, E221129A1690B7B10048D2B2 /* UIKit.framework */, E22112981690B7AA0048D2B2 /* CFNetwork.framework */, + E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */, E2B0D4A818F134A8009A7927 /* libz.dylib */, ); name = "iOS Frameworks and Libraries"; @@ -258,11 +270,21 @@ E2BE851018E79DAF0061360B /* SystemConfiguration.framework */, E208D1B2167BB17E00500836 /* CoreServices.framework */, E208D148167B76B700500836 /* CFNetwork.framework */, + E2A0E80E18F35CA300C580B1 /* libxml2.dylib */, E2B0D4A618F13495009A7927 /* libz.dylib */, ); name = "Mac Frameworks and Libraries"; sourceTree = ""; }; + E2A0E80718F3432600C580B1 /* GCDWebDAVServer */ = { + isa = PBXGroup; + children = ( + E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */, + E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */, + ); + path = GCDWebDAVServer; + sourceTree = ""; + }; E2BE850618E77ECA0061360B /* GCDWebUploader */ = { isa = PBXGroup; children = ( @@ -368,6 +390,7 @@ 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 */, @@ -378,6 +401,7 @@ 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 */, @@ -437,6 +461,7 @@ buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; GCC_OPTIMIZATION_LEVEL = 0; + HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2"; ONLY_ACTIVE_ARCH = YES; WARNING_CFLAGS = ( "-Wall", @@ -451,6 +476,9 @@ "-Wno-assign-enum", "-Wno-format-nonliteral", "-Wno-cast-align", + "-Wno-padded", + "-Wno-documentation", + "-Wno-documentation-unknown-command", ); }; name = Debug; @@ -461,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; diff --git a/Mac/main.m b/Mac/main.m index b3862c5..40a583b 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -25,14 +25,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#import "GCDWebUploader.h" +#import "GCDWebServer.h" #import "GCDWebServerDataRequest.h" #import "GCDWebServerURLEncodedFormRequest.h" #import "GCDWebServerDataResponse.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), 4) : 0); @autoreleasepool { GCDWebServer* webServer = nil; switch (mode) { @@ -90,6 +94,11 @@ int main(int argc, const char* argv[]) { } case 3: { + webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:@"/tmp"]; + break; + } + + case 4: { webServer = [[GCDWebUploader alloc] initWithUploadDirectory:@"/tmp"]; break; } diff --git a/README.md b/README.md index b752461..cfc3b28 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Extra built-in features: 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 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) From 715d9854751a79692300f6999d16e1ffce8765a3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 23:27:44 -0700 Subject: [PATCH 27/60] Update README.md --- README.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cfc3b28..77880d3 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ 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. @@ -83,6 +83,25 @@ 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 and delete files 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). + +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 ======================== From 3401206279acdad976d7ecfb54f8365942a14187 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 23:33:54 -0700 Subject: [PATCH 28/60] Fix --- Mac/main.m | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Mac/main.m b/Mac/main.m index 40a583b..59d2d7d 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -26,9 +26,12 @@ */ #import "GCDWebServer.h" + #import "GCDWebServerDataRequest.h" #import "GCDWebServerURLEncodedFormRequest.h" + #import "GCDWebServerDataResponse.h" +#import "GCDWebServerStreamingResponse.h" #import "GCDWebDAVServer.h" @@ -36,7 +39,7 @@ int main(int argc, const char* argv[]) { BOOL success = NO; - int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 4) : 0); + int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 5) : 0); @autoreleasepool { GCDWebServer* webServer = nil; switch (mode) { @@ -103,6 +106,29 @@ int main(int argc, const char* argv[]) { 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; + } + } success = [webServer runWithPort:8080]; #if !__has_feature(objc_arc) From e26c9b76ead448e853f2eef13376e760089432c1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Mon, 7 Apr 2014 23:45:33 -0700 Subject: [PATCH 29/60] Updated to "instancetype" type --- CGDWebServer/GCDWebServer.h | 1 + CGDWebServer/GCDWebServer.m | 2 +- CGDWebServer/GCDWebServerDataResponse.h | 24 +++++++++---------- CGDWebServer/GCDWebServerDataResponse.m | 24 +++++++++---------- CGDWebServer/GCDWebServerErrorResponse.h | 16 ++++++------- CGDWebServer/GCDWebServerErrorResponse.m | 20 ++++++++-------- CGDWebServer/GCDWebServerFileRequest.m | 2 +- CGDWebServer/GCDWebServerFileResponse.h | 16 ++++++------- CGDWebServer/GCDWebServerFileResponse.m | 16 ++++++------- .../GCDWebServerMultiPartFormRequest.m | 2 +- CGDWebServer/GCDWebServerRequest.h | 2 +- CGDWebServer/GCDWebServerRequest.m | 2 +- CGDWebServer/GCDWebServerResponse.h | 12 +++++----- CGDWebServer/GCDWebServerResponse.m | 12 +++++----- CGDWebServer/GCDWebServerStreamingResponse.h | 4 ++-- CGDWebServer/GCDWebServerStreamingResponse.m | 4 ++-- GCDWebDAVServer/GCDWebDAVServer.h | 2 +- GCDWebDAVServer/GCDWebDAVServer.m | 2 +- GCDWebUploader/GCDWebUploader.h | 2 +- GCDWebUploader/GCDWebUploader.m | 2 +- 20 files changed, 84 insertions(+), 83 deletions(-) diff --git a/CGDWebServer/GCDWebServer.h b/CGDWebServer/GCDWebServer.h index 39b119b..2570b81 100644 --- a/CGDWebServer/GCDWebServer.h +++ b/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; diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 179bbbe..7c555d7 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -261,7 +261,7 @@ static void _SignalHandler(int signal) { [GCDWebServerConnection class]; // Initialize class immediately to make sure it happens on the main thread } -- (id)init { +- (instancetype)init { if ((self = [super init])) { _handlers = [[NSMutableArray alloc] init]; } diff --git a/CGDWebServer/GCDWebServerDataResponse.h b/CGDWebServer/GCDWebServerDataResponse.h index 05ecdb5..deca337 100644 --- a/CGDWebServer/GCDWebServerDataResponse.h +++ b/CGDWebServer/GCDWebServerDataResponse.h @@ -28,19 +28,19 @@ #import "GCDWebServerResponse.h" @interface GCDWebServerDataResponse : GCDWebServerResponse -+ (GCDWebServerDataResponse*)responseWithData:(NSData*)data contentType:(NSString*)type; -- (id)initWithData:(NSData*)data contentType:(NSString*)type; ++ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type; +- (instancetype)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; ++ (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 diff --git a/CGDWebServer/GCDWebServerDataResponse.m b/CGDWebServer/GCDWebServerDataResponse.m index b36d2df..d7f292f 100644 --- a/CGDWebServer/GCDWebServerDataResponse.m +++ b/CGDWebServer/GCDWebServerDataResponse.m @@ -36,11 +36,11 @@ @implementation GCDWebServerDataResponse -+ (GCDWebServerDataResponse*)responseWithData:(NSData*)data contentType:(NSString*)type { ++ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type { return ARC_AUTORELEASE([[[self class] alloc] initWithData:data contentType:type]); } -- (id)initWithData:(NSData*)data contentType:(NSString*)type { +- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type { if (data == nil) { DNOT_REACHED(); ARC_RELEASE(self); @@ -77,27 +77,27 @@ @implementation GCDWebServerDataResponse (Extensions) -+ (GCDWebServerDataResponse*)responseWithText:(NSString*)text { ++ (instancetype)responseWithText:(NSString*)text { return ARC_AUTORELEASE([[self alloc] initWithText:text]); } -+ (GCDWebServerDataResponse*)responseWithHTML:(NSString*)html { ++ (instancetype)responseWithHTML:(NSString*)html { return ARC_AUTORELEASE([[self alloc] initWithHTML:html]); } -+ (GCDWebServerDataResponse*)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables { ++ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables { return ARC_AUTORELEASE([[self alloc] initWithHTMLTemplate:path variables:variables]); } -+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object { ++ (instancetype)responseWithJSONObject:(id)object { return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object]); } -+ (GCDWebServerDataResponse*)responseWithJSONObject:(id)object contentType:(NSString*)type { ++ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type { return ARC_AUTORELEASE([[self alloc] initWithJSONObject:object contentType:type]); } -- (id)initWithText:(NSString*)text { +- (instancetype)initWithText:(NSString*)text { NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding]; if (data == nil) { DNOT_REACHED(); @@ -107,7 +107,7 @@ return [self initWithData:data contentType:@"text/plain; charset=utf-8"]; } -- (id)initWithHTML:(NSString*)html { +- (instancetype)initWithHTML:(NSString*)html { NSData* data = [html dataUsingEncoding:NSUTF8StringEncoding]; if (data == nil) { DNOT_REACHED(); @@ -117,7 +117,7 @@ return [self initWithData:data contentType:@"text/html; charset=utf-8"]; } -- (id)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables { +- (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)]; @@ -127,11 +127,11 @@ return response; } -- (id)initWithJSONObject:(id)object { +- (instancetype)initWithJSONObject:(id)object { return [self initWithJSONObject:object contentType:@"application/json"]; } -- (id)initWithJSONObject:(id)object contentType:(NSString*)type { +- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type { NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL]; if (data == nil) { ARC_RELEASE(self); diff --git a/CGDWebServer/GCDWebServerErrorResponse.h b/CGDWebServer/GCDWebServerErrorResponse.h index 9e51a45..98cbeee 100644 --- a/CGDWebServer/GCDWebServerErrorResponse.h +++ b/CGDWebServer/GCDWebServerErrorResponse.h @@ -30,12 +30,12 @@ // Returns responses with an HTML body containing the error message @interface GCDWebServerErrorResponse : GCDWebServerDataResponse -+ (GCDWebServerErrorResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3); -+ (GCDWebServerErrorResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3); -+ (GCDWebServerErrorResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4); -+ (GCDWebServerErrorResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4); -- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3); -- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3); -- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4); -- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4); ++ (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 diff --git a/CGDWebServer/GCDWebServerErrorResponse.m b/CGDWebServer/GCDWebServerErrorResponse.m index abc368a..5f221cc 100644 --- a/CGDWebServer/GCDWebServerErrorResponse.m +++ b/CGDWebServer/GCDWebServerErrorResponse.m @@ -28,12 +28,12 @@ #import "GCDWebServerPrivate.h" @interface GCDWebServerErrorResponse () -- (id)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments; +- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments; @end @implementation GCDWebServerErrorResponse -+ (GCDWebServerErrorResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { ++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500)); va_list arguments; va_start(arguments, format); @@ -42,7 +42,7 @@ return response; } -+ (GCDWebServerErrorResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { ++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600)); va_list arguments; va_start(arguments, format); @@ -51,7 +51,7 @@ return response; } -+ (GCDWebServerErrorResponse*)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { ++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500)); va_list arguments; va_start(arguments, format); @@ -60,7 +60,7 @@ return response; } -+ (GCDWebServerErrorResponse*)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { ++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600)); va_list arguments; va_start(arguments, format); @@ -73,7 +73,7 @@ static inline NSString* _EscapeHTMLString(NSString* string) { return [string stringByReplacingOccurrencesOfString:@"\"" withString:@"""]; } -- (id)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments { +- (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] : @""; @@ -86,7 +86,7 @@ static inline NSString* _EscapeHTMLString(NSString* string) { return self; } -- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { +- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500)); va_list arguments; va_start(arguments, format); @@ -95,7 +95,7 @@ static inline NSString* _EscapeHTMLString(NSString* string) { return self; } -- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { +- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600)); va_list arguments; va_start(arguments, format); @@ -104,7 +104,7 @@ static inline NSString* _EscapeHTMLString(NSString* string) { return self; } -- (id)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { +- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { DCHECK(((NSInteger)errorCode >= 400) && ((NSInteger)errorCode < 500)); va_list arguments; va_start(arguments, format); @@ -113,7 +113,7 @@ static inline NSString* _EscapeHTMLString(NSString* string) { return self; } -- (id)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { +- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... { DCHECK(((NSInteger)errorCode >= 500) && ((NSInteger)errorCode < 600)); va_list arguments; va_start(arguments, format); diff --git a/CGDWebServer/GCDWebServerFileRequest.m b/CGDWebServer/GCDWebServerFileRequest.m index 11db890..a87f569 100644 --- a/CGDWebServer/GCDWebServerFileRequest.m +++ b/CGDWebServer/GCDWebServerFileRequest.m @@ -42,7 +42,7 @@ static inline NSError* _MakePosixError(int code) { @synthesize filePath=_filePath; -- (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 initWithMethod:method url:url headers:headers path:path query:query])) { _filePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]); } diff --git a/CGDWebServer/GCDWebServerFileResponse.h b/CGDWebServer/GCDWebServerFileResponse.h index ec296d7..2ed57bb 100644 --- a/CGDWebServer/GCDWebServerFileResponse.h +++ b/CGDWebServer/GCDWebServerFileResponse.h @@ -28,12 +28,12 @@ #import "GCDWebServerResponse.h" @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; ++ (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 diff --git a/CGDWebServer/GCDWebServerFileResponse.m b/CGDWebServer/GCDWebServerFileResponse.m index e93de27..190b71b 100644 --- a/CGDWebServer/GCDWebServerFileResponse.m +++ b/CGDWebServer/GCDWebServerFileResponse.m @@ -46,35 +46,35 @@ static inline NSError* _MakePosixError(int code) { @implementation GCDWebServerFileResponse -+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path { ++ (instancetype)responseWithFile:(NSString*)path { return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path]); } -+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment { ++ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment { return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path isAttachment:attachment]); } -+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range { ++ (instancetype)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 { ++ (instancetype)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 { +- (instancetype)initWithFile:(NSString*)path { return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO]; } -- (id)initWithFile:(NSString*)path isAttachment:(BOOL)attachment { +- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment { return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment]; } -- (id)initWithFile:(NSString*)path byteRange:(NSRange)range { +- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range { return [self initWithFile:path byteRange:range isAttachment:NO]; } -- (id)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment { +- (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(); diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.m b/CGDWebServer/GCDWebServerMultiPartFormRequest.m index 0571c1f..d376a39 100644 --- a/CGDWebServer/GCDWebServerMultiPartFormRequest.m +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.m @@ -185,7 +185,7 @@ static NSData* _dashNewlineData = nil; return @"multipart/form-data"; } -- (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 initWithMethod:method url:url headers:headers path:path query:query])) { NSString* boundary = GCDWebServerExtractHeaderParameter(self.contentType, @"boundary"); if (boundary) { diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index 7122223..1a75535 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/CGDWebServer/GCDWebServerRequest.h @@ -42,7 +42,7 @@ @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) 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; +- (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 diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index d39ac95..587f4fe 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -158,7 +158,7 @@ @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 { +- (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); diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index ead69e5..1e22ee1 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -39,15 +39,15 @@ @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; // Default is disabled -+ (GCDWebServerResponse*) response; -- (id)init; ++ (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; ++ (instancetype)responseWithStatusCode:(NSInteger)statusCode; ++ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent; +- (instancetype)initWithStatusCode:(NSInteger)statusCode; +- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent; @end diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 4cc88e6..193dce0 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -171,11 +171,11 @@ @synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, gzipContentEncodingEnabled=_gzipped, additionalHeaders=_headers; -+ (GCDWebServerResponse*)response { ++ (instancetype)response { return ARC_AUTORELEASE([[[self class] alloc] init]); } -- (id)init { +- (instancetype)init { if ((self = [super init])) { _type = nil; _length = NSNotFound; @@ -248,22 +248,22 @@ @implementation GCDWebServerResponse (Extensions) -+ (GCDWebServerResponse*)responseWithStatusCode:(NSInteger)statusCode { ++ (instancetype)responseWithStatusCode:(NSInteger)statusCode { return ARC_AUTORELEASE([[self alloc] initWithStatusCode:statusCode]); } -+ (GCDWebServerResponse*)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent { ++ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent { return ARC_AUTORELEASE([[self alloc] initWithRedirect:location permanent:permanent]); } -- (id)initWithStatusCode:(NSInteger)statusCode { +- (instancetype)initWithStatusCode:(NSInteger)statusCode { if ((self = [self init])) { self.statusCode = statusCode; } return self; } -- (id)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent { +- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent { if ((self = [self init])) { self.statusCode = permanent ? kGCDWebServerHTTPStatusCode_MovedPermanently : kGCDWebServerHTTPStatusCode_TemporaryRedirect; [self setValue:[location absoluteString] forAdditionalHeader:@"Location"]; diff --git a/CGDWebServer/GCDWebServerStreamingResponse.h b/CGDWebServer/GCDWebServerStreamingResponse.h index f0500a7..313eec3 100644 --- a/CGDWebServer/GCDWebServerStreamingResponse.h +++ b/CGDWebServer/GCDWebServerStreamingResponse.h @@ -30,6 +30,6 @@ typedef NSData* (^GCDWebServerStreamBlock)(NSError** error); @interface GCDWebServerStreamingResponse : GCDWebServerResponse // Automatically enables chunked transfer encoding -+ (GCDWebServerStreamingResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block; -- (id)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block; // Block must return empty NSData when done or nil on error ++ (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 diff --git a/CGDWebServer/GCDWebServerStreamingResponse.m b/CGDWebServer/GCDWebServerStreamingResponse.m index ab4ed6c..fd79a8a 100644 --- a/CGDWebServer/GCDWebServerStreamingResponse.m +++ b/CGDWebServer/GCDWebServerStreamingResponse.m @@ -35,11 +35,11 @@ @implementation GCDWebServerStreamingResponse -+ (GCDWebServerStreamingResponse*)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { ++ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]); } -- (id)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { +- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { if ((self = [super init])) { _block = [block copy]; diff --git a/GCDWebDAVServer/GCDWebDAVServer.h b/GCDWebDAVServer/GCDWebDAVServer.h index 9f96f4d..2dff7c2 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.h +++ b/GCDWebDAVServer/GCDWebDAVServer.h @@ -46,7 +46,7 @@ @property(nonatomic, assign) id delegate; @property(nonatomic, copy) NSArray* allowedFileExtensions; // Default is nil i.e. all file extensions are allowed @property(nonatomic) BOOL showHiddenFiles; // Default is NO -- (id)initWithUploadDirectory:(NSString*)path; +- (instancetype)initWithUploadDirectory:(NSString*)path; @end @interface GCDWebDAVServer (Subclassing) diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m index bf57bbd..24a2d42 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.m +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -452,7 +452,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name @synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden; -- (id)initWithUploadDirectory:(NSString*)path { +- (instancetype)initWithUploadDirectory:(NSString*)path { if ((self = [super init])) { _uploadDirectory = [[path stringByStandardizingPath] copy]; GCDWebDAVServer* __unsafe_unretained server = self; diff --git a/GCDWebUploader/GCDWebUploader.h b/GCDWebUploader/GCDWebUploader.h index 449ef41..109ecc5 100644 --- a/GCDWebUploader/GCDWebUploader.h +++ b/GCDWebUploader/GCDWebUploader.h @@ -48,7 +48,7 @@ @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) diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index b887d93..147b5b6 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -267,7 +267,7 @@ @synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden, title=_title, header=_header, prologue=_prologue, epilogue=_epilogue, footer=_footer; -- (id)initWithUploadDirectory:(NSString*)path { +- (instancetype)initWithUploadDirectory:(NSString*)path { if ((self = [super init])) { NSBundle* siteBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"GCDWebUploader" ofType:@"bundle"]]; if (siteBundle == nil) { From 881cc3b00cae9b3c1b87342e06909231dcd8a760 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 17:49:17 -0700 Subject: [PATCH 30/60] Added JSON and text extensions to GCDWebServerDataRequest --- CGDWebServer/GCDWebServerDataRequest.h | 5 +++ CGDWebServer/GCDWebServerDataRequest.m | 32 +++++++++++++++++++ .../GCDWebServerURLEncodedFormRequest.h | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerDataRequest.h b/CGDWebServer/GCDWebServerDataRequest.h index 8a224a2..02b078f 100644 --- a/CGDWebServer/GCDWebServerDataRequest.h +++ b/CGDWebServer/GCDWebServerDataRequest.h @@ -30,3 +30,8 @@ @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 diff --git a/CGDWebServer/GCDWebServerDataRequest.m b/CGDWebServer/GCDWebServerDataRequest.m index cfb6aaa..40ca12e 100644 --- a/CGDWebServer/GCDWebServerDataRequest.m +++ b/CGDWebServer/GCDWebServerDataRequest.m @@ -30,6 +30,9 @@ @interface GCDWebServerDataRequest () { @private NSMutableData* _data; + + NSString* _text; + id _jsonObject; } @end @@ -39,6 +42,8 @@ - (void)dealloc { ARC_RELEASE(_data); + ARC_RELEASE(_text); + ARC_RELEASE(_jsonObject); ARC_DEALLOC(super); } @@ -69,3 +74,30 @@ } @end + +@implementation GCDWebServerDataRequest (Extensions) + +- (NSString*)text { + if (_text == nil) { + if ([self.contentType hasPrefix:@"text/"]) { + NSString* charset = GCDWebServerExtractHeaderParameter(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 diff --git a/CGDWebServer/GCDWebServerURLEncodedFormRequest.h b/CGDWebServer/GCDWebServerURLEncodedFormRequest.h index 8fe6567..9615e8e 100644 --- a/CGDWebServer/GCDWebServerURLEncodedFormRequest.h +++ b/CGDWebServer/GCDWebServerURLEncodedFormRequest.h @@ -28,6 +28,6 @@ #import "GCDWebServerDataRequest.h" @interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest -@property(nonatomic, readonly) NSDictionary* arguments; +@property(nonatomic, readonly) NSDictionary* arguments; // Text encoding is extracted from Content-Type or defaults to UTF-8 + (NSString*)mimeType; @end From b3a700d38a01e2afbdbeb95bb1b18b2b07b521e4 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 17:49:35 -0700 Subject: [PATCH 31/60] Ensure Content-Type header is lowercased --- CGDWebServer/GCDWebServerConnection.m | 2 +- CGDWebServer/GCDWebServerRequest.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index c9d7a47..6b8a6cf 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -431,7 +431,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { 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)[_response.contentType lowercaseString]); } if (_response.contentLength != NSNotFound) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]); diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 587f4fe..3d9bdee 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -166,7 +166,7 @@ _path = [path copy]; _query = ARC_RETAIN(query); - _type = ARC_RETAIN([_headers objectForKey:@"Content-Type"]); + _type = ARC_RETAIN([[_headers objectForKey:@"Content-Type"] lowercaseString]); _chunked = [[[_headers objectForKey:@"Transfer-Encoding"] lowercaseString] isEqualToString:@"chunked"]; NSString* lengthHeader = [_headers objectForKey:@"Content-Length"]; if (lengthHeader) { From 60f9ee975ed2a7de0d0c428fa682236c292dec71 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 19:41:25 -0700 Subject: [PATCH 32/60] Fix --- Mac/main.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mac/main.m b/Mac/main.m index 59d2d7d..9a39b23 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -97,12 +97,12 @@ int main(int argc, const char* argv[]) { } case 3: { - webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:@"/tmp"]; + webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]]; break; } case 4: { - webServer = [[GCDWebUploader alloc] initWithUploadDirectory:@"/tmp"]; + webServer = [[GCDWebUploader alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]]; break; } From 4c993ebfac147c7d7475d8db21983cd6c1c82020 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 19:51:01 -0700 Subject: [PATCH 33/60] Update README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 77880d3..970181f 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ GCDWebServer is a lightweight GCD based HTTP 1.1 server designed to be embedded * 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) * No dependencies on third-party source code * Available under a friendly [New BSD License](LICENSE) Extra built-in features: +* 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) +* JSON * [Chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) for both requests and responses * [HTTP compression](https://en.wikipedia.org/wiki/HTTP_compression) with gzip for both requests and responses @@ -67,7 +68,7 @@ int main(int argc, const char* argv[]) { 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: @@ -86,7 +87,7 @@ 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 and delete files 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 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). Simply instantiate and run a GCDWebDAVServer instance then connect to http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ using a WebDAV client: @@ -248,6 +249,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 lets users upload, download and organize comic files inside the app using their web browser directly over WiFi. 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. From 30756fc8f90ef8a36a0f8c2e3f387ec9b9c8e3d5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 19:59:41 -0700 Subject: [PATCH 34/60] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 970181f..92f09b1 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ 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 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 +* 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: -* 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) -* JSON -* [Chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) for both requests and responses -* [HTTP compression](https://en.wikipedia.org/wiki/HTTP_compression) with gzip for both requests and responses +* 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 From 1be1966252ad1a77a247cc96d93762422149fc00 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 01:37:05 -0700 Subject: [PATCH 35/60] Fix --- CGDWebServer/GCDWebServerConnection.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 6b8a6cf..c4e5441 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -533,7 +533,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [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); NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(_requestMessage)); DCHECK(requestURL); @@ -545,7 +545,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { 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)); From 7339a7a2a69a061f6e10277a36d7510b5b291fdb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 22:52:52 -0700 Subject: [PATCH 36/60] Factored out HTTP date parsing and formatting --- CGDWebServer/GCDWebServer.m | 31 ++++++++++++++++++++++++++- CGDWebServer/GCDWebServerConnection.m | 19 +--------------- CGDWebServer/GCDWebServerPrivate.h | 2 ++ 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 7c555d7..cc8bb3e 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/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 @@ -115,6 +117,22 @@ NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* 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* GCDWebServerGetMimeTypeForExtension(NSString* extension) { static NSDictionary* _overrides = nil; if (_overrides == nil) { @@ -258,7 +276,18 @@ 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); + } } - (instancetype)init { diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index c4e5441..54c0e8e 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -45,8 +45,6 @@ static NSData* _CRLFData = nil; static NSData* _CRLFCRLFData = nil; static NSData* _continueData = nil; static NSData* _lastChunkData = nil; -static NSDateFormatter* _dateFormatter = nil; -static dispatch_queue_t _formatterQueue = NULL; @interface GCDWebServerConnection () { @private @@ -384,18 +382,6 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (_lastChunkData == nil) { _lastChunkData = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5]; } - 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); - } } - (void)_initializeResponseHeadersWithStatusCode:(NSInteger)statusCode { @@ -403,10 +389,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { _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); - }); + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatHTTPDate([NSDate date])); } // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 08662c2..5941487 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -110,6 +110,8 @@ static inline BOOL GCDWebServerIsValidByteRange(NSRange range) { extern NSString* GCDWebServerExtractHeaderParameter(NSString* header, NSString* attribute); extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset); +extern NSString* GCDWebServerFormatHTTPDate(NSDate* date); +extern NSDate* GCDWebServerParseHTTPDate(NSString* string); @interface GCDWebServerConnection () - (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket; From f1a79ffd1178df95854dc6767d5f672a67fafb32 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 22:55:28 -0700 Subject: [PATCH 37/60] Added support for "Last-Modified" response header --- CGDWebServer/GCDWebServerConnection.m | 3 +++ CGDWebServer/GCDWebServerFileResponse.m | 5 +++++ CGDWebServer/GCDWebServerResponse.h | 3 ++- CGDWebServer/GCDWebServerResponse.m | 4 +++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 54c0e8e..ad7e436 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -408,6 +408,9 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (_response) { [self _initializeResponseHeadersWithStatusCode:_response.statusCode]; + if (_response.lastModifiedDate) { + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Last-Modified"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatHTTPDate(_response.lastModifiedDate)); + } if (_response.cacheControlMaxAge > 0) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)_response.cacheControlMaxAge]); } else { diff --git a/CGDWebServer/GCDWebServerFileResponse.m b/CGDWebServer/GCDWebServerFileResponse.m index 190b71b..a23f2e4 100644 --- a/CGDWebServer/GCDWebServerFileResponse.m +++ b/CGDWebServer/GCDWebServerFileResponse.m @@ -74,6 +74,10 @@ static inline NSError* _MakePosixError(int code) { 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)) { @@ -121,6 +125,7 @@ static inline NSError* _MakePosixError(int code) { self.contentType = GCDWebServerGetMimeTypeForExtension([path pathExtension]); self.contentLength = (range.location != NSNotFound ? range.length : (NSUInteger)info.st_size); + self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec); } return self; } diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index 1e22ee1..16a20b3 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -37,7 +37,8 @@ @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. "no-cache" +@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, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; // Default is disabled + (instancetype)response; - (instancetype)init; diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 193dce0..30f819c 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -156,6 +156,7 @@ NSUInteger _length; NSInteger _status; NSUInteger _maxAge; + NSDate* _lastModified; NSMutableDictionary* _headers; BOOL _chunked; BOOL _gzipped; @@ -168,7 +169,7 @@ @implementation GCDWebServerResponse -@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, +@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, lastModifiedDate=_lastModified, gzipContentEncodingEnabled=_gzipped, additionalHeaders=_headers; + (instancetype)response { @@ -189,6 +190,7 @@ - (void)dealloc { ARC_RELEASE(_type); + ARC_RELEASE(_lastModified); ARC_RELEASE(_headers); ARC_RELEASE(_encoders); From 289059c875ff5ae8e31046aa87c44e582ad194a3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 22:59:29 -0700 Subject: [PATCH 38/60] Added support for "If-Modified-Since" and "Accept-Encoding" headers --- CGDWebServer/GCDWebServerRequest.h | 2 ++ CGDWebServer/GCDWebServerRequest.m | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index 1a75535..3f97085 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/CGDWebServer/GCDWebServerRequest.h @@ -41,7 +41,9 @@ @property(nonatomic, readonly) NSDictionary* query; // May be nil @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* ifModifiedSinceDate; // Automatically parsed from headers (nil if request has no "If-Modified-Since" header or it is malformatted) @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) +@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" diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 3d9bdee..4f5e0ae 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -145,7 +145,9 @@ NSString* _type; BOOL _chunked; NSUInteger _length; + NSDate* _modifiedSinceDate; NSRange _range; + BOOL _gzipAccepted; BOOL _opened; NSMutableArray* _decoders; @@ -155,8 +157,8 @@ @implementation GCDWebServerRequest : NSObject -@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, byteRange=_range, - usesChunkedTransferEncoding=_chunked; +@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, ifModifiedSinceDate=_modifiedSinceDate, + byteRange=_range, acceptsGzipContentEncoding=_gzipAccepted, usesChunkedTransferEncoding=_chunked; - (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { if ((self = [super init])) { @@ -189,6 +191,11 @@ _length = NSNotFound; } + NSString* ifModifiedSinceHeader = [_headers objectForKey:@"If-Modified-Since"]; + if (ifModifiedSinceHeader) { + _modifiedSinceDate = [GCDWebServerParseHTTPDate(ifModifiedSinceHeader) copy]; + } + _range = NSMakeRange(NSNotFound, 0); NSString* rangeHeader = [[_headers objectForKey:@"Range"] lowercaseString]; if (rangeHeader) { @@ -219,6 +226,10 @@ } } + if ([[_headers objectForKey:@"Accept-Encoding"] rangeOfString:@"gzip"].location != NSNotFound) { + _gzipAccepted = YES; + } + _decoders = [[NSMutableArray alloc] init]; } return self; @@ -231,6 +242,7 @@ ARC_RELEASE(_path); ARC_RELEASE(_query); ARC_RELEASE(_type); + ARC_RELEASE(_modifiedSinceDate); ARC_RELEASE(_decoders); ARC_DEALLOC(super); From c454dc4e8e1b1d456753ca0c05852d27f00d7367 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 23:00:21 -0700 Subject: [PATCH 39/60] Simplified internal checks for requests and responses --- CGDWebServer/GCDWebServerDataRequest.m | 3 --- CGDWebServer/GCDWebServerFileRequest.m | 8 +------- CGDWebServer/GCDWebServerFileResponse.m | 6 ------ CGDWebServer/GCDWebServerMultiPartFormRequest.m | 4 ---- CGDWebServer/GCDWebServerRequest.m | 3 +++ CGDWebServer/GCDWebServerResponse.m | 3 +++ 6 files changed, 7 insertions(+), 20 deletions(-) diff --git a/CGDWebServer/GCDWebServerDataRequest.m b/CGDWebServer/GCDWebServerDataRequest.m index 40ca12e..ff29f20 100644 --- a/CGDWebServer/GCDWebServerDataRequest.m +++ b/CGDWebServer/GCDWebServerDataRequest.m @@ -49,7 +49,6 @@ } - (BOOL)open:(NSError**)error { - DCHECK(_data == nil); if (self.contentLength != NSNotFound) { _data = [[NSMutableData alloc] initWithCapacity:self.contentLength]; } else { @@ -63,13 +62,11 @@ } - (BOOL)writeData:(NSData*)data error:(NSError**)error { - DCHECK(_data != nil); [_data appendData:data]; return YES; } - (BOOL)close:(NSError**)error { - DCHECK(_data != nil); return YES; } diff --git a/CGDWebServer/GCDWebServerFileRequest.m b/CGDWebServer/GCDWebServerFileRequest.m index a87f569..7b657f2 100644 --- a/CGDWebServer/GCDWebServerFileRequest.m +++ b/CGDWebServer/GCDWebServerFileRequest.m @@ -50,7 +50,6 @@ static inline NSError* _MakePosixError(int code) { } - (void)dealloc { - DCHECK(_file < 0); unlink([_filePath fileSystemRepresentation]); ARC_RELEASE(_filePath); @@ -58,7 +57,6 @@ static inline NSError* _MakePosixError(int code) { } - (BOOL)open:(NSError**)error { - DCHECK(_file == 0); _file = open([_filePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (_file <= 0) { *error = _MakePosixError(errno); @@ -68,7 +66,6 @@ static inline NSError* _MakePosixError(int code) { } - (BOOL)writeData:(NSData*)data error:(NSError**)error { - DCHECK(_file > 0); if (write(_file, data.bytes, data.length) != (ssize_t)data.length) { *error = _MakePosixError(errno); return NO; @@ -77,10 +74,7 @@ static inline NSError* _MakePosixError(int code) { } - (BOOL)close:(NSError**)error { - DCHECK(_file > 0); - int result = close(_file); - _file = -1; - if (result < 0) { + if (close(_file) < 0) { *error = _MakePosixError(errno); return NO; } diff --git a/CGDWebServer/GCDWebServerFileResponse.m b/CGDWebServer/GCDWebServerFileResponse.m index a23f2e4..01943c1 100644 --- a/CGDWebServer/GCDWebServerFileResponse.m +++ b/CGDWebServer/GCDWebServerFileResponse.m @@ -131,14 +131,12 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) { } - (void)dealloc { - DCHECK(_file <= 0); ARC_RELEASE(_path); ARC_DEALLOC(super); } - (BOOL)open:(NSError**)error { - DCHECK(_file <= 0); _file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY); if (_file <= 0) { *error = _MakePosixError(errno); @@ -147,14 +145,12 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) { if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) { *error = _MakePosixError(errno); close(_file); - _file = 0; return NO; } return YES; } - (NSData*)readData:(NSError**)error { - DCHECK(_file > 0); size_t length = MIN((NSUInteger)kFileReadBufferSize, _size); NSMutableData* data = [[NSMutableData alloc] initWithLength:length]; ssize_t result = read(_file, data.mutableBytes, length); @@ -170,9 +166,7 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) { } - (void)close { - DCHECK(_file > 0); close(_file); - _file = 0; } @end diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.m b/CGDWebServer/GCDWebServerMultiPartFormRequest.m index d376a39..69f031a 100644 --- a/CGDWebServer/GCDWebServerMultiPartFormRequest.m +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.m @@ -205,7 +205,6 @@ static NSData* _dashNewlineData = nil; } - (BOOL)open:(NSError**)error { - DCHECK(_parserData == nil); _parserData = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize]; _parserState = kParserState_Start; return YES; @@ -330,7 +329,6 @@ static NSData* _dashNewlineData = nil; } - (BOOL)writeData:(NSData*)data error:(NSError**)error { - DCHECK(_parserData != nil); [_parserData appendBytes:data.bytes length:data.length]; if (![self _parseData]) { *error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}]; @@ -340,7 +338,6 @@ static NSData* _dashNewlineData = nil; } - (BOOL)close:(NSError**)error { - DCHECK(_parserData != nil); ARC_RELEASE(_parserData); _parserData = nil; ARC_RELEASE(_controlName); @@ -364,7 +361,6 @@ static NSData* _dashNewlineData = nil; } - (void)dealloc { - DCHECK(_parserData == nil); ARC_RELEASE(_arguments); ARC_RELEASE(_files); ARC_RELEASE(_boundary); diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 4f5e0ae..e256052 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -269,6 +269,7 @@ } - (BOOL)performOpen:(NSError**)error { + DCHECK(_type); if (_opened) { DNOT_REACHED(); return NO; @@ -286,10 +287,12 @@ } - (BOOL)performWriteData:(NSData*)data error:(NSError**)error { + DCHECK(_opened); return [_writer writeData:data error:error]; } - (BOOL)performClose:(NSError**)error { + DCHECK(_opened); return [_writer close:error]; } diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 30f819c..63c5ee5 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -222,6 +222,7 @@ } - (BOOL)performOpen:(NSError**)error { + DCHECK(_type); if (_opened) { DNOT_REACHED(); return NO; @@ -239,10 +240,12 @@ } - (NSData*)performReadData:(NSError**)error { + DCHECK(_opened); return [_reader readData:error]; } - (void)performClose { + DCHECK(_opened); [_reader close]; } From 6210564bfc174aa809ddfe910e6d27e20cf876a1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Tue, 8 Apr 2014 23:25:33 -0700 Subject: [PATCH 40/60] Added support for "ETag" and "If-None-Match" headers --- CGDWebServer/GCDWebServerConnection.m | 3 +++ CGDWebServer/GCDWebServerFileResponse.m | 1 + CGDWebServer/GCDWebServerRequest.h | 3 ++- CGDWebServer/GCDWebServerRequest.m | 13 ++++++++----- CGDWebServer/GCDWebServerResponse.h | 1 + CGDWebServer/GCDWebServerResponse.m | 4 +++- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index ad7e436..3cfa5bb 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -411,6 +411,9 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { 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.cacheControlMaxAge > 0) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)_response.cacheControlMaxAge]); } else { diff --git a/CGDWebServer/GCDWebServerFileResponse.m b/CGDWebServer/GCDWebServerFileResponse.m index 01943c1..b390b8e 100644 --- a/CGDWebServer/GCDWebServerFileResponse.m +++ b/CGDWebServer/GCDWebServerFileResponse.m @@ -126,6 +126,7 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) { 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; } diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index 3f97085..782ab44 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/CGDWebServer/GCDWebServerRequest.h @@ -41,7 +41,8 @@ @property(nonatomic, readonly) NSDictionary* query; // May be nil @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* ifModifiedSinceDate; // Automatically parsed from headers (nil if request has no "If-Modified-Since" header or it is malformatted) +@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) @property(nonatomic, readonly) BOOL acceptsGzipContentEncoding; - (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query; diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index e256052..a7477d9 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -145,7 +145,8 @@ NSString* _type; BOOL _chunked; NSUInteger _length; - NSDate* _modifiedSinceDate; + NSDate* _modifiedSince; + NSString* _noneMatch; NSRange _range; BOOL _gzipAccepted; @@ -157,7 +158,7 @@ @implementation GCDWebServerRequest : NSObject -@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, ifModifiedSinceDate=_modifiedSinceDate, +@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; - (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { @@ -191,10 +192,11 @@ _length = NSNotFound; } - NSString* ifModifiedSinceHeader = [_headers objectForKey:@"If-Modified-Since"]; - if (ifModifiedSinceHeader) { - _modifiedSinceDate = [GCDWebServerParseHTTPDate(ifModifiedSinceHeader) copy]; + 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]; @@ -243,6 +245,7 @@ ARC_RELEASE(_query); ARC_RELEASE(_type); ARC_RELEASE(_modifiedSinceDate); + ARC_RELEASE(_noneMatch); ARC_RELEASE(_decoders); ARC_DEALLOC(super); diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index 16a20b3..f0cf708 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -39,6 +39,7 @@ @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; diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 63c5ee5..26bf207 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -157,6 +157,7 @@ NSInteger _status; NSUInteger _maxAge; NSDate* _lastModified; + NSString* _eTag; NSMutableDictionary* _headers; BOOL _chunked; BOOL _gzipped; @@ -169,7 +170,7 @@ @implementation GCDWebServerResponse -@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, lastModifiedDate=_lastModified, +@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, lastModifiedDate=_lastModified, eTag=_eTag, gzipContentEncodingEnabled=_gzipped, additionalHeaders=_headers; + (instancetype)response { @@ -191,6 +192,7 @@ - (void)dealloc { ARC_RELEASE(_type); ARC_RELEASE(_lastModified); + ARC_RELEASE(_eTag); ARC_RELEASE(_headers); ARC_RELEASE(_encoders); From bda3d917cadcd68bb7c7b67e577ea495aa40fb73 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 00:01:12 -0700 Subject: [PATCH 41/60] Automatically handle ETag and Last-Modified-Date caching --- CGDWebServer/GCDWebServerConnection.m | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 3cfa5bb..8fbe90f 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -392,17 +392,41 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatHTTPDate([NSDate date])); } +// 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; +} + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - (void)_processRequest { DCHECK(_responseMessage == NULL); GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock]; if (response) { - NSError* error = nil; - if ([response hasBody] && ![response performOpen:&error]) { - LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error); + 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; + _response = [[GCDWebServerResponse alloc] initWithStatusCode:code]; + _response.cacheControlMaxAge = response.cacheControlMaxAge; + _response.lastModifiedDate = response.lastModifiedDate; + _response.eTag = response.eTag; + DCHECK(_response); } else { - _response = ARC_RETAIN(response); + NSError* error = nil; + if ([response hasBody] && ![response performOpen:&error]) { + LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error); + } else { + _response = ARC_RETAIN(response); + } } } From 62ee560d510d882d6f40d7c324ee47360323efee Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 00:16:38 -0700 Subject: [PATCH 42/60] Added -replaceResponse:forRequest: hook --- CGDWebServer/GCDWebServerConnection.h | 1 + CGDWebServer/GCDWebServerConnection.m | 53 +++++++++++++++------------ 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.h b/CGDWebServer/GCDWebServerConnection.h index 4eee105..0f139c4 100644 --- a/CGDWebServer/GCDWebServerConnection.h +++ b/CGDWebServer/GCDWebServerConnection.h @@ -44,6 +44,7 @@ - (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; // 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 diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 8fbe90f..703f870 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -392,35 +392,14 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatHTTPDate([NSDate date])); } -// 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; -} - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - (void)_processRequest { DCHECK(_responseMessage == NULL); GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock]; if (response) { - 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; - _response = [[GCDWebServerResponse alloc] initWithStatusCode:code]; - _response.cacheControlMaxAge = response.cacheControlMaxAge; - _response.lastModifiedDate = response.lastModifiedDate; - _response.eTag = response.eTag; - DCHECK(_response); - } else { + response = [self replaceResponse:response forRequest:_request]; + if (response) { NSError* error = nil; if ([response hasBody] && ![response performOpen:&error]) { LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error); @@ -695,6 +674,34 @@ 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)); From 157b6830827b021e0595c29631006ae3ea74f360 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 00:53:06 -0700 Subject: [PATCH 43/60] Automatically map HEAD requests to GET ones --- CGDWebServer/GCDWebServerConnection.h | 1 + CGDWebServer/GCDWebServerConnection.m | 23 +++++++++++++++++++---- CGDWebServer/GCDWebServerPrivate.h | 2 ++ CGDWebServer/GCDWebServerRequest.m | 19 +++++++++++-------- CGDWebServer/GCDWebServerResponse.m | 19 +++++++++++-------- 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.h b/CGDWebServer/GCDWebServerConnection.h index 0f139c4..366c5c8 100644 --- a/CGDWebServer/GCDWebServerConnection.h +++ b/CGDWebServer/GCDWebServerConnection.h @@ -40,6 +40,7 @@ @end @interface GCDWebServerConnection (Subclassing) ++ (BOOL)shouldAutomaticallyMapHEADToGET; // Default is YES which means HEAD requests are mapped to GET requests with the response body being discarded - (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 diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 703f870..5a263d1 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -54,6 +54,7 @@ static NSData* _lastChunkData = nil; CFSocketNativeHandle _socket; NSUInteger _bytesRead; NSUInteger _bytesWritten; + BOOL _virtualHEAD; CFHTTPMessageRef _requestMessage; GCDWebServerRequest* _request; @@ -395,13 +396,18 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { // 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) { response = [self replaceResponse:response forRequest:_request]; if (response) { + if ([response hasBody]) { + [response prepareForReading]; + hasBody = !_virtualHEAD; + } NSError* error = nil; - if ([response hasBody] && ![response performOpen:&error]) { + if (hasBody && ![response performOpen:&error]) { LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error); } else { _response = ARC_RETAIN(response); @@ -437,14 +443,14 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _writeHeadersWithCompletionBlock:^(BOOL success) { if (success) { - if ([_response hasBody]) { + if (hasBody) { [self _writeBodyWithCompletionBlock:^(BOOL successInner) { [_response performClose]; // TODO: There's nothing we can do on failure as headers have already been sent }]; } - } else if ([_response hasBody]) { + } else if (hasBody) { [_response performClose]; } @@ -527,6 +533,10 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (extraData) { NSString* requestMethod = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(_requestMessage)); // Method verbs are case-sensitive and uppercase DCHECK(requestMethod); + if ([[self 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 @@ -546,7 +556,8 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } } if (_request) { - if (_request.hasBody) { + if ([_request hasBody]) { + [_request prepareForWriting]; if (_request.usesChunkedTransferEncoding || (extraData.length <= _request.contentLength)) { NSString* expectHeader = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(_requestMessage, CFSTR("Expect"))); if (expectHeader) { @@ -649,6 +660,10 @@ static NSString* _StringFromAddressData(NSData* data) { @implementation GCDWebServerConnection (Subclassing) ++ (BOOL)shouldAutomaticallyMapHEADToGET { + return YES; +} + - (void)open { LOG_DEBUG(@"Did open connection on socket %i", _socket); [self _readRequestHeaders]; diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 5941487..6d94c6f 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -129,6 +129,7 @@ extern NSDate* GCDWebServerParseHTTPDate(NSString* string); @interface GCDWebServerRequest () @property(nonatomic, readonly) BOOL usesChunkedTransferEncoding; +- (void)prepareForWriting; - (BOOL)performOpen:(NSError**)error; - (BOOL)performWriteData:(NSData*)data error:(NSError**)error; - (BOOL)performClose:(NSError**)error; @@ -137,6 +138,7 @@ extern NSDate* GCDWebServerParseHTTPDate(NSString* string); @interface GCDWebServerResponse () @property(nonatomic, readonly) NSDictionary* additionalHeaders; @property(nonatomic, readonly) BOOL usesChunkedTransferEncoding; +- (void)prepareForReading; - (BOOL)performOpen:(NSError**)error; - (NSData*)performReadData:(NSError**)error; - (void)performClose; diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index a7477d9..e44fb72 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -271,14 +271,7 @@ return YES; } -- (BOOL)performOpen:(NSError**)error { - DCHECK(_type); - if (_opened) { - DNOT_REACHED(); - return NO; - } - _opened = YES; - +- (void)prepareForWriting { _writer = self; if ([[[self.headers objectForKey:@"Content-Encoding"] lowercaseString] isEqualToString:@"gzip"]) { GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer]; @@ -286,6 +279,16 @@ ARC_RELEASE(decoder); _writer = decoder; } +} + +- (BOOL)performOpen:(NSError**)error { + DCHECK(_type); + DCHECK(_writer); + if (_opened) { + DNOT_REACHED(); + return NO; + } + _opened = YES; return [_writer open:error]; } diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 26bf207..25714c6 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -223,14 +223,7 @@ ; } -- (BOOL)performOpen:(NSError**)error { - DCHECK(_type); - if (_opened) { - DNOT_REACHED(); - return NO; - } - _opened = YES; - +- (void)prepareForReading { _reader = self; if (_gzipped) { GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader]; @@ -238,6 +231,16 @@ ARC_RELEASE(encoder); _reader = encoder; } +} + +- (BOOL)performOpen:(NSError**)error { + DCHECK(_type); + DCHECK(_reader); + if (_opened) { + DNOT_REACHED(); + return NO; + } + _opened = YES; return [_reader open:error]; } From b494e404427047cbcdd72c0e6f5f34cc52925c96 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 01:26:07 -0700 Subject: [PATCH 44/60] Fix --- CGDWebServer/GCDWebServerRequest.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index e44fb72..ca7aeee 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -183,8 +183,13 @@ if (_type == nil) { _type = kGCDWebServerDefaultMimeType; } + } else if (_chunked) { + if (_type == nil) { + _type = kGCDWebServerDefaultMimeType; + } + _length = NSNotFound; } else { - if (_type && !_chunked) { + if (_type) { DNOT_REACHED(); ARC_RELEASE(self); return nil; From d5811fe6df01288bf52be7024abba3d20d497ffa Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 01:37:15 -0700 Subject: [PATCH 45/60] Added compatibility with OS X Finder for WebDAV --- GCDWebDAVServer/GCDWebDAVServer.h | 1 + GCDWebDAVServer/GCDWebDAVServer.m | 189 ++++++++++++++++++++++++------ Mac/main.m | 2 +- README.md | 6 +- 4 files changed, 158 insertions(+), 40 deletions(-) diff --git a/GCDWebDAVServer/GCDWebDAVServer.h b/GCDWebDAVServer/GCDWebDAVServer.h index 2dff7c2..59860c0 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.h +++ b/GCDWebDAVServer/GCDWebDAVServer.h @@ -47,6 +47,7 @@ @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; +- (instancetype)initWithUploadDirectory:(NSString*)path macFinderMode:(BOOL)macFinderMode; // If Mac Finder mode is ON, WebDAV server can be mounted read-write instead of read-only in OS X Finder @end @interface GCDWebDAVServer (Subclassing) diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m index 24a2d42..1bd58ef 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.m +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -51,6 +51,7 @@ typedef NS_ENUM(NSInteger, DAVProperties) { @interface GCDWebDAVServer () { @private NSString* _uploadDirectory; + BOOL _macMode; id __unsafe_unretained _delegate; NSArray* _allowedExtensions; BOOL _showHidden; @@ -66,29 +67,17 @@ typedef NS_ENUM(NSInteger, DAVProperties) { return YES; } -- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request { - GCDWebServerResponse* response = [GCDWebServerResponse response]; - [response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1 - return response; +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*)performHEAD:(GCDWebServerRequest*)request { - NSString* relativePath = request.path; - NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - - NSError* error = nil; - NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:absolutePath error:&error]; - if (!attributes) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound underlyingError:error message:@"Failed retrieving attributes for \"%@\"", relativePath]; - } - +- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request { GCDWebServerResponse* response = [GCDWebServerResponse response]; - if ([[attributes fileType] isEqualToString:NSFileTypeRegular]) { - [response setValue:GCDWebServerGetMimeTypeForExtension([absolutePath pathExtension]) forAdditionalHeader:@"Content-Type"]; - [response setValue:[NSString stringWithFormat:@"%llu", [attributes fileSize]] forAdditionalHeader:@"Content-Length"]; + if (_macMode && _IsMacFinder(request)) { + [response setValue:@"1, 2" forAdditionalHeader:@"DAV"]; // Classes 1 and 2 + } else { + [response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1 } return response; } @@ -374,6 +363,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name 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"); @@ -398,13 +388,18 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name node = node->next; } } else { - NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding]; - [self logError:@"Invalid DAV properties\n%@", string]; -#if !__has_feature(objc_arc) - [string release]; -#endif + 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; @@ -412,14 +407,18 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name NSString* relativePath = request.path; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + BOOL isDirectory = NO; + if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; } - NSError* error = nil; - NSArray* items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error]; - if (items == nil) { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath]; + 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:@""]; @@ -446,6 +445,114 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name return response; } +- (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request { + if (!_macMode || !_IsMacFinder(request)) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"]; + } + + NSString* relativePath = request.path; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + 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]; + } + + 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:@""]; + [xmlString appendString:@"\n"]; + [xmlString appendString:@"\n\n"]; + [xmlString appendFormat:@"\n", type]; + [xmlString appendFormat:@"\n", scope]; + [xmlString appendFormat:@"%@\n", depthHeader]; + if (owner) { + [xmlString appendFormat:@"%@\n", owner]; + } + if (timeoutHeader) { + [xmlString appendFormat:@"%@\n", timeoutHeader]; + } + [xmlString appendFormat:@"%@\n", token]; + NSString* lockroot = [@"http://" stringByAppendingString:[[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]]; + [xmlString appendFormat:@"%@\n", lockroot]; + [xmlString appendString:@"\n\n"]; + [xmlString appendString:@""]; + + [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 (!_macMode || !_IsMacFinder(request)) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"]; + } + + NSString* relativePath = request.path; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + 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"]; + } + + [self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath]; + return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent]; +} + @end @implementation GCDWebDAVServer @@ -453,8 +560,13 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name @synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden; - (instancetype)initWithUploadDirectory:(NSString*)path { + return [self initWithUploadDirectory:path macFinderMode:NO]; +} + +- (instancetype)initWithUploadDirectory:(NSString*)path macFinderMode:(BOOL)macFinderMode { if ((self = [super init])) { _uploadDirectory = [[path stringByStandardizingPath] copy]; + _macMode = macFinderMode; GCDWebDAVServer* __unsafe_unretained server = self; // 9.1 PROPFIND method @@ -467,12 +579,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name return [server performMKCOL:(GCDWebServerDataRequest*)request]; }]; - // 9.4 HEAD method - [self addDefaultHandlerForMethod:@"HEAD" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) { - return [server performHEAD:request]; - }]; - - // 9.4 GET method + // 9.4 GET & HEAD methods [self addDefaultHandlerForMethod:@"GET" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) { return [server performGET:request]; }]; @@ -497,6 +604,16 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name 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]; diff --git a/Mac/main.m b/Mac/main.m index 9a39b23..99efce8 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -97,7 +97,7 @@ int main(int argc, const char* argv[]) { } case 3: { - webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]]; + webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath] macFinderMode:YES]; break; } diff --git a/README.md b/README.md index 92f09b1..dae3f7e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Extra built-in features: 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 +* [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) @@ -87,7 +87,7 @@ 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 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: @@ -249,6 +249,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 lets users upload, download and organize comic files inside the app using their web browser directly over WiFi. +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. From d2c0d6da2be3cb148f523d9eb73b32c4740d9712 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 01:40:16 -0700 Subject: [PATCH 46/60] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dae3f7e..a0c0dbc 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,9 @@ 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). +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: From 9f0544b4494016a73103a2daf3ad40a1359d5363 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 01:41:54 -0700 Subject: [PATCH 47/60] Removed Mac Finder mode --- GCDWebDAVServer/GCDWebDAVServer.h | 1 - GCDWebDAVServer/GCDWebDAVServer.m | 12 +++--------- Mac/main.m | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/GCDWebDAVServer/GCDWebDAVServer.h b/GCDWebDAVServer/GCDWebDAVServer.h index 59860c0..2dff7c2 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.h +++ b/GCDWebDAVServer/GCDWebDAVServer.h @@ -47,7 +47,6 @@ @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; -- (instancetype)initWithUploadDirectory:(NSString*)path macFinderMode:(BOOL)macFinderMode; // If Mac Finder mode is ON, WebDAV server can be mounted read-write instead of read-only in OS X Finder @end @interface GCDWebDAVServer (Subclassing) diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m index 1bd58ef..45003c3 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.m +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -51,7 +51,6 @@ typedef NS_ENUM(NSInteger, DAVProperties) { @interface GCDWebDAVServer () { @private NSString* _uploadDirectory; - BOOL _macMode; id __unsafe_unretained _delegate; NSArray* _allowedExtensions; BOOL _showHidden; @@ -74,7 +73,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { - (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request { GCDWebServerResponse* response = [GCDWebServerResponse response]; - if (_macMode && _IsMacFinder(request)) { + if (_IsMacFinder(request)) { [response setValue:@"1, 2" forAdditionalHeader:@"DAV"]; // Classes 1 and 2 } else { [response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1 @@ -446,7 +445,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name } - (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request { - if (!_macMode || !_IsMacFinder(request)) { + if (!_IsMacFinder(request)) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"]; } @@ -534,7 +533,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name } - (GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request { - if (!_macMode || !_IsMacFinder(request)) { + if (!_IsMacFinder(request)) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"]; } @@ -560,13 +559,8 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name @synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden; - (instancetype)initWithUploadDirectory:(NSString*)path { - return [self initWithUploadDirectory:path macFinderMode:NO]; -} - -- (instancetype)initWithUploadDirectory:(NSString*)path macFinderMode:(BOOL)macFinderMode { if ((self = [super init])) { _uploadDirectory = [[path stringByStandardizingPath] copy]; - _macMode = macFinderMode; GCDWebDAVServer* __unsafe_unretained server = self; // 9.1 PROPFIND method diff --git a/Mac/main.m b/Mac/main.m index 99efce8..9a39b23 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -97,7 +97,7 @@ int main(int argc, const char* argv[]) { } case 3: { - webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath] macFinderMode:YES]; + webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]]; break; } From e5550bf290fd5c43c06cdfb7aed3efe2f4eaa938 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 01:42:52 -0700 Subject: [PATCH 48/60] Fix non-ARC build failure --- CGDWebServer/GCDWebServerRequest.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index ca7aeee..827c0cd 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -249,7 +249,7 @@ ARC_RELEASE(_path); ARC_RELEASE(_query); ARC_RELEASE(_type); - ARC_RELEASE(_modifiedSinceDate); + ARC_RELEASE(_modifiedSince); ARC_RELEASE(_noneMatch); ARC_RELEASE(_decoders); From f14dda522c23f2fecb915b6044155193efc01786 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 01:56:15 -0700 Subject: [PATCH 49/60] Update README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index a0c0dbc..6031e3f 100644 --- a/README.md +++ b/README.md @@ -31,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 =========== From 811e45ab2662ce5a1351d931f6f36ac4a622cdab Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 10:34:33 -0700 Subject: [PATCH 50/60] Properly handle casing of header values --- CGDWebServer/GCDWebServer.m | 45 +++++++++++++------ CGDWebServer/GCDWebServerConnection.m | 2 +- CGDWebServer/GCDWebServerDataRequest.m | 2 +- .../GCDWebServerMultiPartFormRequest.h | 4 +- .../GCDWebServerMultiPartFormRequest.m | 29 ++++++------ CGDWebServer/GCDWebServerPrivate.h | 4 +- CGDWebServer/GCDWebServerRequest.m | 8 ++-- .../GCDWebServerURLEncodedFormRequest.m | 2 +- 8 files changed, 57 insertions(+), 39 deletions(-) diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index cc8bb3e..a60535d 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -90,24 +90,42 @@ void GCDLogMessage(long level, NSString* format, ...) { #endif -NSString* GCDWebServerExtractHeaderParameter(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]; - } +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]; } - ARC_RELEASE(scanner); } 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:¶meter]; + } else { + [scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:¶meter]; + } + } + ARC_RELEASE(scanner); + return parameter; +} + // http://www.w3schools.com/tags/ref_charactersets.asp NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset) { NSStringEncoding encoding = kCFStringEncodingInvalidId; @@ -163,6 +181,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]; diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 5a263d1..9a08b7e 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -429,7 +429,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache")); } if (_response.contentType != nil) { - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)[_response.contentType lowercaseString]); + 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]); diff --git a/CGDWebServer/GCDWebServerDataRequest.m b/CGDWebServer/GCDWebServerDataRequest.m index ff29f20..0e9955b 100644 --- a/CGDWebServer/GCDWebServerDataRequest.m +++ b/CGDWebServer/GCDWebServerDataRequest.m @@ -77,7 +77,7 @@ - (NSString*)text { if (_text == nil) { if ([self.contentType hasPrefix:@"text/"]) { - NSString* charset = GCDWebServerExtractHeaderParameter(self.contentType, @"charset"); + NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset"); _text = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)]; } else { DNOT_REACHED(); diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.h b/CGDWebServer/GCDWebServerMultiPartFormRequest.h index e318d67..b09a169 100644 --- a/CGDWebServer/GCDWebServerMultiPartFormRequest.h +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.h @@ -28,8 +28,8 @@ #import "GCDWebServerRequest.h" @interface GCDWebServerMultiPart : NSObject -@property(nonatomic, readonly) NSString* contentType; // May be nil -@property(nonatomic, readonly) NSString* mimeType; // Defaults to "text/plain" per specifications if undefined +@property(nonatomic, readonly) NSString* contentType; // Defaults to "text/plain" per specifications if undefined +@property(nonatomic, readonly) NSString* mimeType; @end @interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.m b/CGDWebServer/GCDWebServerMultiPartFormRequest.m index 69f031a..2659be6 100644 --- a/CGDWebServer/GCDWebServerMultiPartFormRequest.m +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.m @@ -55,13 +55,7 @@ static NSData* _dashNewlineData = nil; - (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"; - } + _mimeType = ARC_RETAIN(GCDWebServerTruncateHeaderValue(_contentType)); } return self; } @@ -90,8 +84,8 @@ static NSData* _dashNewlineData = nil; if ((self = [super initWithContentType:contentType])) { _data = ARC_RETAIN(data); - if ([self.mimeType hasPrefix:@"text/"]) { - NSString* charset = GCDWebServerExtractHeaderParameter(self.contentType, @"charset"); + if ([self.contentType hasPrefix:@"text/"]) { + NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset"); _string = [[NSString alloc] initWithData:_data encoding:GCDWebServerStringEncodingFromCharset(charset)]; } } @@ -187,7 +181,7 @@ static NSData* _dashNewlineData = nil; - (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 = GCDWebServerExtractHeaderParameter(self.contentType, @"boundary"); + NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary"); if (boundary) { NSData* data = [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding]; _boundary = ARC_RETAIN(data); @@ -210,7 +204,7 @@ static NSData* _dashNewlineData = nil; return YES; } -// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4 +// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 - (BOOL)_parseData { BOOL success = YES; @@ -234,14 +228,17 @@ static NSData* _dashNewlineData = nil; 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 = GCDWebServerExtractHeaderParameter(contentDisposition, @"name"); - fileName = GCDWebServerExtractHeaderParameter(contentDisposition, @"filename"); + 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([headers objectForKey:@"Content-Type"]); + _contentType = ARC_RETAIN(GCDWebServerNormalizeHeaderValue([headers objectForKey:@"Content-Type"])); + if (_contentType == nil) { + _contentType = @"text/plain"; + } } CFRelease(message); if (_controlName) { diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 6d94c6f..d25c819 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -108,7 +108,9 @@ static inline BOOL GCDWebServerIsValidByteRange(NSRange range) { return ((range.location != NSNotFound) || (range.length > 0)); } -extern NSString* GCDWebServerExtractHeaderParameter(NSString* header, NSString* attribute); +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); diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 827c0cd..72bd41d 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -169,8 +169,8 @@ _path = [path copy]; _query = ARC_RETAIN(query); - _type = ARC_RETAIN([[_headers objectForKey:@"Content-Type"] lowercaseString]); - _chunked = [[[_headers objectForKey:@"Transfer-Encoding"] lowercaseString] isEqualToString:@"chunked"]; + _type = ARC_RETAIN(GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"])); + _chunked = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"]; NSString* lengthHeader = [_headers objectForKey:@"Content-Length"]; if (lengthHeader) { NSInteger length = [lengthHeader integerValue]; @@ -204,7 +204,7 @@ _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:@","]; @@ -278,7 +278,7 @@ - (void)prepareForWriting { _writer = self; - if ([[[self.headers objectForKey:@"Content-Encoding"] lowercaseString] isEqualToString:@"gzip"]) { + if ([GCDWebServerNormalizeHeaderValue([self.headers objectForKey:@"Content-Encoding"]) isEqualToString:@"gzip"]) { GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer]; [_decoders addObject:decoder]; ARC_RELEASE(decoder); diff --git a/CGDWebServer/GCDWebServerURLEncodedFormRequest.m b/CGDWebServer/GCDWebServerURLEncodedFormRequest.m index 7962e54..31207c4 100644 --- a/CGDWebServer/GCDWebServerURLEncodedFormRequest.m +++ b/CGDWebServer/GCDWebServerURLEncodedFormRequest.m @@ -52,7 +52,7 @@ return NO; } - NSString* charset = GCDWebServerExtractHeaderParameter(self.contentType, @"charset"); + NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset"); NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)]; _arguments = ARC_RETAIN(GCDWebServerParseURLEncodedForm(string)); DCHECK(_arguments); From efad06f50649a47cf7f316b40f87ab1b8910ed0c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 11:10:45 -0700 Subject: [PATCH 51/60] __unsafe_unretained does not prevent self retain-cycles when not under ARC --- CGDWebServer/GCDWebServer.m | 4 ++++ GCDWebDAVServer/GCDWebDAVServer.m | 4 ++++ GCDWebUploader/GCDWebUploader.m | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index a60535d..c5e5872 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -663,7 +663,11 @@ 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) GCDWebServer* __unsafe_unretained server = self; +#else + __block GCDWebServer* server = self; +#endif [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { if (![requestMethod isEqualToString:@"GET"]) { diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m index 45003c3..7d46156 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.m +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -561,7 +561,11 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name - (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) { diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index 147b5b6..1f28c74 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -277,7 +277,11 @@ return nil; } _uploadDirectory = [[path stringByStandardizingPath] copy]; +#if __has_feature(objc_arc) GCDWebUploader* __unsafe_unretained server = self; +#else + __block GCDWebUploader* server = self; +#endif // Resource files [self addGETHandlerForBasePath:@"/" directoryPath:[siteBundle resourcePath] indexFilename:nil cacheAge:3600 allowRangeRequests:NO]; From 6f90a3e6ced06c8e68c2590dcb87e91548f8b891 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 12:20:37 -0700 Subject: [PATCH 52/60] Log real request method --- CGDWebServer/GCDWebServerConnection.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 9a08b7e..82f3d1b 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -678,7 +678,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)_bytesRead); + 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); @@ -731,9 +731,10 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET 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, _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten); + LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten); } @end From 30eb01ca6f94e4ec7a4693d5d4b1c0be5a29a6df Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 13:38:30 -0700 Subject: [PATCH 53/60] Fixed memory corruption --- CGDWebServer/GCDWebServerMultiPartFormRequest.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.m b/CGDWebServer/GCDWebServerMultiPartFormRequest.m index 2659be6..39b365a 100644 --- a/CGDWebServer/GCDWebServerMultiPartFormRequest.m +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.m @@ -292,7 +292,7 @@ static NSData* _dashNewlineData = nil; ARC_RELEASE(_tmpPath); _tmpPath = nil; } else { - NSData* data = [[NSData alloc] initWithBytesNoCopy:(void*)dataBytes length:dataLength freeWhenDone:NO]; + 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); From e49b9219ea52bf7681d63821732fc29f5f9e43ab Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 13:46:43 -0700 Subject: [PATCH 54/60] Renamed "filePath" to "temporaryPath" --- CGDWebServer/GCDWebServerFileRequest.h | 2 +- CGDWebServer/GCDWebServerFileRequest.m | 18 ++++++++++++------ GCDWebDAVServer/GCDWebDAVServer.m | 6 +++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CGDWebServer/GCDWebServerFileRequest.h b/CGDWebServer/GCDWebServerFileRequest.h index b511e2b..94cb4c2 100644 --- a/CGDWebServer/GCDWebServerFileRequest.h +++ b/CGDWebServer/GCDWebServerFileRequest.h @@ -28,5 +28,5 @@ #import "GCDWebServerRequest.h" @interface GCDWebServerFileRequest : GCDWebServerRequest -@property(nonatomic, readonly) NSString* filePath; +@property(nonatomic, readonly) NSString* temporaryPath; @end diff --git a/CGDWebServer/GCDWebServerFileRequest.m b/CGDWebServer/GCDWebServerFileRequest.m index 7b657f2..466d0e5 100644 --- a/CGDWebServer/GCDWebServerFileRequest.m +++ b/CGDWebServer/GCDWebServerFileRequest.m @@ -29,7 +29,7 @@ @interface GCDWebServerFileRequest () { @private - NSString* _filePath; + NSString* _temporaryPath; int _file; } @end @@ -40,24 +40,24 @@ static inline NSError* _MakePosixError(int code) { @implementation GCDWebServerFileRequest -@synthesize filePath=_filePath; +@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])) { - _filePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]); + _temporaryPath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]); } return self; } - (void)dealloc { - unlink([_filePath fileSystemRepresentation]); - ARC_RELEASE(_filePath); + unlink([_temporaryPath fileSystemRepresentation]); + ARC_RELEASE(_temporaryPath); ARC_DEALLOC(super); } - (BOOL)open:(NSError**)error { - _file = open([_filePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + _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; @@ -81,4 +81,10 @@ static inline NSError* _MakePosixError(int code) { return YES; } +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + [description appendFormat:@"\n\n{%@}", _temporaryPath]; + return description; +} + @end diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m index 7d46156..5d8e6c2 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.m +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -125,13 +125,13 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", fileName]; } - if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.filePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not allowed", relativePath]; + 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.filePath toPath:absolutePath error:&error]) { + if (![[NSFileManager defaultManager] moveItemAtPath:request.temporaryPath toPath:absolutePath error:&error]) { return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath]; } From 4008b5b4760003435abcc084481d7f6de2d0c6b9 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 13:43:33 -0700 Subject: [PATCH 55/60] Only set "Cache-Control" on successful responses --- CGDWebServer/GCDWebServerConnection.m | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 82f3d1b..5ae3b37 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -423,10 +423,12 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (_response.eTag) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("ETag"), (ARC_BRIDGE CFStringRef)_response.eTag); } - 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.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)GCDWebServerNormalizeHeaderValue(_response.contentType)); From 97929f7d89ade6b5fc9ad8c01b770b637c2418aa Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 13:44:53 -0700 Subject: [PATCH 56/60] Added -description methods --- CGDWebServer/GCDWebServer.m | 11 ++++++ CGDWebServer/GCDWebServerDataRequest.m | 9 +++++ CGDWebServer/GCDWebServerDataResponse.m | 7 ++++ CGDWebServer/GCDWebServerFileResponse.m | 6 ++++ .../GCDWebServerMultiPartFormRequest.m | 34 +++++++++++++++---- CGDWebServer/GCDWebServerPrivate.h | 1 + CGDWebServer/GCDWebServerRequest.m | 12 +++++++ CGDWebServer/GCDWebServerResponse.m | 24 +++++++++++++ CGDWebServer/GCDWebServerStreamingResponse.m | 6 ++++ .../GCDWebServerURLEncodedFormRequest.m | 9 +++++ 10 files changed, 112 insertions(+), 7 deletions(-) diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index c5e5872..445ab78 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -151,6 +151,17 @@ NSDate* GCDWebServerParseHTTPDate(NSString* string) { 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) { diff --git a/CGDWebServer/GCDWebServerDataRequest.m b/CGDWebServer/GCDWebServerDataRequest.m index 0e9955b..c3836de 100644 --- a/CGDWebServer/GCDWebServerDataRequest.m +++ b/CGDWebServer/GCDWebServerDataRequest.m @@ -70,6 +70,15 @@ 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) diff --git a/CGDWebServer/GCDWebServerDataResponse.m b/CGDWebServer/GCDWebServerDataResponse.m index d7f292f..fb53106 100644 --- a/CGDWebServer/GCDWebServerDataResponse.m +++ b/CGDWebServer/GCDWebServerDataResponse.m @@ -73,6 +73,13 @@ 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) diff --git a/CGDWebServer/GCDWebServerFileResponse.m b/CGDWebServer/GCDWebServerFileResponse.m index b390b8e..314b587 100644 --- a/CGDWebServer/GCDWebServerFileResponse.m +++ b/CGDWebServer/GCDWebServerFileResponse.m @@ -170,4 +170,10 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) { close(_file); } +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + [description appendFormat:@"\n\n{%@}", _path]; + return description; +} + @end diff --git a/CGDWebServer/GCDWebServerMultiPartFormRequest.m b/CGDWebServer/GCDWebServerMultiPartFormRequest.m index 39b365a..2cb4a6f 100644 --- a/CGDWebServer/GCDWebServerMultiPartFormRequest.m +++ b/CGDWebServer/GCDWebServerMultiPartFormRequest.m @@ -100,7 +100,7 @@ static NSData* _dashNewlineData = nil; } - (NSString*)description { - return [NSString stringWithFormat:@"<%@ | '%@' | %i bytes>", [self class], self.mimeType, (int)_data.length]; + return [NSString stringWithFormat:@"<%@ | '%@' | %lu bytes>", [self class], self.mimeType, (unsigned long)_data.length]; } @end @@ -198,6 +198,14 @@ static NSData* _dashNewlineData = nil; 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; @@ -357,12 +365,24 @@ static NSData* _dashNewlineData = nil; return YES; } -- (void)dealloc { - ARC_RELEASE(_arguments); - ARC_RELEASE(_files); - ARC_RELEASE(_boundary); - - ARC_DEALLOC(super); +- (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 diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index d25c819..e9a0902 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -114,6 +114,7 @@ extern NSString* GCDWebServerExtractHeaderValueParameter(NSString* header, NSStr 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; diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 72bd41d..3df02f3 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -307,4 +307,16 @@ return [_writer close:error]; } +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path]; + for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [description appendFormat:@"\n %@ = %@", argument, [_query objectForKey:argument]]; + } + [description appendString:@"\n"]; + for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]]; + } + return description; +} + @end diff --git a/CGDWebServer/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 25714c6..ce68cc7 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -254,6 +254,30 @@ [_reader close]; } +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_status]; + if (_type) { + [description appendFormat:@"\nContent Type = %@", _type]; + } + if (_length != NSNotFound) { + [description appendFormat:@"\nContent Length = %lu", (unsigned long)_length]; + } + [description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge]; + if (_lastModified) { + [description appendFormat:@"\nLast Modified Date = %@", _lastModified]; + } + 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]]; + } + } + return description; +} + @end @implementation GCDWebServerResponse (Extensions) diff --git a/CGDWebServer/GCDWebServerStreamingResponse.m b/CGDWebServer/GCDWebServerStreamingResponse.m index fd79a8a..4ed68d4 100644 --- a/CGDWebServer/GCDWebServerStreamingResponse.m +++ b/CGDWebServer/GCDWebServerStreamingResponse.m @@ -58,4 +58,10 @@ return _block(error); } +- (NSString*)description { + NSMutableString* description = [NSMutableString stringWithString:[super description]]; + [description appendString:@"\n\n"]; + return description; +} + @end diff --git a/CGDWebServer/GCDWebServerURLEncodedFormRequest.m b/CGDWebServer/GCDWebServerURLEncodedFormRequest.m index 31207c4..c65747d 100644 --- a/CGDWebServer/GCDWebServerURLEncodedFormRequest.m +++ b/CGDWebServer/GCDWebServerURLEncodedFormRequest.m @@ -61,4 +61,13 @@ 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 From d383845fcc0b3cb99d09e6195fd6ca3763144863 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 13:45:15 -0700 Subject: [PATCH 57/60] Cleaned up file servers error handling --- GCDWebDAVServer/GCDWebDAVServer.m | 54 +++++++++++++++++++++++-------- GCDWebUploader/GCDWebUploader.m | 52 ++++++++++++++++------------- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m index 5d8e6c2..a78d291 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.m +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -84,7 +84,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { - (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request { NSString* relativePath = request.path; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - BOOL isDirectory = YES; + BOOL isDirectory = NO; if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; } @@ -92,6 +92,11 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a file", 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(davServer:didDownloadFileAtPath:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [_delegate davServer:self didDownloadFileAtPath:absolutePath]; @@ -122,7 +127,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { NSString* fileName = [absolutePath lastPathComponent]; if (([fileName hasPrefix:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", fileName]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName]; } if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.temporaryPath]) { @@ -151,12 +156,18 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { NSString* relativePath = request.path; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - if (![absolutePath hasPrefix:_uploadDirectory]) { + 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 allowed", relativePath]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath]; } NSError* error = nil; @@ -189,11 +200,11 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { NSString* directoryName = [absolutePath lastPathComponent]; if (!_showHidden && [directoryName hasPrefix:@"."]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Directory name \"%@\" is not allowed", directoryName]; + 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 allowed", relativePath]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath]; } NSError* error = nil; @@ -239,9 +250,9 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Invalid destination \"%@\"", dstRelativePath]; } - NSString* fileName = [dstAbsolutePath lastPathComponent]; - if ((!_showHidden && [fileName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:fileName])) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Destination name \"%@\" is not allowed", fileName]; + 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"]; @@ -252,11 +263,11 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { if (isMove) { if (![self shouldMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not allowed", srcRelativePath, dstRelativePath]; + 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 allowed", srcRelativePath, dstRelativePath]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath]; } } @@ -411,6 +422,11 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name 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; @@ -451,7 +467,8 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name NSString* relativePath = request.path; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + BOOL isDirectory = NO; + if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; } @@ -500,6 +517,11 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name 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); @@ -539,7 +561,8 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name NSString* relativePath = request.path; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + BOOL isDirectory = NO; + if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; } @@ -548,6 +571,11 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name 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]; } diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index 1f28c74..466c041 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -94,6 +94,11 @@ 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) { @@ -133,6 +138,11 @@ 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]; @@ -153,7 +163,7 @@ 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 allowed", file.fileName, relativePath]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath]; } NSError* error = nil; @@ -179,22 +189,16 @@ NSString* newRelativePath = [request.arguments objectForKey:@"newPath"]; NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]]; - if (!_showHidden) { - for (NSString* component in [newRelativePath pathComponents]) { - if ([component hasPrefix:@"."]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Item path \"%@\" is not allowed", newRelativePath]; - } - } - } - if (!isDirectory && ![self _checkFileExtension:newRelativePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Item path \"%@\" is not allowed", 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 allowed", oldRelativePath, newRelativePath]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", oldRelativePath, newRelativePath]; } - [[NSFileManager defaultManager] createDirectoryAtPath:[newAbsolutePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; 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]; @@ -211,12 +215,18 @@ - (GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request { NSString* relativePath = [request.arguments objectForKey:@"path"]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) { + 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 allowed", relativePath]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath]; } NSError* error = nil; @@ -235,20 +245,18 @@ - (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request { NSString* relativePath = [request.arguments objectForKey:@"path"]; NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]]; - if (!_showHidden) { - for (NSString* component in [relativePath pathComponents]) { - if ([component hasPrefix:@"."]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Directory path \"%@\" is not allowed", 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 allowed", relativePath]; + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath]; } NSError* error = nil; - if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:YES attributes:nil error:&error]) { + if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) { return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath]; } From fcea9cad44b94a33fa8876b24ea79fcec02fa36a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 13:53:46 -0700 Subject: [PATCH 58/60] Moved +shouldAutomaticallyMapHEADToGET to GCDWebServer class --- CGDWebServer/GCDWebServer.h | 1 + CGDWebServer/GCDWebServer.m | 4 ++++ CGDWebServer/GCDWebServerConnection.h | 1 - CGDWebServer/GCDWebServerConnection.m | 6 +----- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CGDWebServer/GCDWebServer.h b/CGDWebServer/GCDWebServer.h index 2570b81..32e03ee 100644 --- a/CGDWebServer/GCDWebServer.h +++ b/CGDWebServer/GCDWebServer.h @@ -63,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) diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 445ab78..5705c25 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -510,6 +510,10 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er return NSStringFromClass(self); } ++ (BOOL)shouldAutomaticallyMapHEADToGET { + return YES; +} + @end @implementation GCDWebServer (Extensions) diff --git a/CGDWebServer/GCDWebServerConnection.h b/CGDWebServer/GCDWebServerConnection.h index 366c5c8..0f139c4 100644 --- a/CGDWebServer/GCDWebServerConnection.h +++ b/CGDWebServer/GCDWebServerConnection.h @@ -40,7 +40,6 @@ @end @interface GCDWebServerConnection (Subclassing) -+ (BOOL)shouldAutomaticallyMapHEADToGET; // Default is YES which means HEAD requests are mapped to GET requests with the response body being discarded - (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 diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 5ae3b37..7798598 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -535,7 +535,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (extraData) { NSString* requestMethod = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(_requestMessage)); // Method verbs are case-sensitive and uppercase DCHECK(requestMethod); - if ([[self class] shouldAutomaticallyMapHEADToGET] && [requestMethod isEqualToString:@"HEAD"]) { + if ([[_server class] shouldAutomaticallyMapHEADToGET] && [requestMethod isEqualToString:@"HEAD"]) { requestMethod = @"GET"; _virtualHEAD = YES; } @@ -662,10 +662,6 @@ static NSString* _StringFromAddressData(NSData* data) { @implementation GCDWebServerConnection (Subclassing) -+ (BOOL)shouldAutomaticallyMapHEADToGET { - return YES; -} - - (void)open { LOG_DEBUG(@"Did open connection on socket %i", _socket); [self _readRequestHeaders]; From d78aa3baae59336877444a928e4aa8a5c10190a1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 13:59:20 -0700 Subject: [PATCH 59/60] Allow HEAD requests on collections --- GCDWebDAVServer/GCDWebDAVServer.m | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m index a78d291..12622d4 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.m +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -88,13 +88,15 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) { if (![absolutePath hasPrefix:_uploadDirectory] || ![[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 file", 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]; } - NSString* fileName = [absolutePath lastPathComponent]; - if (([fileName hasPrefix:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading file name \"%@\" is not allowed", fileName]; + // 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:)]) { From 2ecbfea72f92acc9a4a717f1bd7b8f2ad2cb27d8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 9 Apr 2014 14:02:58 -0700 Subject: [PATCH 60/60] Fix --- CGDWebServer/GCDWebServerRequest.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 3df02f3..9f454b7 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -110,7 +110,7 @@ if ((result != Z_OK) && (result != Z_STREAM_END)) { ARC_RELEASE(decodedData); *error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil]; - return nil; + return NO; } length += maxLength - _stream.avail_out; if (_stream.avail_out > 0) {