diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 275f1be..9abe9db 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -365,6 +365,10 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er return [GCDWebServerFileResponse responseWithFile:path]; } +- (GCDWebServerResponse*)_responseWithPartialContentsOfFile:(NSString*)path byteRange:(NSRange)range { + return [GCDWebServerFileResponse responseWithFile:path byteRange:range]; +} + - (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path { NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path]; if (enumerator == nil) { @@ -423,11 +427,17 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er } response = [server _responseWithContentsOfDirectory:filePath]; } else { - response = [server _responseWithContentsOfFile:filePath]; + NSRange range = request.byteRange; + if ((range.location != NSNotFound) || (range.length > 0)) { + response = [server _responseWithPartialContentsOfFile:filePath byteRange:range]; + } else { + response = [server _responseWithContentsOfFile:filePath]; + } } } if (response) { response.cacheControlMaxAge = age; + [response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"]; } else { response = [GCDWebServerResponse responseWithStatusCode:404]; } diff --git a/CGDWebServer/GCDWebServerRequest.h b/CGDWebServer/GCDWebServerRequest.h index 0cebea6..9b12f14 100644 --- a/CGDWebServer/GCDWebServerRequest.h +++ b/CGDWebServer/GCDWebServerRequest.h @@ -35,6 +35,7 @@ @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) NSRange byteRange; // Automatically parsed from headers ([NSNotFound, 0] if request has no "Range" header, [offset, length] for byte range from beginning or [NSNotFound, -bytes] from end) - (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query; - (BOOL)hasBody; // Convenience method @end diff --git a/CGDWebServer/GCDWebServerRequest.m b/CGDWebServer/GCDWebServerRequest.m index 2e37cd4..3bcc5d9 100644 --- a/CGDWebServer/GCDWebServerRequest.m +++ b/CGDWebServer/GCDWebServerRequest.m @@ -46,6 +46,7 @@ enum { NSDictionary* _query; NSString* _type; NSUInteger _length; + NSRange _range; } @end @@ -133,7 +134,7 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) { @implementation GCDWebServerRequest : NSObject -@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length; +@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, byteRange=_range; - (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { if ((self = [super init])) { @@ -151,10 +152,39 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) { return nil; } _length = length; - if ((_length > 0) && (_type == nil)) { _type = [kGCDWebServerDefaultMimeType copy]; } + + _range = NSMakeRange(NSNotFound, 0); + NSString* rangeHeader = [[_headers objectForKey:@"Range"] lowercaseString]; + if (rangeHeader) { + if ([rangeHeader hasPrefix:@"bytes="]) { + NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","]; + if (components.count == 1) { + components = [[components firstObject] componentsSeparatedByString:@"-"]; + if (components.count == 2) { + NSString* startString = [components objectAtIndex:0]; + NSInteger startValue = [startString integerValue]; + NSString* endString = [components objectAtIndex:1]; + NSInteger endValue = [endString integerValue]; + if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999" + _range.location = startValue; + _range.length = endValue - startValue + 1; + } else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-" + _range.location = startValue; + _range.length = NSUIntegerMax; + } else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500" + _range.location = NSNotFound; + _range.length = endValue; + } + } + } + } + if ((_range.location == NSNotFound) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid + LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url); + } + } } return self; } diff --git a/CGDWebServer/GCDWebServerResponse.h b/CGDWebServer/GCDWebServerResponse.h index 1076e99..ad2924d 100644 --- a/CGDWebServer/GCDWebServerResponse.h +++ b/CGDWebServer/GCDWebServerResponse.h @@ -70,6 +70,10 @@ @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/GCDWebServerResponse.m b/CGDWebServer/GCDWebServerResponse.m index 04d764a..47f3a73 100644 --- a/CGDWebServer/GCDWebServerResponse.m +++ b/CGDWebServer/GCDWebServerResponse.m @@ -49,6 +49,8 @@ @interface GCDWebServerFileResponse () { @private NSString* _path; + NSUInteger _offset; + NSUInteger _size; int _file; } @end @@ -251,24 +253,63 @@ 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 isAttachment:NO]; + 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, info.st_size); + range.length = MIN(range.length, info.st_size - range.location); + } else { + range.length = MIN(range.length, info.st_size); + range.location = 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 + } + } NSString* type = GCDWebServerGetMimeTypeForExtension([path pathExtension]); if (type == nil) { type = kGCDWebServerDefaultMimeType; } - if ((self = [super initWithContentType:type contentLength:(NSUInteger)info.st_size])) { + if ((self = [super initWithContentType:type contentLength:(range.location != NSNotFound ? range.length : info.st_size)])) { _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 = 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; @@ -293,12 +334,24 @@ - (BOOL)open { DCHECK(_file <= 0); _file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY); - return (_file > 0 ? YES : NO); + if (_file <= 0) { + return NO; + } + if (lseek(_file, _offset, SEEK_SET) != _offset) { + close(_file); + _file = 0; + return NO; + } + return YES; } - (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length { DCHECK(_file > 0); - return read(_file, buffer, length); + ssize_t result = read(_file, buffer, MIN(length, _size)); + if (result > 0) { + _size -= result; + } + return result; } - (BOOL)close {