rewriting portions of nattraverso
authorForrest Voight <forrest@forre.st>
Mon, 8 Aug 2011 22:06:50 +0000 (18:06 -0400)
committerForrest Voight <forrest@forre.st>
Mon, 8 Aug 2011 22:06:50 +0000 (18:06 -0400)
nattraverso/ipdiscover.py
nattraverso/pynupnp/upnp.py

index 1a22fb8..7baea11 100644 (file)
@@ -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()
     
index e3af52a..eae568f 100644 (file)
@@ -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)