|
@@ -26,47 +26,208 @@
|
|
|
*/
|
|
|
|
|
|
#import <sys/stat.h>
|
|
|
+#import <zlib.h>
|
|
|
|
|
|
#import "GCDWebServerPrivate.h"
|
|
|
|
|
|
-@interface GCDWebServerResponse () {
|
|
|
+#define kZlibErrorDomain @"ZlibErrorDomain"
|
|
|
+#define kGZipInitialBufferSize (256 * 1024)
|
|
|
+#define kFileReadBufferSize (32 * 1024)
|
|
|
+
|
|
|
+@interface GCDWebServerBodyEncoder : NSObject <GCDWebServerBodyReader>
|
|
|
+- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader;
|
|
|
+@end
|
|
|
+
|
|
|
+@interface GCDWebServerChunkEncoder : GCDWebServerBodyEncoder
|
|
|
+@end
|
|
|
+
|
|
|
+@interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder
|
|
|
+@end
|
|
|
+
|
|
|
+@interface GCDWebServerBodyEncoder () {
|
|
|
@private
|
|
|
- NSString* _type;
|
|
|
- NSUInteger _length;
|
|
|
- NSInteger _status;
|
|
|
- NSUInteger _maxAge;
|
|
|
- NSMutableDictionary* _headers;
|
|
|
+ GCDWebServerResponse* __unsafe_unretained _response;
|
|
|
+ id<GCDWebServerBodyReader> __unsafe_unretained _reader;
|
|
|
}
|
|
|
@end
|
|
|
|
|
|
-@interface GCDWebServerDataResponse () {
|
|
|
+@implementation GCDWebServerBodyEncoder
|
|
|
+
|
|
|
+- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)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
|
|
|
- NSData* _data;
|
|
|
- NSInteger _offset;
|
|
|
+ BOOL _finished;
|
|
|
}
|
|
|
@end
|
|
|
|
|
|
-@interface GCDWebServerFileResponse () {
|
|
|
+@implementation GCDWebServerChunkEncoder
|
|
|
+
|
|
|
+- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
|
|
+ if ((self = [super initWithResponse:response reader:reader])) {
|
|
|
+ response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set
|
|
|
+ [response 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
|
|
|
- NSString* _path;
|
|
|
- NSUInteger _offset;
|
|
|
- NSUInteger _size;
|
|
|
- int _file;
|
|
|
+ z_stream _stream;
|
|
|
+ BOOL _finished;
|
|
|
}
|
|
|
@end
|
|
|
|
|
|
-@interface GCDWebServerChunkedResponse () {
|
|
|
+@implementation GCDWebServerGZipEncoder
|
|
|
+
|
|
|
+- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
|
|
+ if ((self = [super initWithResponse:response reader:reader])) {
|
|
|
+ response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set
|
|
|
+ [response 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
|
|
|
- GCDWebServerChunkBlock _block;
|
|
|
- NSData* _chunk;
|
|
|
- NSUInteger _offset;
|
|
|
- BOOL _terminated;
|
|
|
+ NSString* _type;
|
|
|
+ NSUInteger _length;
|
|
|
+ NSInteger _status;
|
|
|
+ NSUInteger _maxAge;
|
|
|
+ NSMutableDictionary* _headers;
|
|
|
+ BOOL _gzipped;
|
|
|
+ BOOL _chunked;
|
|
|
+
|
|
|
+ BOOL _opened;
|
|
|
+ NSMutableArray* _encoders;
|
|
|
+ id<GCDWebServerBodyReader> __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
|
|
|
+- (BOOL)open:(NSError**)error {
|
|
|
+ return YES;
|
|
|
+}
|
|
|
|
|
|
-@implementation GCDWebServerResponse (Subclassing)
|
|
|
+- (NSData*)readData:(NSError**)error {
|
|
|
+ return nil;
|
|
|
+}
|
|
|
|
|
|
-- (BOOL)open {
|
|
|
- [self doesNotRecognizeSelector:_cmd];
|
|
|
- return NO;
|
|
|
+- (void)close {
|
|
|
+ ;
|
|
|
}
|
|
|
|
|
|
-- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
|
|
|
- [self doesNotRecognizeSelector:_cmd];
|
|
|
- return -1;
|
|
|
+- (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];
|
|
|
}
|
|
|
|
|
|
-- (BOOL)close {
|
|
|
- [self doesNotRecognizeSelector:_cmd];
|
|
|
- return NO;
|
|
|
+- (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
|
|
|
+
|
|
|
+@implementation GCDWebServerStreamResponse
|
|
|
|
|
|
-+ (GCDWebServerChunkedResponse*)responseWithContentType:(NSString*)type chunkBlock:(GCDWebServerChunkBlock)block {
|
|
|
- return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type chunkBlock:block]);
|
|
|
++ (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
|