1/* 2 Copyright (c) 2012-2019, Pierre-Olivier Latour 3 All rights reserved. 4 5 Redistribution and use in source and binary forms, with or without 6 modification, are permitted provided that the following conditions are met: 7 * Redistributions of source code must retain the above copyright 8 notice, this list of conditions and the following disclaimer. 9 * Redistributions in binary form must reproduce the above copyright 10 notice, this list of conditions and the following disclaimer in the 11 documentation and/or other materials provided with the distribution. 12 * The name of Pierre-Olivier Latour may not be used to endorse 13 or promote products derived from this software without specific 14 prior written permission. 15 16 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY 20 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 */ 27 28#if !__has_feature(objc_arc) 29#error GCDWebServer requires ARC 30#endif 31 32#import <TargetConditionals.h> 33#if TARGET_OS_IPHONE 34#import <UIKit/UIKit.h> 35#else 36#ifdef __GCDWEBSERVER_ENABLE_TESTING__ 37#import <AppKit/AppKit.h> 38#endif 39#endif 40#import <netinet/in.h> 41#import <dns_sd.h> 42 43#import "GCDWebServerPrivate.h" 44 45#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR 46#define kDefaultPort 80 47#else 48#define kDefaultPort 8080 49#endif 50 51#define kBonjourResolutionTimeout 5.0 52 53NSString* const GCDWebServerOption_Port = @"Port"; 54NSString* const GCDWebServerOption_BonjourName = @"BonjourName"; 55NSString* const GCDWebServerOption_BonjourType = @"BonjourType"; 56NSString* const GCDWebServerOption_RequestNATPortMapping = @"RequestNATPortMapping"; 57NSString* const GCDWebServerOption_BindToLocalhost = @"BindToLocalhost"; 58NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections"; 59NSString* const GCDWebServerOption_ServerName = @"ServerName"; 60NSString* const GCDWebServerOption_AuthenticationMethod = @"AuthenticationMethod"; 61NSString* const GCDWebServerOption_AuthenticationRealm = @"AuthenticationRealm"; 62NSString* const GCDWebServerOption_AuthenticationAccounts = @"AuthenticationAccounts"; 63NSString* const GCDWebServerOption_ConnectionClass = @"ConnectionClass"; 64NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET = @"AutomaticallyMapHEADToGET"; 65NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval = @"ConnectedStateCoalescingInterval"; 66NSString* const GCDWebServerOption_DispatchQueuePriority = @"DispatchQueuePriority"; 67#if TARGET_OS_IPHONE 68NSString* const GCDWebServerOption_AutomaticallySuspendInBackground = @"AutomaticallySuspendInBackground"; 69#endif 70 71NSString* const GCDWebServerAuthenticationMethod_Basic = @"Basic"; 72NSString* const GCDWebServerAuthenticationMethod_DigestAccess = @"DigestAccess"; 73 74#if defined(__GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__) 75#if DEBUG 76GCDWebServerLoggingLevel GCDWebServerLogLevel = kGCDWebServerLoggingLevel_Debug; 77#else 78GCDWebServerLoggingLevel GCDWebServerLogLevel = kGCDWebServerLoggingLevel_Info; 79#endif 80#endif 81 82#if !TARGET_OS_IPHONE 83static BOOL _run; 84#endif 85 86#ifdef __GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__ 87 88void GCDWebServerLogMessage(GCDWebServerLoggingLevel level, NSString* format, ...) { 89 static const char* levelNames[] = {"DEBUG", "VERBOSE", "INFO", "WARNING", "ERROR"}; 90 static int enableLogging = -1; 91 if (enableLogging < 0) { 92 enableLogging = (isatty(STDERR_FILENO) ? 1 : 0); 93 } 94 if (enableLogging) { 95 va_list arguments; 96 va_start(arguments, format); 97 NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; 98 va_end(arguments); 99 fprintf(stderr, "[%s] %s\n", levelNames[level], [message UTF8String]); 100 } 101} 102 103#endif 104 105#if !TARGET_OS_IPHONE 106 107static void _SignalHandler(int signal) { 108 _run = NO; 109 printf("\n"); 110} 111 112#endif 113 114#if !TARGET_OS_IPHONE || defined(__GCDWEBSERVER_ENABLE_TESTING__) 115 116// This utility function is used to ensure scheduled callbacks on the main thread are called when running the server synchronously 117// https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html 118// The main queue works with the application’s run loop to interleave the execution of queued tasks with the execution of other event sources attached to the run loop 119// TODO: Ensure all scheduled blocks on the main queue are also executed 120static void _ExecuteMainThreadRunLoopSources() { 121 SInt32 result; 122 do { 123 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true); 124 } while (result == kCFRunLoopRunHandledSource); 125} 126 127#endif 128 129@implementation GCDWebServerHandler 130 131- (instancetype)initWithMatchBlock:(GCDWebServerMatchBlock _Nonnull)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock _Nonnull)processBlock { 132 if ((self = [super init])) { 133 _matchBlock = [matchBlock copy]; 134 _asyncProcessBlock = [processBlock copy]; 135 } 136 return self; 137} 138 139@end 140 141@implementation GCDWebServer { 142 dispatch_queue_t _syncQueue; 143 dispatch_group_t _sourceGroup; 144 NSMutableArray<GCDWebServerHandler*>* _handlers; 145 NSInteger _activeConnections; // Accessed through _syncQueue only 146 BOOL _connected; // Accessed on main thread only 147 CFRunLoopTimerRef _disconnectTimer; // Accessed on main thread only 148 149 NSDictionary<NSString*, id>* _options; 150 NSMutableDictionary<NSString*, NSString*>* _authenticationBasicAccounts; 151 NSMutableDictionary<NSString*, NSString*>* _authenticationDigestAccounts; 152 Class _connectionClass; 153 CFTimeInterval _disconnectDelay; 154 dispatch_source_t _source4; 155 dispatch_source_t _source6; 156 CFNetServiceRef _registrationService; 157 CFNetServiceRef _resolutionService; 158 DNSServiceRef _dnsService; 159 CFSocketRef _dnsSocket; 160 CFRunLoopSourceRef _dnsSource; 161 NSString* _dnsAddress; 162 NSUInteger _dnsPort; 163 BOOL _bindToLocalhost; 164#if TARGET_OS_IPHONE 165 BOOL _suspendInBackground; 166 UIBackgroundTaskIdentifier _backgroundTask; 167#endif 168#ifdef __GCDWEBSERVER_ENABLE_TESTING__ 169 BOOL _recording; 170#endif 171} 172 173+ (void)initialize { 174 GCDWebServerInitializeFunctions(); 175} 176 177- (instancetype)init { 178 if ((self = [super init])) { 179 _syncQueue = dispatch_queue_create([NSStringFromClass([self class]) UTF8String], DISPATCH_QUEUE_SERIAL); 180 _sourceGroup = dispatch_group_create(); 181 _handlers = [[NSMutableArray alloc] init]; 182#if TARGET_OS_IPHONE 183 _backgroundTask = UIBackgroundTaskInvalid; 184#endif 185 } 186 return self; 187} 188 189- (void)dealloc { 190 GWS_DCHECK(_connected == NO); 191 GWS_DCHECK(_activeConnections == 0); 192 GWS_DCHECK(_options == nil); // The server can never be dealloc'ed while running because of the retain-cycle with the dispatch source 193 GWS_DCHECK(_disconnectTimer == NULL); // The server can never be dealloc'ed while the disconnect timer is pending because of the retain-cycle 194 195#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE 196 dispatch_release(_sourceGroup); 197 dispatch_release(_syncQueue); 198#endif 199} 200 201#if TARGET_OS_IPHONE 202 203// Always called on main thread 204- (void)_startBackgroundTask { 205 GWS_DCHECK([NSThread isMainThread]); 206 if (_backgroundTask == UIBackgroundTaskInvalid) { 207 GWS_LOG_DEBUG(@"Did start background task"); 208 _backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ 209 GWS_LOG_WARNING(@"Application is being suspended while %@ is still connected", [self class]); 210 [self _endBackgroundTask]; 211 }]; 212 } else { 213 GWS_DNOT_REACHED(); 214 } 215} 216 217#endif 218 219// Always called on main thread 220- (void)_didConnect { 221 GWS_DCHECK([NSThread isMainThread]); 222 GWS_DCHECK(_connected == NO); 223 _connected = YES; 224 GWS_LOG_DEBUG(@"Did connect"); 225 226#if TARGET_OS_IPHONE 227 if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) { 228 [self _startBackgroundTask]; 229 } 230#endif 231 232 if ([_delegate respondsToSelector:@selector(webServerDidConnect:)]) { 233 [_delegate webServerDidConnect:self]; 234 } 235} 236 237- (void)willStartConnection:(GCDWebServerConnection*)connection { 238 dispatch_sync(_syncQueue, ^{ 239 GWS_DCHECK(self->_activeConnections >= 0); 240 if (self->_activeConnections == 0) { 241 dispatch_async(dispatch_get_main_queue(), ^{ 242 if (self->_disconnectTimer) { 243 CFRunLoopTimerInvalidate(self->_disconnectTimer); 244 CFRelease(self->_disconnectTimer); 245 self->_disconnectTimer = NULL; 246 } 247 if (self->_connected == NO) { 248 [self _didConnect]; 249 } 250 }); 251 } 252 self->_activeConnections += 1; 253 }); 254} 255 256#if TARGET_OS_IPHONE 257 258// Always called on main thread 259- (void)_endBackgroundTask { 260 GWS_DCHECK([NSThread isMainThread]); 261 if (_backgroundTask != UIBackgroundTaskInvalid) { 262 if (_suspendInBackground && ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) && _source4) { 263 [self _stop]; 264 } 265 [[UIApplication sharedApplication] endBackgroundTask:_backgroundTask]; 266 _backgroundTask = UIBackgroundTaskInvalid; 267 GWS_LOG_DEBUG(@"Did end background task"); 268 } 269} 270 271#endif 272 273// Always called on main thread 274- (void)_didDisconnect { 275 GWS_DCHECK([NSThread isMainThread]); 276 GWS_DCHECK(_connected == YES); 277 _connected = NO; 278 GWS_LOG_DEBUG(@"Did disconnect"); 279 280#if TARGET_OS_IPHONE 281 [self _endBackgroundTask]; 282#endif 283 284 if ([_delegate respondsToSelector:@selector(webServerDidDisconnect:)]) { 285 [_delegate webServerDidDisconnect:self]; 286 } 287} 288 289- (void)didEndConnection:(GCDWebServerConnection*)connection { 290 dispatch_sync(_syncQueue, ^{ 291 GWS_DCHECK(self->_activeConnections > 0); 292 self->_activeConnections -= 1; 293 if (self->_activeConnections == 0) { 294 dispatch_async(dispatch_get_main_queue(), ^{ 295 if ((self->_disconnectDelay > 0.0) && (self->_source4 != NULL)) { 296 if (self->_disconnectTimer) { 297 CFRunLoopTimerInvalidate(self->_disconnectTimer); 298 CFRelease(self->_disconnectTimer); 299 } 300 self->_disconnectTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + self->_disconnectDelay, 0.0, 0, 0, ^(CFRunLoopTimerRef timer) { 301 GWS_DCHECK([NSThread isMainThread]); 302 [self _didDisconnect]; 303 CFRelease(self->_disconnectTimer); 304 self->_disconnectTimer = NULL; 305 }); 306 CFRunLoopAddTimer(CFRunLoopGetMain(), self->_disconnectTimer, kCFRunLoopCommonModes); 307 } else { 308 [self _didDisconnect]; 309 } 310 }); 311 } 312 }); 313} 314 315- (NSString*)bonjourName { 316 CFStringRef name = _resolutionService ? CFNetServiceGetName(_resolutionService) : NULL; 317 return name && CFStringGetLength(name) ? CFBridgingRelease(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil; 318} 319 320- (NSString*)bonjourType { 321 CFStringRef type = _resolutionService ? CFNetServiceGetType(_resolutionService) : NULL; 322 return type && CFStringGetLength(type) ? CFBridgingRelease(CFStringCreateCopy(kCFAllocatorDefault, type)) : nil; 323} 324 325- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock { 326 [self addHandlerWithMatchBlock:matchBlock 327 asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { 328 completionBlock(processBlock(request)); 329 }]; 330} 331 332- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock { 333 GWS_DCHECK(_options == nil); 334 GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock asyncProcessBlock:processBlock]; 335 [_handlers insertObject:handler atIndex:0]; 336} 337 338- (void)removeAllHandlers { 339 GWS_DCHECK(_options == nil); 340 [_handlers removeAllObjects]; 341} 342 343static void _NetServiceRegisterCallBack(CFNetServiceRef service, CFStreamError* error, void* info) { 344 GWS_DCHECK([NSThread isMainThread]); 345 @autoreleasepool { 346 if (error->error) { 347 GWS_LOG_ERROR(@"Bonjour registration error %i (domain %i)", (int)error->error, (int)error->domain); 348 } else { 349 GCDWebServer* server = (__bridge GCDWebServer*)info; 350 GWS_LOG_VERBOSE(@"Bonjour registration complete for %@", [server class]); 351 if (!CFNetServiceResolveWithTimeout(server->_resolutionService, kBonjourResolutionTimeout, NULL)) { 352 GWS_LOG_ERROR(@"Failed starting Bonjour resolution"); 353 GWS_DNOT_REACHED(); 354 } 355 } 356 } 357} 358 359static void _NetServiceResolveCallBack(CFNetServiceRef service, CFStreamError* error, void* info) { 360 GWS_DCHECK([NSThread isMainThread]); 361 @autoreleasepool { 362 if (error->error) { 363 if ((error->domain != kCFStreamErrorDomainNetServices) && (error->error != kCFNetServicesErrorTimeout)) { 364 GWS_LOG_ERROR(@"Bonjour resolution error %i (domain %i)", (int)error->error, (int)error->domain); 365 } 366 } else { 367 GCDWebServer* server = (__bridge GCDWebServer*)info; 368 GWS_LOG_INFO(@"%@ now locally reachable at %@", [server class], server.bonjourServerURL); 369 if ([server.delegate respondsToSelector:@selector(webServerDidCompleteBonjourRegistration:)]) { 370 [server.delegate webServerDidCompleteBonjourRegistration:server]; 371 } 372 } 373 } 374} 375 376static void _DNSServiceCallBack(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, uint32_t externalAddress, DNSServiceProtocol protocol, uint16_t internalPort, uint16_t externalPort, uint32_t ttl, void* context) { 377 GWS_DCHECK([NSThread isMainThread]); 378 @autoreleasepool { 379 GCDWebServer* server = (__bridge GCDWebServer*)context; 380 if ((errorCode == kDNSServiceErr_NoError) || (errorCode == kDNSServiceErr_DoubleNAT)) { 381 struct sockaddr_in addr4; 382 bzero(&addr4, sizeof(addr4)); 383 addr4.sin_len = sizeof(addr4); 384 addr4.sin_family = AF_INET; 385 addr4.sin_addr.s_addr = externalAddress; // Already in network byte order 386 server->_dnsAddress = GCDWebServerStringFromSockAddr((const struct sockaddr*)&addr4, NO); 387 server->_dnsPort = ntohs(externalPort); 388 GWS_LOG_INFO(@"%@ now publicly reachable at %@", [server class], server.publicServerURL); 389 } else { 390 GWS_LOG_ERROR(@"DNS service error %i", errorCode); 391 server->_dnsAddress = nil; 392 server->_dnsPort = 0; 393 } 394 if ([server.delegate respondsToSelector:@selector(webServerDidUpdateNATPortMapping:)]) { 395 [server.delegate webServerDidUpdateNATPortMapping:server]; 396 } 397 } 398} 399 400static void _SocketCallBack(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void* data, void* info) { 401 GWS_DCHECK([NSThread isMainThread]); 402 @autoreleasepool { 403 GCDWebServer* server = (__bridge GCDWebServer*)info; 404 DNSServiceErrorType status = DNSServiceProcessResult(server->_dnsService); 405 if (status != kDNSServiceErr_NoError) { 406 GWS_LOG_ERROR(@"DNS service error %i", status); 407 } 408 } 409} 410 411static inline id _GetOption(NSDictionary<NSString*, id>* options, NSString* key, id defaultValue) { 412 id value = [options objectForKey:key]; 413 return value ? value : defaultValue; 414} 415 416static inline NSString* _EncodeBase64(NSString* string) { 417 NSData* data = [string dataUsingEncoding:NSUTF8StringEncoding]; 418#if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9) 419 return [[NSString alloc] initWithData:[data base64EncodedDataWithOptions:0] encoding:NSASCIIStringEncoding]; 420#else 421 if (@available(macOS 10.9, *)) { 422 return [[NSString alloc] initWithData:[data base64EncodedDataWithOptions:0] encoding:NSASCIIStringEncoding]; 423 } 424 return [data base64Encoding]; 425#endif 426} 427 428- (int)_createListeningSocket:(BOOL)useIPv6 429 localAddress:(const void*)address 430 length:(socklen_t)length 431 maxPendingConnections:(NSUInteger)maxPendingConnections 432 error:(NSError**)error { 433 int listeningSocket = socket(useIPv6 ? PF_INET6 : PF_INET, SOCK_STREAM, IPPROTO_TCP); 434 if (listeningSocket > 0) { 435 int yes = 1; 436 setsockopt(listeningSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); 437 438 if (bind(listeningSocket, address, length) == 0) { 439 if (listen(listeningSocket, (int)maxPendingConnections) == 0) { 440 GWS_LOG_DEBUG(@"Did open %s listening socket %i", useIPv6 ? "IPv6" : "IPv4", listeningSocket); 441 return listeningSocket; 442 } else { 443 if (error) { 444 *error = GCDWebServerMakePosixError(errno); 445 } 446 GWS_LOG_ERROR(@"Failed starting %s listening socket: %s (%i)", useIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); 447 close(listeningSocket); 448 } 449 } else { 450 if (error) { 451 *error = GCDWebServerMakePosixError(errno); 452 } 453 GWS_LOG_ERROR(@"Failed binding %s listening socket: %s (%i)", useIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); 454 close(listeningSocket); 455 } 456 457 } else { 458 if (error) { 459 *error = GCDWebServerMakePosixError(errno); 460 } 461 GWS_LOG_ERROR(@"Failed creating %s listening socket: %s (%i)", useIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); 462 } 463 return -1; 464} 465 466- (dispatch_source_t)_createDispatchSourceWithListeningSocket:(int)listeningSocket isIPv6:(BOOL)isIPv6 { 467 dispatch_group_enter(_sourceGroup); 468 dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, dispatch_get_global_queue(_dispatchQueuePriority, 0)); 469 dispatch_source_set_cancel_handler(source, ^{ 470 @autoreleasepool { 471 int result = close(listeningSocket); 472 if (result != 0) { 473 GWS_LOG_ERROR(@"Failed closing %s listening socket: %s (%i)", isIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); 474 } else { 475 GWS_LOG_DEBUG(@"Did close %s listening socket %i", isIPv6 ? "IPv6" : "IPv4", listeningSocket); 476 } 477 } 478 dispatch_group_leave(self->_sourceGroup); 479 }); 480 dispatch_source_set_event_handler(source, ^{ 481 @autoreleasepool { 482 struct sockaddr_storage remoteSockAddr; 483 socklen_t remoteAddrLen = sizeof(remoteSockAddr); 484 int socket = accept(listeningSocket, (struct sockaddr*)&remoteSockAddr, &remoteAddrLen); 485 if (socket > 0) { 486 NSData* remoteAddress = [NSData dataWithBytes:&remoteSockAddr length:remoteAddrLen]; 487 488 struct sockaddr_storage localSockAddr; 489 socklen_t localAddrLen = sizeof(localSockAddr); 490 NSData* localAddress = nil; 491 if (getsockname(socket, (struct sockaddr*)&localSockAddr, &localAddrLen) == 0) { 492 localAddress = [NSData dataWithBytes:&localSockAddr length:localAddrLen]; 493 GWS_DCHECK((!isIPv6 && localSockAddr.ss_family == AF_INET) || (isIPv6 && localSockAddr.ss_family == AF_INET6)); 494 } else { 495 GWS_DNOT_REACHED(); 496 } 497 498 int noSigPipe = 1; 499 setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); // Make sure this socket cannot generate SIG_PIPE 500 501 GCDWebServerConnection* connection = [(GCDWebServerConnection*)[self->_connectionClass alloc] initWithServer:self localAddress:localAddress remoteAddress:remoteAddress socket:socket]; // Connection will automatically retain itself while opened 502 [connection self]; // Prevent compiler from complaining about unused variable / useless statement 503 } else { 504 GWS_LOG_ERROR(@"Failed accepting %s socket: %s (%i)", isIPv6 ? "IPv6" : "IPv4", strerror(errno), errno); 505 } 506 } 507 }); 508 return source; 509} 510 511- (BOOL)_start:(NSError**)error { 512 GWS_DCHECK(_source4 == NULL); 513 514 NSUInteger port = [(NSNumber*)_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue]; 515 BOOL bindToLocalhost = [(NSNumber*)_GetOption(_options, GCDWebServerOption_BindToLocalhost, @NO) boolValue]; 516 NSUInteger maxPendingConnections = [(NSNumber*)_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue]; 517 518 struct sockaddr_in addr4; 519 bzero(&addr4, sizeof(addr4)); 520 addr4.sin_len = sizeof(addr4); 521 addr4.sin_family = AF_INET; 522 addr4.sin_port = htons(port); 523 addr4.sin_addr.s_addr = bindToLocalhost ? htonl(INADDR_LOOPBACK) : htonl(INADDR_ANY); 524 int listeningSocket4 = [self _createListeningSocket:NO localAddress:&addr4 length:sizeof(addr4) maxPendingConnections:maxPendingConnections error:error]; 525 if (listeningSocket4 <= 0) { 526 return NO; 527 } 528 if (port == 0) { 529 struct sockaddr_in addr; 530 socklen_t addrlen = sizeof(addr); 531 if (getsockname(listeningSocket4, (struct sockaddr*)&addr, &addrlen) == 0) { 532 port = ntohs(addr.sin_port); 533 } else { 534 GWS_LOG_ERROR(@"Failed retrieving socket address: %s (%i)", strerror(errno), errno); 535 } 536 } 537 538 struct sockaddr_in6 addr6; 539 bzero(&addr6, sizeof(addr6)); 540 addr6.sin6_len = sizeof(addr6); 541 addr6.sin6_family = AF_INET6; 542 addr6.sin6_port = htons(port); 543 addr6.sin6_addr = bindToLocalhost ? in6addr_loopback : in6addr_any; 544 int listeningSocket6 = [self _createListeningSocket:YES localAddress:&addr6 length:sizeof(addr6) maxPendingConnections:maxPendingConnections error:error]; 545 if (listeningSocket6 <= 0) { 546 close(listeningSocket4); 547 return NO; 548 } 549 550 _serverName = [(NSString*)_GetOption(_options, GCDWebServerOption_ServerName, NSStringFromClass([self class])) copy]; 551 NSString* authenticationMethod = _GetOption(_options, GCDWebServerOption_AuthenticationMethod, nil); 552 if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_Basic]) { 553 _authenticationRealm = [(NSString*)_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy]; 554 _authenticationBasicAccounts = [[NSMutableDictionary alloc] init]; 555 NSDictionary* accounts = _GetOption(_options, GCDWebServerOption_AuthenticationAccounts, @{}); 556 [accounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* password, BOOL* stop) { 557 [self->_authenticationBasicAccounts setObject:_EncodeBase64([NSString stringWithFormat:@"%@:%@", username, password]) forKey:username]; 558 }]; 559 } else if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_DigestAccess]) { 560 _authenticationRealm = [(NSString*)_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy]; 561 _authenticationDigestAccounts = [[NSMutableDictionary alloc] init]; 562 NSDictionary* accounts = _GetOption(_options, GCDWebServerOption_AuthenticationAccounts, @{}); 563 [accounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* password, BOOL* stop) { 564 [self->_authenticationDigestAccounts setObject:GCDWebServerComputeMD5Digest(@"%@:%@:%@", username, self->_authenticationRealm, password) forKey:username]; 565 }]; 566 } 567 _connectionClass = _GetOption(_options, GCDWebServerOption_ConnectionClass, [GCDWebServerConnection class]); 568 _shouldAutomaticallyMapHEADToGET = [(NSNumber*)_GetOption(_options, GCDWebServerOption_AutomaticallyMapHEADToGET, @YES) boolValue]; 569 _disconnectDelay = [(NSNumber*)_GetOption(_options, GCDWebServerOption_ConnectedStateCoalescingInterval, @1.0) doubleValue]; 570 _dispatchQueuePriority = [(NSNumber*)_GetOption(_options, GCDWebServerOption_DispatchQueuePriority, @(DISPATCH_QUEUE_PRIORITY_DEFAULT)) longValue]; 571 572 _source4 = [self _createDispatchSourceWithListeningSocket:listeningSocket4 isIPv6:NO]; 573 _source6 = [self _createDispatchSourceWithListeningSocket:listeningSocket6 isIPv6:YES]; 574 _port = port; 575 _bindToLocalhost = bindToLocalhost; 576 577 NSString* bonjourName = _GetOption(_options, GCDWebServerOption_BonjourName, nil); 578 NSString* bonjourType = _GetOption(_options, GCDWebServerOption_BonjourType, @"_http._tcp"); 579 if (bonjourName) { 580 _registrationService = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), (__bridge CFStringRef)bonjourType, (__bridge CFStringRef)(bonjourName.length ? bonjourName : _serverName), (SInt32)_port); 581 if (_registrationService) { 582 CFNetServiceClientContext context = {0, (__bridge void*)self, NULL, NULL, NULL}; 583 584 CFNetServiceSetClient(_registrationService, _NetServiceRegisterCallBack, &context); 585 CFNetServiceScheduleWithRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes); 586 CFStreamError streamError = {0}; 587 CFNetServiceRegisterWithOptions(_registrationService, 0, &streamError); 588 589 _resolutionService = CFNetServiceCreateCopy(kCFAllocatorDefault, _registrationService); 590 if (_resolutionService) { 591 CFNetServiceSetClient(_resolutionService, _NetServiceResolveCallBack, &context); 592 CFNetServiceScheduleWithRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes); 593 } else { 594 GWS_LOG_ERROR(@"Failed creating CFNetService for resolution"); 595 } 596 } else { 597 GWS_LOG_ERROR(@"Failed creating CFNetService for registration"); 598 } 599 } 600 601 if ([(NSNumber*)_GetOption(_options, GCDWebServerOption_RequestNATPortMapping, @NO) boolValue]) { 602 DNSServiceErrorType status = DNSServiceNATPortMappingCreate(&_dnsService, 0, 0, kDNSServiceProtocol_TCP, htons(port), htons(port), 0, _DNSServiceCallBack, (__bridge void*)self); 603 if (status == kDNSServiceErr_NoError) { 604 CFSocketContext context = {0, (__bridge void*)self, NULL, NULL, NULL}; 605 _dnsSocket = CFSocketCreateWithNative(kCFAllocatorDefault, DNSServiceRefSockFD(_dnsService), kCFSocketReadCallBack, _SocketCallBack, &context); 606 if (_dnsSocket) { 607 CFSocketSetSocketFlags(_dnsSocket, CFSocketGetSocketFlags(_dnsSocket) & ~kCFSocketCloseOnInvalidate); 608 _dnsSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _dnsSocket, 0); 609 if (_dnsSource) { 610 CFRunLoopAddSource(CFRunLoopGetMain(), _dnsSource, kCFRunLoopCommonModes); 611 } else { 612 GWS_LOG_ERROR(@"Failed creating CFRunLoopSource"); 613 GWS_DNOT_REACHED(); 614 } 615 } else { 616 GWS_LOG_ERROR(@"Failed creating CFSocket"); 617 GWS_DNOT_REACHED(); 618 } 619 } else { 620 GWS_LOG_ERROR(@"Failed creating NAT port mapping (%i)", status); 621 } 622 } 623 624 dispatch_resume(_source4); 625 dispatch_resume(_source6); 626 GWS_LOG_INFO(@"%@ started on port %i and reachable at %@", [self class], (int)_port, self.serverURL); 627 if ([_delegate respondsToSelector:@selector(webServerDidStart:)]) { 628 dispatch_async(dispatch_get_main_queue(), ^{ 629 [self->_delegate webServerDidStart:self]; 630 }); 631 } 632 633 return YES; 634} 635 636- (void)_stop { 637 GWS_DCHECK(_source4 != NULL); 638 639 if (_dnsService) { 640 _dnsAddress = nil; 641 _dnsPort = 0; 642 if (_dnsSource) { 643 CFRunLoopSourceInvalidate(_dnsSource); 644 CFRelease(_dnsSource); 645 _dnsSource = NULL; 646 } 647 if (_dnsSocket) { 648 CFRelease(_dnsSocket); 649 _dnsSocket = NULL; 650 } 651 DNSServiceRefDeallocate(_dnsService); 652 _dnsService = NULL; 653 } 654 655 if (_registrationService) { 656 if (_resolutionService) { 657 CFNetServiceUnscheduleFromRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes); 658 CFNetServiceSetClient(_resolutionService, NULL, NULL); 659 CFNetServiceCancel(_resolutionService); 660 CFRelease(_resolutionService); 661 _resolutionService = NULL; 662 } 663 CFNetServiceUnscheduleFromRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes); 664 CFNetServiceSetClient(_registrationService, NULL, NULL); 665 CFNetServiceCancel(_registrationService); 666 CFRelease(_registrationService); 667 _registrationService = NULL; 668 } 669 670 dispatch_source_cancel(_source6); 671 dispatch_source_cancel(_source4); 672 dispatch_group_wait(_sourceGroup, DISPATCH_TIME_FOREVER); // Wait until the cancellation handlers have been called which guarantees the listening sockets are closed 673#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE 674 dispatch_release(_source6); 675#endif 676 _source6 = NULL; 677#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE 678 dispatch_release(_source4); 679#endif 680 _source4 = NULL; 681 _port = 0; 682 _bindToLocalhost = NO; 683 684 _serverName = nil; 685 _authenticationRealm = nil; 686 _authenticationBasicAccounts = nil; 687 _authenticationDigestAccounts = nil; 688 689 dispatch_async(dispatch_get_main_queue(), ^{ 690 if (self->_disconnectTimer) { 691 CFRunLoopTimerInvalidate(self->_disconnectTimer); 692 CFRelease(self->_disconnectTimer); 693 self->_disconnectTimer = NULL; 694 [self _didDisconnect]; 695 } 696 }); 697 698 GWS_LOG_INFO(@"%@ stopped", [self class]); 699 if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) { 700 dispatch_async(dispatch_get_main_queue(), ^{ 701 [self->_delegate webServerDidStop:self]; 702 }); 703 } 704} 705 706#if TARGET_OS_IPHONE 707 708- (void)_didEnterBackground:(NSNotification*)notification { 709 GWS_DCHECK([NSThread isMainThread]); 710 GWS_LOG_DEBUG(@"Did enter background"); 711 if ((_backgroundTask == UIBackgroundTaskInvalid) && _source4) { 712 [self _stop]; 713 } 714} 715 716- (void)_willEnterForeground:(NSNotification*)notification { 717 GWS_DCHECK([NSThread isMainThread]); 718 GWS_LOG_DEBUG(@"Will enter foreground"); 719 if (!_source4) { 720 [self _start:NULL]; // TODO: There's probably nothing we can do on failure 721 } 722} 723 724#endif 725 726- (BOOL)startWithOptions:(NSDictionary<NSString*, id>*)options error:(NSError**)error { 727 if (_options == nil) { 728 _options = options ? [options copy] : @{}; 729#if TARGET_OS_IPHONE 730 _suspendInBackground = [(NSNumber*)_GetOption(_options, GCDWebServerOption_AutomaticallySuspendInBackground, @YES) boolValue]; 731 if (((_suspendInBackground == NO) || ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground)) && ![self _start:error]) 732#else 733 if (![self _start:error]) 734#endif 735 { 736 _options = nil; 737 return NO; 738 } 739#if TARGET_OS_IPHONE 740 if (_suspendInBackground) { 741 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; 742 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; 743 } 744#endif 745 return YES; 746 } else { 747 GWS_DNOT_REACHED(); 748 } 749 return NO; 750} 751 752- (BOOL)isRunning { 753 return (_options ? YES : NO); 754} 755 756- (void)stop { 757 if (_options) { 758#if TARGET_OS_IPHONE 759 if (_suspendInBackground) { 760 [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; 761 [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; 762 } 763#endif 764 if (_source4) { 765 [self _stop]; 766 } 767 _options = nil; 768 } else { 769 GWS_DNOT_REACHED(); 770 } 771} 772 773@end 774 775@implementation GCDWebServer (Extensions) 776 777- (NSURL*)serverURL { 778 if (_source4) { 779 NSString* ipAddress = _bindToLocalhost ? @"localhost" : GCDWebServerGetPrimaryIPAddress(NO); // We can't really use IPv6 anyway as it doesn't work great with HTTP URLs in practice 780 if (ipAddress) { 781 if (_port != 80) { 782 return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", ipAddress, (int)_port]]; 783 } else { 784 return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", ipAddress]]; 785 } 786 } 787 } 788 return nil; 789} 790 791- (NSURL*)bonjourServerURL { 792 if (_source4 && _resolutionService) { 793 NSString* name = (__bridge NSString*)CFNetServiceGetTargetHost(_resolutionService); 794 if (name.length) { 795 name = [name substringToIndex:(name.length - 1)]; // Strip trailing period at end of domain 796 if (_port != 80) { 797 return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", name, (int)_port]]; 798 } else { 799 return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", name]]; 800 } 801 } 802 } 803 return nil; 804} 805 806- (NSURL*)publicServerURL { 807 if (_source4 && _dnsService && _dnsAddress && _dnsPort) { 808 if (_dnsPort != 80) { 809 return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", _dnsAddress, (int)_dnsPort]]; 810 } else { 811 return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", _dnsAddress]]; 812 } 813 } 814 return nil; 815} 816 817- (BOOL)start { 818 return [self startWithPort:kDefaultPort bonjourName:@""]; 819} 820 821- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name { 822 NSMutableDictionary* options = [NSMutableDictionary dictionary]; 823 [options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port]; 824 [options setValue:name forKey:GCDWebServerOption_BonjourName]; 825 return [self startWithOptions:options error:NULL]; 826} 827 828#if !TARGET_OS_IPHONE 829 830- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name { 831 NSMutableDictionary* options = [NSMutableDictionary dictionary]; 832 [options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port]; 833 [options setValue:name forKey:GCDWebServerOption_BonjourName]; 834 return [self runWithOptions:options error:NULL]; 835} 836 837- (BOOL)runWithOptions:(NSDictionary<NSString*, id>*)options error:(NSError**)error { 838 GWS_DCHECK([NSThread isMainThread]); 839 BOOL success = NO; 840 _run = YES; 841 void (*termHandler)(int) = signal(SIGTERM, _SignalHandler); 842 void (*intHandler)(int) = signal(SIGINT, _SignalHandler); 843 if ((termHandler != SIG_ERR) && (intHandler != SIG_ERR)) { 844 if ([self startWithOptions:options error:error]) { 845 while (_run) { 846 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true); 847 } 848 [self stop]; 849 success = YES; 850 } 851 _ExecuteMainThreadRunLoopSources(); 852 signal(SIGINT, intHandler); 853 signal(SIGTERM, termHandler); 854 } 855 return success; 856} 857 858#endif 859 860@end 861 862@implementation GCDWebServer (Handlers) 863 864- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { 865 [self addDefaultHandlerForMethod:method 866 requestClass:aClass 867 asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { 868 completionBlock(block(request)); 869 }]; 870} 871 872- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { 873 [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) { 874 if (![requestMethod isEqualToString:method]) { 875 return nil; 876 } 877 return [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; 878 } 879 asyncProcessBlock:block]; 880} 881 882- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { 883 [self addHandlerForMethod:method 884 path:path 885 requestClass:aClass 886 asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { 887 completionBlock(block(request)); 888 }]; 889} 890 891- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { 892 if ([path hasPrefix:@"/"] && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) { 893 [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) { 894 if (![requestMethod isEqualToString:method]) { 895 return nil; 896 } 897 if ([urlPath caseInsensitiveCompare:path] != NSOrderedSame) { 898 return nil; 899 } 900 return [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; 901 } 902 asyncProcessBlock:block]; 903 } else { 904 GWS_DNOT_REACHED(); 905 } 906} 907 908- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { 909 [self addHandlerForMethod:method 910 pathRegex:regex 911 requestClass:aClass 912 asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { 913 completionBlock(block(request)); 914 }]; 915} 916 917- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { 918 NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL]; 919 if (expression && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) { 920 [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) { 921 if (![requestMethod isEqualToString:method]) { 922 return nil; 923 } 924 925 NSArray* matches = [expression matchesInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)]; 926 if (matches.count == 0) { 927 return nil; 928 } 929 930 NSMutableArray* captures = [NSMutableArray array]; 931 for (NSTextCheckingResult* result in matches) { 932 // Start at 1; index 0 is the whole string 933 for (NSUInteger i = 1; i < result.numberOfRanges; i++) { 934 NSRange range = [result rangeAtIndex:i]; 935 // range is {NSNotFound, 0} "if one of the capture groups did not participate in this particular match" 936 // see discussion in -[NSRegularExpression firstMatchInString:options:range:] 937 if (range.location != NSNotFound) { 938 [captures addObject:[urlPath substringWithRange:range]]; 939 } 940 } 941 } 942 943 GCDWebServerRequest* request = [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; 944 [request setAttribute:captures forKey:GCDWebServerRequestAttribute_RegexCaptures]; 945 return request; 946 } 947 asyncProcessBlock:block]; 948 } else { 949 GWS_DNOT_REACHED(); 950 } 951} 952 953@end 954 955@implementation GCDWebServer (GETHandlers) 956 957- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge { 958 [self addHandlerForMethod:@"GET" 959 path:path 960 requestClass:[GCDWebServerRequest class] 961 processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) { 962 GCDWebServerResponse* response = [GCDWebServerDataResponse responseWithData:staticData contentType:contentType]; 963 response.cacheControlMaxAge = cacheAge; 964 return response; 965 }]; 966} 967 968- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests { 969 [self addHandlerForMethod:@"GET" 970 path:path 971 requestClass:[GCDWebServerRequest class] 972 processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) { 973 GCDWebServerResponse* response = nil; 974 if (allowRangeRequests) { 975 response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange isAttachment:isAttachment]; 976 [response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"]; 977 } else { 978 response = [GCDWebServerFileResponse responseWithFile:filePath isAttachment:isAttachment]; 979 } 980 response.cacheControlMaxAge = cacheAge; 981 return response; 982 }]; 983} 984 985- (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path { 986 NSArray* contents = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL] sortedArrayUsingSelector:@selector(localizedStandardCompare:)]; 987 if (contents == nil) { 988 return nil; 989 } 990 NSMutableString* html = [NSMutableString string]; 991 [html appendString:@"<!DOCTYPE html>\n"]; 992 [html appendString:@"<html><head><meta charset=\"utf-8\"></head><body>\n"]; 993 [html appendString:@"<ul>\n"]; 994 for (NSString* entry in contents) { 995 if (![entry hasPrefix:@"."]) { 996 NSString* type = [[[NSFileManager defaultManager] attributesOfItemAtPath:[path stringByAppendingPathComponent:entry] error:NULL] objectForKey:NSFileType]; 997 GWS_DCHECK(type); 998#pragma clang diagnostic push 999#pragma clang diagnostic ignored "-Wdeprecated-declarations" 1000 NSString* escapedFile = [entry stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 1001#pragma clang diagnostic pop 1002 GWS_DCHECK(escapedFile); 1003 if ([type isEqualToString:NSFileTypeRegular]) { 1004 [html appendFormat:@"<li><a href=\"%@\">%@</a></li>\n", escapedFile, entry]; 1005 } else if ([type isEqualToString:NSFileTypeDirectory]) { 1006 [html appendFormat:@"<li><a href=\"%@/\">%@/</a></li>\n", escapedFile, entry]; 1007 } 1008 } 1009 } 1010 [html appendString:@"</ul>\n"]; 1011 [html appendString:@"</body></html>\n"]; 1012 return [GCDWebServerDataResponse responseWithHTML:html]; 1013} 1014 1015- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests { 1016 if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) { 1017 GCDWebServer* __unsafe_unretained server = self; 1018 [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) { 1019 if (![requestMethod isEqualToString:@"GET"]) { 1020 return nil; 1021 } 1022 if (![urlPath hasPrefix:basePath]) { 1023 return nil; 1024 } 1025 return [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; 1026 } 1027 processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) { 1028 GCDWebServerResponse* response = nil; 1029 NSString* filePath = [directoryPath stringByAppendingPathComponent:GCDWebServerNormalizePath([request.path substringFromIndex:basePath.length])]; 1030 NSString* fileType = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL] fileType]; 1031 if (fileType) { 1032 if ([fileType isEqualToString:NSFileTypeDirectory]) { 1033 if (indexFilename) { 1034 NSString* indexPath = [filePath stringByAppendingPathComponent:indexFilename]; 1035 NSString* indexType = [[[NSFileManager defaultManager] attributesOfItemAtPath:indexPath error:NULL] fileType]; 1036 if ([indexType isEqualToString:NSFileTypeRegular]) { 1037 return [GCDWebServerFileResponse responseWithFile:indexPath]; 1038 } 1039 } 1040 response = [server _responseWithContentsOfDirectory:filePath]; 1041 } else if ([fileType isEqualToString:NSFileTypeRegular]) { 1042 if (allowRangeRequests) { 1043 response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange]; 1044 [response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"]; 1045 } else { 1046 response = [GCDWebServerFileResponse responseWithFile:filePath]; 1047 } 1048 } 1049 } 1050 if (response) { 1051 response.cacheControlMaxAge = cacheAge; 1052 } else { 1053 response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NotFound]; 1054 } 1055 return response; 1056 }]; 1057 } else { 1058 GWS_DNOT_REACHED(); 1059 } 1060} 1061 1062@end 1063 1064@implementation GCDWebServer (Logging) 1065 1066+ (void)setLogLevel:(int)level { 1067#if defined(__GCDWEBSERVER_LOGGING_FACILITY_XLFACILITY__) 1068 [XLSharedFacility setMinLogLevel:level]; 1069#elif defined(__GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__) 1070 GCDWebServerLogLevel = level; 1071#endif 1072} 1073 1074- (void)logVerbose:(NSString*)format, ... { 1075 va_list arguments; 1076 va_start(arguments, format); 1077 GWS_LOG_VERBOSE(@"%@", [[NSString alloc] initWithFormat:format arguments:arguments]); 1078 va_end(arguments); 1079} 1080 1081- (void)logInfo:(NSString*)format, ... { 1082 va_list arguments; 1083 va_start(arguments, format); 1084 GWS_LOG_INFO(@"%@", [[NSString alloc] initWithFormat:format arguments:arguments]); 1085 va_end(arguments); 1086} 1087 1088- (void)logWarning:(NSString*)format, ... { 1089 va_list arguments; 1090 va_start(arguments, format); 1091 GWS_LOG_WARNING(@"%@", [[NSString alloc] initWithFormat:format arguments:arguments]); 1092 va_end(arguments); 1093} 1094 1095- (void)logError:(NSString*)format, ... { 1096 va_list arguments; 1097 va_start(arguments, format); 1098 GWS_LOG_ERROR(@"%@", [[NSString alloc] initWithFormat:format arguments:arguments]); 1099 va_end(arguments); 1100} 1101 1102@end 1103 1104#ifdef __GCDWEBSERVER_ENABLE_TESTING__ 1105 1106@implementation GCDWebServer (Testing) 1107 1108- (void)setRecordingEnabled:(BOOL)flag { 1109 _recording = flag; 1110} 1111 1112- (BOOL)isRecordingEnabled { 1113 return _recording; 1114} 1115 1116static CFHTTPMessageRef _CreateHTTPMessageFromData(NSData* data, BOOL isRequest) { 1117 CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, isRequest); 1118 if (CFHTTPMessageAppendBytes(message, data.bytes, data.length)) { 1119 return message; 1120 } 1121 CFRelease(message); 1122 return NULL; 1123} 1124 1125static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData, NSUInteger port) { 1126 CFHTTPMessageRef response = NULL; 1127 int httpSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); 1128 if (httpSocket > 0) { 1129 struct sockaddr_in addr4; 1130 bzero(&addr4, sizeof(addr4)); 1131 addr4.sin_len = sizeof(addr4); 1132 addr4.sin_family = AF_INET; 1133 addr4.sin_port = htons(port); 1134 addr4.sin_addr.s_addr = htonl(INADDR_ANY); 1135 if (connect(httpSocket, (void*)&addr4, sizeof(addr4)) == 0) { 1136 if (write(httpSocket, inData.bytes, inData.length) == (ssize_t)inData.length) { 1137 NSMutableData* outData = [[NSMutableData alloc] initWithLength:(256 * 1024)]; 1138 NSUInteger length = 0; 1139 while (1) { 1140 ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length); 1141 if (result < 0) { 1142 length = NSUIntegerMax; 1143 break; 1144 } else if (result == 0) { 1145 break; 1146 } 1147 length += result; 1148 if (length >= outData.length) { 1149 outData.length = 2 * outData.length; 1150 } 1151 } 1152 if (length != NSUIntegerMax) { 1153 outData.length = length; 1154 response = _CreateHTTPMessageFromData(outData, NO); 1155 } else { 1156 GWS_DNOT_REACHED(); 1157 } 1158 } 1159 } 1160 close(httpSocket); 1161 } 1162 return response; 1163} 1164 1165static void _LogResult(NSString* format, ...) { 1166 va_list arguments; 1167 va_start(arguments, format); 1168 NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; 1169 va_end(arguments); 1170 fprintf(stdout, "%s\n", [message UTF8String]); 1171} 1172 1173- (NSInteger)runTestsWithOptions:(NSDictionary<NSString*, id>*)options inDirectory:(NSString*)path { 1174 GWS_DCHECK([NSThread isMainThread]); 1175 NSArray* ignoredHeaders = @[ @"Date", @"Etag" ]; // Dates are always different by definition and ETags depend on file system node IDs 1176 NSInteger result = -1; 1177 if ([self startWithOptions:options error:NULL]) { 1178 _ExecuteMainThreadRunLoopSources(); 1179 1180 result = 0; 1181 NSArray* files = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL] sortedArrayUsingSelector:@selector(localizedStandardCompare:)]; 1182 for (NSString* requestFile in files) { 1183 if (![requestFile hasSuffix:@".request"]) { 1184 continue; 1185 } 1186 @autoreleasepool { 1187 NSString* index = [[requestFile componentsSeparatedByString:@"-"] firstObject]; 1188 BOOL success = NO; 1189 NSData* requestData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:requestFile]]; 1190 if (requestData) { 1191 CFHTTPMessageRef request = _CreateHTTPMessageFromData(requestData, YES); 1192 if (request) { 1193 NSString* requestMethod = CFBridgingRelease(CFHTTPMessageCopyRequestMethod(request)); 1194 NSURL* requestURL = CFBridgingRelease(CFHTTPMessageCopyRequestURL(request)); 1195 _LogResult(@"[%i] %@ %@", (int)[index integerValue], requestMethod, requestURL.path); 1196 NSString* prefix = [index stringByAppendingString:@"-"]; 1197 for (NSString* responseFile in files) { 1198 if ([responseFile hasPrefix:prefix] && [responseFile hasSuffix:@".response"]) { 1199 NSData* responseData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:responseFile]]; 1200 if (responseData) { 1201 CFHTTPMessageRef expectedResponse = _CreateHTTPMessageFromData(responseData, NO); 1202 if (expectedResponse) { 1203 CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromPerformingRequest(requestData, self.port); 1204 if (actualResponse) { 1205 success = YES; 1206 1207 CFIndex expectedStatusCode = CFHTTPMessageGetResponseStatusCode(expectedResponse); 1208 CFIndex actualStatusCode = CFHTTPMessageGetResponseStatusCode(actualResponse); 1209 if (actualStatusCode != expectedStatusCode) { 1210 _LogResult(@" Status code not matching:\n Expected: %i\n Actual: %i", (int)expectedStatusCode, (int)actualStatusCode); 1211 success = NO; 1212 } 1213 1214 NSDictionary* expectedHeaders = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(expectedResponse)); 1215 NSDictionary* actualHeaders = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(actualResponse)); 1216 for (NSString* expectedHeader in expectedHeaders) { 1217 if ([ignoredHeaders containsObject:expectedHeader]) { 1218 continue; 1219 } 1220 NSString* expectedValue = [expectedHeaders objectForKey:expectedHeader]; 1221 NSString* actualValue = [actualHeaders objectForKey:expectedHeader]; 1222 if (![actualValue isEqualToString:expectedValue]) { 1223 _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", expectedHeader, expectedValue, actualValue); 1224 success = NO; 1225 } 1226 } 1227 for (NSString* actualHeader in actualHeaders) { 1228 if (![expectedHeaders objectForKey:actualHeader]) { 1229 _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", actualHeader, nil, [actualHeaders objectForKey:actualHeader]); 1230 success = NO; 1231 } 1232 } 1233 1234 NSString* expectedContentLength = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(expectedResponse, CFSTR("Content-Length"))); 1235 NSData* expectedBody = CFBridgingRelease(CFHTTPMessageCopyBody(expectedResponse)); 1236 NSString* actualContentLength = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(actualResponse, CFSTR("Content-Length"))); 1237 NSData* actualBody = CFBridgingRelease(CFHTTPMessageCopyBody(actualResponse)); 1238 if ([actualContentLength isEqualToString:expectedContentLength] && (actualBody.length > expectedBody.length)) { // Handle web browser closing connection before retrieving entire body (e.g. when playing a video file) 1239 actualBody = [actualBody subdataWithRange:NSMakeRange(0, expectedBody.length)]; 1240 } 1241 if ((actualBody && expectedBody && ![actualBody isEqualToData:expectedBody]) || (actualBody && !expectedBody) || (!actualBody && expectedBody)) { 1242 _LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length); 1243 success = NO; 1244#if !TARGET_OS_IPHONE 1245#if DEBUG 1246 if (GCDWebServerIsTextContentType((NSString*)[expectedHeaders objectForKey:@"Content-Type"])) { 1247 NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:(NSString*)[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; 1248 NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:(NSString*)[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; 1249 if ([expectedBody writeToFile:expectedPath atomically:YES] && [actualBody writeToFile:actualPath atomically:YES]) { 1250 NSTask* task = [[NSTask alloc] init]; 1251 [task setLaunchPath:@"/usr/bin/opendiff"]; 1252 [task setArguments:@[ expectedPath, actualPath ]]; 1253 [task launch]; 1254 } 1255 } 1256#endif 1257#endif 1258 } 1259 1260 CFRelease(actualResponse); 1261 } 1262 CFRelease(expectedResponse); 1263 } 1264 } else { 1265 GWS_DNOT_REACHED(); 1266 } 1267 break; 1268 } 1269 } 1270 CFRelease(request); 1271 } 1272 } else { 1273 GWS_DNOT_REACHED(); 1274 } 1275 _LogResult(@""); 1276 if (!success) { 1277 ++result; 1278 } 1279 } 1280 _ExecuteMainThreadRunLoopSources(); 1281 } 1282 1283 [self stop]; 1284 1285 _ExecuteMainThreadRunLoopSources(); 1286 } 1287 return result; 1288} 1289 1290@end 1291 1292#endif 1293