mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-05-13 00:02:02 +08:00
Organized source code in subfolders
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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>
|
||||
|
||||
#import "GCDWebServerRequest.h"
|
||||
#import "GCDWebServerResponse.h"
|
||||
|
||||
typedef NS_ENUM(int, GCDWebServerLogLevel) {
|
||||
kGCDWebServerLogLevel_Debug = 0, // Only available if "NDEBUG" is not defined when building
|
||||
kGCDWebServerLogLevel_Verbose,
|
||||
kGCDWebServerLogLevel_Info,
|
||||
kGCDWebServerLogLevel_Warning,
|
||||
kGCDWebServerLogLevel_Error,
|
||||
kGCDWebServerLogLevel_Exception,
|
||||
};
|
||||
|
||||
typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery);
|
||||
typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request);
|
||||
|
||||
@interface GCDWebServer : NSObject
|
||||
@property(nonatomic, readonly, getter=isRunning) BOOL running;
|
||||
@property(nonatomic, readonly) NSUInteger port;
|
||||
@property(nonatomic, readonly) NSString* bonjourName; // Only non-nil if Bonjour registration is active
|
||||
- (instancetype)init;
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||
- (void)removeAllHandlers;
|
||||
|
||||
- (BOOL)start; // Default is port 8080 (OS X & iOS Simulator) or 80 (iOS) and computer name
|
||||
- (BOOL)startWithPort:(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
|
||||
+ (BOOL)shouldAutomaticallyMapHEADToGET; // Default is YES which means HEAD requests are mapped to GET requests with the response body being discarded
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Extensions)
|
||||
@property(nonatomic, readonly) NSURL* serverURL; // Only non-nil if server is running
|
||||
@property(nonatomic, readonly) NSURL* bonjourServerURL; // Only non-nil if server is running and Bonjour registration is active
|
||||
#if !TARGET_OS_IPHONE
|
||||
@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled; // Creates files in the current directory containing the raw data for all requests and responses (directory most NOT contain prior recordings)
|
||||
- (BOOL)runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only)
|
||||
#endif
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port; // Returns number of failed tests or -1 if server failed to start
|
||||
#endif
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Handlers)
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Path is case-insensitive
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (GETHandlers)
|
||||
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge; // Path is case-insensitive
|
||||
- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Path is case-insensitive
|
||||
- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Base path is recursive and case-sensitive
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Logging)
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
+ (void)setLogLevel:(GCDWebServerLogLevel)level; // Default level is DEBUG or INFO if "NDEBUG" is defined when building (it can also be set at runtime with the "logLevel" environment variable)
|
||||
#endif
|
||||
- (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
- (void)logInfo:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
@end
|
||||
@@ -0,0 +1,772 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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>
|
||||
#import <netinet/in.h>
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR
|
||||
#define kDefaultPort 80
|
||||
#else
|
||||
#define kDefaultPort 8080
|
||||
#endif
|
||||
#define kMaxPendingConnections 16
|
||||
|
||||
@interface GCDWebServer () {
|
||||
@private
|
||||
NSMutableArray* _handlers;
|
||||
|
||||
NSUInteger _port;
|
||||
dispatch_source_t _source;
|
||||
CFNetServiceRef _service;
|
||||
#if !TARGET_OS_IPHONE
|
||||
BOOL _recording;
|
||||
#endif
|
||||
}
|
||||
@end
|
||||
|
||||
@interface GCDWebServerHandler () {
|
||||
@private
|
||||
GCDWebServerMatchBlock _matchBlock;
|
||||
GCDWebServerProcessBlock _processBlock;
|
||||
}
|
||||
@end
|
||||
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
#ifdef NDEBUG
|
||||
GCDWebServerLogLevel GCDLogLevel = kGCDWebServerLogLevel_Info;
|
||||
#else
|
||||
GCDWebServerLogLevel GCDLogLevel = kGCDWebServerLogLevel_Debug;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
static BOOL _run;
|
||||
#endif
|
||||
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
|
||||
void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) {
|
||||
static const char* levelNames[] = {"DEBUG", "VERBOSE", "INFO", "WARNING", "ERROR", "EXCEPTION"};
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
va_end(arguments);
|
||||
fprintf(stderr, "[%s] %s\n", levelNames[level], [message UTF8String]);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
|
||||
static void _SignalHandler(int signal) {
|
||||
_run = NO;
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@implementation GCDWebServerHandler
|
||||
|
||||
@synthesize matchBlock=_matchBlock, processBlock=_processBlock;
|
||||
|
||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock {
|
||||
if ((self = [super init])) {
|
||||
_matchBlock = [matchBlock copy];
|
||||
_processBlock = [processBlock copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_matchBlock);
|
||||
ARC_RELEASE(_processBlock);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer
|
||||
|
||||
@synthesize handlers=_handlers, port=_port;
|
||||
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
|
||||
+ (void)load {
|
||||
const char* logLevel = getenv("logLevel");
|
||||
if (logLevel) {
|
||||
GCDLogLevel = atoi(logLevel);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+ (void)initialize {
|
||||
GCDWebServerInitializeFunctions();
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
_handlers = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_source) {
|
||||
[self stop];
|
||||
}
|
||||
|
||||
ARC_RELEASE(_handlers);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
- (NSString*)bonjourName {
|
||||
CFStringRef name = _service ? CFNetServiceGetName(_service) : NULL;
|
||||
return name && CFStringGetLength(name) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil;
|
||||
}
|
||||
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
|
||||
DCHECK(_source == NULL);
|
||||
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
|
||||
[_handlers insertObject:handler atIndex:0];
|
||||
ARC_RELEASE(handler);
|
||||
}
|
||||
|
||||
- (void)removeAllHandlers {
|
||||
DCHECK(_source == NULL);
|
||||
[_handlers removeAllObjects];
|
||||
}
|
||||
|
||||
- (BOOL)start {
|
||||
return [self startWithPort:kDefaultPort bonjourName:@""];
|
||||
}
|
||||
|
||||
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||
@autoreleasepool {
|
||||
if (error->error) {
|
||||
LOG_ERROR(@"Bonjour error %i (domain %i)", (int)error->error, (int)error->domain);
|
||||
} else {
|
||||
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
||||
LOG_INFO(@"%@ now reachable at %@", [server class], server.bonjourServerURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name {
|
||||
DCHECK(_source == NULL);
|
||||
int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (listeningSocket > 0) {
|
||||
int yes = 1;
|
||||
setsockopt(listeningSocket, 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 (bind(listeningSocket, (void*)&addr4, sizeof(addr4)) == 0) {
|
||||
if (listen(listeningSocket, kMaxPendingConnections) == 0) {
|
||||
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, kGCDWebServerGCDQueue);
|
||||
dispatch_source_set_cancel_handler(_source, ^{
|
||||
|
||||
@autoreleasepool {
|
||||
int result = close(listeningSocket);
|
||||
if (result != 0) {
|
||||
LOG_ERROR(@"Failed closing socket (%i): %s", errno, strerror(errno));
|
||||
} else {
|
||||
LOG_DEBUG(@"Closed listening socket");
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
dispatch_source_set_event_handler(_source, ^{
|
||||
|
||||
@autoreleasepool {
|
||||
struct sockaddr remoteSockAddr;
|
||||
socklen_t remoteAddrLen = sizeof(remoteSockAddr);
|
||||
int socket = accept(listeningSocket, &remoteSockAddr, &remoteAddrLen);
|
||||
if (socket > 0) {
|
||||
NSData* remoteAddress = [NSData dataWithBytes:&remoteSockAddr length:remoteAddrLen];
|
||||
|
||||
struct sockaddr localSockAddr;
|
||||
socklen_t localAddrLen = sizeof(localSockAddr);
|
||||
NSData* localAddress = nil;
|
||||
if (getsockname(socket, &localSockAddr, &localAddrLen) == 0) {
|
||||
localAddress = [NSData dataWithBytes:&localSockAddr length:localAddrLen];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
|
||||
int noSigPipe = 1;
|
||||
setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); // Make sure this socket cannot generate SIG_PIPE
|
||||
|
||||
Class connectionClass = [[self class] connectionClass];
|
||||
GCDWebServerConnection* connection = [[connectionClass alloc] initWithServer:self localAddress:localAddress remoteAddress:remoteAddress socket:socket]; // Connection will automatically retain itself while opened
|
||||
#if __has_feature(objc_arc)
|
||||
[connection self]; // Prevent compiler from complaining about unused variable / useless statement
|
||||
#else
|
||||
[connection release];
|
||||
#endif
|
||||
} else {
|
||||
LOG_ERROR(@"Failed accepting socket (%i): %s", errno, strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (port == 0) { // Determine the actual port we are listening on
|
||||
struct sockaddr addr;
|
||||
socklen_t addrlen = sizeof(addr);
|
||||
if (getsockname(listeningSocket, &addr, &addrlen) == 0) {
|
||||
struct sockaddr_in* sockaddr = (struct sockaddr_in*)&addr;
|
||||
_port = ntohs(sockaddr->sin_port);
|
||||
} else {
|
||||
LOG_ERROR(@"Failed retrieving socket address (%i): %s", errno, strerror(errno));
|
||||
}
|
||||
} else {
|
||||
_port = port;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (ARC_BRIDGE CFStringRef)name, (SInt32)_port);
|
||||
if (_service) {
|
||||
CFNetServiceClientContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
||||
CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFStreamError error = {0};
|
||||
CFNetServiceRegisterWithOptions(_service, 0, &error);
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating CFNetService");
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_resume(_source);
|
||||
LOG_INFO(@"%@ started on port %i and reachable at %@", [self class], (int)_port, self.serverURL);
|
||||
} else {
|
||||
LOG_ERROR(@"Failed listening on socket (%i): %s", errno, strerror(errno));
|
||||
close(listeningSocket);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed binding socket (%i): %s", errno, strerror(errno));
|
||||
close(listeningSocket);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating socket (%i): %s", errno, strerror(errno));
|
||||
}
|
||||
return (_source ? YES : NO);
|
||||
}
|
||||
|
||||
- (BOOL)isRunning {
|
||||
return (_source ? YES : NO);
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
DCHECK(_source != NULL);
|
||||
if (_source) {
|
||||
if (_service) {
|
||||
CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFNetServiceSetClient(_service, NULL, NULL);
|
||||
CFRelease(_service);
|
||||
_service = NULL;
|
||||
}
|
||||
|
||||
dispatch_source_cancel(_source); // This will close the socket
|
||||
ARC_DISPATCH_RELEASE(_source);
|
||||
_source = NULL;
|
||||
|
||||
LOG_INFO(@"%@ stopped", [self class]);
|
||||
}
|
||||
_port = 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer (Subclassing)
|
||||
|
||||
+ (Class)connectionClass {
|
||||
return [GCDWebServerConnection class];
|
||||
}
|
||||
|
||||
+ (NSString*)serverName {
|
||||
return NSStringFromClass(self);
|
||||
}
|
||||
|
||||
+ (BOOL)shouldAutomaticallyMapHEADToGET {
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer (Extensions)
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
|
||||
- (void)setRecordingEnabled:(BOOL)flag {
|
||||
_recording = flag;
|
||||
}
|
||||
|
||||
- (BOOL)isRecordingEnabled {
|
||||
return _recording;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (NSURL*)serverURL {
|
||||
if (_source) {
|
||||
NSString* ipAddress = GCDWebServerGetPrimaryIPv4Address();
|
||||
if (ipAddress) {
|
||||
if (_port != 80) {
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", ipAddress, (int)_port]];
|
||||
} else {
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", ipAddress]];
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSURL*)bonjourServerURL {
|
||||
if (_source && _service) {
|
||||
CFStringRef name = CFNetServiceGetName(_service);
|
||||
if (name && CFStringGetLength(name)) {
|
||||
if (_port != 80) {
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@.local:%i/", name, (int)_port]];
|
||||
} else {
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@.local/", name]];
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
|
||||
- (BOOL)runWithPort:(NSUInteger)port {
|
||||
BOOL success = NO;
|
||||
_run = YES;
|
||||
void (*handler)(int) = signal(SIGINT, _SignalHandler);
|
||||
if (handler != SIG_ERR) {
|
||||
if ([self startWithPort:port bonjourName:@""]) {
|
||||
while (_run) {
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
|
||||
}
|
||||
[self stop];
|
||||
success = YES;
|
||||
}
|
||||
signal(SIGINT, handler);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
|
||||
static CFHTTPMessageRef _CreateHTTPMessageFromData(NSData* data, BOOL isRequest) {
|
||||
CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, isRequest);
|
||||
if (CFHTTPMessageAppendBytes(message, data.bytes, data.length)) {
|
||||
return message;
|
||||
}
|
||||
CFRelease(message);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData, NSUInteger port) {
|
||||
CFHTTPMessageRef response = NULL;
|
||||
int httpSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (httpSocket > 0) {
|
||||
struct sockaddr_in addr4;
|
||||
bzero(&addr4, sizeof(addr4));
|
||||
addr4.sin_len = sizeof(port);
|
||||
addr4.sin_family = AF_INET;
|
||||
addr4.sin_port = htons(8080);
|
||||
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
if (connect(httpSocket, (void*)&addr4, sizeof(addr4)) == 0) {
|
||||
if (write(httpSocket, inData.bytes, inData.length) == (ssize_t)inData.length) {
|
||||
NSMutableData* outData = [[NSMutableData alloc] initWithLength:(256 * 1024)];
|
||||
NSUInteger length = 0;
|
||||
while (1) {
|
||||
ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length);
|
||||
if (result < 0) {
|
||||
length = NSNotFound;
|
||||
break;
|
||||
} else if (result == 0) {
|
||||
break;
|
||||
}
|
||||
length += result;
|
||||
if (length >= outData.length) {
|
||||
outData.length = 2 * outData.length;
|
||||
}
|
||||
}
|
||||
if (length != NSNotFound) {
|
||||
outData.length = length;
|
||||
response = _CreateHTTPMessageFromData(outData, NO);
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
ARC_RELEASE(outData);
|
||||
}
|
||||
}
|
||||
close(httpSocket);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static void _LogResult(NSString* format, ...) {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
va_end(arguments);
|
||||
fprintf(stdout, "%s\n", [message UTF8String]);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port {
|
||||
NSArray* ignoredHeaders = @[@"Date", @"Etag"]; // Dates are always different by definition and ETags depend on file system node IDs
|
||||
NSInteger result = -1;
|
||||
if ([self startWithPort:port bonjourName:nil]) {
|
||||
|
||||
result = 0;
|
||||
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
|
||||
for (NSString* requestFile in files) {
|
||||
if (![requestFile hasSuffix:@".request"]) {
|
||||
continue;
|
||||
}
|
||||
@autoreleasepool {
|
||||
NSString* index = [[requestFile componentsSeparatedByString:@"-"] firstObject];
|
||||
BOOL success = NO;
|
||||
NSData* requestData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:requestFile]];
|
||||
if (requestData) {
|
||||
CFHTTPMessageRef request = _CreateHTTPMessageFromData(requestData, YES);
|
||||
if (request) {
|
||||
NSString* requestMethod = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(request));
|
||||
NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(request));
|
||||
_LogResult(@"[%i] %@ %@", (int)[index integerValue], requestMethod, requestURL.path);
|
||||
NSString* prefix = [index stringByAppendingString:@"-"];
|
||||
for (NSString* responseFile in files) {
|
||||
if ([responseFile hasPrefix:prefix] && [responseFile hasSuffix:@".response"]) {
|
||||
NSData* responseData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:responseFile]];
|
||||
if (responseData) {
|
||||
CFHTTPMessageRef expectedResponse = _CreateHTTPMessageFromData(responseData, NO);
|
||||
if (expectedResponse) {
|
||||
CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromPerformingRequest(requestData, port);
|
||||
if (actualResponse) {
|
||||
success = YES;
|
||||
|
||||
CFIndex expectedStatusCode = CFHTTPMessageGetResponseStatusCode(expectedResponse);
|
||||
CFIndex actualStatusCode = CFHTTPMessageGetResponseStatusCode(actualResponse);
|
||||
if (actualStatusCode != expectedStatusCode) {
|
||||
_LogResult(@" Status code not matching:\n Expected: %i\n Actual: %i", (int)expectedStatusCode, (int)actualStatusCode);
|
||||
success = NO;
|
||||
}
|
||||
|
||||
NSDictionary* expectedHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(expectedResponse));
|
||||
NSDictionary* actualHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(actualResponse));
|
||||
for (NSString* expectedHeader in expectedHeaders) {
|
||||
if ([ignoredHeaders containsObject:expectedHeader]) {
|
||||
continue;
|
||||
}
|
||||
NSString* expectedValue = [expectedHeaders objectForKey:expectedHeader];
|
||||
NSString* actualValue = [actualHeaders objectForKey:expectedHeader];
|
||||
if (![actualValue isEqualToString:expectedValue]) {
|
||||
_LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", expectedHeader, expectedValue, actualValue);
|
||||
success = NO;
|
||||
}
|
||||
}
|
||||
for (NSString* actualHeader in actualHeaders) {
|
||||
if (![expectedHeaders objectForKey:actualHeader]) {
|
||||
_LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", actualHeader, nil, [actualHeaders objectForKey:actualHeader]);
|
||||
success = NO;
|
||||
}
|
||||
}
|
||||
|
||||
NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse));
|
||||
NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse));
|
||||
if (![actualBody isEqualToData:expectedBody]) {
|
||||
_LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length);
|
||||
success = NO;
|
||||
#ifndef NDEBUG
|
||||
if (GCDWebServerIsTextContentType([expectedHeaders objectForKey:@"Content-Type"])) {
|
||||
NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]];
|
||||
NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]];
|
||||
if ([expectedBody writeToFile:expectedPath atomically:YES] && [actualBody writeToFile:actualPath atomically:YES]) {
|
||||
NSTask* task = [[NSTask alloc] init];
|
||||
[task setLaunchPath:@"/usr/bin/opendiff"];
|
||||
[task setArguments:@[expectedPath, actualPath]];
|
||||
[task launch];
|
||||
ARC_RELEASE(task);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
CFRelease(actualResponse);
|
||||
}
|
||||
CFRelease(expectedResponse);
|
||||
}
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
CFRelease(request);
|
||||
}
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
_LogResult(@"");
|
||||
if (!success) {
|
||||
++result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[self stop];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer (Handlers)
|
||||
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
||||
|
||||
if (![requestMethod isEqualToString:method]) {
|
||||
return nil;
|
||||
}
|
||||
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
} processBlock:block];
|
||||
}
|
||||
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
if ([path hasPrefix:@"/"] && [aClass 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 ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
} processBlock:block];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL];
|
||||
if (expression && [aClass 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 ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
} processBlock:block];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer (GETHandlers)
|
||||
|
||||
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge {
|
||||
GCDWebServerResponse* response = [GCDWebServerDataResponse responseWithData:staticData contentType:contentType];
|
||||
response.cacheControlMaxAge = cacheAge;
|
||||
[self addHandlerForMethod:@"GET" path:path requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
return response;
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests {
|
||||
[self addHandlerForMethod:@"GET" path:path requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
GCDWebServerResponse* response = nil;
|
||||
if (allowRangeRequests) {
|
||||
response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange isAttachment:isAttachment];
|
||||
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
|
||||
} else {
|
||||
response = [GCDWebServerFileResponse responseWithFile:filePath isAttachment:isAttachment];
|
||||
}
|
||||
response.cacheControlMaxAge = cacheAge;
|
||||
return response;
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path {
|
||||
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path];
|
||||
if (enumerator == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableString* html = [NSMutableString string];
|
||||
[html appendString:@"<!DOCTYPE html>\n"];
|
||||
[html appendString:@"<html><head><meta charset=\"utf-8\"></head><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)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests {
|
||||
if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) {
|
||||
#if __has_feature(objc_arc)
|
||||
GCDWebServer* __unsafe_unretained server = self;
|
||||
#else
|
||||
__block GCDWebServer* server = self;
|
||||
#endif
|
||||
[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 ARC_AUTORELEASE([[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
} processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
GCDWebServerResponse* response = nil;
|
||||
NSString* filePath = [directoryPath 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 [GCDWebServerFileResponse responseWithFile:indexPath];
|
||||
}
|
||||
}
|
||||
response = [server _responseWithContentsOfDirectory:filePath];
|
||||
} else {
|
||||
if (allowRangeRequests) {
|
||||
response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange];
|
||||
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
|
||||
} else {
|
||||
response = [GCDWebServerFileResponse responseWithFile:filePath];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
response.cacheControlMaxAge = cacheAge;
|
||||
} else {
|
||||
response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NotFound];
|
||||
}
|
||||
return response;
|
||||
|
||||
}];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer (Logging)
|
||||
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
|
||||
+ (void)setLogLevel:(GCDWebServerLogLevel)level {
|
||||
GCDLogLevel = level;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (void)logVerbose:(NSString*)format, ... {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
va_end(arguments);
|
||||
LOG_VERBOSE(@"%@", message);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (void)logInfo:(NSString*)format, ... {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
va_end(arguments);
|
||||
LOG_INFO(@"%@", message);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (void)logWarning:(NSString*)format, ... {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
va_end(arguments);
|
||||
LOG_WARNING(@"%@", message);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (void)logError:(NSString*)format, ... {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
va_end(arguments);
|
||||
LOG_ERROR(@"%@", message);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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"
|
||||
|
||||
@class GCDWebServerHandler;
|
||||
|
||||
@interface GCDWebServerConnection : NSObject
|
||||
@property(nonatomic, readonly) GCDWebServer* server;
|
||||
@property(nonatomic, readonly) NSData* localAddressData; // struct sockaddr
|
||||
@property(nonatomic, readonly) NSString* localAddressString;
|
||||
@property(nonatomic, readonly) NSData* remoteAddressData; // struct sockaddr
|
||||
@property(nonatomic, readonly) NSString* remoteAddressString;
|
||||
@property(nonatomic, readonly) NSUInteger totalBytesRead;
|
||||
@property(nonatomic, readonly) NSUInteger totalBytesWritten;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerConnection (Subclassing)
|
||||
- (void)open;
|
||||
- (void)didUpdateBytesRead; // Called from arbitrary thread after @totalBytesRead is updated - Default implementation does nothing
|
||||
- (void)didUpdateBytesWritten; // Called from arbitrary thread after @totalBytesWritten is updated - Default implementation does nothing
|
||||
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; // Only called if the request can be processed
|
||||
- (GCDWebServerResponse*)replaceResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request; // Default implementation replaces any response matching the "ETag" or "Last-Modified-Date" header of the request by a barebone "Not-Modified" (304) one
|
||||
- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; // If request headers was malformed, "request" will be nil
|
||||
- (void)close;
|
||||
@end
|
||||
@@ -0,0 +1,839 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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>
|
||||
#import <netdb.h>
|
||||
#if !TARGET_OS_IPHONE
|
||||
#import <libkern/OSAtomic.h>
|
||||
#endif
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
#define kHeadersReadBuffer 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);
|
||||
|
||||
static NSData* _CRLFData = nil;
|
||||
static NSData* _CRLFCRLFData = nil;
|
||||
static NSData* _continueData = nil;
|
||||
static NSData* _lastChunkData = nil;
|
||||
#if !TARGET_OS_IPHONE
|
||||
static int32_t _connectionCounter = 0;
|
||||
#endif
|
||||
|
||||
@interface GCDWebServerConnection () {
|
||||
@private
|
||||
GCDWebServer* _server;
|
||||
NSData* _localAddress;
|
||||
NSData* _remoteAddress;
|
||||
CFSocketNativeHandle _socket;
|
||||
NSUInteger _bytesRead;
|
||||
NSUInteger _bytesWritten;
|
||||
BOOL _virtualHEAD;
|
||||
|
||||
CFHTTPMessageRef _requestMessage;
|
||||
GCDWebServerRequest* _request;
|
||||
GCDWebServerHandler* _handler;
|
||||
CFHTTPMessageRef _responseMessage;
|
||||
GCDWebServerResponse* _response;
|
||||
NSInteger _statusCode;
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
NSUInteger _connectionIndex;
|
||||
NSString* _requestPath;
|
||||
int _requestFD;
|
||||
NSString* _responsePath;
|
||||
int _responseFD;
|
||||
#endif
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerConnection (Read)
|
||||
|
||||
- (void)_readBufferWithLength:(NSUInteger)length completionBlock:(ReadBufferCompletionBlock)block {
|
||||
dispatch_read(_socket, length, kGCDWebServerGCDQueue, ^(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 %zu bytes on socket %i", size, _socket);
|
||||
_bytesRead += size;
|
||||
[self didUpdateBytesRead];
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (_requestFD > 0) {
|
||||
bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
|
||||
return (write(_requestFD, chunkBytes, chunkSize) == (ssize_t)chunkSize);
|
||||
});
|
||||
if (!success) {
|
||||
LOG_ERROR(@"Failed recording request data: %s (%i)", strerror(errno), errno);
|
||||
close(_requestFD);
|
||||
_requestFD = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
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 chunkOffset, const void* chunkBytes, size_t chunkSize) {
|
||||
[data appendBytes:chunkBytes length:chunkSize];
|
||||
return true;
|
||||
});
|
||||
block(data);
|
||||
ARC_RELEASE(data);
|
||||
} else {
|
||||
block(nil);
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)_readHeadersWithCompletionBlock:(ReadHeadersCompletionBlock)block {
|
||||
DCHECK(_requestMessage);
|
||||
[self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) {
|
||||
|
||||
if (buffer) {
|
||||
NSMutableData* data = [NSMutableData dataWithCapacity:kHeadersReadBuffer];
|
||||
dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
|
||||
[data appendBytes:chunkBytes length:chunkSize];
|
||||
return true;
|
||||
});
|
||||
NSRange range = [data rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, data.length)];
|
||||
if (range.location == NSNotFound) {
|
||||
if (CFHTTPMessageAppendBytes(_requestMessage, data.bytes, data.length)) {
|
||||
[self _readHeadersWithCompletionBlock:block];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed appending request headers data from socket %i", _socket);
|
||||
block(nil);
|
||||
}
|
||||
} 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] && ![_request usesChunkedTransferEncoding]);
|
||||
[self _readBufferWithLength:length completionBlock:^(dispatch_data_t buffer) {
|
||||
|
||||
if (buffer) {
|
||||
if (dispatch_data_get_size(buffer) <= length) {
|
||||
bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
|
||||
NSData* data = [NSData dataWithBytesNoCopy:(void*)chunkBytes length:chunkSize freeWhenDone:NO];
|
||||
NSError* error = nil;
|
||||
if (![_request performWriteData:data error:&error]) {
|
||||
LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (success) {
|
||||
NSUInteger remainingLength = length - dispatch_data_get_size(buffer);
|
||||
if (remainingLength) {
|
||||
[self _readBodyWithRemainingLength:remainingLength completionBlock:block];
|
||||
} else {
|
||||
block(YES);
|
||||
}
|
||||
} else {
|
||||
block(NO);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Unexpected extra content reading request body on socket %i", _socket);
|
||||
block(NO);
|
||||
DNOT_REACHED();
|
||||
}
|
||||
} else {
|
||||
block(NO);
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
char buffer[size + 1];
|
||||
bcopy(bytes, buffer, size);
|
||||
buffer[size] = 0;
|
||||
char* end = NULL;
|
||||
long result = strtol(buffer, &end, 16);
|
||||
return ((end != NULL) && (*end == 0) && (result >= 0) ? result : NSNotFound);
|
||||
}
|
||||
|
||||
- (void)_readNextBodyChunk:(NSMutableData*)chunkData completionBlock:(ReadBodyCompletionBlock)block {
|
||||
DCHECK([_request hasBody] && [_request usesChunkedTransferEncoding]);
|
||||
|
||||
while (1) {
|
||||
NSRange range = [chunkData rangeOfData:_CRLFData options:0 range:NSMakeRange(0, chunkData.length)];
|
||||
if (range.location == NSNotFound) {
|
||||
break;
|
||||
}
|
||||
NSRange extensionRange = [chunkData rangeOfData:[NSData dataWithBytes:";" length:1] options:0 range:NSMakeRange(0, range.location)]; // Ignore chunk extensions
|
||||
NSUInteger length = _ScanHexNumber((char*)chunkData.bytes, extensionRange.location != NSNotFound ? extensionRange.location : range.location);
|
||||
if (length != NSNotFound) {
|
||||
if (length) {
|
||||
if (chunkData.length < range.location + range.length + length + 2) {
|
||||
break;
|
||||
}
|
||||
const char* ptr = (char*)chunkData.bytes + range.location + range.length + length;
|
||||
if ((*ptr == '\r') && (*(ptr + 1) == '\n')) {
|
||||
NSError* error = nil;
|
||||
if ([_request performWriteData:[chunkData subdataWithRange:NSMakeRange(range.location + range.length, length)] error:&error]) {
|
||||
[chunkData replaceBytesInRange:NSMakeRange(0, range.location + range.length + length + 2) withBytes:NULL length:0];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
|
||||
block(NO);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Missing terminating CRLF sequence for chunk reading request body on socket %i", _socket);
|
||||
block(NO);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
NSRange trailerRange = [chunkData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(range.location, chunkData.length - range.location)]; // Ignore trailers
|
||||
if (trailerRange.location != NSNotFound) {
|
||||
block(YES);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Invalid chunk length reading request body on socket %i", _socket);
|
||||
block(NO);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) {
|
||||
|
||||
if (buffer) {
|
||||
dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
|
||||
[chunkData appendBytes:chunkBytes length:chunkSize];
|
||||
return true;
|
||||
});
|
||||
[self _readNextBodyChunk:chunkData completionBlock:block];
|
||||
} else {
|
||||
block(NO);
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerConnection (Write)
|
||||
|
||||
- (void)_writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block {
|
||||
size_t size = dispatch_data_get_size(buffer);
|
||||
#if !TARGET_OS_IPHONE
|
||||
ARC_DISPATCH_RETAIN(buffer);
|
||||
#endif
|
||||
dispatch_write(_socket, buffer, kGCDWebServerGCDQueue, ^(dispatch_data_t data, int error) {
|
||||
|
||||
@autoreleasepool {
|
||||
if (error == 0) {
|
||||
DCHECK(data == NULL);
|
||||
LOG_DEBUG(@"Connection sent %zu bytes on socket %i", size, _socket);
|
||||
_bytesWritten += size;
|
||||
[self didUpdateBytesWritten];
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (_responseFD > 0) {
|
||||
bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
|
||||
return (write(_responseFD, chunkBytes, chunkSize) == (ssize_t)chunkSize);
|
||||
});
|
||||
if (!success) {
|
||||
LOG_ERROR(@"Failed recording response data: %s (%i)", strerror(errno), errno);
|
||||
close(_responseFD);
|
||||
_responseFD = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
block(YES);
|
||||
} else {
|
||||
LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error);
|
||||
block(NO);
|
||||
}
|
||||
}
|
||||
#if !TARGET_OS_IPHONE
|
||||
ARC_DISPATCH_RELEASE(buffer);
|
||||
#endif
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
- (void)_writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block {
|
||||
#if !__has_feature(objc_arc)
|
||||
[data retain];
|
||||
#endif
|
||||
dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, kGCDWebServerGCDQueue, ^{
|
||||
#if __has_feature(objc_arc)
|
||||
[data self]; // Keeps ARC from releasing data too early
|
||||
#else
|
||||
[data release];
|
||||
#endif
|
||||
});
|
||||
[self _writeBuffer:buffer withCompletionBlock:block];
|
||||
ARC_DISPATCH_RELEASE(buffer);
|
||||
}
|
||||
|
||||
- (void)_writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block {
|
||||
DCHECK(_responseMessage);
|
||||
CFDataRef message = CFHTTPMessageCopySerializedMessage(_responseMessage);
|
||||
[self _writeData:(ARC_BRIDGE NSData*)message withCompletionBlock:block];
|
||||
CFRelease(message);
|
||||
}
|
||||
|
||||
- (void)_writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block {
|
||||
DCHECK([_response hasBody]);
|
||||
NSError* error = nil;
|
||||
NSData* data = [_response performReadData:&error];
|
||||
if (data) {
|
||||
if (data.length) {
|
||||
if (_response.usesChunkedTransferEncoding) {
|
||||
const char* hexString = [[NSString stringWithFormat:@"%lx", (unsigned long)data.length] UTF8String];
|
||||
size_t hexLength = strlen(hexString);
|
||||
NSData* chunk = [NSMutableData dataWithLength:(hexLength + 2 + data.length + 2)];
|
||||
if (chunk == nil) {
|
||||
LOG_ERROR(@"Failed allocating memory for response body chunk for socket %i: %@", _socket, error);
|
||||
block(NO);
|
||||
return;
|
||||
}
|
||||
char* ptr = (char*)[(NSMutableData*)chunk mutableBytes];
|
||||
bcopy(hexString, ptr, hexLength);
|
||||
ptr += hexLength;
|
||||
*ptr++ = '\r';
|
||||
*ptr++ = '\n';
|
||||
bcopy(data.bytes, ptr, data.length);
|
||||
ptr += data.length;
|
||||
*ptr++ = '\r';
|
||||
*ptr = '\n';
|
||||
data = chunk;
|
||||
}
|
||||
[self _writeData:data withCompletionBlock:^(BOOL success) {
|
||||
|
||||
if (success) {
|
||||
[self _writeBodyWithCompletionBlock:block];
|
||||
} else {
|
||||
block(NO);
|
||||
}
|
||||
|
||||
}];
|
||||
} else {
|
||||
if (_response.usesChunkedTransferEncoding) {
|
||||
[self _writeData:_lastChunkData withCompletionBlock:^(BOOL success) {
|
||||
|
||||
block(success);
|
||||
|
||||
}];
|
||||
} else {
|
||||
block(YES);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error);
|
||||
block(NO);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerConnection
|
||||
|
||||
@synthesize server=_server, localAddressData=_localAddress, remoteAddressData=_remoteAddress, totalBytesRead=_bytesRead, totalBytesWritten=_bytesWritten;
|
||||
|
||||
+ (void)initialize {
|
||||
if (_CRLFData == nil) {
|
||||
_CRLFData = [[NSData alloc] initWithBytes:"\r\n" length:2];
|
||||
DCHECK(_CRLFData);
|
||||
}
|
||||
if (_CRLFCRLFData == nil) {
|
||||
_CRLFCRLFData = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
|
||||
DCHECK(_CRLFCRLFData);
|
||||
}
|
||||
if (_continueData == nil) {
|
||||
CFHTTPMessageRef message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 100, NULL, kCFHTTPVersion1_1);
|
||||
#if __has_feature(objc_arc)
|
||||
_continueData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(message));
|
||||
#else
|
||||
_continueData = (NSData*)CFHTTPMessageCopySerializedMessage(message);
|
||||
#endif
|
||||
CFRelease(message);
|
||||
DCHECK(_continueData);
|
||||
}
|
||||
if (_lastChunkData == nil) {
|
||||
_lastChunkData = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_initializeResponseHeadersWithStatusCode:(NSInteger)statusCode {
|
||||
_statusCode = statusCode;
|
||||
_responseMessage = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_1);
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Connection"), CFSTR("Close"));
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Server"), (ARC_BRIDGE CFStringRef)[[_server class] serverName]);
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatRFC822([NSDate date]));
|
||||
}
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
- (void)_processRequest {
|
||||
DCHECK(_responseMessage == NULL);
|
||||
BOOL hasBody = NO;
|
||||
|
||||
GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock];
|
||||
if (response) {
|
||||
response = [self replaceResponse:response forRequest:_request];
|
||||
if (response) {
|
||||
if ([response hasBody]) {
|
||||
[response prepareForReading];
|
||||
hasBody = !_virtualHEAD;
|
||||
}
|
||||
NSError* error = nil;
|
||||
if (hasBody && ![response performOpen:&error]) {
|
||||
LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error);
|
||||
} else {
|
||||
_response = ARC_RETAIN(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_response) {
|
||||
[self _initializeResponseHeadersWithStatusCode:_response.statusCode];
|
||||
if (_response.lastModifiedDate) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Last-Modified"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatRFC822(_response.lastModifiedDate));
|
||||
}
|
||||
if (_response.eTag) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("ETag"), (ARC_BRIDGE CFStringRef)_response.eTag);
|
||||
}
|
||||
if ((_response.statusCode >= 200) && (_response.statusCode < 300)) {
|
||||
if (_response.cacheControlMaxAge > 0) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"max-age=%i, public", (int)_response.cacheControlMaxAge]);
|
||||
} else {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Cache-Control"), CFSTR("no-cache"));
|
||||
}
|
||||
}
|
||||
if (_response.contentType != nil) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType));
|
||||
}
|
||||
if (_response.contentLength != NSNotFound) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
|
||||
}
|
||||
if (_response.usesChunkedTransferEncoding) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Transfer-Encoding"), CFSTR("chunked"));
|
||||
}
|
||||
[_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, (ARC_BRIDGE CFStringRef)key, (ARC_BRIDGE CFStringRef)obj);
|
||||
}];
|
||||
[self _writeHeadersWithCompletionBlock:^(BOOL success) {
|
||||
|
||||
if (success) {
|
||||
if (hasBody) {
|
||||
[self _writeBodyWithCompletionBlock:^(BOOL successInner) {
|
||||
|
||||
[_response performClose]; // TODO: There's nothing we can do on failure as headers have already been sent
|
||||
|
||||
}];
|
||||
}
|
||||
} else if (hasBody) {
|
||||
[_response performClose];
|
||||
}
|
||||
|
||||
}];
|
||||
} else {
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)_readBodyWithLength:(NSUInteger)length initialData:(NSData*)initialData {
|
||||
NSError* error = nil;
|
||||
if (![_request performOpen:&error]) {
|
||||
LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
return;
|
||||
}
|
||||
|
||||
if (initialData.length) {
|
||||
if (![_request performWriteData:initialData error:&error]) {
|
||||
LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error);
|
||||
if (![_request performClose:&error]) {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
}
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
return;
|
||||
}
|
||||
length -= initialData.length;
|
||||
}
|
||||
|
||||
if (length) {
|
||||
[self _readBodyWithRemainingLength:length completionBlock:^(BOOL success) {
|
||||
|
||||
NSError* localError = nil;
|
||||
if ([_request performClose:&localError]) {
|
||||
[self _processRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
}
|
||||
|
||||
}];
|
||||
} else {
|
||||
if ([_request performClose:&error]) {
|
||||
[self _processRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_readChunkedBodyWithInitialData:(NSData*)initialData {
|
||||
NSError* error = nil;
|
||||
if (![_request performOpen:&error]) {
|
||||
LOG_ERROR(@"Failed opening request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableData* chunkData = [[NSMutableData alloc] initWithData:initialData];
|
||||
[self _readNextBodyChunk:chunkData completionBlock:^(BOOL success) {
|
||||
|
||||
NSError* localError = nil;
|
||||
if ([_request performClose:&localError]) {
|
||||
[self _processRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
}
|
||||
|
||||
}];
|
||||
ARC_RELEASE(chunkData);
|
||||
}
|
||||
|
||||
- (void)_readRequestHeaders {
|
||||
_requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true);
|
||||
[self _readHeadersWithCompletionBlock:^(NSData* extraData) {
|
||||
|
||||
if (extraData) {
|
||||
NSString* requestMethod = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(_requestMessage)); // Method verbs are case-sensitive and uppercase
|
||||
DCHECK(requestMethod);
|
||||
if ([[_server class] shouldAutomaticallyMapHEADToGET] && [requestMethod isEqualToString:@"HEAD"]) {
|
||||
requestMethod = @"GET";
|
||||
_virtualHEAD = YES;
|
||||
}
|
||||
NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(_requestMessage));
|
||||
DCHECK(requestURL);
|
||||
NSString* requestPath = GCDWebServerUnescapeURLString(ARC_BRIDGE_RELEASE(CFURLCopyPath((CFURLRef)requestURL))); // Don't use -[NSURL path] which strips the ending slash
|
||||
DCHECK(requestPath);
|
||||
NSDictionary* requestQuery = nil;
|
||||
NSString* queryString = ARC_BRIDGE_RELEASE(CFURLCopyQueryString((CFURLRef)requestURL, NULL)); // Don't use -[NSURL query] to make sure query is not unescaped;
|
||||
if (queryString.length) {
|
||||
requestQuery = GCDWebServerParseURLEncodedForm(queryString);
|
||||
DCHECK(requestQuery);
|
||||
}
|
||||
NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
|
||||
DCHECK(requestHeaders);
|
||||
for (_handler in _server.handlers) {
|
||||
_request = ARC_RETAIN(_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery));
|
||||
if (_request) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_request) {
|
||||
if ([_request hasBody]) {
|
||||
[_request prepareForWriting];
|
||||
if (_request.usesChunkedTransferEncoding || (extraData.length <= _request.contentLength)) {
|
||||
NSString* expectHeader = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(_requestMessage, CFSTR("Expect")));
|
||||
if (expectHeader) {
|
||||
if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) {
|
||||
[self _writeData:_continueData withCompletionBlock:^(BOOL success) {
|
||||
|
||||
if (success) {
|
||||
if (_request.usesChunkedTransferEncoding) {
|
||||
[self _readChunkedBodyWithInitialData:extraData];
|
||||
} else {
|
||||
[self _readBodyWithLength:_request.contentLength initialData:extraData];
|
||||
}
|
||||
}
|
||||
|
||||
}];
|
||||
} else {
|
||||
LOG_ERROR(@"Unsupported 'Expect' / 'Content-Length' header combination on socket %i", _socket);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_ExpectationFailed];
|
||||
}
|
||||
} else {
|
||||
if (_request.usesChunkedTransferEncoding) {
|
||||
[self _readChunkedBodyWithInitialData:extraData];
|
||||
} else {
|
||||
[self _readBodyWithLength:_request.contentLength initialData:extraData];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Unexpected 'Content-Length' header value on socket %i", _socket);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest];
|
||||
}
|
||||
} else {
|
||||
[self _processRequest];
|
||||
}
|
||||
} else {
|
||||
_request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery];
|
||||
DCHECK(_request);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_MethodNotAllowed];
|
||||
}
|
||||
} else {
|
||||
[self abortRequest:nil withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket {
|
||||
if ((self = [super init])) {
|
||||
_server = ARC_RETAIN(server);
|
||||
_localAddress = ARC_RETAIN(localAddress);
|
||||
_remoteAddress = ARC_RETAIN(remoteAddress);
|
||||
_socket = socket;
|
||||
|
||||
[self open];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static NSString* _StringFromAddressData(NSData* data) {
|
||||
NSString* string = nil;
|
||||
const struct sockaddr* addr = data.bytes;
|
||||
char hostBuffer[NI_MAXHOST];
|
||||
char serviceBuffer[NI_MAXSERV];
|
||||
if (getnameinfo(addr, addr->sa_len, hostBuffer, sizeof(hostBuffer), serviceBuffer, sizeof(serviceBuffer), NI_NUMERICHOST | NI_NUMERICSERV | NI_NOFQDN) >= 0) {
|
||||
string = [NSString stringWithFormat:@"%s:%s", hostBuffer, serviceBuffer];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
- (NSString*)localAddressString {
|
||||
return _StringFromAddressData(_localAddress);
|
||||
}
|
||||
|
||||
- (NSString*)remoteAddressString {
|
||||
return _StringFromAddressData(_remoteAddress);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self close];
|
||||
|
||||
ARC_RELEASE(_server);
|
||||
ARC_RELEASE(_localAddress);
|
||||
ARC_RELEASE(_remoteAddress);
|
||||
|
||||
if (_requestMessage) {
|
||||
CFRelease(_requestMessage);
|
||||
}
|
||||
ARC_RELEASE(_request);
|
||||
|
||||
if (_responseMessage) {
|
||||
CFRelease(_responseMessage);
|
||||
}
|
||||
ARC_RELEASE(_response);
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
ARC_RELEASE(_requestPath);
|
||||
ARC_RELEASE(_responsePath);
|
||||
#endif
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerConnection (Subclassing)
|
||||
|
||||
- (void)open {
|
||||
LOG_DEBUG(@"Did open connection on socket %i", _socket);
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (_server.recordingEnabled) {
|
||||
_connectionIndex = OSAtomicIncrement32(&_connectionCounter);
|
||||
|
||||
_requestPath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
|
||||
_requestFD = open([_requestPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY);
|
||||
DCHECK(_requestFD > 0);
|
||||
|
||||
_responsePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
|
||||
_responseFD = open([_responsePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY);
|
||||
DCHECK(_responseFD > 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
[self _readRequestHeaders];
|
||||
}
|
||||
|
||||
- (void)didUpdateBytesRead {
|
||||
;
|
||||
}
|
||||
|
||||
- (void)didUpdateBytesWritten {
|
||||
;
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block {
|
||||
LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
|
||||
GCDWebServerResponse* response = nil;
|
||||
@try {
|
||||
response = block(request);
|
||||
}
|
||||
@catch (NSException* exception) {
|
||||
LOG_EXCEPTION(exception);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
|
||||
static inline BOOL _CompareResources(NSString* responseETag, NSString* requestETag, NSDate* responseLastModified, NSDate* requestLastModified) {
|
||||
if ([requestETag isEqualToString:@"*"] && (!responseLastModified || !requestLastModified || ([responseLastModified compare:requestLastModified] != NSOrderedDescending))) {
|
||||
return YES;
|
||||
} else {
|
||||
if ([responseETag isEqualToString:requestETag]) {
|
||||
return YES;
|
||||
}
|
||||
if (responseLastModified && requestLastModified && ([responseLastModified compare:requestLastModified] != NSOrderedDescending)) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*)replaceResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request {
|
||||
if ((response.statusCode >= 200) && (response.statusCode < 300) && _CompareResources(response.eTag, request.ifNoneMatch, response.lastModifiedDate, request.ifModifiedSince)) {
|
||||
NSInteger code = [request.method isEqualToString:@"HEAD"] || [request.method isEqualToString:@"GET"] ? kGCDWebServerHTTPStatusCode_NotModified : kGCDWebServerHTTPStatusCode_PreconditionFailed;
|
||||
GCDWebServerResponse* newResponse = [GCDWebServerResponse responseWithStatusCode:code];
|
||||
newResponse.cacheControlMaxAge = response.cacheControlMaxAge;
|
||||
newResponse.lastModifiedDate = response.lastModifiedDate;
|
||||
newResponse.eTag = response.eTag;
|
||||
DCHECK(newResponse);
|
||||
return newResponse;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)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", (int)statusCode, _socket);
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
int result = close(_socket);
|
||||
if (result != 0) {
|
||||
LOG_ERROR(@"Failed closing socket %i for connection (%i): %s", _socket, errno, strerror(errno));
|
||||
} else {
|
||||
LOG_DEBUG(@"Did close connection on socket %i", _socket);
|
||||
}
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (_requestPath) {
|
||||
BOOL success = NO;
|
||||
NSError* error = nil;
|
||||
if (_requestFD > 0) {
|
||||
close(_requestFD);
|
||||
NSString* name = [NSString stringWithFormat:@"%03lu-%@.request", (unsigned long)_connectionIndex, _virtualHEAD ? @"HEAD" : _request.method];
|
||||
success = [[NSFileManager defaultManager] moveItemAtPath:_requestPath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error];
|
||||
}
|
||||
if (!success) {
|
||||
LOG_ERROR(@"Failed saving recorded request: %@", error);
|
||||
DNOT_REACHED();
|
||||
}
|
||||
unlink([_requestPath fileSystemRepresentation]);
|
||||
}
|
||||
|
||||
if (_responsePath) {
|
||||
BOOL success = NO;
|
||||
NSError* error = nil;
|
||||
if (_responseFD > 0) {
|
||||
close(_responseFD);
|
||||
NSString* name = [NSString stringWithFormat:@"%03lu-%i.response", (unsigned long)_connectionIndex, (int)_statusCode];
|
||||
success = [[NSFileManager defaultManager] moveItemAtPath:_responsePath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error];
|
||||
}
|
||||
if (!success) {
|
||||
LOG_ERROR(@"Failed saving recorded response: %@", error);
|
||||
DNOT_REACHED();
|
||||
}
|
||||
unlink([_responsePath fileSystemRepresentation]);
|
||||
}
|
||||
#endif
|
||||
if (_request) {
|
||||
LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten);
|
||||
} else {
|
||||
LOG_VERBOSE(@"[%@] %@ %i \"(invalid request)\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, (unsigned long)_bytesRead, (unsigned long)_bytesWritten);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension);
|
||||
NSString* GCDWebServerEscapeURLString(NSString* string);
|
||||
NSString* GCDWebServerUnescapeURLString(NSString* string);
|
||||
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form);
|
||||
NSString* GCDWebServerGetPrimaryIPv4Address(); // Returns IPv4 address of primary connected service on OS X or of WiFi interface on iOS if connected
|
||||
NSString* GCDWebServerFormatRFC822(NSDate* date);
|
||||
NSDate* GCDWebServerParseRFC822(NSString* string);
|
||||
NSString* GCDWebServerFormatISO8601(NSDate* date);
|
||||
NSDate* GCDWebServerParseISO8601(NSString* string);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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 <MobileCoreServices/MobileCoreServices.h>
|
||||
#else
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
#endif
|
||||
|
||||
#import <ifaddrs.h>
|
||||
#import <net/if.h>
|
||||
#import <netdb.h>
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
static NSDateFormatter* _dateFormatterRFC822 = nil;
|
||||
static NSDateFormatter* _dateFormatterISO8601 = nil;
|
||||
static dispatch_queue_t _dateFormatterQueue = NULL;
|
||||
|
||||
// HTTP/1.1 server must use RFC822
|
||||
// TODO: Handle RFC 850 and ANSI C's asctime() format (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3)
|
||||
void GCDWebServerInitializeFunctions() {
|
||||
DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
|
||||
if (_dateFormatterRFC822 == nil) {
|
||||
_dateFormatterRFC822 = [[NSDateFormatter alloc] init];
|
||||
_dateFormatterRFC822.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
|
||||
_dateFormatterRFC822.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
|
||||
_dateFormatterRFC822.locale = ARC_AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]);
|
||||
DCHECK(_dateFormatterRFC822);
|
||||
}
|
||||
if (_dateFormatterISO8601 == nil) {
|
||||
_dateFormatterISO8601 = [[NSDateFormatter alloc] init];
|
||||
_dateFormatterISO8601.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
|
||||
_dateFormatterISO8601.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
|
||||
_dateFormatterISO8601.locale = ARC_AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]);
|
||||
DCHECK(_dateFormatterISO8601);
|
||||
}
|
||||
if (_dateFormatterQueue == NULL) {
|
||||
_dateFormatterQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
|
||||
DCHECK(_dateFormatterQueue);
|
||||
}
|
||||
}
|
||||
|
||||
NSString* GCDWebServerNormalizeHeaderValue(NSString* value) {
|
||||
if (value) {
|
||||
NSRange range = [value rangeOfString:@";"]; // Assume part before ";" separator is case-insensitive
|
||||
if (range.location != NSNotFound) {
|
||||
value = [[[value substringToIndex:range.location] lowercaseString] stringByAppendingString:[value substringFromIndex:range.location]];
|
||||
} else {
|
||||
value = [value lowercaseString];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
NSString* GCDWebServerTruncateHeaderValue(NSString* value) {
|
||||
DCHECK([value isEqualToString:GCDWebServerNormalizeHeaderValue(value)]);
|
||||
NSRange range = [value rangeOfString:@";"];
|
||||
return range.location != NSNotFound ? [value substringToIndex:range.location] : value;
|
||||
}
|
||||
|
||||
NSString* GCDWebServerExtractHeaderValueParameter(NSString* value, NSString* name) {
|
||||
DCHECK([value isEqualToString:GCDWebServerNormalizeHeaderValue(value)]);
|
||||
NSString* parameter = nil;
|
||||
NSScanner* scanner = [[NSScanner alloc] initWithString:value];
|
||||
[scanner setCaseSensitive:NO]; // Assume parameter names are case-insensitive
|
||||
NSString* string = [NSString stringWithFormat:@"%@=", name];
|
||||
if ([scanner scanUpToString:string intoString:NULL]) {
|
||||
[scanner scanString:string intoString:NULL];
|
||||
if ([scanner scanString:@"\"" intoString:NULL]) {
|
||||
[scanner scanUpToString:@"\"" intoString:¶meter];
|
||||
} else {
|
||||
[scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:¶meter];
|
||||
}
|
||||
}
|
||||
ARC_RELEASE(scanner);
|
||||
return parameter;
|
||||
}
|
||||
|
||||
// http://www.w3schools.com/tags/ref_charactersets.asp
|
||||
NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset) {
|
||||
NSStringEncoding encoding = kCFStringEncodingInvalidId;
|
||||
if (charset) {
|
||||
encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)charset));
|
||||
}
|
||||
return (encoding != kCFStringEncodingInvalidId ? encoding : NSUTF8StringEncoding);
|
||||
}
|
||||
|
||||
NSString* GCDWebServerFormatRFC822(NSDate* date) {
|
||||
__block NSString* string;
|
||||
dispatch_sync(_dateFormatterQueue, ^{
|
||||
string = [_dateFormatterRFC822 stringFromDate:date];
|
||||
});
|
||||
return string;
|
||||
}
|
||||
|
||||
NSDate* GCDWebServerParseRFC822(NSString* string) {
|
||||
__block NSDate* date;
|
||||
dispatch_sync(_dateFormatterQueue, ^{
|
||||
date = [_dateFormatterRFC822 dateFromString:string];
|
||||
});
|
||||
return date;
|
||||
}
|
||||
|
||||
NSString* GCDWebServerFormatISO8601(NSDate* date) {
|
||||
__block NSString* string;
|
||||
dispatch_sync(_dateFormatterQueue, ^{
|
||||
string = [_dateFormatterISO8601 stringFromDate:date];
|
||||
});
|
||||
return string;
|
||||
}
|
||||
|
||||
NSDate* GCDWebServerParseISO8601(NSString* string) {
|
||||
__block NSDate* date;
|
||||
dispatch_sync(_dateFormatterQueue, ^{
|
||||
date = [_dateFormatterISO8601 dateFromString:string];
|
||||
});
|
||||
return date;
|
||||
}
|
||||
|
||||
BOOL GCDWebServerIsTextContentType(NSString* type) {
|
||||
return ([type hasPrefix:@"text/"] || [type hasPrefix:@"application/json"] || [type hasPrefix:@"application/xml"]);
|
||||
}
|
||||
|
||||
NSString* GCDWebServerDescribeData(NSData* data, NSString* type) {
|
||||
if (GCDWebServerIsTextContentType(type)) {
|
||||
NSString* charset = GCDWebServerExtractHeaderValueParameter(type, @"charset");
|
||||
NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)];
|
||||
if (string) {
|
||||
return ARC_AUTORELEASE(string);
|
||||
}
|
||||
}
|
||||
return [NSString stringWithFormat:@"<%lu bytes>", (unsigned long)data.length];
|
||||
}
|
||||
|
||||
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, (ARC_BRIDGE CFStringRef)extension, NULL);
|
||||
if (uti) {
|
||||
mimeType = ARC_BRIDGE_RELEASE(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
||||
CFRelease(uti);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mimeType ? mimeType : kGCDWebServerDefaultMimeType;
|
||||
}
|
||||
|
||||
NSString* GCDWebServerEscapeURLString(NSString* string) {
|
||||
return ARC_BRIDGE_RELEASE(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":@/?&=+"), kCFStringEncodingUTF8));
|
||||
}
|
||||
|
||||
NSString* GCDWebServerUnescapeURLString(NSString* string) {
|
||||
return ARC_BRIDGE_RELEASE(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
|
||||
}
|
||||
|
||||
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
|
||||
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:@" "];
|
||||
if (key && value) {
|
||||
[parameters setObject:GCDWebServerUnescapeURLString(value) forKey:GCDWebServerUnescapeURLString(key)];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
|
||||
if ([scanner isAtEnd]) {
|
||||
break;
|
||||
}
|
||||
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
||||
}
|
||||
ARC_RELEASE(scanner);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
NSString* GCDWebServerGetPrimaryIPv4Address() {
|
||||
NSString* address = nil;
|
||||
#if TARGET_OS_IPHONE
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
const char* primaryInterface = "en0"; // WiFi interface on iOS
|
||||
#endif
|
||||
#else
|
||||
const char* primaryInterface = NULL;
|
||||
SCDynamicStoreRef store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("GCDWebServer"), NULL, NULL);
|
||||
if (store) {
|
||||
CFPropertyListRef info = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4"));
|
||||
if (info) {
|
||||
primaryInterface = [[NSString stringWithString:[(ARC_BRIDGE NSDictionary*)info objectForKey:@"PrimaryInterface"]] UTF8String];
|
||||
CFRelease(info);
|
||||
}
|
||||
CFRelease(store);
|
||||
}
|
||||
if (primaryInterface == NULL) {
|
||||
primaryInterface = "lo0";
|
||||
}
|
||||
#endif
|
||||
struct ifaddrs* list;
|
||||
if (getifaddrs(&list) >= 0) {
|
||||
for (struct ifaddrs* ifap = list; ifap; ifap = ifap->ifa_next) {
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
if (strcmp(ifap->ifa_name, "en0") && strcmp(ifap->ifa_name, "en1")) // Assume en0 is Ethernet and en1 is WiFi since there is no way to use SystemConfiguration framework in iOS Simulator
|
||||
#else
|
||||
if (strcmp(ifap->ifa_name, primaryInterface))
|
||||
#endif
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ((ifap->ifa_flags & IFF_UP) && (ifap->ifa_addr->sa_family == AF_INET)) {
|
||||
char buffer[NI_MAXHOST];
|
||||
if (getnameinfo(ifap->ifa_addr, ifap->ifa_addr->sa_len, buffer, sizeof(buffer), NULL, 0, NI_NUMERICHOST | NI_NOFQDN) >= 0) {
|
||||
address = [NSString stringWithUTF8String:buffer];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
freeifaddrs(list);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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.
|
||||
*/
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
// http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_Continue = 100,
|
||||
kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
|
||||
kGCDWebServerHTTPStatusCode_Processing = 102
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_OK = 200,
|
||||
kGCDWebServerHTTPStatusCode_Created = 201,
|
||||
kGCDWebServerHTTPStatusCode_Accepted = 202,
|
||||
kGCDWebServerHTTPStatusCode_NonAuthoritativeInformation = 203,
|
||||
kGCDWebServerHTTPStatusCode_NoContent = 204,
|
||||
kGCDWebServerHTTPStatusCode_ResetContent = 205,
|
||||
kGCDWebServerHTTPStatusCode_PartialContent = 206,
|
||||
kGCDWebServerHTTPStatusCode_MultiStatus = 207,
|
||||
kGCDWebServerHTTPStatusCode_AlreadyReported = 208
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_MultipleChoices = 300,
|
||||
kGCDWebServerHTTPStatusCode_MovedPermanently = 301,
|
||||
kGCDWebServerHTTPStatusCode_Found = 302,
|
||||
kGCDWebServerHTTPStatusCode_SeeOther = 303,
|
||||
kGCDWebServerHTTPStatusCode_NotModified = 304,
|
||||
kGCDWebServerHTTPStatusCode_UseProxy = 305,
|
||||
kGCDWebServerHTTPStatusCode_TemporaryRedirect = 307,
|
||||
kGCDWebServerHTTPStatusCode_PermanentRedirect = 308
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_BadRequest = 400,
|
||||
kGCDWebServerHTTPStatusCode_Unauthorized = 401,
|
||||
kGCDWebServerHTTPStatusCode_PaymentRequired = 402,
|
||||
kGCDWebServerHTTPStatusCode_Forbidden = 403,
|
||||
kGCDWebServerHTTPStatusCode_NotFound = 404,
|
||||
kGCDWebServerHTTPStatusCode_MethodNotAllowed = 405,
|
||||
kGCDWebServerHTTPStatusCode_NotAcceptable = 406,
|
||||
kGCDWebServerHTTPStatusCode_ProxyAuthenticationRequired = 407,
|
||||
kGCDWebServerHTTPStatusCode_RequestTimeout = 408,
|
||||
kGCDWebServerHTTPStatusCode_Conflict = 409,
|
||||
kGCDWebServerHTTPStatusCode_Gone = 410,
|
||||
kGCDWebServerHTTPStatusCode_LengthRequired = 411,
|
||||
kGCDWebServerHTTPStatusCode_PreconditionFailed = 412,
|
||||
kGCDWebServerHTTPStatusCode_RequestEntityTooLarge = 413,
|
||||
kGCDWebServerHTTPStatusCode_RequestURITooLong = 414,
|
||||
kGCDWebServerHTTPStatusCode_UnsupportedMediaType = 415,
|
||||
kGCDWebServerHTTPStatusCode_RequestedRangeNotSatisfiable = 416,
|
||||
kGCDWebServerHTTPStatusCode_ExpectationFailed = 417,
|
||||
kGCDWebServerHTTPStatusCode_UnprocessableEntity = 422,
|
||||
kGCDWebServerHTTPStatusCode_Locked = 423,
|
||||
kGCDWebServerHTTPStatusCode_FailedDependency = 424,
|
||||
kGCDWebServerHTTPStatusCode_UpgradeRequired = 426,
|
||||
kGCDWebServerHTTPStatusCode_PreconditionRequired = 428,
|
||||
kGCDWebServerHTTPStatusCode_TooManyRequests = 429,
|
||||
kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_InternalServerError = 500,
|
||||
kGCDWebServerHTTPStatusCode_NotImplemented = 501,
|
||||
kGCDWebServerHTTPStatusCode_BadGateway = 502,
|
||||
kGCDWebServerHTTPStatusCode_ServiceUnavailable = 503,
|
||||
kGCDWebServerHTTPStatusCode_GatewayTimeout = 504,
|
||||
kGCDWebServerHTTPStatusCode_HTTPVersionNotSupported = 505,
|
||||
kGCDWebServerHTTPStatusCode_InsufficientStorage = 507,
|
||||
kGCDWebServerHTTPStatusCode_LoopDetected = 508,
|
||||
kGCDWebServerHTTPStatusCode_NotExtended = 510,
|
||||
kGCDWebServerHTTPStatusCode_NetworkAuthenticationRequired = 511
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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>
|
||||
#import <AvailabilityMacros.h>
|
||||
|
||||
#if __has_feature(objc_arc)
|
||||
#define ARC_BRIDGE __bridge
|
||||
#define ARC_BRIDGE_RELEASE(__OBJECT__) CFBridgingRelease(__OBJECT__)
|
||||
#define ARC_RETAIN(__OBJECT__) __OBJECT__
|
||||
#define ARC_RELEASE(__OBJECT__)
|
||||
#define ARC_AUTORELEASE(__OBJECT__) __OBJECT__
|
||||
#define ARC_DEALLOC(__OBJECT__)
|
||||
#if (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_6_0)) || (!TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8))
|
||||
#define ARC_DISPATCH_RETAIN(__OBJECT__)
|
||||
#define ARC_DISPATCH_RELEASE(__OBJECT__)
|
||||
#else
|
||||
#define ARC_DISPATCH_RETAIN(__OBJECT__) dispatch_retain(__OBJECT__)
|
||||
#define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
|
||||
#endif
|
||||
#else
|
||||
#define ARC_BRIDGE
|
||||
#define ARC_BRIDGE_RELEASE(__OBJECT__) [(id)__OBJECT__ autorelease]
|
||||
#define ARC_RETAIN(__OBJECT__) [__OBJECT__ retain]
|
||||
#define ARC_RELEASE(__OBJECT__) [__OBJECT__ release]
|
||||
#define ARC_AUTORELEASE(__OBJECT__) [__OBJECT__ autorelease]
|
||||
#define ARC_DEALLOC(__OBJECT__) [__OBJECT__ dealloc]
|
||||
#define ARC_DISPATCH_RETAIN(__OBJECT__) dispatch_retain(__OBJECT__)
|
||||
#define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
|
||||
#endif
|
||||
|
||||
#import "GCDWebServerHTTPStatusCodes.h"
|
||||
#import "GCDWebServerFunctions.h"
|
||||
|
||||
#import "GCDWebServer.h"
|
||||
#import "GCDWebServerConnection.h"
|
||||
|
||||
#import "GCDWebServerDataRequest.h"
|
||||
#import "GCDWebServerFileRequest.h"
|
||||
#import "GCDWebServerMultiPartFormRequest.h"
|
||||
#import "GCDWebServerURLEncodedFormRequest.h"
|
||||
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
#import "GCDWebServerErrorResponse.h"
|
||||
#import "GCDWebServerFileResponse.h"
|
||||
#import "GCDWebServerStreamingResponse.h"
|
||||
|
||||
#ifdef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
|
||||
// Define __GCDWEBSERVER_LOGGING_HEADER__ as a preprocessor constant to redirect GCDWebServer logging to your own system
|
||||
#import __GCDWEBSERVER_LOGGING_HEADER__
|
||||
|
||||
#else
|
||||
|
||||
extern GCDWebServerLogLevel GCDLogLevel;
|
||||
extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_FORMAT_FUNCTION(2, 3);
|
||||
|
||||
#define LOG_VERBOSE(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Verbose) GCDLogMessage(kGCDWebServerLogLevel_Verbose, __VA_ARGS__); } while (0)
|
||||
#define LOG_INFO(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Info) GCDLogMessage(kGCDWebServerLogLevel_Info, __VA_ARGS__); } while (0)
|
||||
#define LOG_WARNING(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Warning) GCDLogMessage(kGCDWebServerLogLevel_Warning, __VA_ARGS__); } while (0)
|
||||
#define LOG_ERROR(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Error) GCDLogMessage(kGCDWebServerLogLevel_Error, __VA_ARGS__); } while (0)
|
||||
#define LOG_EXCEPTION(__EXCEPTION__) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Exception) GCDLogMessage(kGCDWebServerLogLevel_Exception, @"%@", __EXCEPTION__); } while (0)
|
||||
|
||||
#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(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Debug) GCDLogMessage(kGCDWebServerLogLevel_Debug, __VA_ARGS__); } while (0)
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#define kGCDWebServerDefaultMimeType @"application/octet-stream"
|
||||
#define kGCDWebServerGCDQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
|
||||
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
|
||||
|
||||
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
|
||||
return ((range.location != NSNotFound) || (range.length > 0));
|
||||
}
|
||||
|
||||
extern void GCDWebServerInitializeFunctions();
|
||||
extern NSString* GCDWebServerNormalizeHeaderValue(NSString* value);
|
||||
extern NSString* GCDWebServerTruncateHeaderValue(NSString* value);
|
||||
extern NSString* GCDWebServerExtractHeaderValueParameter(NSString* header, NSString* attribute);
|
||||
extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset);
|
||||
extern BOOL GCDWebServerIsTextContentType(NSString* type);
|
||||
extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType);
|
||||
|
||||
@interface GCDWebServerConnection ()
|
||||
- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket;
|
||||
@end
|
||||
|
||||
@interface GCDWebServer ()
|
||||
@property(nonatomic, readonly) NSArray* handlers;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerHandler : NSObject
|
||||
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
|
||||
@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock;
|
||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerRequest ()
|
||||
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
|
||||
- (void)prepareForWriting;
|
||||
- (BOOL)performOpen:(NSError**)error;
|
||||
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
|
||||
- (BOOL)performClose:(NSError**)error;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerResponse ()
|
||||
@property(nonatomic, readonly) NSDictionary* additionalHeaders;
|
||||
@property(nonatomic, readonly) BOOL usesChunkedTransferEncoding;
|
||||
- (void)prepareForReading;
|
||||
- (BOOL)performOpen:(NSError**)error;
|
||||
- (NSData*)performReadData:(NSError**)error;
|
||||
- (void)performClose;
|
||||
@end
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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>
|
||||
|
||||
@protocol GCDWebServerBodyWriter <NSObject>
|
||||
- (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
||||
- (BOOL)close:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
||||
@end
|
||||
|
||||
@interface GCDWebServerRequest : NSObject <GCDWebServerBodyWriter>
|
||||
@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 or set to "application/octet-stream" if a body is present without a "Content-Type" header)
|
||||
@property(nonatomic, readonly) NSUInteger contentLength; // Automatically parsed from headers (NSNotFound if request has no "Content-Length" header)
|
||||
@property(nonatomic, readonly) NSDate* ifModifiedSince; // Automatically parsed from headers (nil if request has no "If-Modified-Since" header or it is malformatted)
|
||||
@property(nonatomic, readonly) NSString* ifNoneMatch; // Automatically parsed from headers (nil if request has no "If-None-Match" header)
|
||||
@property(nonatomic, readonly) NSRange byteRange; // Automatically parsed from headers ([NSNotFound, 0] if request has no "Range" header, [offset, length] for byte range from beginning or [NSNotFound, -bytes] from end)
|
||||
@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding; // Automatically parsed from headers
|
||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query;
|
||||
- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil
|
||||
- (BOOL)hasByteRange; // Convenience method that checks "byteRange"
|
||||
@end
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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 <zlib.h>
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
#define kZlibErrorDomain @"ZlibErrorDomain"
|
||||
#define kGZipInitialBufferSize (256 * 1024)
|
||||
|
||||
@interface GCDWebServerBodyDecoder : NSObject <GCDWebServerBodyWriter>
|
||||
- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id<GCDWebServerBodyWriter>)writer;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerGZipDecoder : GCDWebServerBodyDecoder
|
||||
@end
|
||||
|
||||
@interface GCDWebServerBodyDecoder () {
|
||||
@private
|
||||
GCDWebServerRequest* __unsafe_unretained _request;
|
||||
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerBodyDecoder
|
||||
|
||||
- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id<GCDWebServerBodyWriter>)writer {
|
||||
if ((self = [super init])) {
|
||||
_request = request;
|
||||
_writer = writer;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
return [_writer open:error];
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||
return [_writer writeData:data error:error];
|
||||
}
|
||||
|
||||
- (BOOL)close:(NSError**)error {
|
||||
return [_writer close:error];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerGZipDecoder () {
|
||||
@private
|
||||
z_stream _stream;
|
||||
BOOL _finished;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerGZipDecoder
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
int result = inflateInit2(&_stream, 15 + 16);
|
||||
if (result != Z_OK) {
|
||||
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
||||
return NO;
|
||||
}
|
||||
if (![super open:error]) {
|
||||
deflateEnd(&_stream);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||
DCHECK(!_finished);
|
||||
_stream.next_in = (Bytef*)data.bytes;
|
||||
_stream.avail_in = (uInt)data.length;
|
||||
NSMutableData* decodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
|
||||
if (decodedData == nil) {
|
||||
DNOT_REACHED();
|
||||
return NO;
|
||||
}
|
||||
NSUInteger length = 0;
|
||||
while (1) {
|
||||
NSUInteger maxLength = decodedData.length - length;
|
||||
_stream.next_out = (Bytef*)((char*)decodedData.mutableBytes + length);
|
||||
_stream.avail_out = (uInt)maxLength;
|
||||
int result = inflate(&_stream, Z_NO_FLUSH);
|
||||
if ((result != Z_OK) && (result != Z_STREAM_END)) {
|
||||
ARC_RELEASE(decodedData);
|
||||
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
||||
return NO;
|
||||
}
|
||||
length += maxLength - _stream.avail_out;
|
||||
if (_stream.avail_out > 0) {
|
||||
if (result == Z_STREAM_END) {
|
||||
_finished = YES;
|
||||
}
|
||||
break;
|
||||
}
|
||||
decodedData.length = 2 * decodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available
|
||||
}
|
||||
decodedData.length = length;
|
||||
BOOL success = length ? [super writeData:decodedData error:error] : YES; // No need to call writer if we have no data yet
|
||||
ARC_RELEASE(decodedData);
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)close:(NSError**)error {
|
||||
DCHECK(_finished);
|
||||
inflateEnd(&_stream);
|
||||
return [super close:error];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerRequest () {
|
||||
@private
|
||||
NSString* _method;
|
||||
NSURL* _url;
|
||||
NSDictionary* _headers;
|
||||
NSString* _path;
|
||||
NSDictionary* _query;
|
||||
NSString* _type;
|
||||
BOOL _chunked;
|
||||
NSUInteger _length;
|
||||
NSDate* _modifiedSince;
|
||||
NSString* _noneMatch;
|
||||
NSRange _range;
|
||||
BOOL _gzipAccepted;
|
||||
|
||||
BOOL _opened;
|
||||
NSMutableArray* _decoders;
|
||||
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerRequest : NSObject
|
||||
|
||||
@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, ifModifiedSince=_modifiedSince, ifNoneMatch=_noneMatch,
|
||||
byteRange=_range, acceptsGzipContentEncoding=_gzipAccepted, usesChunkedTransferEncoding=_chunked;
|
||||
|
||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
||||
if ((self = [super init])) {
|
||||
_method = [method copy];
|
||||
_url = ARC_RETAIN(url);
|
||||
_headers = ARC_RETAIN(headers);
|
||||
_path = [path copy];
|
||||
_query = ARC_RETAIN(query);
|
||||
|
||||
_type = ARC_RETAIN(GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]));
|
||||
_chunked = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"];
|
||||
NSString* lengthHeader = [_headers objectForKey:@"Content-Length"];
|
||||
if (lengthHeader) {
|
||||
NSInteger length = [lengthHeader integerValue];
|
||||
if (_chunked || (length < 0)) {
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
_length = length;
|
||||
if (_type == nil) {
|
||||
_type = kGCDWebServerDefaultMimeType;
|
||||
}
|
||||
} else if (_chunked) {
|
||||
if (_type == nil) {
|
||||
_type = kGCDWebServerDefaultMimeType;
|
||||
}
|
||||
_length = NSNotFound;
|
||||
} else {
|
||||
if (_type) {
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
_length = NSNotFound;
|
||||
}
|
||||
|
||||
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
|
||||
if (modifiedHeader) {
|
||||
_modifiedSince = [GCDWebServerParseRFC822(modifiedHeader) copy];
|
||||
}
|
||||
_noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]);
|
||||
|
||||
_range = NSMakeRange(NSNotFound, 0);
|
||||
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
|
||||
if (rangeHeader) {
|
||||
if ([rangeHeader hasPrefix:@"bytes="]) {
|
||||
NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","];
|
||||
if (components.count == 1) {
|
||||
components = [[components firstObject] componentsSeparatedByString:@"-"];
|
||||
if (components.count == 2) {
|
||||
NSString* startString = [components objectAtIndex:0];
|
||||
NSInteger startValue = [startString integerValue];
|
||||
NSString* endString = [components objectAtIndex:1];
|
||||
NSInteger endValue = [endString integerValue];
|
||||
if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999"
|
||||
_range.location = startValue;
|
||||
_range.length = endValue - startValue + 1;
|
||||
} else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-"
|
||||
_range.location = startValue;
|
||||
_range.length = NSUIntegerMax;
|
||||
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
|
||||
_range.location = NSNotFound;
|
||||
_range.length = endValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((_range.location == NSNotFound) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
|
||||
LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
|
||||
}
|
||||
}
|
||||
|
||||
if ([[_headers objectForKey:@"Accept-Encoding"] rangeOfString:@"gzip"].location != NSNotFound) {
|
||||
_gzipAccepted = YES;
|
||||
}
|
||||
|
||||
_decoders = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_method);
|
||||
ARC_RELEASE(_url);
|
||||
ARC_RELEASE(_headers);
|
||||
ARC_RELEASE(_path);
|
||||
ARC_RELEASE(_query);
|
||||
ARC_RELEASE(_type);
|
||||
ARC_RELEASE(_modifiedSince);
|
||||
ARC_RELEASE(_noneMatch);
|
||||
ARC_RELEASE(_decoders);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
- (BOOL)hasBody {
|
||||
return _type ? YES : NO;
|
||||
}
|
||||
|
||||
- (BOOL)hasByteRange {
|
||||
return GCDWebServerIsValidByteRange(_range);
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)close:(NSError**)error {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)prepareForWriting {
|
||||
_writer = self;
|
||||
if ([GCDWebServerNormalizeHeaderValue([self.headers objectForKey:@"Content-Encoding"]) isEqualToString:@"gzip"]) {
|
||||
GCDWebServerGZipDecoder* decoder = [[GCDWebServerGZipDecoder alloc] initWithRequest:self writer:_writer];
|
||||
[_decoders addObject:decoder];
|
||||
ARC_RELEASE(decoder);
|
||||
_writer = decoder;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)performOpen:(NSError**)error {
|
||||
DCHECK(_type);
|
||||
DCHECK(_writer);
|
||||
if (_opened) {
|
||||
DNOT_REACHED();
|
||||
return NO;
|
||||
}
|
||||
_opened = YES;
|
||||
return [_writer open:error];
|
||||
}
|
||||
|
||||
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error {
|
||||
DCHECK(_opened);
|
||||
return [_writer writeData:data error:error];
|
||||
}
|
||||
|
||||
- (BOOL)performClose:(NSError**)error {
|
||||
DCHECK(_opened);
|
||||
return [_writer close:error];
|
||||
}
|
||||
|
||||
- (NSString*)description {
|
||||
NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path];
|
||||
for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
[description appendFormat:@"\n %@ = %@", argument, [_query objectForKey:argument]];
|
||||
}
|
||||
[description appendString:@"\n"];
|
||||
for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
[description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]];
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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>
|
||||
|
||||
@protocol GCDWebServerBodyReader <NSObject>
|
||||
- (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
||||
- (NSData*)readData:(NSError**)error; // Must return nil on error or empty NSData if at end ("error" is guaranteed to be non-NULL)
|
||||
- (void)close;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerResponse : NSObject <GCDWebServerBodyReader>
|
||||
@property(nonatomic, copy) NSString* contentType; // Default is nil i.e. no body (must be set if a body is present)
|
||||
@property(nonatomic) NSUInteger contentLength; // Default is NSNotFound i.e. undefined (if a body is present but length is undefined, chunked transfer encoding will be enabled)
|
||||
@property(nonatomic) NSInteger statusCode; // Default is 200
|
||||
@property(nonatomic) NSUInteger cacheControlMaxAge; // Default is 0 seconds i.e. "Cache-Control: no-cache"
|
||||
@property(nonatomic, retain) NSDate* lastModifiedDate; // Default is nil i.e. no "Last-Modified" header
|
||||
@property(nonatomic, copy) NSString* eTag; // Default is nil i.e. no "ETag" header
|
||||
@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; // Default is disabled
|
||||
+ (instancetype)response;
|
||||
- (instancetype)init;
|
||||
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; // Pass nil value to remove header
|
||||
- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil
|
||||
@end
|
||||
|
||||
@interface GCDWebServerResponse (Extensions)
|
||||
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode;
|
||||
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||
- (instancetype)initWithStatusCode:(NSInteger)statusCode;
|
||||
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||
@end
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
Copyright (c) 2012-2014, 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.
|
||||
* The name of Pierre-Olivier Latour may not 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 PIERRE-OLIVIER LATOUR 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 <zlib.h>
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
#define kZlibErrorDomain @"ZlibErrorDomain"
|
||||
#define kGZipInitialBufferSize (256 * 1024)
|
||||
|
||||
@interface GCDWebServerBodyEncoder : NSObject <GCDWebServerBodyReader>
|
||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder
|
||||
@end
|
||||
|
||||
@interface GCDWebServerBodyEncoder () {
|
||||
@private
|
||||
GCDWebServerResponse* __unsafe_unretained _response;
|
||||
id<GCDWebServerBodyReader> __unsafe_unretained _reader;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerBodyEncoder
|
||||
|
||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
||||
if ((self = [super init])) {
|
||||
_response = response;
|
||||
_reader = reader;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
return [_reader open:error];
|
||||
}
|
||||
|
||||
- (NSData*)readData:(NSError**)error {
|
||||
return [_reader readData:error];
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
[_reader close];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerGZipEncoder () {
|
||||
@private
|
||||
z_stream _stream;
|
||||
BOOL _finished;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerGZipEncoder
|
||||
|
||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
||||
if ((self = [super initWithResponse:response reader:reader])) {
|
||||
response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set since we don't know it (client will determine body length when connection is closed)
|
||||
[response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
int result = deflateInit2(&_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
|
||||
if (result != Z_OK) {
|
||||
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
||||
return NO;
|
||||
}
|
||||
if (![super open:error]) {
|
||||
deflateEnd(&_stream);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSData*)readData:(NSError**)error {
|
||||
NSMutableData* encodedData;
|
||||
if (_finished) {
|
||||
encodedData = [[NSMutableData alloc] init];
|
||||
} else {
|
||||
encodedData = [[NSMutableData alloc] initWithLength:kGZipInitialBufferSize];
|
||||
if (encodedData == nil) {
|
||||
DNOT_REACHED();
|
||||
return nil;
|
||||
}
|
||||
NSUInteger length = 0;
|
||||
do {
|
||||
NSData* data = [super readData:error];
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
_stream.next_in = (Bytef*)data.bytes;
|
||||
_stream.avail_in = (uInt)data.length;
|
||||
while (1) {
|
||||
NSUInteger maxLength = encodedData.length - length;
|
||||
_stream.next_out = (Bytef*)((char*)encodedData.mutableBytes + length);
|
||||
_stream.avail_out = (uInt)maxLength;
|
||||
int result = deflate(&_stream, data.length ? Z_NO_FLUSH : Z_FINISH);
|
||||
if (result == Z_STREAM_END) {
|
||||
_finished = YES;
|
||||
} else if (result != Z_OK) {
|
||||
ARC_RELEASE(encodedData);
|
||||
*error = [NSError errorWithDomain:kZlibErrorDomain code:result userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
length += maxLength - _stream.avail_out;
|
||||
if (_stream.avail_out > 0) {
|
||||
break;
|
||||
}
|
||||
encodedData.length = 2 * encodedData.length; // zlib has used all the output buffer so resize it and try again in case more data is available
|
||||
}
|
||||
DCHECK(_stream.avail_in == 0);
|
||||
} while (length == 0); // Make sure we don't return an empty NSData if not in finished state
|
||||
encodedData.length = length;
|
||||
}
|
||||
return ARC_AUTORELEASE(encodedData);
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
deflateEnd(&_stream);
|
||||
[super close];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerResponse () {
|
||||
@private
|
||||
NSString* _type;
|
||||
NSUInteger _length;
|
||||
NSInteger _status;
|
||||
NSUInteger _maxAge;
|
||||
NSDate* _lastModified;
|
||||
NSString* _eTag;
|
||||
NSMutableDictionary* _headers;
|
||||
BOOL _chunked;
|
||||
BOOL _gzipped;
|
||||
|
||||
BOOL _opened;
|
||||
NSMutableArray* _encoders;
|
||||
id<GCDWebServerBodyReader> __unsafe_unretained _reader;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerResponse
|
||||
|
||||
@synthesize contentType=_type, contentLength=_length, statusCode=_status, cacheControlMaxAge=_maxAge, lastModifiedDate=_lastModified, eTag=_eTag,
|
||||
gzipContentEncodingEnabled=_gzipped, additionalHeaders=_headers;
|
||||
|
||||
+ (instancetype)response {
|
||||
return ARC_AUTORELEASE([[[self class] alloc] init]);
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
_type = nil;
|
||||
_length = NSNotFound;
|
||||
_status = kGCDWebServerHTTPStatusCode_OK;
|
||||
_maxAge = 0;
|
||||
_headers = [[NSMutableDictionary alloc] init];
|
||||
_encoders = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_type);
|
||||
ARC_RELEASE(_lastModified);
|
||||
ARC_RELEASE(_eTag);
|
||||
ARC_RELEASE(_headers);
|
||||
ARC_RELEASE(_encoders);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header {
|
||||
[_headers setValue:value forKey:header];
|
||||
}
|
||||
|
||||
- (BOOL)hasBody {
|
||||
return _type ? YES : NO;
|
||||
}
|
||||
|
||||
- (BOOL)usesChunkedTransferEncoding {
|
||||
return (_type != nil) && (_length == NSNotFound);
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSData*)readData:(NSError**)error {
|
||||
return [NSData data];
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
;
|
||||
}
|
||||
|
||||
- (void)prepareForReading {
|
||||
_reader = self;
|
||||
if (_gzipped) {
|
||||
GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader];
|
||||
[_encoders addObject:encoder];
|
||||
ARC_RELEASE(encoder);
|
||||
_reader = encoder;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)performOpen:(NSError**)error {
|
||||
DCHECK(_type);
|
||||
DCHECK(_reader);
|
||||
if (_opened) {
|
||||
DNOT_REACHED();
|
||||
return NO;
|
||||
}
|
||||
_opened = YES;
|
||||
return [_reader open:error];
|
||||
}
|
||||
|
||||
- (NSData*)performReadData:(NSError**)error {
|
||||
DCHECK(_opened);
|
||||
return [_reader readData:error];
|
||||
}
|
||||
|
||||
- (void)performClose {
|
||||
DCHECK(_opened);
|
||||
[_reader close];
|
||||
}
|
||||
|
||||
- (NSString*)description {
|
||||
NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_status];
|
||||
if (_type) {
|
||||
[description appendFormat:@"\nContent Type = %@", _type];
|
||||
}
|
||||
if (_length != NSNotFound) {
|
||||
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
|
||||
}
|
||||
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];
|
||||
if (_lastModified) {
|
||||
[description appendFormat:@"\nLast Modified Date = %@", _lastModified];
|
||||
}
|
||||
if (_eTag) {
|
||||
[description appendFormat:@"\nETag = %@", _eTag];
|
||||
}
|
||||
if (_headers.count) {
|
||||
[description appendString:@"\n"];
|
||||
for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
[description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]];
|
||||
}
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerResponse (Extensions)
|
||||
|
||||
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode {
|
||||
return ARC_AUTORELEASE([[self alloc] initWithStatusCode:statusCode]);
|
||||
}
|
||||
|
||||
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
|
||||
return ARC_AUTORELEASE([[self alloc] initWithRedirect:location permanent:permanent]);
|
||||
}
|
||||
|
||||
- (instancetype)initWithStatusCode:(NSInteger)statusCode {
|
||||
if ((self = [self init])) {
|
||||
self.statusCode = statusCode;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent {
|
||||
if ((self = [self init])) {
|
||||
self.statusCode = permanent ? kGCDWebServerHTTPStatusCode_MovedPermanently : kGCDWebServerHTTPStatusCode_TemporaryRedirect;
|
||||
[self setValue:[location absoluteString] forAdditionalHeader:@"Location"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user