__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
"""
# 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
@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):
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()
"""
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
"""
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):
@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):
"""
@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'
"""
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):
@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.
"""
# 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."""
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.
@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)
# 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)