jens@26: // jens@26: // MYPortMapper.m jens@26: // MYNetwork jens@26: // jens@26: // Created by Jens Alfke on 1/4/08. jens@26: // Copyright 2008 Jens Alfke. All rights reserved. jens@26: // jens@26: jens@26: #import "MYPortMapper.h" jens@26: #import "IPAddress.h" jens@26: #import "CollectionUtils.h" jens@26: #import "Logging.h" jens@26: #import "ExceptionUtils.h" jens@26: jens@26: #import jens@26: #import jens@26: #import jens@26: #import jens@26: #import jens@26: #import jens@26: jens@26: jens@26: NSString* const MYPortMapperChangedNotification = @"MYPortMapperChanged"; jens@26: jens@26: jens@26: @interface MYPortMapper () jens@26: // Redeclare these properties as settable, internally: jens@26: @property (readwrite) SInt32 error; jens@26: @property (retain) IPAddress* publicAddress, *localAddress; jens@26: // Private getter: jens@26: @property (readonly) void* _service; jens@26: - (void) priv_updateLocalAddress; jens@26: - (void) priv_disconnect; jens@26: @end jens@26: jens@26: jens@26: @implementation MYPortMapper jens@26: jens@26: jens@26: - (id) initWithLocalPort: (UInt16)localPort jens@26: { jens@26: self = [super init]; jens@26: if (self != nil) { jens@26: _localPort = localPort; jens@26: _mapTCP = YES; jens@26: [self priv_updateLocalAddress]; jens@26: } jens@26: return self; jens@26: } jens@26: jens@26: - (id) initWithNullMapping jens@26: { jens@26: // A PortMapper with no port or protocols will cause the DNSService to look up jens@26: // our public address without creating a mapping. jens@26: if ([self initWithLocalPort: 0]) { jens@26: _mapTCP = _mapUDP = NO; jens@26: } jens@26: return self; jens@26: } jens@26: jens@26: jens@26: - (void) dealloc jens@26: { jens@26: if( _service ) jens@26: [self priv_disconnect]; jens@26: [_publicAddress release]; jens@26: [_localAddress release]; jens@26: [super dealloc]; jens@26: } jens@26: jens@26: - (void) finalize jens@26: { jens@26: if( _service ) jens@26: [self priv_disconnect]; jens@26: [super finalize]; jens@26: } jens@26: jens@26: jens@26: @synthesize localAddress=_localAddress, publicAddress=_publicAddress, jens@26: error=_error, _service=_service, jens@26: mapTCP=_mapTCP, mapUDP=_mapUDP, jens@26: desiredPublicPort=_desiredPublicPort; jens@26: jens@26: jens@26: - (BOOL) isMapped jens@26: { jens@26: return ! $equal(_publicAddress,_localAddress); jens@26: } jens@26: jens@26: - (void) priv_updateLocalAddress jens@26: { jens@26: IPAddress *localAddress = [IPAddress localAddressWithPort: _localPort]; jens@26: if (!$equal(localAddress,_localAddress)) jens@26: self.localAddress = localAddress; jens@26: } jens@26: jens@26: jens@26: static IPAddress* makeIPAddr( UInt32 rawAddr, UInt16 port ) { jens@26: if (rawAddr) jens@26: return [[[IPAddress alloc] initWithIPv4: rawAddr port: port] autorelease]; jens@26: else jens@26: return nil; jens@26: } jens@26: jens@26: /** Called whenever the port mapping changes (see comment for callback, below.) */ jens@26: - (void) priv_portMapStatus: (DNSServiceErrorType)errorCode jens@26: publicAddress: (UInt32)rawPublicAddress jens@26: publicPort: (UInt16)publicPort jens@26: { jens@26: LogTo(PortMapper,@"Callback got err %i, addr %08X:%hu", jens@26: errorCode, rawPublicAddress, publicPort); jens@26: if( errorCode==kDNSServiceErr_NoError ) { jens@26: if( rawPublicAddress==0 || (publicPort==0 && (_mapTCP || _mapUDP)) ) { jens@26: LogTo(PortMapper,@"(Callback reported no mapping available)"); jens@26: errorCode = kDNSServiceErr_NATPortMappingUnsupported; jens@26: } jens@26: } jens@26: if( errorCode != self.error ) jens@26: self.error = errorCode; jens@26: jens@26: [self priv_updateLocalAddress]; jens@26: IPAddress *publicAddress = makeIPAddr(rawPublicAddress,publicPort); jens@26: if (!$equal(publicAddress,_publicAddress)) jens@26: self.publicAddress = publicAddress; jens@26: jens@26: if( ! errorCode ) { jens@26: LogTo(PortMapper,@"Callback got %08X:%hu -> %@ (mapped=%i)", jens@26: rawPublicAddress,publicPort, self.publicAddress, self.isMapped); jens@26: } jens@26: [[NSNotificationCenter defaultCenter] postNotificationName: MYPortMapperChangedNotification jens@26: object: self]; jens@26: } jens@26: jens@26: jens@26: /** Asynchronous callback from DNSServiceNATPortMappingCreate. jens@26: This is invoked whenever the status of the port mapping changes. jens@26: All it does is dispatch to the object's priv_portMapStatus:publicAddress:publicPort: method. */ jens@26: static void portMapCallback ( jens@26: DNSServiceRef sdRef, jens@26: DNSServiceFlags flags, jens@26: uint32_t interfaceIndex, jens@26: DNSServiceErrorType errorCode, jens@26: uint32_t publicAddress, /* four byte IPv4 address in network byte order */ jens@26: DNSServiceProtocol protocol, jens@26: uint16_t privatePort, jens@26: uint16_t publicPort, /* may be different than the requested port */ jens@26: uint32_t ttl, /* may be different than the requested ttl */ jens@26: void *context jens@26: ) jens@26: { jens@26: NSAutoreleasePool *pool = [NSAutoreleasePool new]; jens@26: @try{ jens@26: [(MYPortMapper*)context priv_portMapStatus: errorCode jens@26: publicAddress: publicAddress jens@26: publicPort: ntohs(publicPort)]; // port #s in network byte order! jens@26: }catchAndReport(@"PortMapper"); jens@26: [pool drain]; jens@26: } jens@26: jens@26: jens@26: /** CFSocket callback, informing us that _socket has data available, which means jens@26: that the DNS service has an incoming result to be processed. This will end up invoking jens@26: the portMapCallback. */ jens@26: static void serviceCallback(CFSocketRef s, jens@26: CFSocketCallBackType type, jens@26: CFDataRef address, const void *data, void *clientCallBackInfo) jens@26: { jens@26: MYPortMapper *mapper = (MYPortMapper*)clientCallBackInfo; jens@26: DNSServiceRef service = mapper._service; jens@26: DNSServiceErrorType err = DNSServiceProcessResult(service); jens@26: if( err ) { jens@26: // An error here means the socket has failed and should be closed. jens@26: [mapper priv_portMapStatus: err publicAddress: 0 publicPort: 0]; jens@26: [mapper priv_disconnect]; jens@26: } jens@26: } jens@26: jens@26: jens@26: jens@26: - (BOOL) open jens@26: { jens@26: NSAssert(!_service,@"Already open"); jens@26: // Create the DNSService: jens@26: DNSServiceProtocol protocols = 0; jens@26: if( _mapTCP ) protocols |= kDNSServiceProtocol_TCP; jens@26: if( _mapUDP ) protocols |= kDNSServiceProtocol_UDP; jens@26: self.error = DNSServiceNATPortMappingCreate((DNSServiceRef*)&_service, jens@26: 0 /*flags*/, jens@26: 0 /*interfaceIndex*/, jens@26: protocols, jens@26: htons(_localPort), jens@26: htons(_desiredPublicPort), jens@26: 0 /*ttl*/, jens@26: &portMapCallback, jens@26: self); jens@26: if( _error ) { jens@26: LogTo(PortMapper,@"Error %i creating port mapping",_error); jens@26: return NO; jens@26: } jens@26: jens@26: // Wrap a CFSocket around the service's socket: jens@26: CFSocketContext ctxt = { 0, self, CFRetain, CFRelease, NULL }; jens@26: _socket = CFSocketCreateWithNative(NULL, jens@26: DNSServiceRefSockFD(_service), jens@26: kCFSocketReadCallBack, jens@26: &serviceCallback, &ctxt); jens@26: if( _socket ) { jens@26: CFSocketSetSocketFlags(_socket, CFSocketGetSocketFlags(_socket) & ~kCFSocketCloseOnInvalidate); jens@26: // Attach the socket to the runloop so the serviceCallback will be invoked: jens@26: _socketSource = CFSocketCreateRunLoopSource(NULL, _socket, 0); jens@26: if( _socketSource ) jens@26: CFRunLoopAddSource(CFRunLoopGetCurrent(), _socketSource, kCFRunLoopCommonModes); jens@26: } jens@26: if( _socketSource ) { jens@26: LogTo(PortMapper,@"Opening"); jens@26: return YES; jens@26: } else { jens@26: Warn(@"Failed to open PortMapper"); jens@26: [self close]; jens@26: _error = kDNSServiceErr_Unknown; jens@26: return NO; jens@26: } jens@26: } jens@26: jens@26: jens@26: - (BOOL) waitTillOpened jens@26: { jens@26: if( ! _socketSource ) jens@26: if( ! [self open] ) jens@26: return NO; jens@26: // Run the runloop until there's either an error or a result: jens@26: while( _error==0 && _publicAddress==nil ) jens@26: if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode jens@26: beforeDate: [NSDate distantFuture]] ) jens@26: break; jens@26: return (_error==0); jens@26: } jens@26: jens@26: jens@26: // Close down, but _without_ clearing the 'error' property jens@26: - (void) priv_disconnect jens@26: { jens@26: if( _socketSource ) { jens@26: CFRunLoopSourceInvalidate(_socketSource); jens@26: CFRelease(_socketSource); jens@26: _socketSource = NULL; jens@26: } jens@26: if( _socket ) { jens@26: CFSocketInvalidate(_socket); jens@26: CFRelease(_socket); jens@26: _socket = NULL; jens@26: } jens@26: if( _service ) { jens@26: LogTo(PortMapper,@"Deleting port mapping"); jens@26: DNSServiceRefDeallocate(_service); jens@26: _service = NULL; jens@26: self.publicAddress = nil; jens@26: } jens@26: } jens@26: jens@26: - (void) close jens@26: { jens@26: [self priv_disconnect]; jens@26: self.error = 0; jens@26: } jens@26: jens@26: jens@26: + (IPAddress*) findPublicAddress jens@26: { jens@26: IPAddress *addr = nil; jens@26: MYPortMapper *mapper = [[self alloc] initWithNullMapping]; jens@26: if( [mapper waitTillOpened] ) jens@26: addr = [mapper.publicAddress retain]; jens@26: [mapper close]; jens@26: [mapper release]; jens@26: return [addr autorelease]; jens@26: } jens@26: jens@26: jens@26: @end jens@26: jens@26: jens@26: /* jens@26: Copyright (c) 2008-2009, Jens Alfke . All rights reserved. jens@26: jens@26: Redistribution and use in source and binary forms, with or without modification, are permitted jens@26: provided that the following conditions are met: jens@26: jens@26: * Redistributions of source code must retain the above copyright notice, this list of conditions jens@26: and the following disclaimer. jens@26: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions jens@26: and the following disclaimer in the documentation and/or other materials provided with the jens@26: distribution. jens@26: jens@26: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR jens@26: IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND jens@26: FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- jens@26: BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES jens@26: (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR jens@26: PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN jens@26: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF jens@26: THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. jens@26: */