mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-05-13 00:02:02 +08:00
Initial import
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright (c) 2012-2013, 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.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may 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 <COPYRIGHT HOLDER> 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"
|
||||
#import "GCDWebServerResponse.h"
|
||||
|
||||
typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery);
|
||||
typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request);
|
||||
|
||||
@class GCDWebServer, GCDWebServerHandler;
|
||||
|
||||
@interface GCDWebServerConnection : NSObject {
|
||||
@private
|
||||
GCDWebServer* _server;
|
||||
NSData* _address;
|
||||
CFSocketNativeHandle _socket;
|
||||
NSUInteger _bytesRead;
|
||||
NSUInteger _bytesWritten;
|
||||
|
||||
CFHTTPMessageRef _requestMessage;
|
||||
GCDWebServerRequest* _request;
|
||||
GCDWebServerHandler* _handler;
|
||||
CFHTTPMessageRef _responseMessage;
|
||||
GCDWebServerResponse* _response;
|
||||
}
|
||||
@property(nonatomic, readonly) GCDWebServer* server;
|
||||
@property(nonatomic, readonly) NSData* address; // struct sockaddr
|
||||
@property(nonatomic, readonly) NSUInteger totalBytesRead;
|
||||
@property(nonatomic, readonly) NSUInteger totalBytesWritten;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerConnection (Subclassing)
|
||||
- (void) open;
|
||||
- (GCDWebServerResponse*) processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block;
|
||||
- (void) close;
|
||||
@end
|
||||
|
||||
@interface GCDWebServer : NSObject {
|
||||
@private
|
||||
NSMutableArray* _handlers;
|
||||
|
||||
NSUInteger _port;
|
||||
NSRunLoop* _runLoop;
|
||||
CFSocketRef _socket;
|
||||
CFNetServiceRef _service;
|
||||
}
|
||||
@property(nonatomic, readonly, getter=isRunning) BOOL running;
|
||||
@property(nonatomic, readonly) NSUInteger port;
|
||||
- (void) addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||
- (void) removeAllHandlers;
|
||||
|
||||
- (BOOL) start; // Default is main runloop, 8080 port and computer name
|
||||
- (BOOL) startWithRunloop:(NSRunLoop*)runloop port:(NSUInteger)port bonjourName:(NSString*)name; // Pass nil name to disable Bonjour or empty string to use computer name
|
||||
- (void) stop;
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Subclassing)
|
||||
+ (Class) connectionClass;
|
||||
+ (NSString*) serverName; // Default is class name
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Extensions)
|
||||
- (BOOL) runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only)
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Handlers)
|
||||
- (void) addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block;
|
||||
- (void) addHandlerForBasePath:(NSString*)basePath localPath:(NSString*)localPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge; // Base path is recursive and case-sensitive
|
||||
- (void) addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block; // Path is case-insensitive
|
||||
- (void) addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive
|
||||
@end
|
||||
@@ -0,0 +1,888 @@
|
||||
/*
|
||||
Copyright (c) 2012-2013, 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.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may 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 <COPYRIGHT HOLDER> 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 <TargetConditionals.h>
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <CFNetwork/CFNetwork.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#else
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#endif
|
||||
#import <sys/fcntl.h>
|
||||
#import <sys/stat.h>
|
||||
#import <netinet/in.h>
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
#define kReadWriteQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
|
||||
#define kHeadersReadBuffer 1024
|
||||
#define kBodyWriteBufferSize (32 * 1024)
|
||||
|
||||
typedef void (^ReadBufferCompletionBlock)(dispatch_data_t buffer);
|
||||
typedef void (^ReadDataCompletionBlock)(NSData* data);
|
||||
typedef void (^ReadHeadersCompletionBlock)(NSData* extraData);
|
||||
typedef void (^ReadBodyCompletionBlock)(BOOL success);
|
||||
|
||||
typedef void (^WriteBufferCompletionBlock)(BOOL success);
|
||||
typedef void (^WriteDataCompletionBlock)(BOOL success);
|
||||
typedef void (^WriteHeadersCompletionBlock)(BOOL success);
|
||||
typedef void (^WriteBodyCompletionBlock)(BOOL success);
|
||||
|
||||
@interface GCDWebServerHandler : NSObject {
|
||||
@private
|
||||
GCDWebServerMatchBlock _matchBlock;
|
||||
GCDWebServerProcessBlock _processBlock;
|
||||
}
|
||||
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
|
||||
@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock;
|
||||
- (id) initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerConnection ()
|
||||
- (id) initWithServer:(GCDWebServer*)server address:(NSData*)address socket:(CFSocketNativeHandle)socket;
|
||||
@end
|
||||
|
||||
@interface GCDWebServer ()
|
||||
@property(nonatomic, readonly) NSArray* handlers;
|
||||
@end
|
||||
|
||||
static NSData* _separatorData = nil;
|
||||
static NSData* _continueData = nil;
|
||||
static NSDateFormatter* _dateFormatter = nil;
|
||||
static dispatch_queue_t _formatterQueue = NULL;
|
||||
static BOOL _run;
|
||||
|
||||
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) {
|
||||
static NSDictionary* _overrides = nil;
|
||||
if (_overrides == nil) {
|
||||
_overrides = [[NSDictionary alloc] initWithObjectsAndKeys:
|
||||
@"text/css", @"css",
|
||||
nil];
|
||||
}
|
||||
NSString* mimeType = nil;
|
||||
extension = [extension lowercaseString];
|
||||
if (extension.length) {
|
||||
mimeType = [_overrides objectForKey:extension];
|
||||
if (mimeType == nil) {
|
||||
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)extension, NULL);
|
||||
if (uti) {
|
||||
mimeType = [(id)UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType) autorelease];
|
||||
CFRelease(uti);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
static NSString* _UnescapeURLString(NSString* string) {
|
||||
return [(id)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""),
|
||||
kCFStringEncodingUTF8) autorelease];
|
||||
}
|
||||
|
||||
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
|
||||
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
|
||||
NSScanner* scanner = [[NSScanner alloc] initWithString:form];
|
||||
[scanner setCharactersToBeSkipped:nil];
|
||||
while (1) {
|
||||
NSString* key = nil;
|
||||
if (![scanner scanUpToString:@"=" intoString:&key] || [scanner isAtEnd]) {
|
||||
break;
|
||||
}
|
||||
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
||||
|
||||
NSString* value = nil;
|
||||
if (![scanner scanUpToString:@"&" intoString:&value]) {
|
||||
break;
|
||||
}
|
||||
|
||||
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||
[parameters setObject:_UnescapeURLString(value) forKey:_UnescapeURLString(key)];
|
||||
|
||||
if ([scanner isAtEnd]) {
|
||||
break;
|
||||
}
|
||||
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
||||
}
|
||||
[scanner release];
|
||||
return parameters;
|
||||
}
|
||||
|
||||
static void _SignalHandler(int signal) {
|
||||
_run = NO;
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
@implementation GCDWebServerHandler
|
||||
|
||||
@synthesize matchBlock=_matchBlock, processBlock=_processBlock;
|
||||
|
||||
- (id) initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock {
|
||||
if ((self = [super init])) {
|
||||
_matchBlock = Block_copy(matchBlock);
|
||||
_processBlock = Block_copy(processBlock);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
Block_release(_matchBlock);
|
||||
Block_release(_processBlock);
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerConnection (Read)
|
||||
|
||||
- (void) _readBufferWithLength:(NSUInteger)length completionBlock:(ReadBufferCompletionBlock)block {
|
||||
dispatch_read(_socket, length, kReadWriteQueue, ^(dispatch_data_t buffer, int error) {
|
||||
|
||||
@autoreleasepool {
|
||||
if (error == 0) {
|
||||
size_t size = dispatch_data_get_size(buffer);
|
||||
if (size > 0) {
|
||||
LOG_DEBUG(@"Connection received %i bytes on socket %i", size, _socket);
|
||||
_bytesRead += size;
|
||||
block(buffer);
|
||||
} else {
|
||||
if (_bytesRead > 0) {
|
||||
LOG_ERROR(@"No more data available on socket %i", _socket);
|
||||
} else {
|
||||
LOG_WARNING(@"No data received from socket %i", _socket);
|
||||
}
|
||||
block(NULL);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Error while reading from socket %i: %s (%i)", _socket, strerror(error), error);
|
||||
block(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
- (void) _readDataWithCompletionBlock:(ReadDataCompletionBlock)block {
|
||||
[self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) {
|
||||
|
||||
if (buffer) {
|
||||
NSMutableData* data = [[NSMutableData alloc] initWithCapacity:dispatch_data_get_size(buffer)];
|
||||
dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* buffer, size_t size) {
|
||||
[data appendBytes:buffer length:size];
|
||||
return true;
|
||||
});
|
||||
block(data);
|
||||
[data release];
|
||||
} else {
|
||||
block(nil);
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) _readHeadersWithCompletionBlock:(ReadHeadersCompletionBlock)block {
|
||||
DCHECK(_requestMessage);
|
||||
NSMutableData* data = [NSMutableData dataWithCapacity:kHeadersReadBuffer];
|
||||
[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* buffer, size_t size) {
|
||||
[data appendBytes:buffer length:size];
|
||||
return true;
|
||||
});
|
||||
NSRange range = [data rangeOfData:_separatorData options:0 range:NSMakeRange(0, data.length)];
|
||||
if (range.location == NSNotFound) {
|
||||
[self _readHeadersWithCompletionBlock:block];
|
||||
} else {
|
||||
NSUInteger length = range.location + range.length;
|
||||
if (CFHTTPMessageAppendBytes(_requestMessage, data.bytes, length)) {
|
||||
if (CFHTTPMessageIsHeaderComplete(_requestMessage)) {
|
||||
block([data subdataWithRange:NSMakeRange(length, data.length - length)]);
|
||||
} else {
|
||||
LOG_ERROR(@"Failed parsing request headers from socket %i", _socket);
|
||||
block(nil);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed appending request headers data from socket %i", _socket);
|
||||
block(nil);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
block(nil);
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) _readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block {
|
||||
DCHECK([_request hasBody]);
|
||||
[self _readBufferWithLength:length completionBlock:^(dispatch_data_t buffer) {
|
||||
|
||||
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* buffer, size_t size) {
|
||||
NSInteger result = [_request write:buffer maxLength:size];
|
||||
if (result != size) {
|
||||
LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (success) {
|
||||
if (remainingLength > 0) {
|
||||
[self _readBodyWithRemainingLength:remainingLength completionBlock:block];
|
||||
} else {
|
||||
block(YES);
|
||||
}
|
||||
} else {
|
||||
block(NO);
|
||||
}
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
block(NO);
|
||||
}
|
||||
} else {
|
||||
block(NO);
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerConnection (Write)
|
||||
|
||||
- (void) _writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block {
|
||||
size_t size = dispatch_data_get_size(buffer);
|
||||
dispatch_write(_socket, buffer, kReadWriteQueue, ^(dispatch_data_t data, int error) {
|
||||
|
||||
@autoreleasepool {
|
||||
if (error == 0) {
|
||||
DCHECK(data == NULL);
|
||||
LOG_DEBUG(@"Connection sent %i bytes on socket %i", size, _socket);
|
||||
_bytesWritten += size;
|
||||
block(YES);
|
||||
} else {
|
||||
LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error);
|
||||
block(NO);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
- (void) _writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block {
|
||||
[data retain];
|
||||
dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_current_queue(), ^{
|
||||
[data release];
|
||||
});
|
||||
[self _writeBuffer:buffer withCompletionBlock:block];
|
||||
dispatch_release(buffer);
|
||||
}
|
||||
|
||||
- (void) _writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block {
|
||||
DCHECK(_responseMessage);
|
||||
CFDataRef message = CFHTTPMessageCopySerializedMessage(_responseMessage);
|
||||
[self _writeData:(NSData*)message withCompletionBlock:block];
|
||||
CFRelease(message);
|
||||
}
|
||||
|
||||
- (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);
|
||||
}
|
||||
|
||||
}];
|
||||
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);
|
||||
} else {
|
||||
block(YES);
|
||||
free(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerConnection
|
||||
|
||||
@synthesize server=_server, address=_address, totalBytesRead=_bytesRead, totalBytesWritten=_bytesWritten;
|
||||
|
||||
- (void) _initializeResponseHeadersWithStatusCode:(NSInteger)statusCode {
|
||||
_responseMessage = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_1);
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Connection"), CFSTR("Close"));
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Server"), (CFStringRef)[[_server class] serverName]);
|
||||
dispatch_sync(_formatterQueue, ^{
|
||||
NSString* date = [_dateFormatter stringFromDate:[NSDate date]];
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (CFStringRef)date);
|
||||
});
|
||||
}
|
||||
|
||||
- (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", statusCode, _socket);
|
||||
}
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
- (void) _processRequest {
|
||||
DCHECK(_responseMessage == NULL);
|
||||
|
||||
GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock];
|
||||
if (![response hasBody] || [response open]) {
|
||||
_response = [response retain];
|
||||
}
|
||||
|
||||
if (_response) {
|
||||
[self _initializeResponseHeadersWithStatusCode:_response.statusCode];
|
||||
NSUInteger maxAge = _response.cacheControlMaxAge;
|
||||
if (maxAge > 0) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)maxAge]);
|
||||
} else {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache"));
|
||||
}
|
||||
[_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, (CFStringRef)key, (CFStringRef)obj);
|
||||
}];
|
||||
if ([_response hasBody]) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (CFStringRef)_response.contentType);
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (CFStringRef)[NSString stringWithFormat:@"%i", (int)_response.contentLength]);
|
||||
}
|
||||
[self _writeHeadersWithCompletionBlock:^(BOOL success) {
|
||||
|
||||
if (success) {
|
||||
if ([_response hasBody]) {
|
||||
[self _writeBodyWithCompletionBlock:^(BOOL success) {
|
||||
|
||||
[_response close]; // Can't do anything with result anyway
|
||||
|
||||
}];
|
||||
}
|
||||
} else if ([_response hasBody]) {
|
||||
[_response close]; // Can't do anything with result anyway
|
||||
}
|
||||
|
||||
}];
|
||||
} else {
|
||||
[self _abortWithStatusCode:500];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void) _readRequestBody:(NSData*)initialData {
|
||||
if ([_request open]) {
|
||||
NSInteger length = _request.contentLength;
|
||||
if (initialData.length) {
|
||||
NSInteger result = [_request write:initialData.bytes maxLength:initialData.length];
|
||||
if (result == initialData.length) {
|
||||
length -= initialData.length;
|
||||
DCHECK(length >= 0);
|
||||
} else {
|
||||
LOG_ERROR(@"Failed writing request body on socket %i (error %i)", _socket, (int)result);
|
||||
length = -1;
|
||||
}
|
||||
}
|
||||
if (length > 0) {
|
||||
[self _readBodyWithRemainingLength:length completionBlock:^(BOOL success) {
|
||||
|
||||
if (![_request close]) {
|
||||
success = NO;
|
||||
}
|
||||
if (success) {
|
||||
[self _processRequest];
|
||||
} else {
|
||||
[self _abortWithStatusCode:500];
|
||||
}
|
||||
|
||||
}];
|
||||
} else if (length == 0) {
|
||||
if ([_request close]) {
|
||||
[self _processRequest];
|
||||
} else {
|
||||
[self _abortWithStatusCode:500];
|
||||
}
|
||||
} else {
|
||||
[_request close]; // Can't do anything with result anyway
|
||||
[self _abortWithStatusCode:500];
|
||||
}
|
||||
} else {
|
||||
[self _abortWithStatusCode:500];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) _readRequestHeaders {
|
||||
_requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true);
|
||||
[self _readHeadersWithCompletionBlock:^(NSData* extraData) {
|
||||
|
||||
if (extraData) {
|
||||
NSString* requestMethod = [[(id)CFHTTPMessageCopyRequestMethod(_requestMessage) autorelease] uppercaseString];
|
||||
DCHECK(requestMethod);
|
||||
NSURL* requestURL = [(id)CFHTTPMessageCopyRequestURL(_requestMessage) autorelease];
|
||||
DCHECK(requestURL);
|
||||
NSString* requestPath = _UnescapeURLString([(id)CFURLCopyPath((CFURLRef)requestURL) autorelease]); // Don't use -[NSURL path] which strips the ending slash
|
||||
DCHECK(requestPath);
|
||||
NSDictionary* requestQuery = nil;
|
||||
NSString* queryString = [(id)CFURLCopyQueryString((CFURLRef)requestURL, NULL) autorelease]; // Don't use -[NSURL query] to make sure query is not unescaped;
|
||||
if (queryString.length) {
|
||||
requestQuery = GCDWebServerParseURLEncodedForm(queryString);
|
||||
DCHECK(requestQuery);
|
||||
}
|
||||
NSDictionary* requestHeaders = [(id)CFHTTPMessageCopyAllHeaderFields(_requestMessage) autorelease];
|
||||
DCHECK(requestHeaders);
|
||||
for (_handler in _server.handlers) {
|
||||
_request = [_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery) retain];
|
||||
if (_request) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_request) {
|
||||
if (_request.hasBody) {
|
||||
if (extraData.length <= _request.contentLength) {
|
||||
NSString* expectHeader = [(id)CFHTTPMessageCopyHeaderFieldValue(_requestMessage, CFSTR("Expect")) autorelease];
|
||||
if (expectHeader) {
|
||||
if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) {
|
||||
[self _writeData:_continueData withCompletionBlock:^(BOOL success) {
|
||||
|
||||
if (success) {
|
||||
[self _readRequestBody:extraData];
|
||||
}
|
||||
|
||||
}];
|
||||
} else {
|
||||
LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket);
|
||||
[self _abortWithStatusCode:417];
|
||||
}
|
||||
} else {
|
||||
[self _readRequestBody:extraData];
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket);
|
||||
[self _abortWithStatusCode:400];
|
||||
}
|
||||
} else {
|
||||
[self _processRequest];
|
||||
}
|
||||
} else {
|
||||
[self _abortWithStatusCode:405];
|
||||
}
|
||||
} else {
|
||||
[self _abortWithStatusCode:500];
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (id) initWithServer:(GCDWebServer*)server address:(NSData*)address socket:(CFSocketNativeHandle)socket {
|
||||
if ((self = [super init])) {
|
||||
_server = [server retain];
|
||||
_address = [address retain];
|
||||
_socket = socket;
|
||||
|
||||
[self open];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
[self close];
|
||||
|
||||
[_server release];
|
||||
[_address release];
|
||||
|
||||
if (_requestMessage) {
|
||||
CFRelease(_requestMessage);
|
||||
}
|
||||
[_request release];
|
||||
|
||||
if (_responseMessage) {
|
||||
CFRelease(_responseMessage);
|
||||
}
|
||||
[_response release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerConnection (Subclassing)
|
||||
|
||||
- (void) open {
|
||||
LOG_DEBUG(@"Did open connection on socket %i", _socket);
|
||||
[self _readRequestHeaders];
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*) processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block {
|
||||
LOG_DEBUG(@"Connection on socket %i processing %@ request for \"%@\" (%i bytes body)", _socket, _request.method, _request.path, _request.contentLength);
|
||||
GCDWebServerResponse* response = nil;
|
||||
@try {
|
||||
response = block(request);
|
||||
}
|
||||
@catch (NSException* exception) {
|
||||
LOG_EXCEPTION(exception);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
- (void) close {
|
||||
close(_socket);
|
||||
LOG_DEBUG(@"Did close connection on socket %i", _socket);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer
|
||||
|
||||
@synthesize handlers=_handlers, port=_port;
|
||||
|
||||
+ (void) initialize {
|
||||
DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
|
||||
if (_separatorData == nil) {
|
||||
_separatorData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
|
||||
DCHECK(_separatorData);
|
||||
}
|
||||
if (_continueData == nil) {
|
||||
CFHTTPMessageRef message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, NULL, kCFHTTPVersion1_1);
|
||||
_continueData = (NSData*)CFHTTPMessageCopySerializedMessage(message);
|
||||
CFRelease(message);
|
||||
DCHECK(_continueData);
|
||||
}
|
||||
if (_dateFormatter == nil) {
|
||||
_dateFormatter = [[NSDateFormatter alloc] init];
|
||||
_dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
|
||||
_dateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
|
||||
_dateFormatter.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease];
|
||||
DCHECK(_dateFormatter);
|
||||
}
|
||||
if (_formatterQueue == NULL) {
|
||||
_formatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
|
||||
DCHECK(_formatterQueue);
|
||||
}
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
if ((self = [super init])) {
|
||||
_handlers = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
if (_runLoop) {
|
||||
[self stop];
|
||||
}
|
||||
|
||||
[_handlers release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void) addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
|
||||
DCHECK(_runLoop == nil);
|
||||
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
|
||||
[_handlers insertObject:handler atIndex:0];
|
||||
[handler release];
|
||||
}
|
||||
|
||||
- (void) removeAllHandlers {
|
||||
DCHECK(_runLoop == nil);
|
||||
[_handlers removeAllObjects];
|
||||
}
|
||||
|
||||
- (BOOL) start {
|
||||
return [self startWithRunloop:[NSRunLoop mainRunLoop] port:8080 bonjourName:@""];
|
||||
}
|
||||
|
||||
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||
@autoreleasepool {
|
||||
if (error->error) {
|
||||
LOG_ERROR(@"Bonjour error %i (domain %i)", error->error, (int)error->domain);
|
||||
} else {
|
||||
LOG_VERBOSE(@"Registered Bonjour service \"%@\" with type '%@' on port %i", CFNetServiceGetName(service), CFNetServiceGetType(service), CFNetServiceGetPortNumber(service));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _SocketCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void* data, void* info) {
|
||||
if (type == kCFSocketAcceptCallBack) {
|
||||
CFSocketNativeHandle handle = *(CFSocketNativeHandle*)data;
|
||||
int set = 1;
|
||||
setsockopt(handle, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int)); // Make sure this socket cannot generate SIG_PIPE
|
||||
@autoreleasepool {
|
||||
Class class = [[(GCDWebServer*)info class] connectionClass];
|
||||
GCDWebServerConnection* connection = [[class alloc] initWithServer:(GCDWebServer*)info address:(NSData*)address socket:handle];
|
||||
[connection release]; // Connection will automatically retain itself while opened
|
||||
}
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL) startWithRunloop:(NSRunLoop*)runloop port:(NSUInteger)port bonjourName:(NSString*)name {
|
||||
DCHECK(runloop);
|
||||
DCHECK(port);
|
||||
DCHECK(_runLoop == nil);
|
||||
CFSocketContext context = {0, self, NULL, NULL, NULL};
|
||||
_socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, _SocketCallBack, &context);
|
||||
if (_socket) {
|
||||
int yes = 1;
|
||||
setsockopt(CFSocketGetNative(_socket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
|
||||
struct sockaddr_in addr4;
|
||||
bzero(&addr4, sizeof(addr4));
|
||||
addr4.sin_len = sizeof(addr4);
|
||||
addr4.sin_family = AF_INET;
|
||||
addr4.sin_port = htons(port);
|
||||
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
if (CFSocketSetAddress(_socket, (CFDataRef)[NSData dataWithBytes:&addr4 length:sizeof(addr4)]) == kCFSocketSuccess) {
|
||||
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
|
||||
CFRunLoopAddSource([runloop getCFRunLoop], source, kCFRunLoopCommonModes);
|
||||
CFRelease(source);
|
||||
|
||||
if (name) {
|
||||
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (CFStringRef)name, port);
|
||||
if (_service) {
|
||||
CFNetServiceClientContext context = {0, self, NULL, NULL, NULL};
|
||||
CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_service, [runloop getCFRunLoop], kCFRunLoopCommonModes);
|
||||
CFStreamError error = {0};
|
||||
CFNetServiceRegisterWithOptions(_service, 0, &error);
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating CFNetService");
|
||||
}
|
||||
}
|
||||
|
||||
_port = port;
|
||||
_runLoop = [runloop retain];
|
||||
LOG_VERBOSE(@"%@ started on port %i", [self class], (int)port);
|
||||
} else {
|
||||
LOG_ERROR(@"Failed binding socket");
|
||||
CFRelease(_socket);
|
||||
_socket = NULL;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating CFSocket");
|
||||
}
|
||||
return (_runLoop != nil ? YES : NO);
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return (_runLoop != nil ? YES : NO);
|
||||
}
|
||||
|
||||
- (void) stop {
|
||||
DCHECK(_runLoop != nil);
|
||||
if (_socket) {
|
||||
if (_service) {
|
||||
CFNetServiceUnscheduleFromRunLoop(_service, [_runLoop getCFRunLoop], kCFRunLoopCommonModes);
|
||||
CFNetServiceSetClient(_service, NULL, NULL);
|
||||
CFRelease(_service);
|
||||
}
|
||||
|
||||
CFSocketInvalidate(_socket);
|
||||
CFRelease(_socket);
|
||||
_socket = NULL;
|
||||
LOG_VERBOSE(@"%@ stopped", [self class]);
|
||||
}
|
||||
[_runLoop release];
|
||||
_runLoop = nil;
|
||||
_port = 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer (Subclassing)
|
||||
|
||||
+ (Class) connectionClass {
|
||||
return [GCDWebServerConnection class];
|
||||
}
|
||||
|
||||
+ (NSString*) serverName {
|
||||
return NSStringFromClass(self);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer (Extensions)
|
||||
|
||||
- (BOOL) runWithPort:(NSUInteger)port {
|
||||
BOOL success = NO;
|
||||
_run = YES;
|
||||
void* handler = signal(SIGINT, _SignalHandler);
|
||||
if (handler != SIG_ERR) {
|
||||
if ([self startWithRunloop:[NSRunLoop currentRunLoop] port:port bonjourName:@""]) {
|
||||
while (_run) {
|
||||
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
|
||||
}
|
||||
[self stop];
|
||||
success = YES;
|
||||
}
|
||||
signal(SIGINT, handler);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer (Handlers)
|
||||
|
||||
- (void) addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block {
|
||||
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
||||
|
||||
return [[[class alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery] autorelease];
|
||||
|
||||
} processBlock:block];
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*) _responseWithContentsOfFile:(NSString*)path {
|
||||
return [GCDWebServerFileResponse responseWithFile:path];
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*) _responseWithContentsOfDirectory:(NSString*)path {
|
||||
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path];
|
||||
if (enumerator == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableString* html = [NSMutableString string];
|
||||
[html appendString:@"<html><body>\n"];
|
||||
[html appendString:@"<ul>\n"];
|
||||
for (NSString* file in enumerator) {
|
||||
if (![file hasPrefix:@"."]) {
|
||||
NSString* type = [[enumerator fileAttributes] objectForKey:NSFileType];
|
||||
NSString* escapedFile = [file stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||
DCHECK(escapedFile);
|
||||
if ([type isEqualToString:NSFileTypeRegular]) {
|
||||
[html appendFormat:@"<li><a href=\"%@\">%@</a></li>\n", escapedFile, file];
|
||||
} else if ([type isEqualToString:NSFileTypeDirectory]) {
|
||||
[html appendFormat:@"<li><a href=\"%@/\">%@/</a></li>\n", escapedFile, file];
|
||||
}
|
||||
}
|
||||
[enumerator skipDescendents];
|
||||
}
|
||||
[html appendString:@"</ul>\n"];
|
||||
[html appendString:@"</body></html>\n"];
|
||||
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||
}
|
||||
|
||||
- (void) addHandlerForBasePath:(NSString*)basePath localPath:(NSString*)localPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge {
|
||||
if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) {
|
||||
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
||||
|
||||
if (![requestMethod isEqualToString:@"GET"]) {
|
||||
return nil;
|
||||
}
|
||||
if (![urlPath hasPrefix:basePath]) {
|
||||
return nil;
|
||||
}
|
||||
return [[[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery] autorelease];
|
||||
|
||||
} processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
GCDWebServerResponse* response = nil;
|
||||
NSString* filePath = [localPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]];
|
||||
BOOL isDirectory;
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) {
|
||||
if (isDirectory) {
|
||||
if (indexFilename) {
|
||||
NSString* indexPath = [filePath stringByAppendingPathComponent:indexFilename];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath isDirectory:&isDirectory] && !isDirectory) {
|
||||
return [self _responseWithContentsOfFile:indexPath];
|
||||
}
|
||||
}
|
||||
response = [self _responseWithContentsOfDirectory:filePath];
|
||||
} else {
|
||||
response = [self _responseWithContentsOfFile:filePath];
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
response.cacheControlMaxAge = cacheAge;
|
||||
} else {
|
||||
response = [GCDWebServerResponse responseWithStatusCode:404];
|
||||
}
|
||||
return response;
|
||||
|
||||
}];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
- (void) addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block {
|
||||
if ([path hasPrefix:@"/"] && [class isSubclassOfClass:[GCDWebServerRequest class]]) {
|
||||
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
||||
|
||||
if (![requestMethod isEqualToString:method]) {
|
||||
return nil;
|
||||
}
|
||||
if ([urlPath caseInsensitiveCompare:path] != NSOrderedSame) {
|
||||
return nil;
|
||||
}
|
||||
return [[[class alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery] autorelease];
|
||||
|
||||
} processBlock:block];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
- (void) addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)class processBlock:(GCDWebServerProcessBlock)block {
|
||||
NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL];
|
||||
if (expression && [class isSubclassOfClass:[GCDWebServerRequest class]]) {
|
||||
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
||||
|
||||
if (![requestMethod isEqualToString:method]) {
|
||||
return nil;
|
||||
}
|
||||
if ([expression firstMatchInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)] == nil) {
|
||||
return nil;
|
||||
}
|
||||
return [[[class alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery] autorelease];
|
||||
|
||||
} processBlock:block];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright (c) 2012-2013, 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.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may 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 <COPYRIGHT HOLDER> 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 "GCDWebServer.h"
|
||||
|
||||
#ifdef __LOGGING_HEADER__
|
||||
|
||||
#import __LOGGING_HEADER__
|
||||
|
||||
#else
|
||||
|
||||
static inline void __LogMessage(long level, NSString* format, ...) {
|
||||
static const char* levelNames[] = {"DEBUG", "VERBOSE", "INFO", "WARNING", "ERROR", "EXCEPTION"};
|
||||
static long minLevel = -1;
|
||||
if (minLevel < 0) {
|
||||
const char* logLevel = getenv("logLevel");
|
||||
minLevel = logLevel ? atoi(logLevel) : 0;
|
||||
}
|
||||
if (level >= minLevel) {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
va_end(arguments);
|
||||
printf("[%s] %s\n", levelNames[level], [message UTF8String]);
|
||||
[message release];
|
||||
}
|
||||
}
|
||||
|
||||
#define LOG_VERBOSE(...) __LogMessage(1, __VA_ARGS__)
|
||||
#define LOG_INFO(...) __LogMessage(2, __VA_ARGS__)
|
||||
#define LOG_WARNING(...) __LogMessage(3, __VA_ARGS__)
|
||||
#define LOG_ERROR(...) __LogMessage(4, __VA_ARGS__)
|
||||
#define LOG_EXCEPTION(__EXCEPTION__) __LogMessage(5, @"%@", __EXCEPTION__)
|
||||
|
||||
#ifdef NDEBUG
|
||||
|
||||
#define DCHECK(__CONDITION__)
|
||||
#define DNOT_REACHED()
|
||||
#define LOG_DEBUG(...)
|
||||
|
||||
#else
|
||||
|
||||
#define DCHECK(__CONDITION__) \
|
||||
do { \
|
||||
if (!(__CONDITION__)) { \
|
||||
abort(); \
|
||||
} \
|
||||
} while (0)
|
||||
#define DNOT_REACHED() abort()
|
||||
#define LOG_DEBUG(...) __LogMessage(0, __VA_ARGS__)
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#define kGCDWebServerDefaultMimeType @"application/octet-stream"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension);
|
||||
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
Copyright (c) 2012-2013, 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.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may 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 <COPYRIGHT HOLDER> 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 <Foundation/Foundation.h>
|
||||
|
||||
@interface GCDWebServerRequest : NSObject {
|
||||
@private
|
||||
NSString* _method;
|
||||
NSURL* _url;
|
||||
NSDictionary* _headers;
|
||||
NSString* _path;
|
||||
NSDictionary* _query;
|
||||
NSString* _type;
|
||||
NSUInteger _length;
|
||||
}
|
||||
@property(nonatomic, readonly) NSString* method;
|
||||
@property(nonatomic, readonly) NSURL* URL;
|
||||
@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) NSUInteger contentLength; // Automatically parsed from headers
|
||||
- (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
|
||||
|
||||
@interface GCDWebServerDataRequest : GCDWebServerRequest {
|
||||
@private
|
||||
NSMutableData* _data;
|
||||
}
|
||||
@property(nonatomic, readonly) NSData* data; // Only valid after open / write / close sequence
|
||||
@end
|
||||
|
||||
@interface GCDWebServerFileRequest : GCDWebServerRequest {
|
||||
@private
|
||||
NSString* _filePath;
|
||||
int _file;
|
||||
}
|
||||
@property(nonatomic, readonly) NSString* filePath; // Only valid after open / write / close sequence
|
||||
@end
|
||||
|
||||
@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest {
|
||||
@private
|
||||
NSDictionary* _arguments;
|
||||
}
|
||||
@property(nonatomic, readonly) NSDictionary* arguments; // Only valid after open / write / close sequence
|
||||
+ (NSString*) mimeType;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerMultiPart : NSObject {
|
||||
@private
|
||||
NSString* _contentType;
|
||||
NSString* _mimeType;
|
||||
}
|
||||
@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 {
|
||||
@private
|
||||
NSData* _data;
|
||||
NSString* _string;
|
||||
}
|
||||
@property(nonatomic, readonly) NSData* data;
|
||||
@property(nonatomic, readonly) NSString* string; // May be nil (only valid for text mime types
|
||||
@end
|
||||
|
||||
@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart {
|
||||
@private
|
||||
NSString* _fileName;
|
||||
NSString* _temporaryPath;
|
||||
}
|
||||
@property(nonatomic, readonly) NSString* fileName; // May be nil
|
||||
@property(nonatomic, readonly) NSString* temporaryPath;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest {
|
||||
@private
|
||||
NSData* _boundary;
|
||||
|
||||
NSUInteger _parserState;
|
||||
NSMutableData* _parserData;
|
||||
NSString* _controlName;
|
||||
NSString* _fileName;
|
||||
NSString* _contentType;
|
||||
NSString* _tmpPath;
|
||||
int _tmpFile;
|
||||
|
||||
NSMutableDictionary* _arguments;
|
||||
NSMutableDictionary* _files;
|
||||
}
|
||||
@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
|
||||
@@ -0,0 +1,512 @@
|
||||
/*
|
||||
Copyright (c) 2012-2013, 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.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may 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 <COPYRIGHT HOLDER> 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;
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
[scanner release];
|
||||
}
|
||||
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;
|
||||
|
||||
- (id) initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
||||
if ((self = [super init])) {
|
||||
_method = [method copy];
|
||||
_url = [url retain];
|
||||
_headers = [headers retain];
|
||||
_path = [path copy];
|
||||
_query = [query retain];
|
||||
|
||||
_type = [[_headers objectForKey:@"Content-Type"] retain];
|
||||
NSInteger length = [[_headers objectForKey:@"Content-Length"] integerValue];
|
||||
if (length < 0) {
|
||||
DNOT_REACHED();
|
||||
[self release];
|
||||
return nil;
|
||||
}
|
||||
_length = length;
|
||||
|
||||
if ((_length > 0) && (_type == nil)) {
|
||||
_type = [kGCDWebServerDefaultMimeType copy];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
[_method release];
|
||||
[_url release];
|
||||
[_headers release];
|
||||
[_path release];
|
||||
[_query release];
|
||||
[_type release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (BOOL) hasBody {
|
||||
return _type ? YES : NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerRequest (Subclassing)
|
||||
|
||||
- (BOOL) open {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSInteger) write:(const void*)buffer maxLength:(NSUInteger)length {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return -1;
|
||||
}
|
||||
|
||||
- (BOOL) close {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerDataRequest
|
||||
|
||||
@synthesize data=_data;
|
||||
|
||||
- (void) dealloc {
|
||||
DCHECK(_data != nil);
|
||||
[_data release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (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 = [[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]] retain];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
DCHECK(_file < 0);
|
||||
unlink([_filePath fileSystemRepresentation]);
|
||||
[_filePath release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (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 {
|
||||
[_arguments release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (BOOL) close {
|
||||
if (![super close]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString* charset = _ExtractHeaderParameter(self.contentType, @"charset");
|
||||
NSString* string = [[NSString alloc] initWithData:self.data encoding:_StringEncodingFromCharset(charset)];
|
||||
_arguments = [GCDWebServerParseURLEncodedForm(string) retain];
|
||||
[string release];
|
||||
|
||||
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 = [[[components objectAtIndex:0] lowercaseString] retain];
|
||||
}
|
||||
if (_mimeType == nil) {
|
||||
_mimeType = @"text/plain";
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
[_contentType release];
|
||||
[_mimeType release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerMultiPartArgument
|
||||
|
||||
@synthesize data=_data, string=_string;
|
||||
|
||||
- (id) initWithContentType:(NSString*)contentType data:(NSData*)data {
|
||||
if ((self = [super initWithContentType:contentType])) {
|
||||
_data = [data retain];
|
||||
|
||||
if ([self.mimeType hasPrefix:@"text/"]) {
|
||||
NSString* charset = _ExtractHeaderParameter(self.contentType, @"charset");
|
||||
_string = [[NSString alloc] initWithData:_data encoding:_StringEncodingFromCharset(charset)];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
[_data release];
|
||||
[_string release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (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]);
|
||||
|
||||
[_fileName release];
|
||||
[_temporaryPath release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (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) {
|
||||
_boundary = [[[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding] retain];
|
||||
}
|
||||
if (_boundary == nil) {
|
||||
DNOT_REACHED();
|
||||
[self release];
|
||||
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) {
|
||||
|
||||
[_controlName release];
|
||||
_controlName = nil;
|
||||
[_fileName release];
|
||||
_fileName = nil;
|
||||
[_contentType release];
|
||||
_contentType = nil;
|
||||
[_tmpPath release];
|
||||
_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 = [(id)CFHTTPMessageCopyAllHeaderFields(message) autorelease];
|
||||
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 = [[headers objectForKey:@"Content-Type"] retain];
|
||||
}
|
||||
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) {
|
||||
int result = write(_tmpFile, dataBytes, dataLength);
|
||||
if (result == dataLength) {
|
||||
if (close(_tmpFile) == 0) {
|
||||
_tmpFile = 0;
|
||||
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithContentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
||||
[_files setObject:file forKey:_controlName];
|
||||
[file release];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
[_tmpPath release];
|
||||
_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];
|
||||
[argument release];
|
||||
[data release];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
int result = write(_tmpFile, _parserData.bytes, length);
|
||||
if (result == 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);
|
||||
[_parserData release];
|
||||
_parserData = nil;
|
||||
[_controlName release];
|
||||
[_fileName release];
|
||||
[_contentType release];
|
||||
if (_tmpFile > 0) {
|
||||
close(_tmpFile);
|
||||
unlink([_tmpPath fileSystemRepresentation]);
|
||||
}
|
||||
[_tmpPath release];
|
||||
return (_parserState == kParserState_End ? YES : NO);
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
DCHECK(_parserData == nil);
|
||||
[_arguments release];
|
||||
[_files release];
|
||||
[_boundary release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright (c) 2012-2013, 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.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may 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 <COPYRIGHT HOLDER> 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 <Foundation/Foundation.h>
|
||||
|
||||
@interface GCDWebServerResponse : NSObject {
|
||||
@private
|
||||
NSString* _type;
|
||||
NSUInteger _length;
|
||||
NSInteger _status;
|
||||
NSUInteger _maxAge;
|
||||
NSMutableDictionary* _headers;
|
||||
}
|
||||
@property(nonatomic, readonly) NSString* contentType;
|
||||
@property(nonatomic, readonly) NSUInteger contentLength;
|
||||
@property(nonatomic) NSInteger statusCode; // Default is 200
|
||||
@property(nonatomic) NSUInteger cacheControlMaxAge; // Default is 0 seconds i.e. "no-cache"
|
||||
@property(nonatomic, readonly) NSDictionary* additionalHeaders;
|
||||
+ (GCDWebServerResponse*) response;
|
||||
- (id) init;
|
||||
- (id) initWithContentType:(NSString*)type contentLength:(NSUInteger)length; // Pass nil contentType to indicate empty body
|
||||
- (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;
|
||||
- (id) initWithStatusCode:(NSInteger)statusCode;
|
||||
- (id) initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerDataResponse : GCDWebServerResponse {
|
||||
@private
|
||||
NSData* _data;
|
||||
NSInteger _offset;
|
||||
}
|
||||
+ (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; // Simple template system that replaces all occurences of "%variable%" with corresponding value (encodes using UTF-8)
|
||||
- (id) initWithText:(NSString*)text; // Encodes using UTF-8
|
||||
- (id) initWithHTML:(NSString*)html; // Encodes using UTF-8
|
||||
- (id) initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerFileResponse : GCDWebServerResponse {
|
||||
@private
|
||||
NSString* _path;
|
||||
int _file;
|
||||
}
|
||||
+ (GCDWebServerFileResponse*) responseWithFile:(NSString*)path;
|
||||
+ (GCDWebServerFileResponse*) responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
||||
- (id) initWithFile:(NSString*)path;
|
||||
- (id) initWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
||||
@end
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
Copyright (c) 2012-2013, 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.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may 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 <COPYRIGHT HOLDER> 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 <sys/stat.h>
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
@implementation GCDWebServerResponse
|
||||
|
||||
@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, additionalHeaders=_headers;
|
||||
|
||||
+ (GCDWebServerResponse*) response {
|
||||
return [[[[self class] alloc] init] autorelease];
|
||||
}
|
||||
|
||||
- (id) init {
|
||||
return [self initWithContentType:nil contentLength:0];
|
||||
}
|
||||
|
||||
- (id) initWithContentType:(NSString*)type contentLength:(NSUInteger)length {
|
||||
if ((self = [super init])) {
|
||||
_type = [type copy];
|
||||
_length = length;
|
||||
_status = 200;
|
||||
_maxAge = 0;
|
||||
_headers = [[NSMutableDictionary alloc] init];
|
||||
|
||||
if ((_length > 0) && (_type == nil)) {
|
||||
_type = [kGCDWebServerDefaultMimeType copy];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
[_type release];
|
||||
[_headers release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void) setValue:(NSString*)value forAdditionalHeader:(NSString*)header {
|
||||
[_headers setValue:value forKey:header];
|
||||
}
|
||||
|
||||
- (BOOL) hasBody {
|
||||
return _type ? YES : NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerResponse (Subclassing)
|
||||
|
||||
- (BOOL) open {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSInteger) read:(void*)buffer maxLength:(NSUInteger)length {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return -1;
|
||||
}
|
||||
|
||||
- (BOOL) close {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerResponse (Extensions)
|
||||
|
||||
+ (GCDWebServerResponse*) responseWithStatusCode:(NSInteger)statusCode {
|
||||
return [[[self alloc] initWithStatusCode:statusCode] autorelease];
|
||||
}
|
||||
|
||||
+ (GCDWebServerResponse*) responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
|
||||
return [[[self alloc] initWithRedirect:location permanent:permanent] autorelease];
|
||||
}
|
||||
|
||||
- (id) initWithStatusCode:(NSInteger)statusCode {
|
||||
if ((self = [self initWithContentType:nil contentLength:0])) {
|
||||
self.statusCode = statusCode;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) initWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
|
||||
if ((self = [self initWithContentType:nil contentLength:0])) {
|
||||
self.statusCode = permanent ? 301 : 307;
|
||||
[self setValue:[location absoluteString] forAdditionalHeader:@"Location"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerDataResponse
|
||||
|
||||
+ (GCDWebServerDataResponse*) responseWithData:(NSData*)data contentType:(NSString*)type {
|
||||
return [[[[self class] alloc] initWithData:data contentType:type] autorelease];
|
||||
}
|
||||
|
||||
- (id) initWithData:(NSData*)data contentType:(NSString*)type {
|
||||
if (data == nil) {
|
||||
DNOT_REACHED();
|
||||
[self release];
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ((self = [super initWithContentType:type contentLength:data.length])) {
|
||||
_data = [data retain];
|
||||
_offset = -1;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
DCHECK(_offset < 0);
|
||||
[_data release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (BOOL) open {
|
||||
DCHECK(_offset < 0);
|
||||
_offset = 0;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSInteger) read:(void*)buffer maxLength:(NSUInteger)length {
|
||||
DCHECK(_offset >= 0);
|
||||
NSInteger size = 0;
|
||||
if (_offset < _data.length) {
|
||||
size = MIN(_data.length - _offset, length);
|
||||
bcopy((char*)_data.bytes + _offset, buffer, size);
|
||||
_offset += size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
- (BOOL) close {
|
||||
DCHECK(_offset >= 0);
|
||||
_offset = -1;
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerDataResponse (Extensions)
|
||||
|
||||
+ (GCDWebServerDataResponse*) responseWithText:(NSString*)text {
|
||||
return [[[self alloc] initWithText:text] autorelease];
|
||||
}
|
||||
|
||||
+ (GCDWebServerDataResponse*) responseWithHTML:(NSString*)html {
|
||||
return [[[self alloc] initWithHTML:html] autorelease];
|
||||
}
|
||||
|
||||
+ (GCDWebServerDataResponse*) responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables {
|
||||
return [[[self alloc] initWithHTMLTemplate:path variables:variables] autorelease];
|
||||
}
|
||||
|
||||
- (id) initWithText:(NSString*)text {
|
||||
NSData* data = [text dataUsingEncoding:NSUTF8StringEncoding];
|
||||
if (data == nil) {
|
||||
DNOT_REACHED();
|
||||
[self release];
|
||||
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();
|
||||
[self release];
|
||||
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];
|
||||
[html release];
|
||||
return response;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerFileResponse
|
||||
|
||||
+ (GCDWebServerFileResponse*) responseWithFile:(NSString*)path {
|
||||
return [[[[self class] alloc] initWithFile:path] autorelease];
|
||||
}
|
||||
|
||||
+ (GCDWebServerFileResponse*) responseWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
||||
return [[[[self class] alloc] initWithFile:path isAttachment:attachment] autorelease];
|
||||
}
|
||||
|
||||
- (id) initWithFile:(NSString*)path {
|
||||
return [self initWithFile:path isAttachment:NO];
|
||||
}
|
||||
|
||||
- (id) initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
||||
struct stat info;
|
||||
if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
|
||||
DNOT_REACHED();
|
||||
[self release];
|
||||
return nil;
|
||||
}
|
||||
NSString* type = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
|
||||
if (type == nil) {
|
||||
type = kGCDWebServerDefaultMimeType;
|
||||
}
|
||||
|
||||
if ((self = [super initWithContentType:type contentLength:info.st_size])) {
|
||||
_path = [path copy];
|
||||
if (attachment) {
|
||||
NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES]; // ISO 8859-1
|
||||
NSString* fileName = data ? [[[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] autorelease] : nil;
|
||||
if (fileName) {
|
||||
[self setValue:[NSString stringWithFormat:@"attachment; filename=\"%@\"", fileName] forAdditionalHeader:@"Content-Disposition"]; // TODO: Use http://tools.ietf.org/html/rfc5987
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc {
|
||||
DCHECK(_file <= 0);
|
||||
[_path release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (BOOL) open {
|
||||
DCHECK(_file <= 0);
|
||||
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
|
||||
return (_file > 0 ? YES : NO);
|
||||
}
|
||||
|
||||
- (NSInteger) read:(void*)buffer maxLength:(NSUInteger)length {
|
||||
DCHECK(_file > 0);
|
||||
return read(_file, buffer, length);
|
||||
}
|
||||
|
||||
- (BOOL) close {
|
||||
DCHECK(_file > 0);
|
||||
int result = close(_file);
|
||||
_file = 0;
|
||||
return (result == 0 ? YES : NO);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,222 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
E208D143167B723200500836 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E208D142167B723200500836 /* libsqlite3.dylib */; };
|
||||
E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E208D148167B76B700500836 /* CFNetwork.framework */; };
|
||||
E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E208D1B2167BB17E00500836 /* CoreServices.framework */; };
|
||||
E209F812169005AB00FF3062 /* GCDWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E209F80D169005AB00FF3062 /* GCDWebServer.m */; };
|
||||
E209F813169005AB00FF3062 /* GCDWebServerRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E209F80F169005AB00FF3062 /* GCDWebServerRequest.m */; };
|
||||
E209F814169005AB00FF3062 /* GCDWebServerResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E209F811169005AB00FF3062 /* GCDWebServerResponse.m */; };
|
||||
E2EE638D147DAE630004D40B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E2EE638C147DAE630004D40B /* main.m */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
8DD76FB20486AB0100D96B5E /* GCDWebServer */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = GCDWebServer; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E208D142167B723200500836 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; };
|
||||
E208D148167B76B700500836 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; };
|
||||
E208D1B2167BB17E00500836 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
|
||||
E209F80C169005AB00FF3062 /* GCDWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServer.h; sourceTree = "<group>"; };
|
||||
E209F80D169005AB00FF3062 /* GCDWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServer.m; sourceTree = "<group>"; };
|
||||
E209F80E169005AB00FF3062 /* GCDWebServerRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerRequest.h; sourceTree = "<group>"; };
|
||||
E209F80F169005AB00FF3062 /* GCDWebServerRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerRequest.m; sourceTree = "<group>"; };
|
||||
E209F810169005AB00FF3062 /* GCDWebServerResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerResponse.h; sourceTree = "<group>"; };
|
||||
E209F811169005AB00FF3062 /* GCDWebServerResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerResponse.m; sourceTree = "<group>"; };
|
||||
E2448DF616900A550069FA25 /* GCDWebServerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerPrivate.h; sourceTree = "<group>"; };
|
||||
E2EE638C147DAE630004D40B /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
8DD76FAD0486AB0100D96B5E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */,
|
||||
E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */,
|
||||
E208D143167B723200500836 /* libsqlite3.dylib in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
08FB7794FE84155DC02AAC07 /* LittleCMS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
08FB7795FE84155DC02AAC07 /* Source */,
|
||||
E282F1A7150FF0630004D7C0 /* Frameworks and Libraries */,
|
||||
1AB674ADFE9D54B511CA2CBB /* Products */,
|
||||
);
|
||||
name = LittleCMS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
08FB7795FE84155DC02AAC07 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2EE638C147DAE630004D40B /* main.m */,
|
||||
E209F80B169005AB00FF3062 /* CGDWebServer */,
|
||||
);
|
||||
name = Source;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1AB674ADFE9D54B511CA2CBB /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8DD76FB20486AB0100D96B5E /* GCDWebServer */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E209F80B169005AB00FF3062 /* CGDWebServer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E209F80C169005AB00FF3062 /* GCDWebServer.h */,
|
||||
E209F80D169005AB00FF3062 /* GCDWebServer.m */,
|
||||
E2448DF616900A550069FA25 /* GCDWebServerPrivate.h */,
|
||||
E209F80E169005AB00FF3062 /* GCDWebServerRequest.h */,
|
||||
E209F80F169005AB00FF3062 /* GCDWebServerRequest.m */,
|
||||
E209F810169005AB00FF3062 /* GCDWebServerResponse.h */,
|
||||
E209F811169005AB00FF3062 /* GCDWebServerResponse.m */,
|
||||
);
|
||||
path = CGDWebServer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E282F1A7150FF0630004D7C0 /* Frameworks and Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E208D1B2167BB17E00500836 /* CoreServices.framework */,
|
||||
E208D148167B76B700500836 /* CFNetwork.framework */,
|
||||
E208D142167B723200500836 /* libsqlite3.dylib */,
|
||||
);
|
||||
name = "Frameworks and Libraries";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
8DD76FA90486AB0100D96B5E /* GCDWebServer */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 1DEB928508733DD80010E9CD /* Build configuration list for PBXNativeTarget "GCDWebServer" */;
|
||||
buildPhases = (
|
||||
8DD76FAB0486AB0100D96B5E /* Sources */,
|
||||
8DD76FAD0486AB0100D96B5E /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = GCDWebServer;
|
||||
productInstallPath = "$(HOME)/bin";
|
||||
productName = LittleCMS;
|
||||
productReference = 8DD76FB20486AB0100D96B5E /* GCDWebServer */;
|
||||
productType = "com.apple.product-type.tool";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
08FB7793FE84155DC02AAC07 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
buildConfigurationList = 1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 1;
|
||||
knownRegions = (
|
||||
English,
|
||||
Japanese,
|
||||
French,
|
||||
German,
|
||||
);
|
||||
mainGroup = 08FB7794FE84155DC02AAC07 /* LittleCMS */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
8DD76FA90486AB0100D96B5E /* GCDWebServer */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
8DD76FAB0486AB0100D96B5E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E2EE638D147DAE630004D40B /* main.m in Sources */,
|
||||
E209F812169005AB00FF3062 /* GCDWebServer.m in Sources */,
|
||||
E209F813169005AB00FF3062 /* GCDWebServerRequest.m in Sources */,
|
||||
E209F814169005AB00FF3062 /* GCDWebServerResponse.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
1DEB928608733DD80010E9CD /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
1DEB928708733DD80010E9CD /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
1DEB928A08733DD80010E9CD /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD_32_64_BIT)";
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.7;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
WARNING_CFLAGS = "-Wall";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
1DEB928B08733DD80010E9CD /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD_32_64_BIT)";
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
NDEBUG,
|
||||
NS_BLOCK_ASSERTIONS,
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.7;
|
||||
SDKROOT = macosx;
|
||||
WARNING_CFLAGS = "-Wall";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
1DEB928508733DD80010E9CD /* Build configuration list for PBXNativeTarget "GCDWebServer" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1DEB928608733DD80010E9CD /* Debug */,
|
||||
1DEB928708733DD80010E9CD /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1DEB928A08733DD80010E9CD /* Debug */,
|
||||
1DEB928B08733DD80010E9CD /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 08FB7793FE84155DC02AAC07 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright (c) 2012-2013, 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.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may 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 <COPYRIGHT HOLDER> 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 "GCDWebServer.h"
|
||||
|
||||
int main(int argc, const char* argv[]) {
|
||||
BOOL success = NO;
|
||||
@autoreleasepool {
|
||||
GCDWebServer* webServer = [[GCDWebServer alloc] init];
|
||||
[webServer addHandlerForBasePath:@"/" localPath:NSHomeDirectory() indexFilename:nil cacheAge:0];
|
||||
success = [webServer runWithPort:8080];
|
||||
[webServer release];
|
||||
}
|
||||
return success ? 0 : -1;
|
||||
}
|
||||
Reference in New Issue
Block a user