mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-02-11 00:00:07 +08:00
Merge branch 'Range-Requests'
This commit is contained in:
@@ -54,7 +54,7 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* r
|
||||
|
||||
@interface GCDWebServer (Handlers)
|
||||
- (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 pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive
|
||||
@end
|
||||
|
||||
@@ -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) {
|
||||
@@ -391,7 +395,7 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
|
||||
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 __has_feature(objc_arc)
|
||||
__unsafe_unretained GCDWebServer* server = self;
|
||||
@@ -422,12 +426,20 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
|
||||
}
|
||||
}
|
||||
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 {
|
||||
response = [server _responseWithContentsOfFile:filePath];
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
response.cacheControlMaxAge = age;
|
||||
response.cacheControlMaxAge = cacheAge;
|
||||
} else {
|
||||
response = [GCDWebServerResponse responseWithStatusCode:404];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,7 +34,7 @@ int main(int argc, const char* argv[]) {
|
||||
switch (0) {
|
||||
|
||||
case 0: {
|
||||
[webServer addHandlerForBasePath:@"/" localPath:NSHomeDirectory() indexFilename:nil cacheAge:0];
|
||||
[webServer addHandlerForBasePath:@"/" localPath:NSHomeDirectory() indexFilename:nil cacheAge:0 allowRangeRequests:YES];
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ What's not supported (but not really required from an embedded HTTP server):
|
||||
* HTTPS
|
||||
|
||||
Requirements:
|
||||
* OS X 10.7 or later
|
||||
* iOS 5.0 or later
|
||||
* OS X 10.7 or later (x86_64)
|
||||
* iOS 5.0 or later (armv7, armv7s or arm64)
|
||||
|
||||
Hello World
|
||||
===========
|
||||
@@ -67,7 +67,7 @@ int main(int argc, const char* argv[]) {
|
||||
@autoreleasepool {
|
||||
|
||||
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 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];
|
||||
|
||||
// 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
|
||||
[self addHandlerForMethod:@"GET"
|
||||
|
||||
Reference in New Issue
Block a user