Merge branch 'Range-Requests'
This commit is contained in:
@ -54,7 +54,7 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* r
|
|||||||
|
|
||||||
@interface GCDWebServer (Handlers)
|
@interface GCDWebServer (Handlers)
|
||||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||||
- (void)addHandlerForBasePath:(NSString*)basePath localPath:(NSString*)localPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)age; // Base path is recursive and case-sensitive
|
- (void)addHandlerForBasePath:(NSString*)basePath localPath:(NSString*)localPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Base path is recursive and case-sensitive
|
||||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Path is case-insensitive
|
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Path is case-insensitive
|
||||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive
|
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive
|
||||||
@end
|
@end
|
||||||
|
|||||||
@ -365,6 +365,10 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
|
|||||||
return [GCDWebServerFileResponse responseWithFile:path];
|
return [GCDWebServerFileResponse responseWithFile:path];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (GCDWebServerResponse*)_responseWithPartialContentsOfFile:(NSString*)path byteRange:(NSRange)range {
|
||||||
|
return [GCDWebServerFileResponse responseWithFile:path byteRange:range];
|
||||||
|
}
|
||||||
|
|
||||||
- (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path {
|
- (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path {
|
||||||
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path];
|
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path];
|
||||||
if (enumerator == nil) {
|
if (enumerator == nil) {
|
||||||
@ -391,7 +395,7 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
|
|||||||
return [GCDWebServerDataResponse responseWithHTML:html];
|
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)addHandlerForBasePath:(NSString*)basePath localPath:(NSString*)localPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)age {
|
- (void)addHandlerForBasePath:(NSString*)basePath localPath:(NSString*)localPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests {
|
||||||
if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) {
|
if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) {
|
||||||
#if __has_feature(objc_arc)
|
#if __has_feature(objc_arc)
|
||||||
__unsafe_unretained GCDWebServer* server = self;
|
__unsafe_unretained GCDWebServer* server = self;
|
||||||
@ -422,12 +426,20 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
response = [server _responseWithContentsOfDirectory:filePath];
|
response = [server _responseWithContentsOfDirectory:filePath];
|
||||||
|
} else if (allowRangeRequests) {
|
||||||
|
NSRange range = request.byteRange;
|
||||||
|
if ((range.location != NSNotFound) || (range.length > 0)) {
|
||||||
|
response = [server _responseWithPartialContentsOfFile:filePath byteRange:range];
|
||||||
|
} else {
|
||||||
|
response = [server _responseWithContentsOfFile:filePath];
|
||||||
|
}
|
||||||
|
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
|
||||||
} else {
|
} else {
|
||||||
response = [server _responseWithContentsOfFile:filePath];
|
response = [server _responseWithContentsOfFile:filePath];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (response) {
|
if (response) {
|
||||||
response.cacheControlMaxAge = age;
|
response.cacheControlMaxAge = cacheAge;
|
||||||
} else {
|
} else {
|
||||||
response = [GCDWebServerResponse responseWithStatusCode:404];
|
response = [GCDWebServerResponse responseWithStatusCode:404];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@
|
|||||||
@property(nonatomic, readonly) NSDictionary* query; // May be nil
|
@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)
|
||||||
@property(nonatomic, readonly) NSUInteger contentLength; // Automatically parsed from headers
|
@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;
|
- (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query;
|
||||||
- (BOOL)hasBody; // Convenience method
|
- (BOOL)hasBody; // Convenience method
|
||||||
@end
|
@end
|
||||||
|
|||||||
@ -46,6 +46,7 @@ enum {
|
|||||||
NSDictionary* _query;
|
NSDictionary* _query;
|
||||||
NSString* _type;
|
NSString* _type;
|
||||||
NSUInteger _length;
|
NSUInteger _length;
|
||||||
|
NSRange _range;
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
|
|||||||
|
|
||||||
@implementation GCDWebServerRequest : NSObject
|
@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 {
|
- (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
@ -151,10 +152,39 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
_length = length;
|
_length = length;
|
||||||
|
|
||||||
if ((_length > 0) && (_type == nil)) {
|
if ((_length > 0) && (_type == nil)) {
|
||||||
_type = [kGCDWebServerDefaultMimeType copy];
|
_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;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,6 +70,10 @@
|
|||||||
@interface GCDWebServerFileResponse : GCDWebServerResponse
|
@interface GCDWebServerFileResponse : GCDWebServerResponse
|
||||||
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path;
|
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path;
|
||||||
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
+ (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;
|
||||||
- (id)initWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
- (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
|
@end
|
||||||
|
|||||||
@ -49,6 +49,8 @@
|
|||||||
@interface GCDWebServerFileResponse () {
|
@interface GCDWebServerFileResponse () {
|
||||||
@private
|
@private
|
||||||
NSString* _path;
|
NSString* _path;
|
||||||
|
NSUInteger _offset;
|
||||||
|
NSUInteger _size;
|
||||||
int _file;
|
int _file;
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
@ -251,24 +253,63 @@
|
|||||||
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path isAttachment: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 {
|
- (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 {
|
- (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;
|
struct stat info;
|
||||||
if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
|
if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
|
||||||
DNOT_REACHED();
|
DNOT_REACHED();
|
||||||
ARC_RELEASE(self);
|
ARC_RELEASE(self);
|
||||||
return nil;
|
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]);
|
NSString* type = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
|
||||||
if (type == nil) {
|
if (type == nil) {
|
||||||
type = kGCDWebServerDefaultMimeType;
|
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];
|
_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
|
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];
|
NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
|
||||||
NSString* fileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
|
NSString* fileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
|
||||||
@ -293,12 +334,24 @@
|
|||||||
- (BOOL)open {
|
- (BOOL)open {
|
||||||
DCHECK(_file <= 0);
|
DCHECK(_file <= 0);
|
||||||
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
|
_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 {
|
- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
|
||||||
DCHECK(_file > 0);
|
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 {
|
- (BOOL)close {
|
||||||
|
|||||||
@ -34,7 +34,7 @@ int main(int argc, const char* argv[]) {
|
|||||||
switch (0) {
|
switch (0) {
|
||||||
|
|
||||||
case 0: {
|
case 0: {
|
||||||
[webServer addHandlerForBasePath:@"/" localPath:NSHomeDirectory() indexFilename:nil cacheAge:0];
|
[webServer addHandlerForBasePath:@"/" localPath:NSHomeDirectory() indexFilename:nil cacheAge:0 allowRangeRequests:YES];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,8 @@ What's not supported (but not really required from an embedded HTTP server):
|
|||||||
* HTTPS
|
* HTTPS
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
* OS X 10.7 or later
|
* OS X 10.7 or later (x86_64)
|
||||||
* iOS 5.0 or later
|
* iOS 5.0 or later (armv7, armv7s or arm64)
|
||||||
|
|
||||||
Hello World
|
Hello World
|
||||||
===========
|
===========
|
||||||
@ -67,7 +67,7 @@ int main(int argc, const char* argv[]) {
|
|||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
|
|
||||||
GCDWebServer* webServer = [[GCDWebServer alloc] init];
|
GCDWebServer* webServer = [[GCDWebServer alloc] init];
|
||||||
[webServer addHandlerForBasePath:@"/" localPath:NSHomeDirectory() indexFilename:nil cacheAge:3600];
|
[webServer addHandlerForBasePath:@"/" localPath:NSHomeDirectory() indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
|
||||||
[webServer runWithPort:8080];
|
[webServer runWithPort:8080];
|
||||||
[webServer release];
|
[webServer release];
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ Assuming you have a website directory in your app containing HTML template files
|
|||||||
NSString* websitePath = [[NSBundle mainBundle] pathForResource:@"Website" ofType:nil];
|
NSString* websitePath = [[NSBundle mainBundle] pathForResource:@"Website" ofType:nil];
|
||||||
|
|
||||||
// Add a default handler to serve static files (i.e. anything other than HTML files)
|
// Add a default handler to serve static files (i.e. anything other than HTML files)
|
||||||
[self addHandlerForBasePath:@"/" localPath:websitePath indexFilename:nil cacheAge:3600];
|
[self addHandlerForBasePath:@"/" localPath:websitePath indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
|
||||||
|
|
||||||
// Add an override handler for all requests to "*.html" URLs to do the special HTML templatization
|
// Add an override handler for all requests to "*.html" URLs to do the special HTML templatization
|
||||||
[self addHandlerForMethod:@"GET"
|
[self addHandlerForMethod:@"GET"
|
||||||
|
|||||||
Reference in New Issue
Block a user