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