PageRenderTime 34ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

/io/libraries/CocoaHttpServer/HTTPServer.m

https://gitlab.com/base.io/io
Objective C | 761 lines | 438 code | 181 blank | 142 comment | 20 complexity | 81af6856b8deb2364f3b86623a031aa0 MD5 | raw file
  1. #import "HTTPServer.h"
  2. #import "GCDAsyncSocket.h"
  3. #import "HTTPConnection.h"
  4. #import "WebSocket.h"
  5. #import "HTTPLogging.h"
  6. #if ! __has_feature(objc_arc)
  7. #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
  8. #endif
  9. // Log levels: off, error, warn, info, verbose
  10. // Other flags: trace
  11. static const int httpLogLevel = HTTP_LOG_LEVEL_INFO; // | HTTP_LOG_FLAG_TRACE;
  12. @interface HTTPServer (PrivateAPI)
  13. - (void)unpublishBonjour;
  14. - (void)publishBonjour;
  15. + (void)startBonjourThreadIfNeeded;
  16. + (void)performBonjourBlock:(dispatch_block_t)block;
  17. @end
  18. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  19. #pragma mark -
  20. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  21. @implementation HTTPServer
  22. /**
  23. * Standard Constructor.
  24. * Instantiates an HTTP server, but does not start it.
  25. **/
  26. - (id)init
  27. {
  28. if ((self = [super init]))
  29. {
  30. HTTPLogTrace();
  31. // Initialize underlying dispatch queue and GCD based tcp socket
  32. serverQueue = dispatch_queue_create("HTTPServer", NULL);
  33. asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:serverQueue];
  34. // Use default connection class of HTTPConnection
  35. connectionQueue = dispatch_queue_create("HTTPConnection", NULL);
  36. connectionClass = [HTTPConnection self];
  37. // By default bind on all available interfaces, en1, wifi etc
  38. interface = nil;
  39. // Use a default port of 0
  40. // This will allow the kernel to automatically pick an open port for us
  41. port = 0;
  42. // Configure default values for bonjour service
  43. // Bonjour domain. Use the local domain by default
  44. domain = @"local.";
  45. // If using an empty string ("") for the service name when registering,
  46. // the system will automatically use the "Computer Name".
  47. // Passing in an empty string will also handle name conflicts
  48. // by automatically appending a digit to the end of the name.
  49. name = @"";
  50. // Initialize arrays to hold all the HTTP and webSocket connections
  51. connections = [[NSMutableArray alloc] init];
  52. webSockets = [[NSMutableArray alloc] init];
  53. connectionsLock = [[NSLock alloc] init];
  54. webSocketsLock = [[NSLock alloc] init];
  55. // Register for notifications of closed connections
  56. [[NSNotificationCenter defaultCenter] addObserver:self
  57. selector:@selector(connectionDidDie:)
  58. name:HTTPConnectionDidDieNotification
  59. object:nil];
  60. // Register for notifications of closed websocket connections
  61. [[NSNotificationCenter defaultCenter] addObserver:self
  62. selector:@selector(webSocketDidDie:)
  63. name:WebSocketDidDieNotification
  64. object:nil];
  65. isRunning = NO;
  66. }
  67. return self;
  68. }
  69. /**
  70. * Standard Deconstructor.
  71. * Stops the server, and clients, and releases any resources connected with this instance.
  72. **/
  73. - (void)dealloc
  74. {
  75. HTTPLogTrace();
  76. // Remove notification observer
  77. [[NSNotificationCenter defaultCenter] removeObserver:self];
  78. // Stop the server if it's running
  79. [self stop];
  80. // Release all instance variables
  81. dispatch_release(serverQueue);
  82. dispatch_release(connectionQueue);
  83. [asyncSocket setDelegate:nil delegateQueue:NULL];
  84. }
  85. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  86. #pragma mark Server Configuration
  87. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  88. /**
  89. * The document root is filesystem root for the webserver.
  90. * Thus requests for /index.html will be referencing the index.html file within the document root directory.
  91. * All file requests are relative to this document root.
  92. **/
  93. - (NSString *)documentRoot
  94. {
  95. __block NSString *result;
  96. dispatch_sync(serverQueue, ^{
  97. result = documentRoot;
  98. });
  99. return result;
  100. }
  101. - (void)setDocumentRoot:(NSString *)value
  102. {
  103. HTTPLogTrace();
  104. // Document root used to be of type NSURL.
  105. // Add type checking for early warning to developers upgrading from older versions.
  106. if (value && ![value isKindOfClass:[NSString class]])
  107. {
  108. HTTPLogWarn(@"%@: %@ - Expecting NSString parameter, received %@ parameter",
  109. THIS_FILE, THIS_METHOD, NSStringFromClass([value class]));
  110. return;
  111. }
  112. NSString *valueCopy = [value copy];
  113. dispatch_async(serverQueue, ^{
  114. documentRoot = valueCopy;
  115. });
  116. }
  117. /**
  118. * The connection class is the class that will be used to handle connections.
  119. * That is, when a new connection is created, an instance of this class will be intialized.
  120. * The default connection class is HTTPConnection.
  121. * If you use a different connection class, it is assumed that the class extends HTTPConnection
  122. **/
  123. - (Class)connectionClass
  124. {
  125. __block Class result;
  126. dispatch_sync(serverQueue, ^{
  127. result = connectionClass;
  128. });
  129. return result;
  130. }
  131. - (void)setConnectionClass:(Class)value
  132. {
  133. HTTPLogTrace();
  134. dispatch_async(serverQueue, ^{
  135. connectionClass = value;
  136. });
  137. }
  138. /**
  139. * What interface to bind the listening socket to.
  140. **/
  141. - (NSString *)interface
  142. {
  143. __block NSString *result;
  144. dispatch_sync(serverQueue, ^{
  145. result = interface;
  146. });
  147. return result;
  148. }
  149. - (void)setInterface:(NSString *)value
  150. {
  151. NSString *valueCopy = [value copy];
  152. dispatch_async(serverQueue, ^{
  153. interface = valueCopy;
  154. });
  155. }
  156. /**
  157. * The port to listen for connections on.
  158. * By default this port is initially set to zero, which allows the kernel to pick an available port for us.
  159. * After the HTTP server has started, the port being used may be obtained by this method.
  160. **/
  161. - (UInt16)port
  162. {
  163. __block UInt16 result;
  164. dispatch_sync(serverQueue, ^{
  165. result = port;
  166. });
  167. return result;
  168. }
  169. - (UInt16)listeningPort
  170. {
  171. __block UInt16 result;
  172. dispatch_sync(serverQueue, ^{
  173. if (isRunning)
  174. result = [asyncSocket localPort];
  175. else
  176. result = 0;
  177. });
  178. return result;
  179. }
  180. - (void)setPort:(UInt16)value
  181. {
  182. HTTPLogTrace();
  183. dispatch_async(serverQueue, ^{
  184. port = value;
  185. });
  186. }
  187. /**
  188. * Domain on which to broadcast this service via Bonjour.
  189. * The default domain is @"local".
  190. **/
  191. - (NSString *)domain
  192. {
  193. __block NSString *result;
  194. dispatch_sync(serverQueue, ^{
  195. result = domain;
  196. });
  197. return domain;
  198. }
  199. - (void)setDomain:(NSString *)value
  200. {
  201. HTTPLogTrace();
  202. NSString *valueCopy = [value copy];
  203. dispatch_async(serverQueue, ^{
  204. domain = valueCopy;
  205. });
  206. }
  207. /**
  208. * The name to use for this service via Bonjour.
  209. * The default name is an empty string,
  210. * which should result in the published name being the host name of the computer.
  211. **/
  212. - (NSString *)name
  213. {
  214. __block NSString *result;
  215. dispatch_sync(serverQueue, ^{
  216. result = name;
  217. });
  218. return name;
  219. }
  220. - (NSString *)publishedName
  221. {
  222. __block NSString *result;
  223. dispatch_sync(serverQueue, ^{
  224. if (netService == nil)
  225. {
  226. result = nil;
  227. }
  228. else
  229. {
  230. dispatch_block_t bonjourBlock = ^{
  231. result = [[netService name] copy];
  232. };
  233. [[self class] performBonjourBlock:bonjourBlock];
  234. }
  235. });
  236. return result;
  237. }
  238. - (void)setName:(NSString *)value
  239. {
  240. NSString *valueCopy = [value copy];
  241. dispatch_async(serverQueue, ^{
  242. name = valueCopy;
  243. });
  244. }
  245. /**
  246. * The type of service to publish via Bonjour.
  247. * No type is set by default, and one must be set in order for the service to be published.
  248. **/
  249. - (NSString *)type
  250. {
  251. __block NSString *result;
  252. dispatch_sync(serverQueue, ^{
  253. result = type;
  254. });
  255. return result;
  256. }
  257. - (void)setType:(NSString *)value
  258. {
  259. NSString *valueCopy = [value copy];
  260. dispatch_async(serverQueue, ^{
  261. type = valueCopy;
  262. });
  263. }
  264. /**
  265. * The extra data to use for this service via Bonjour.
  266. **/
  267. - (NSDictionary *)TXTRecordDictionary
  268. {
  269. __block NSDictionary *result;
  270. dispatch_sync(serverQueue, ^{
  271. result = txtRecordDictionary;
  272. });
  273. return result;
  274. }
  275. - (void)setTXTRecordDictionary:(NSDictionary *)value
  276. {
  277. HTTPLogTrace();
  278. NSDictionary *valueCopy = [value copy];
  279. dispatch_async(serverQueue, ^{
  280. txtRecordDictionary = valueCopy;
  281. // Update the txtRecord of the netService if it has already been published
  282. if (netService)
  283. {
  284. NSNetService *theNetService = netService;
  285. NSData *txtRecordData = nil;
  286. if (txtRecordDictionary)
  287. txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDictionary];
  288. dispatch_block_t bonjourBlock = ^{
  289. [theNetService setTXTRecordData:txtRecordData];
  290. };
  291. [[self class] performBonjourBlock:bonjourBlock];
  292. }
  293. });
  294. }
  295. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  296. #pragma mark Server Control
  297. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  298. - (BOOL)start:(NSError **)errPtr
  299. {
  300. HTTPLogTrace();
  301. __block BOOL success = YES;
  302. __block NSError *err = nil;
  303. dispatch_sync(serverQueue, ^{ @autoreleasepool {
  304. success = [asyncSocket acceptOnInterface:interface port:port error:&err];
  305. if (success)
  306. {
  307. HTTPLogInfo(@"%@: Started HTTP server on port %hu", THIS_FILE, [asyncSocket localPort]);
  308. isRunning = YES;
  309. [self publishBonjour];
  310. }
  311. else
  312. {
  313. HTTPLogError(@"%@: Failed to start HTTP Server: %@", THIS_FILE, err);
  314. }
  315. }});
  316. if (errPtr)
  317. *errPtr = err;
  318. return success;
  319. }
  320. - (void)stop
  321. {
  322. [self stop:NO];
  323. }
  324. - (void)stop:(BOOL)keepExistingConnections
  325. {
  326. HTTPLogTrace();
  327. dispatch_sync(serverQueue, ^{ @autoreleasepool {
  328. // First stop publishing the service via bonjour
  329. [self unpublishBonjour];
  330. // Stop listening / accepting incoming connections
  331. [asyncSocket disconnect];
  332. isRunning = NO;
  333. if (!keepExistingConnections)
  334. {
  335. // Stop all HTTP connections the server owns
  336. [connectionsLock lock];
  337. for (HTTPConnection *connection in connections)
  338. {
  339. [connection stop];
  340. }
  341. [connections removeAllObjects];
  342. [connectionsLock unlock];
  343. // Stop all WebSocket connections the server owns
  344. [webSocketsLock lock];
  345. for (WebSocket *webSocket in webSockets)
  346. {
  347. [webSocket stop];
  348. }
  349. [webSockets removeAllObjects];
  350. [webSocketsLock unlock];
  351. }
  352. }});
  353. }
  354. - (BOOL)isRunning
  355. {
  356. __block BOOL result;
  357. dispatch_sync(serverQueue, ^{
  358. result = isRunning;
  359. });
  360. return result;
  361. }
  362. - (void)addWebSocket:(WebSocket *)ws
  363. {
  364. [webSocketsLock lock];
  365. HTTPLogTrace();
  366. [webSockets addObject:ws];
  367. [webSocketsLock unlock];
  368. }
  369. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  370. #pragma mark Server Status
  371. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  372. /**
  373. * Returns the number of http client connections that are currently connected to the server.
  374. **/
  375. - (NSUInteger)numberOfHTTPConnections
  376. {
  377. NSUInteger result = 0;
  378. [connectionsLock lock];
  379. result = [connections count];
  380. [connectionsLock unlock];
  381. return result;
  382. }
  383. /**
  384. * Returns the number of websocket client connections that are currently connected to the server.
  385. **/
  386. - (NSUInteger)numberOfWebSocketConnections
  387. {
  388. NSUInteger result = 0;
  389. [webSocketsLock lock];
  390. result = [webSockets count];
  391. [webSocketsLock unlock];
  392. return result;
  393. }
  394. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  395. #pragma mark Incoming Connections
  396. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  397. - (HTTPConfig *)config
  398. {
  399. // Override me if you want to provide a custom config to the new connection.
  400. //
  401. // Generally this involves overriding the HTTPConfig class to include any custom settings,
  402. // and then having this method return an instance of 'MyHTTPConfig'.
  403. // Note: Think you can make the server faster by putting each connection on its own queue?
  404. // Then benchmark it before and after and discover for yourself the shocking truth!
  405. //
  406. // Try the apache benchmark tool (already installed on your Mac):
  407. // $ ab -n 1000 -c 1 http://localhost:<port>/some_path.html
  408. return [[HTTPConfig alloc] initWithServer:self documentRoot:documentRoot queue:connectionQueue];
  409. }
  410. - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
  411. {
  412. HTTPConnection *newConnection = (HTTPConnection *)[[connectionClass alloc] initWithAsyncSocket:newSocket
  413. configuration:[self config]];
  414. [connectionsLock lock];
  415. [connections addObject:newConnection];
  416. [connectionsLock unlock];
  417. [newConnection start];
  418. }
  419. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  420. #pragma mark Bonjour
  421. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  422. - (void)publishBonjour
  423. {
  424. HTTPLogTrace();
  425. NSAssert(dispatch_get_current_queue() == serverQueue, @"Invalid queue");
  426. if (type)
  427. {
  428. netService = [[NSNetService alloc] initWithDomain:domain type:type name:name port:[asyncSocket localPort]];
  429. [netService setDelegate:self];
  430. NSNetService *theNetService = netService;
  431. NSData *txtRecordData = nil;
  432. if (txtRecordDictionary)
  433. txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDictionary];
  434. dispatch_block_t bonjourBlock = ^{
  435. [theNetService removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
  436. [theNetService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
  437. [theNetService publish];
  438. // Do not set the txtRecordDictionary prior to publishing!!!
  439. // This will cause the OS to crash!!!
  440. if (txtRecordData)
  441. {
  442. [theNetService setTXTRecordData:txtRecordData];
  443. }
  444. };
  445. [[self class] startBonjourThreadIfNeeded];
  446. [[self class] performBonjourBlock:bonjourBlock];
  447. }
  448. }
  449. - (void)unpublishBonjour
  450. {
  451. HTTPLogTrace();
  452. NSAssert(dispatch_get_current_queue() == serverQueue, @"Invalid queue");
  453. if (netService)
  454. {
  455. NSNetService *theNetService = netService;
  456. dispatch_block_t bonjourBlock = ^{
  457. [theNetService stop];
  458. };
  459. [[self class] performBonjourBlock:bonjourBlock];
  460. netService = nil;
  461. }
  462. }
  463. /**
  464. * Republishes the service via bonjour if the server is running.
  465. * If the service was not previously published, this method will publish it (if the server is running).
  466. **/
  467. - (void)republishBonjour
  468. {
  469. HTTPLogTrace();
  470. dispatch_async(serverQueue, ^{
  471. [self unpublishBonjour];
  472. [self publishBonjour];
  473. });
  474. }
  475. /**
  476. * Called when our bonjour service has been successfully published.
  477. * This method does nothing but output a log message telling us about the published service.
  478. **/
  479. - (void)netServiceDidPublish:(NSNetService *)ns
  480. {
  481. // Override me to do something here...
  482. //
  483. // Note: This method is invoked on our bonjour thread.
  484. HTTPLogInfo(@"Bonjour Service Published: domain(%@) type(%@) name(%@)", [ns domain], [ns type], [ns name]);
  485. }
  486. /**
  487. * Called if our bonjour service failed to publish itself.
  488. * This method does nothing but output a log message telling us about the published service.
  489. **/
  490. - (void)netService:(NSNetService *)ns didNotPublish:(NSDictionary *)errorDict
  491. {
  492. // Override me to do something here...
  493. //
  494. // Note: This method in invoked on our bonjour thread.
  495. HTTPLogWarn(@"Failed to Publish Service: domain(%@) type(%@) name(%@) - %@",
  496. [ns domain], [ns type], [ns name], errorDict);
  497. }
  498. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  499. #pragma mark Notifications
  500. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  501. /**
  502. * This method is automatically called when a notification of type HTTPConnectionDidDieNotification is posted.
  503. * It allows us to remove the connection from our array.
  504. **/
  505. - (void)connectionDidDie:(NSNotification *)notification
  506. {
  507. // Note: This method is called on the connection queue that posted the notification
  508. [connectionsLock lock];
  509. HTTPLogTrace();
  510. [connections removeObject:[notification object]];
  511. [connectionsLock unlock];
  512. }
  513. /**
  514. * This method is automatically called when a notification of type WebSocketDidDieNotification is posted.
  515. * It allows us to remove the websocket from our array.
  516. **/
  517. - (void)webSocketDidDie:(NSNotification *)notification
  518. {
  519. // Note: This method is called on the connection queue that posted the notification
  520. [webSocketsLock lock];
  521. HTTPLogTrace();
  522. [webSockets removeObject:[notification object]];
  523. [webSocketsLock unlock];
  524. }
  525. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  526. #pragma mark Bonjour Thread
  527. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  528. /**
  529. * NSNetService is runloop based, so it requires a thread with a runloop.
  530. * This gives us two options:
  531. *
  532. * - Use the main thread
  533. * - Setup our own dedicated thread
  534. *
  535. * Since we have various blocks of code that need to synchronously access the netservice objects,
  536. * using the main thread becomes troublesome and a potential for deadlock.
  537. **/
  538. static NSThread *bonjourThread;
  539. + (void)startBonjourThreadIfNeeded
  540. {
  541. HTTPLogTrace();
  542. static dispatch_once_t predicate;
  543. dispatch_once(&predicate, ^{
  544. HTTPLogVerbose(@"%@: Starting bonjour thread...", THIS_FILE);
  545. bonjourThread = [[NSThread alloc] initWithTarget:self
  546. selector:@selector(bonjourThread)
  547. object:nil];
  548. [bonjourThread start];
  549. });
  550. }
  551. + (void)bonjourThread
  552. {
  553. @autoreleasepool {
  554. HTTPLogVerbose(@"%@: BonjourThread: Started", THIS_FILE);
  555. // We can't run the run loop unless it has an associated input source or a timer.
  556. // So we'll just create a timer that will never fire - unless the server runs for 10,000 years.
  557. [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
  558. target:self
  559. selector:@selector(donothingatall:)
  560. userInfo:nil
  561. repeats:YES];
  562. [[NSRunLoop currentRunLoop] run];
  563. HTTPLogVerbose(@"%@: BonjourThread: Aborted", THIS_FILE);
  564. }
  565. }
  566. + (void)executeBonjourBlock:(dispatch_block_t)block
  567. {
  568. HTTPLogTrace();
  569. NSAssert([NSThread currentThread] == bonjourThread, @"Executed on incorrect thread");
  570. block();
  571. }
  572. + (void)performBonjourBlock:(dispatch_block_t)block
  573. {
  574. HTTPLogTrace();
  575. [self performSelector:@selector(executeBonjourBlock:)
  576. onThread:bonjourThread
  577. withObject:block
  578. waitUntilDone:YES];
  579. }
  580. @end