From 1b0a96d976b82f632d2470ae3c75cf6d49019784 Mon Sep 17 00:00:00 2001 From: Forrest Voight Date: Mon, 8 Aug 2011 18:06:50 -0400 Subject: [PATCH] rewriting portions of nattraverso --- nattraverso/ipdiscover.py | 147 ++++++++++++------------------------------ nattraverso/pynupnp/upnp.py | 121 +++++++++++++++-------------------- 2 files changed, 95 insertions(+), 173 deletions(-) diff --git a/nattraverso/ipdiscover.py b/nattraverso/ipdiscover.py index 1a22fb8..7baea11 100644 --- a/nattraverso/ipdiscover.py +++ b/nattraverso/ipdiscover.py @@ -12,16 +12,18 @@ TODO: Example __revision__ = "$id" -import random, socket, logging +import random, socket, logging, itertools from twisted.internet import defer, reactor from twisted.internet.protocol import DatagramProtocol from twisted.internet.error import CannotListenError from twisted.internet.interfaces import IReactorMulticast +from twisted.python import log from nattraverso.utils import is_rfc1918_ip, is_bogus_ip +@defer.inlineCallbacks def get_local_ip(): """ Returns a deferred which will be called with a @@ -37,10 +39,27 @@ def get_local_ip(): """ # first we try a connected udp socket, then via multicast logging.debug("Resolving dns to get udp ip") - result = reactor.resolve('A.ROOT-SERVERS.NET') - result.addCallbacks(_get_via_connected_udp, lambda x:_get_via_multicast()) - return result + try: + ipaddr = yield reactor.resolve('A.ROOT-SERVERS.NET') + except: + pass + else: + udpprot = DatagramProtocol() + port = reactor.listenUDP(0, udpprot) + udpprot.transport.connect(ipaddr, 7) + localip = udpprot.transport.getHost().host + port.stopListening() + + if is_bogus_ip(localip): + raise RuntimeError, "Invalid IP address returned" + else: + defer.returnValue((is_rfc1918_ip(localip), localip)) + + logging.debug("Multicast ping to retrieve local IP") + ipaddr = yield _discover_multicast() + defer.returnValue((is_rfc1918_ip(ipaddr), ipaddr)) +@defer.inlineCallbacks def get_external_ip(): """ Returns a deferred which will be called with a @@ -54,97 +73,21 @@ def get_external_ip(): @return: A deferred called with the above defined tuple @rtype: L{twisted.internet.defer.Deferred} """ - return get_local_ip().addCallbacks(_on_local_ip, _on_no_local_ip) - -#Private---------- -def _on_upnp_external_found(ipaddr): - """ - Called when an external ip is found through UPNP. - @param ipaddr: The WAN ip address - @type ipaddr: an IP string "x.x.x.x" - """ - return (True, ipaddr) - -def _on_no_upnp_external_found(error, ipaddr): - """ - Called when the UPnP device failed to return external address. - - @param ipaddr: The LAN ip address - @type ipaddr: an IP string "x.x.x.x" - """ - return (False, ipaddr) - -def _on_local_ip(result): - """ - Called when we got the local ip of this machine. If we have a WAN address, - we return immediately, else we try to discover ip address through UPnP. - - @param result: a tuple (lan_flag, ip_addr) - @type result: a tuple (bool, ip string) - """ - local, ipaddr = result + try: + local, ipaddr = yield get_local_ip() + except: + defer.returnValue((None, "127.0.0.1")) if not local: - return (True, ipaddr) - else: - logging.debug("Got local ip, trying to use upnp to get WAN ip") - import nattraverso.pynupnp - return nattraverso.pynupnp.get_external_ip().addCallbacks( - _on_upnp_external_found, - lambda x: _on_no_upnp_external_found(x, ipaddr)) - -def _on_no_local_ip(error): - """ - Called when we could not retreive by any mean the ip of this machine. - We simply assume there is no connectivity, and return localhost address. - """ - return (None, "127.0.0.1") - -def _got_multicast_ip(ipaddr): - """ - Called when we received the ip address via udp multicast. - - @param ipaddr: an ip address - @type ipaddr: a string "x.x.x.x" - """ - return (is_rfc1918_ip(ipaddr), ipaddr) - -def _get_via_multicast(): - """ - Init a multicast ip address discovery. - - @return: A deferred called with the discovered ip address - @rtype: L{twisted.internet.defer.Deferred} - @raise Exception: When an error occurs during the multicast engine init - """ + defer.returnValue((True, ipaddr)) + logging.debug("Got local ip, trying to use upnp to get WAN ip") + import nattraverso.pynupnp try: - # Init multicast engine - IReactorMulticast(reactor) + ipaddr2 = yield nattraverso.pynupnp.get_external_ip() except: - raise - - logging.debug("Multicast ping to retrieve local IP") - return _discover_multicast().addCallback(_got_multicast_ip) - -def _get_via_connected_udp(ipaddr): - """ - Init a UDP socket ip discovery. We do a dns query, and retreive our - ip address from the connected udp socket. - - @param ipaddr: The ip address of a dns server - @type ipaddr: a string "x.x.x.x" - @raise RuntimeError: When the ip is a bogus ip (0.0.0.0 or alike) - """ - udpprot = DatagramProtocol() - port = reactor.listenUDP(0, udpprot) - udpprot.transport.connect(ipaddr, 7) - localip = udpprot.transport.getHost().host - port.stopListening() - - if is_bogus_ip(localip): - raise RuntimeError, "Invalid IP address returned" + defer.returnValue((False, ipaddr)) else: - return (is_rfc1918_ip(localip), localip) + defer.returnValue((True, ipaddr2)) class _LocalNetworkMulticast(DatagramProtocol): def __init__(self, nonce): @@ -172,31 +115,25 @@ def _discover_multicast(): nonce = str(random.randrange(2**64)) p = _LocalNetworkMulticast(nonce) - # 5 different UDP ports - ports = [11000+random.randint(0, 5000) for port in range(5)] - for attempt, port in enumerate(ports): + for attempt in itertools.count(): + port = 11000 + random.randint(0, 5000) try: mcast = reactor.listenMulticast(port, p) - mcast_port = port except CannotListenError: - if attempt < 5: - print "Trying another multicast UDP port", port - else: + if attempt >= 10: raise + continue else: break try: yield mcast.joinGroup('239.255.255.250', socket.INADDR_ANY) - try: - logging.debug("Sending multicast ping") - for i in xrange(3): - p.transport.write(nonce, ('239.255.255.250', mcast_port)) - - address, = yield p.address_received.get_deferred(5) - finally: - mcast.leaveGroup('239.255.255.250', socket.INADDR_ANY) + logging.debug("Sending multicast ping") + for i in xrange(3): + p.transport.write(nonce, ('239.255.255.250', port)) + + address, = yield p.address_received.get_deferred(5) finally: mcast.stopListening() diff --git a/nattraverso/pynupnp/upnp.py b/nattraverso/pynupnp/upnp.py index e3af52a..eae568f 100644 --- a/nattraverso/pynupnp/upnp.py +++ b/nattraverso/pynupnp/upnp.py @@ -28,19 +28,6 @@ class UPnPError(Exception): """ pass -def search_upnp_device (): - """ - Check the network for an UPnP device. Returns a deferred - with the L{UPnPDevice} instance as result, if found. - - @return: A deferred called with the L{UPnPDevice} instance - @rtype: L{twisted.internet.defer.Deferred} - """ - try: - return UPnPProtocol().search_device() - except Exception, msg: - return defer.fail(UPnPError(msg)) - class UPnPMapper(portmapper.NATMapper): """ This is the UPnP port mapper implementing the @@ -326,14 +313,11 @@ class UPnPDevice: """ logging.debug("_on_no_port_mapping_received: %s", failure) err = failure.value - try: - message = err.args[0]["UPnPError"]["errorDescription"] - if "SpecifiedArrayIndexInvalid" == message: - return mappings - else: - raise UPnPError("GetGenericPortMappingEntry got %s"%(message)) - except: - raise UPnPError("GetGenericPortMappingEntry got %s"%(err.args[0])) + message = err.args[0]["UPnPError"]["errorDescription"] + if "SpecifiedArrayIndexInvalid" == message: + return mappings + else: + return failure def _on_port_mapping_added(self, response): @@ -349,7 +333,7 @@ class UPnPDevice: @raise UPnPError: When the port mapping could not be added """ - raise UPnPError(failure.value.args[0]) + return failure def _on_port_mapping_removed(self, response): """ @@ -364,7 +348,7 @@ class UPnPDevice: @raise UPnPError: When the port mapping could not be deleted """ - raise UPnPError(failure.value.args[0]) + return failure # UPNP multicast address, port and request string _UPNP_MCAST = '239.255.255.250' @@ -388,13 +372,11 @@ class UPnPProtocol(DatagramProtocol, object): """ super(UPnPProtocol, self).__init__(*args, **kwargs) - # Url to use to talk to upnp device - self._control_url = None - self._device = None - #Device discovery deferred self._discovery = None self._discovery_timeout = None + self.mcast = None + self._done = False # Public methods def search_device(self): @@ -407,31 +389,33 @@ class UPnPProtocol(DatagramProtocol, object): @return: A deferred called with the detected L{UPnPDevice} instance. @rtype: L{twisted.internet.defer.Deferred} """ + if self._discovery is not None: + raise ValueError('already used') self._discovery = defer.Deferred() + self._discovery_timeout = reactor.callLater(6, self._on_discovery_timeout) attempt = 0 mcast = None while True: try: - mcast = reactor.listenMulticast(1900+attempt, self) + self.mcast = reactor.listenMulticast(1900+attempt, self) break except CannotListenError: attempt = random.randint(0, 500) # joined multicast group, starting upnp search - mcast.joinGroup('239.255.255.250', socket.INADDR_ANY) + self.mcast.joinGroup('239.255.255.250', socket.INADDR_ANY) self.transport.write(_UPNP_SEARCH_REQUEST, (_UPNP_MCAST, _UPNP_PORT)) self.transport.write(_UPNP_SEARCH_REQUEST, (_UPNP_MCAST, _UPNP_PORT)) self.transport.write(_UPNP_SEARCH_REQUEST, (_UPNP_MCAST, _UPNP_PORT)) - self._discovery_timeout = reactor.callLater( - 6, self._on_discovery_timeout) - return self._discovery #Private methods def datagramReceived(self, dgram, address): + if self._done: + return """ This is private, handle the multicast answer from the upnp device. """ @@ -443,16 +427,11 @@ class UPnPProtocol(DatagramProtocol, object): # Prepare status line version, status, textstatus = response.split(None, 2) - if not version.startswith('HTTP') or self._control_url != None: + if not version.startswith('HTTP'): return if status != "200": return - # We had a timeout pending, cancel it - if self._discovery_timeout != None: - self._discovery_timeout.cancel() - self._discovery_timeout = None - # Launch the info fetching def parse_discovery_response(message): """Separate headers and body from the received http answer.""" @@ -480,10 +459,11 @@ class UPnPProtocol(DatagramProtocol, object): loc = headers['location'][0] result = client.getPage(url=loc) - result.addCallback(self._on_gateway_response, loc).addErrback( - self._on_discovery_failed) + result.addCallback(self._on_gateway_response, loc).addErrback(self._on_discovery_failed) def _on_gateway_response(self, body, loc): + if self._done: + return """ Called with the UPnP device XML description fetched via HTTP. @@ -496,8 +476,6 @@ class UPnPProtocol(DatagramProtocol, object): @param body: The xml description of the device. @param loc: the url used to retreive the xml description """ - if self._control_url != None: - return # Parse answer upnpinfo = UPnPXml(body) @@ -510,35 +488,42 @@ class UPnPProtocol(DatagramProtocol, object): # Check the control url, if None, then the device cannot do what we want controlurl = upnpinfo.controlurl if controlurl == None: - self._on_discovery_failed( - UPnPError("upnp response showed no WANConnections")) + self._on_discovery_failed(UPnPError("upnp response showed no WANConnections")) return - self._control_url = urlparse.urljoin(urlbase, controlurl) - - soap_proxy = SoapProxy(self._control_url, upnpinfo.wanservice) - if self._discovery != None: - self._device = UPnPDevice(soap_proxy, upnpinfo.deviceinfos) - self._discovery.callback(self._device) - self._discovery = None + control_url2 = urlparse.urljoin(urlbase, controlurl) + soap_proxy = SoapProxy(control_url2, upnpinfo.wanservice) + self._on_discovery_succeeded(UPnPDevice(soap_proxy, upnpinfo.deviceinfos)) + + def _on_discovery_succeeded(self, res): + if self._done: + return + self._done = True + self.mcast.stopListening() + self._discovery_timeout.cancel() + self._discovery.callback(res) def _on_discovery_failed(self, err): - """ - Called when the UPnP Device discovery has failed. - - The callback returned in L{search_device} is called with - an error, corresponding to the cause of the failure. - """ - self._control_url = None - if self._discovery != None: - self._discovery.errback(err) - self._discovery = None + if self._done: + return + self._done = True + self.mcast.stopListening() + self._discovery_timeout.cancel() + self._discovery.errback(err) def _on_discovery_timeout(self): - """ - Called when the UPnP Device discovery has timed out. - - Calls L{_on_discovery_failed}. - """ - self._discovery_timeout = None - self._on_discovery_failed(UPnPError()) + if self._done: + return + self._done = True + self.mcast.stopListening() + self._discovery.errback(failure.Failure(defer.TimeoutError())) + +def search_upnp_device (): + """ + Check the network for an UPnP device. Returns a deferred + with the L{UPnPDevice} instance as result, if found. + + @return: A deferred called with the L{UPnPDevice} instance + @rtype: L{twisted.internet.defer.Deferred} + """ + return defer.maybeDeferred(UPnPProtocol().search_device) -- 1.7.1