2012-12-30 14:23:49 +08:00
|
|
|
/*
|
2014-02-07 10:44:04 +08:00
|
|
|
Copyright (c) 2012-2014, Pierre-Olivier Latour
|
2012-12-30 14:23:49 +08:00
|
|
|
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.
|
2013-12-30 11:02:01 +08:00
|
|
|
* The name of Pierre-Olivier Latour may not be used to endorse
|
|
|
|
or promote products derived from this software without specific
|
|
|
|
prior written permission.
|
2012-12-30 14:23:49 +08:00
|
|
|
|
|
|
|
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
|
2013-12-30 11:02:01 +08:00
|
|
|
DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
|
2012-12-30 14:23:49 +08:00
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2012-12-31 02:00:41 +08:00
|
|
|
#import <TargetConditionals.h>
|
|
|
|
#if TARGET_OS_IPHONE
|
|
|
|
#import <MobileCoreServices/MobileCoreServices.h>
|
2014-03-30 12:29:07 +08:00
|
|
|
#else
|
|
|
|
#import <SystemConfiguration/SystemConfiguration.h>
|
2012-12-31 02:00:41 +08:00
|
|
|
#endif
|
|
|
|
|
2012-12-30 14:23:49 +08:00
|
|
|
#import <netinet/in.h>
|
2014-03-30 12:29:07 +08:00
|
|
|
#import <ifaddrs.h>
|
|
|
|
#import <net/if.h>
|
|
|
|
#import <netdb.h>
|
2012-12-30 14:23:49 +08:00
|
|
|
|
|
|
|
#import "GCDWebServerPrivate.h"
|
|
|
|
|
2014-03-23 11:16:26 +08:00
|
|
|
#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR
|
|
|
|
#define kDefaultPort 80
|
|
|
|
#else
|
|
|
|
#define kDefaultPort 8080
|
|
|
|
#endif
|
2013-04-02 06:42:16 +08:00
|
|
|
#define kMaxPendingConnections 16
|
|
|
|
|
2014-03-20 00:19:59 +08:00
|
|
|
@interface GCDWebServer () {
|
|
|
|
@private
|
|
|
|
NSMutableArray* _handlers;
|
|
|
|
|
|
|
|
NSUInteger _port;
|
|
|
|
dispatch_source_t _source;
|
|
|
|
CFNetServiceRef _service;
|
|
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
|
|
@interface GCDWebServerHandler () {
|
|
|
|
@private
|
|
|
|
GCDWebServerMatchBlock _matchBlock;
|
|
|
|
GCDWebServerProcessBlock _processBlock;
|
|
|
|
}
|
|
|
|
@end
|
|
|
|
|
2014-03-23 11:11:52 +08:00
|
|
|
#if !TARGET_OS_IPHONE
|
2012-12-30 14:23:49 +08:00
|
|
|
static BOOL _run;
|
2014-03-23 11:11:52 +08:00
|
|
|
#endif
|
2012-12-30 14:23:49 +08:00
|
|
|
|
2014-03-27 00:25:10 +08:00
|
|
|
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
|
|
|
|
|
|
|
void GCDLogMessage(long level, NSString* format, ...) {
|
|
|
|
static const char* levelNames[] = {"DEBUG", "VERBOSE", "INFO", "WARNING", "ERROR", "EXCEPTION"};
|
|
|
|
static long minLevel = -1;
|
|
|
|
if (minLevel < 0) {
|
|
|
|
const char* logLevel = getenv("logLevel");
|
|
|
|
minLevel = logLevel ? atoi(logLevel) : 0;
|
|
|
|
}
|
|
|
|
if (level >= minLevel) {
|
|
|
|
va_list arguments;
|
|
|
|
va_start(arguments, format);
|
|
|
|
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
|
|
|
va_end(arguments);
|
|
|
|
fprintf(stderr, "[%s] %s\n", levelNames[level], [message UTF8String]);
|
|
|
|
ARC_RELEASE(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
2012-12-30 14:23:49 +08:00
|
|
|
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) {
|
|
|
|
static NSDictionary* _overrides = nil;
|
|
|
|
if (_overrides == nil) {
|
2014-04-02 05:07:42 +08:00
|
|
|
_overrides = [[NSDictionary alloc] initWithObjectsAndKeys:
|
|
|
|
@"text/css", @"css",
|
|
|
|
nil];
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
NSString* mimeType = nil;
|
|
|
|
extension = [extension lowercaseString];
|
|
|
|
if (extension.length) {
|
|
|
|
mimeType = [_overrides objectForKey:extension];
|
|
|
|
if (mimeType == nil) {
|
2014-01-09 14:27:15 +08:00
|
|
|
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (ARC_BRIDGE CFStringRef)extension, NULL);
|
2012-12-30 14:23:49 +08:00
|
|
|
if (uti) {
|
2014-01-09 14:27:15 +08:00
|
|
|
mimeType = ARC_BRIDGE_RELEASE(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
2012-12-30 14:23:49 +08:00
|
|
|
CFRelease(uti);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-03-30 02:45:51 +08:00
|
|
|
return mimeType ? mimeType : kGCDWebServerDefaultMimeType;
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
2014-03-30 04:55:22 +08:00
|
|
|
NSString* GCDWebServerEscapeURLString(NSString* string) {
|
|
|
|
return ARC_BRIDGE_RELEASE(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":@/?&=+"), kCFStringEncodingUTF8));
|
|
|
|
}
|
|
|
|
|
2012-12-31 01:42:19 +08:00
|
|
|
NSString* GCDWebServerUnescapeURLString(NSString* string) {
|
2014-03-30 04:55:22 +08:00
|
|
|
return ARC_BRIDGE_RELEASE(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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:@" "];
|
2014-03-04 14:11:34 +08:00
|
|
|
if (key && value) {
|
|
|
|
[parameters setObject:GCDWebServerUnescapeURLString(value) forKey:GCDWebServerUnescapeURLString(key)];
|
|
|
|
} else {
|
|
|
|
DNOT_REACHED();
|
|
|
|
}
|
2012-12-30 14:23:49 +08:00
|
|
|
|
|
|
|
if ([scanner isAtEnd]) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
|
|
|
}
|
2014-01-09 14:27:15 +08:00
|
|
|
ARC_RELEASE(scanner);
|
2012-12-30 14:23:49 +08:00
|
|
|
return parameters;
|
|
|
|
}
|
|
|
|
|
2014-03-30 12:29:07 +08:00
|
|
|
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 = [[(ARC_BRIDGE NSDictionary*)info objectForKey:@"PrimaryInterface"] UTF8String];
|
|
|
|
CFRelease(info);
|
|
|
|
}
|
|
|
|
CFRelease(store);
|
|
|
|
}
|
|
|
|
if (primaryInterface == NULL) {
|
2014-03-31 02:19:43 +08:00
|
|
|
primaryInterface = "lo0";
|
2014-03-30 12:29:07 +08:00
|
|
|
}
|
|
|
|
#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;
|
|
|
|
}
|
|
|
|
|
2014-03-23 11:11:52 +08:00
|
|
|
#if !TARGET_OS_IPHONE
|
|
|
|
|
2012-12-30 14:23:49 +08:00
|
|
|
static void _SignalHandler(int signal) {
|
|
|
|
_run = NO;
|
|
|
|
printf("\n");
|
|
|
|
}
|
|
|
|
|
2014-03-23 11:11:52 +08:00
|
|
|
#endif
|
|
|
|
|
2012-12-30 14:23:49 +08:00
|
|
|
@implementation GCDWebServerHandler
|
|
|
|
|
|
|
|
@synthesize matchBlock=_matchBlock, processBlock=_processBlock;
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock {
|
2012-12-30 14:23:49 +08:00
|
|
|
if ((self = [super init])) {
|
2014-01-09 14:27:15 +08:00
|
|
|
_matchBlock = [matchBlock copy];
|
|
|
|
_processBlock = [processBlock copy];
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (void)dealloc {
|
2014-01-09 14:27:15 +08:00
|
|
|
ARC_RELEASE(_matchBlock);
|
|
|
|
ARC_RELEASE(_processBlock);
|
2012-12-30 14:23:49 +08:00
|
|
|
|
2014-01-09 14:27:15 +08:00
|
|
|
ARC_DEALLOC(super);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation GCDWebServer
|
|
|
|
|
|
|
|
@synthesize handlers=_handlers, port=_port;
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
+ (void)initialize {
|
2012-12-31 01:42:19 +08:00
|
|
|
[GCDWebServerConnection class]; // Initialize class immediately to make sure it happens on the main thread
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (id)init {
|
2012-12-30 14:23:49 +08:00
|
|
|
if ((self = [super init])) {
|
|
|
|
_handlers = [[NSMutableArray alloc] init];
|
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (void)dealloc {
|
2013-04-02 06:42:16 +08:00
|
|
|
if (_source) {
|
2012-12-30 14:23:49 +08:00
|
|
|
[self stop];
|
|
|
|
}
|
|
|
|
|
2014-01-09 14:27:15 +08:00
|
|
|
ARC_RELEASE(_handlers);
|
2012-12-30 14:23:49 +08:00
|
|
|
|
2014-01-09 14:27:15 +08:00
|
|
|
ARC_DEALLOC(super);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
2014-01-24 06:17:44 +08:00
|
|
|
- (NSString*)bonjourName {
|
2014-01-24 06:26:31 +08:00
|
|
|
CFStringRef name = _service ? CFNetServiceGetName(_service) : NULL;
|
|
|
|
return name && CFStringGetLength(name) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil;
|
2014-01-24 06:17:44 +08:00
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
|
2013-04-02 06:42:16 +08:00
|
|
|
DCHECK(_source == NULL);
|
2012-12-30 14:23:49 +08:00
|
|
|
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
|
|
|
|
[_handlers insertObject:handler atIndex:0];
|
2014-01-09 14:27:15 +08:00
|
|
|
ARC_RELEASE(handler);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (void)removeAllHandlers {
|
2013-04-02 06:42:16 +08:00
|
|
|
DCHECK(_source == NULL);
|
2012-12-30 14:23:49 +08:00
|
|
|
[_handlers removeAllObjects];
|
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (BOOL)start {
|
2014-03-23 11:16:26 +08:00
|
|
|
return [self startWithPort:kDefaultPort bonjourName:@""];
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
|
|
|
@autoreleasepool {
|
|
|
|
if (error->error) {
|
2014-03-27 00:25:10 +08:00
|
|
|
LOG_ERROR(@"Bonjour error %i (domain %i)", (int)error->error, (int)error->domain);
|
2012-12-30 14:23:49 +08:00
|
|
|
} else {
|
2014-03-30 12:29:07 +08:00
|
|
|
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
|
|
|
LOG_VERBOSE(@"%@ now reachable at %@", [server class], server.bonjourServerURL);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-04-02 06:42:16 +08:00
|
|
|
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name {
|
|
|
|
DCHECK(_source == NULL);
|
|
|
|
int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
|
|
if (listeningSocket > 0) {
|
2012-12-30 14:23:49 +08:00
|
|
|
int yes = 1;
|
2013-04-02 06:42:16 +08:00
|
|
|
setsockopt(listeningSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
|
|
|
setsockopt(listeningSocket, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
|
2012-12-30 14:23:49 +08:00
|
|
|
|
|
|
|
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);
|
2013-04-02 06:42:16 +08:00
|
|
|
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 {
|
2014-03-30 12:17:51 +08:00
|
|
|
struct sockaddr remoteSockAddr;
|
|
|
|
socklen_t remoteAddrLen = sizeof(remoteSockAddr);
|
|
|
|
int socket = accept(listeningSocket, &remoteSockAddr, &remoteAddrLen);
|
2013-04-02 06:42:16 +08:00
|
|
|
if (socket > 0) {
|
2014-03-30 12:17:51 +08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2014-04-04 18:05:32 +08:00
|
|
|
int noSigPipe = 1;
|
|
|
|
setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); // Make sure this socket cannot generate SIG_PIPE
|
2013-04-02 06:42:16 +08:00
|
|
|
|
|
|
|
Class connectionClass = [[self class] connectionClass];
|
2014-03-30 12:17:51 +08:00
|
|
|
GCDWebServerConnection* connection = [[connectionClass alloc] initWithServer:self localAddress:localAddress remoteAddress:remoteAddress socket:socket]; // Connection will automatically retain itself while opened
|
2014-01-09 14:27:15 +08:00
|
|
|
#if __has_feature(objc_arc)
|
|
|
|
[connection self]; // Prevent compiler from complaining about unused variable / useless statement
|
|
|
|
#else
|
|
|
|
[connection release];
|
|
|
|
#endif
|
2013-04-02 06:42:16 +08:00
|
|
|
} 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));
|
|
|
|
}
|
2012-12-30 14:23:49 +08:00
|
|
|
} else {
|
2013-04-02 06:42:16 +08:00
|
|
|
_port = port;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (name) {
|
2014-01-30 00:38:25 +08:00
|
|
|
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (ARC_BRIDGE CFStringRef)name, (SInt32)_port);
|
2013-04-02 06:42:16 +08:00
|
|
|
if (_service) {
|
2014-01-09 14:27:15 +08:00
|
|
|
CFNetServiceClientContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
2013-04-02 06:42:16 +08:00
|
|
|
CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
|
|
|
|
CFNetServiceScheduleWithRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
|
|
|
CFStreamError error = {0};
|
|
|
|
CFNetServiceRegisterWithOptions(_service, 0, &error);
|
|
|
|
} else {
|
|
|
|
LOG_ERROR(@"Failed creating CFNetService");
|
|
|
|
}
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
2013-04-02 06:42:16 +08:00
|
|
|
|
|
|
|
dispatch_resume(_source);
|
2014-03-30 12:29:07 +08:00
|
|
|
LOG_VERBOSE(@"%@ started on port %i and reachable at %@", [self class], (int)_port, self.serverURL);
|
2013-04-02 06:42:16 +08:00
|
|
|
} else {
|
|
|
|
LOG_ERROR(@"Failed listening on socket (%i): %s", errno, strerror(errno));
|
|
|
|
close(listeningSocket);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
} else {
|
2013-04-02 06:42:16 +08:00
|
|
|
LOG_ERROR(@"Failed binding socket (%i): %s", errno, strerror(errno));
|
|
|
|
close(listeningSocket);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
} else {
|
2013-04-02 06:42:16 +08:00
|
|
|
LOG_ERROR(@"Failed creating socket (%i): %s", errno, strerror(errno));
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
2013-04-02 06:42:16 +08:00
|
|
|
return (_source ? YES : NO);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (BOOL)isRunning {
|
2013-04-02 06:42:16 +08:00
|
|
|
return (_source ? YES : NO);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (void)stop {
|
2013-04-02 06:42:16 +08:00
|
|
|
DCHECK(_source != NULL);
|
|
|
|
if (_source) {
|
2012-12-30 14:23:49 +08:00
|
|
|
if (_service) {
|
2013-04-02 06:42:16 +08:00
|
|
|
CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
2012-12-30 14:23:49 +08:00
|
|
|
CFNetServiceSetClient(_service, NULL, NULL);
|
|
|
|
CFRelease(_service);
|
2013-04-02 06:42:16 +08:00
|
|
|
_service = NULL;
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
2013-04-02 06:42:16 +08:00
|
|
|
dispatch_source_cancel(_source); // This will close the socket
|
2014-01-24 03:33:33 +08:00
|
|
|
ARC_DISPATCH_RELEASE(_source);
|
2013-04-02 06:42:16 +08:00
|
|
|
_source = NULL;
|
|
|
|
|
2012-12-30 14:23:49 +08:00
|
|
|
LOG_VERBOSE(@"%@ stopped", [self class]);
|
|
|
|
}
|
|
|
|
_port = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation GCDWebServer (Subclassing)
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
+ (Class)connectionClass {
|
2012-12-30 14:23:49 +08:00
|
|
|
return [GCDWebServerConnection class];
|
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
+ (NSString*)serverName {
|
2012-12-30 14:23:49 +08:00
|
|
|
return NSStringFromClass(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation GCDWebServer (Extensions)
|
|
|
|
|
2014-03-30 12:29:07 +08:00
|
|
|
- (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
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (BOOL)runWithPort:(NSUInteger)port {
|
2012-12-30 14:23:49 +08:00
|
|
|
BOOL success = NO;
|
|
|
|
_run = YES;
|
2014-01-30 01:14:23 +08:00
|
|
|
void (*handler)(int) = signal(SIGINT, _SignalHandler);
|
2012-12-30 14:23:49 +08:00
|
|
|
if (handler != SIG_ERR) {
|
2013-04-02 06:42:16 +08:00
|
|
|
if ([self startWithPort:port bonjourName:@""]) {
|
2012-12-30 14:23:49 +08:00
|
|
|
while (_run) {
|
2013-04-02 06:42:16 +08:00
|
|
|
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
[self stop];
|
|
|
|
success = YES;
|
|
|
|
}
|
|
|
|
signal(SIGINT, handler);
|
|
|
|
}
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
2014-03-23 11:11:52 +08:00
|
|
|
#endif
|
|
|
|
|
2014-03-30 12:29:07 +08:00
|
|
|
@end
|
|
|
|
|
2012-12-30 14:23:49 +08:00
|
|
|
@implementation GCDWebServer (Handlers)
|
|
|
|
|
2014-03-20 00:10:47 +08:00
|
|
|
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
2012-12-30 14:23:49 +08:00
|
|
|
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
|
|
|
|
2014-03-20 00:10:47 +08:00
|
|
|
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
2012-12-30 14:23:49 +08:00
|
|
|
|
|
|
|
} processBlock:block];
|
|
|
|
}
|
|
|
|
|
2014-03-21 03:55:09 +08:00
|
|
|
- (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;
|
|
|
|
|
|
|
|
}];
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
|
2014-03-21 03:55:09 +08:00
|
|
|
- (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;
|
|
|
|
|
|
|
|
}];
|
2014-03-20 11:57:35 +08:00
|
|
|
}
|
|
|
|
|
2012-12-31 10:48:25 +08:00
|
|
|
- (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path {
|
2012-12-30 14:23:49 +08:00
|
|
|
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path];
|
|
|
|
if (enumerator == nil) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
NSMutableString* html = [NSMutableString string];
|
2014-03-21 03:55:09 +08:00
|
|
|
[html appendString:@"<!DOCTYPE html>\n"];
|
|
|
|
[html appendString:@"<html><head><meta charset=\"utf-8\"></head><body>\n"];
|
2012-12-30 14:23:49 +08:00
|
|
|
[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];
|
|
|
|
}
|
|
|
|
|
2014-03-21 03:55:09 +08:00
|
|
|
- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests {
|
2012-12-30 14:23:49 +08:00
|
|
|
if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) {
|
2014-04-07 01:10:25 +08:00
|
|
|
GCDWebServer* __unsafe_unretained server = self;
|
2012-12-30 14:23:49 +08:00
|
|
|
[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;
|
|
|
|
}
|
2014-01-09 14:27:15 +08:00
|
|
|
return ARC_AUTORELEASE([[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
2012-12-30 14:23:49 +08:00
|
|
|
|
|
|
|
} processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
|
|
|
|
|
|
|
GCDWebServerResponse* response = nil;
|
2014-03-21 03:55:09 +08:00
|
|
|
NSString* filePath = [directoryPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]];
|
2012-12-30 14:23:49 +08:00
|
|
|
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) {
|
2014-03-21 03:55:09 +08:00
|
|
|
return [GCDWebServerFileResponse responseWithFile:indexPath];
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
}
|
2014-01-09 14:27:15 +08:00
|
|
|
response = [server _responseWithContentsOfDirectory:filePath];
|
2014-03-21 03:55:09 +08:00
|
|
|
} else {
|
|
|
|
if (allowRangeRequests) {
|
|
|
|
response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange];
|
|
|
|
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
|
2014-03-20 11:57:35 +08:00
|
|
|
} else {
|
2014-03-21 03:55:09 +08:00
|
|
|
response = [GCDWebServerFileResponse responseWithFile:filePath];
|
2014-03-20 11:57:35 +08:00
|
|
|
}
|
2012-12-30 14:23:49 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (response) {
|
2014-03-20 12:15:25 +08:00
|
|
|
response.cacheControlMaxAge = cacheAge;
|
2012-12-30 14:23:49 +08:00
|
|
|
} else {
|
|
|
|
response = [GCDWebServerResponse responseWithStatusCode:404];
|
|
|
|
}
|
|
|
|
return response;
|
|
|
|
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
DNOT_REACHED();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|