From e70a3338a5222cf59abba6c7539e940b53e5dbfe Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Wed, 16 Sep 2015 11:16:41 -0700 Subject: [PATCH] Added support for NAT port mapping --- GCDWebServer/Core/GCDWebServer.h | 35 +++++++++++- GCDWebServer/Core/GCDWebServer.m | 94 +++++++++++++++++++++++++++++++- Mac/main.m | 8 +++ 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/GCDWebServer/Core/GCDWebServer.h b/GCDWebServer/Core/GCDWebServer.h index fdd4b18..63ed604 100644 --- a/GCDWebServer/Core/GCDWebServer.h +++ b/GCDWebServer/Core/GCDWebServer.h @@ -90,14 +90,26 @@ extern NSString* const GCDWebServerOption_BonjourName; */ extern NSString* const GCDWebServerOption_BonjourType; +/** + * Request a port mapping in the NAT gateway (NSNumber / BOOL). + * + * This uses the DNSService API under the hood which supports IPv4 mappings only. + * + * The default value is NO. + * + * @warning The external port set up by the NAT gateway may be different than + * the one used by the GCDWebServer. + */ +extern NSString* const GCDWebServerOption_RequestNATPortMapping; + /** * Only accept HTTP requests coming from localhost i.e. not from the outside * network (NSNumber / BOOL). * * The default value is NO. * - * @warning Bonjour should be disabled if using this option since the server - * will not be reachable from the outside network anyway. + * @warning Bonjour and NAT port mapping should be disabled if using this option + * since the server will not be reachable from the outside network anyway. */ extern NSString* const GCDWebServerOption_BindToLocalhost; @@ -213,9 +225,20 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; /** * This method is called after the Bonjour registration for the server has * successfully completed. + * + * Use the "bonjourServerURL" property to retrieve the Bonjour address of the + * server. */ - (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server; +/** + * This method is called after the NAT port mapping has been updated. + * + * Use the "publicServerURL" property to retrieve the public address of the + * server. + */ +- (void)webServerDidUpdateNATPortMapping:(GCDWebServer*)server; + /** * This method is called when the first GCDWebServerConnection is opened by the * server to serve a series of HTTP requests. @@ -362,6 +385,14 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; */ @property(nonatomic, readonly) NSURL* bonjourServerURL; +/** + * Returns the server's public URL. + * + * @warning This property is only valid if the server is running and NAT port + * mapping is active. + */ +@property(nonatomic, readonly) NSURL* publicServerURL; + /** * Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS) * using the default Bonjour name. diff --git a/GCDWebServer/Core/GCDWebServer.m b/GCDWebServer/Core/GCDWebServer.m index 312bf75..990aa9c 100644 --- a/GCDWebServer/Core/GCDWebServer.m +++ b/GCDWebServer/Core/GCDWebServer.m @@ -38,6 +38,7 @@ #endif #endif #import +#import #import "GCDWebServerPrivate.h" @@ -50,6 +51,7 @@ NSString* const GCDWebServerOption_Port = @"Port"; NSString* const GCDWebServerOption_BonjourName = @"BonjourName"; NSString* const GCDWebServerOption_BonjourType = @"BonjourType"; +NSString* const GCDWebServerOption_RequestNATPortMapping = @"RequestNATPortMapping"; NSString* const GCDWebServerOption_BindToLocalhost = @"BindToLocalhost"; NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections"; NSString* const GCDWebServerOption_ServerName = @"ServerName"; @@ -171,6 +173,11 @@ static void _ExecuteMainThreadRunLoopSources() { dispatch_source_t _source6; CFNetServiceRef _registrationService; CFNetServiceRef _resolutionService; + DNSServiceRef _dnsService; + CFSocketRef _dnsSocket; + CFRunLoopSourceRef _dnsSource; + NSString* _dnsAddress; + NSUInteger _dnsPort; BOOL _bindToLocalhost; #if TARGET_OS_IPHONE BOOL _suspendInBackground; @@ -383,7 +390,7 @@ static void _NetServiceResolveCallBack(CFNetServiceRef service, CFStreamError* e } } else { GCDWebServer* server = (__bridge GCDWebServer*)info; - GWS_LOG_INFO(@"%@ now reachable at %@", [server class], server.bonjourServerURL); + GWS_LOG_INFO(@"%@ now locally reachable at %@", [server class], server.bonjourServerURL); if ([server.delegate respondsToSelector:@selector(webServerDidCompleteBonjourRegistration:)]) { [server.delegate webServerDidCompleteBonjourRegistration:server]; } @@ -391,6 +398,41 @@ static void _NetServiceResolveCallBack(CFNetServiceRef service, CFStreamError* e } } +static 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) { + GWS_DCHECK([NSThread isMainThread]); + @autoreleasepool { + GCDWebServer* server = (__bridge GCDWebServer*)context; + if ((errorCode == kDNSServiceErr_NoError) || (errorCode == kDNSServiceErr_DoubleNAT)) { + struct sockaddr_in addr4; + bzero(&addr4, sizeof(addr4)); + addr4.sin_len = sizeof(addr4); + addr4.sin_family = AF_INET; + addr4.sin_addr.s_addr = externalAddress; // Already in network byte order + server->_dnsAddress = GCDWebServerStringFromSockAddr((const struct sockaddr*)&addr4, NO); + server->_dnsPort = ntohs(externalPort); + GWS_LOG_INFO(@"%@ now publicly reachable at %@", [server class], server.publicServerURL); + } else { + GWS_LOG_ERROR(@"DNS service error %i", errorCode); + server->_dnsAddress = nil; + server->_dnsPort = 0; + } + if ([server.delegate respondsToSelector:@selector(webServerDidUpdateNATPortMapping:)]) { + [server.delegate webServerDidUpdateNATPortMapping:server]; + } + } +} + +static void _SocketCallBack(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void* data, void* info) { + GWS_DCHECK([NSThread isMainThread]); + @autoreleasepool { + GCDWebServer* server = (__bridge GCDWebServer*)info; + DNSServiceErrorType status = DNSServiceProcessResult(server->_dnsService); + if (status != kDNSServiceErr_NoError) { + GWS_LOG_ERROR(@"DNS service error %i", status); + } + } +} + static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValue) { id value = [options objectForKey:key]; return value ? value : defaultValue; @@ -580,6 +622,29 @@ static inline NSString* _EncodeBase64(NSString* string) { } } + if ([_GetOption(_options, GCDWebServerOption_RequestNATPortMapping, @NO) boolValue]) { + DNSServiceErrorType status = DNSServiceNATPortMappingCreate(&_dnsService, 0, 0, kDNSServiceProtocol_TCP, htons(port), htons(port), 0, _DNSServiceCallBack, (__bridge void*)self); + if (status == kDNSServiceErr_NoError) { + CFSocketContext context = {0, (__bridge void*)self, NULL, NULL, NULL}; + _dnsSocket = CFSocketCreateWithNative(kCFAllocatorDefault, DNSServiceRefSockFD(_dnsService), kCFSocketReadCallBack, _SocketCallBack, &context); + if (_dnsSocket) { + CFSocketSetSocketFlags(_dnsSocket, CFSocketGetSocketFlags(_dnsSocket) & ~kCFSocketCloseOnInvalidate); + _dnsSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _dnsSocket, 0); + if (_dnsSource) { + CFRunLoopAddSource(CFRunLoopGetMain(), _dnsSource, kCFRunLoopCommonModes); + } else { + GWS_LOG_ERROR(@"Failed creating CFRunLoopSource"); + GWS_DNOT_REACHED(); + } + } else { + GWS_LOG_ERROR(@"Failed creating CFSocket"); + GWS_DNOT_REACHED(); + } + } else { + GWS_LOG_ERROR(@"Failed creating NAT port mapping (%i)", status); + } + } + dispatch_resume(_source4); dispatch_resume(_source6); GWS_LOG_INFO(@"%@ started on port %i and reachable at %@", [self class], (int)_port, self.serverURL); @@ -595,6 +660,22 @@ static inline NSString* _EncodeBase64(NSString* string) { - (void)_stop { GWS_DCHECK(_source4 != NULL); + if (_dnsService) { + _dnsAddress = nil; + _dnsPort = 0; + if (_dnsSource) { + CFRunLoopSourceInvalidate(_dnsSource); + CFRelease(_dnsSource); + _dnsSource = NULL; + } + if (_dnsSocket) { + CFRelease(_dnsSocket); + _dnsSocket = NULL; + } + DNSServiceRefDeallocate(_dnsService); + _dnsService = NULL; + } + if (_registrationService) { if (_resolutionService) { CFNetServiceUnscheduleFromRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes); @@ -746,6 +827,17 @@ static inline NSString* _EncodeBase64(NSString* string) { return nil; } +- (NSURL*)publicServerURL { + if (_source4 && _dnsService && _dnsAddress && _dnsPort) { + if (_dnsPort != 80) { + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", _dnsAddress, (int)_dnsPort]]; + } else { + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", _dnsAddress]]; + } + } + return nil; +} + - (BOOL)start { return [self startWithPort:kDefaultPort bonjourName:@""]; } diff --git a/Mac/main.m b/Mac/main.m index 630cd6b..33b25e5 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -72,6 +72,10 @@ typedef enum { [self _logDelegateCall:_cmd]; } +- (void)webServerDidUpdateNATPortMapping:(GCDWebServer*)server { + [self _logDelegateCall:_cmd]; +} + - (void)webServerDidConnect:(GCDWebServer*)server { [self _logDelegateCall:_cmd]; } @@ -142,6 +146,7 @@ int main(int argc, const char* argv[]) { NSString* authenticationUser = nil; NSString* authenticationPassword = nil; BOOL bindToLocalhost = NO; + BOOL requestNATPortMapping = NO; if (argc == 1) { fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | htmlFileUpload | webDAV | webUploader | streamingResponse | asyncResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password] [--localhost]\n\n", basename((char*)argv[0])); @@ -191,6 +196,8 @@ int main(int argc, const char* argv[]) { authenticationPassword = [NSString stringWithUTF8String:argv[i]]; } else if (!strcmp(argv[i], "--localhost")) { bindToLocalhost = YES; + } else if (!strcmp(argv[i], "--nat")) { + requestNATPortMapping = YES; } } } @@ -412,6 +419,7 @@ int main(int argc, const char* argv[]) { fprintf(stdout, "\n"); NSMutableDictionary* options = [NSMutableDictionary dictionary]; [options setObject:@8080 forKey:GCDWebServerOption_Port]; + [options setObject:@(requestNATPortMapping) forKey:GCDWebServerOption_RequestNATPortMapping]; [options setObject:@(bindToLocalhost) forKey:GCDWebServerOption_BindToLocalhost]; [options setObject:@"" forKey:GCDWebServerOption_BonjourName]; if (authenticationUser && authenticationPassword) {