--- /dev/null
+"""
+This package offers ways to retreive ip addresses of the machine, and map ports
+through various protocols.
+
+Currently only UPnP is implemented and available, in the pynupnp module.
+
+@author: Raphael Slinckx
+@copyright: Copyright 2005
+@license: LGPL
+@contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
+@version: 0.1.0
+"""
+
+__revision__ = "$id"
+__version__ = "0.1.0"
--- /dev/null
+"""
+Generic methods to retreive the IP address of the local machine.
+
+TODO: Example
+
+@author: Raphael Slinckx
+@copyright: Copyright 2005
+@license: LGPL
+@contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
+@version: 0.1.0
+"""
+
+__revision__ = "$id"
+
+import random, socket, logging
+
+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 nattraverso.utils import is_rfc1918_ip, is_bogus_ip
+
+def get_local_ip():
+ """
+ Returns a deferred which will be called with a
+ 2-uple (lan_flag, ip_address) :
+ - lan_flag:
+ - True if it's a local network (RFC1918)
+ - False if it's a WAN address
+
+ - ip_address is the actual ip address
+
+ @return: A deferred called with the above defined tuple
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ # 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
+
+def get_external_ip():
+ """
+ Returns a deferred which will be called with a
+ 2-uple (wan_flag, ip_address):
+ - wan_flag:
+ - True if it's a WAN address
+ - False if it's a LAN address
+ - None if it's a localhost (127.0.0.1) address
+ - ip_address: the most accessible ip address of this machine
+
+ @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
+ 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
+ """
+ try:
+ # Init multicast engine
+ IReactorMulticast(reactor)
+ except:
+ raise
+
+ logging.debug("Multicast ping to retreive local IP")
+ return LocalNetworkMulticast().discover().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 addres returned"
+ else:
+ return (is_rfc1918_ip(localip), localip)
+
+class LocalNetworkMulticast(DatagramProtocol, object):
+ """
+ Local IP discovery protocol via multicast:
+ - Broadcast 3 ping multicast packet with "ping" in it
+ - Wait for an answer
+ - Retreive the ip address from the returning packet, which is ours
+ """
+ def __init__(self, *args, **kwargs):
+ super(LocalNetworkMulticast, self).__init__(*args, **kwargs)
+
+ # Nothing found yet
+ self.completed = False
+ self.result = defer.Deferred()
+
+ def discover(self):
+ """
+ Launch the discovery of an UPnP device on the local network. You should
+ always call this after having created the object.
+
+ >>> result = LocalNetworkMulticast().discover().addCallback(got_upnpdevice_ip)
+
+ @return: A deferred called with the IP address of the UPnP device
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ #The port we listen on
+ mcast_port = 0
+ #The result of listenMulticast
+ mcast = None
+
+ # 5 different UDP ports
+ ports = [11000+random.randint(0, 5000) for port in range(5)]
+ for attempt, port in enumerate(ports):
+ try:
+ mcast = reactor.listenMulticast(port, self)
+ mcast_port = port
+ break
+ except CannotListenError:
+ if attempt < 5:
+ print "Trying another multicast UDP port", port
+ else:
+ raise
+
+ logging.debug("Sending multicast ping")
+ mcast.joinGroup('239.255.255.250', socket.INADDR_ANY)
+ self.transport.write('ping', ('239.255.255.250', mcast_port))
+ self.transport.write('ping', ('239.255.255.250', mcast_port))
+ self.transport.write('ping', ('239.255.255.250', mcast_port))
+ return self.result
+
+ def datagramReceived(self, dgram, addr):
+ """Datagram received, we callback the IP address."""
+ logging.debug("Received multicast pong: %s; addr:%r", dgram, addr)
+ if self.completed or dgram != 'ping':
+ # Result already handled, do nothing
+ return
+ else:
+ self.completed = True
+ # Return the upn device ip address
+ self.result.callback(addr[0])
--- /dev/null
+"""
+Generic NAT Port mapping interface.
+
+TODO: Example
+
+@author: Raphael Slinckx
+@copyright: Copyright 2005
+@license: LGPL
+@contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
+@version: 0.1.0
+"""
+
+__revision__ = "$id"
+
+from twisted.internet.base import BasePort
+
+# Public API
+def get_port_mapper(proto="TCP"):
+ """
+ Returns a L{NATMapper} instance, suited to map a port for
+ the given protocol. Defaults to TCP.
+
+ For the moment, only upnp mapper is available. It accepts both UDP and TCP.
+
+ @param proto: The protocol: 'TCP' or 'UDP'
+ @type proto: string
+ @return: A deferred called with a L{NATMapper} instance
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ import nattraverso.pynupnp
+ return nattraverso.pynupnp.get_port_mapper()
+
+class NATMapper:
+ """
+ Define methods to map port objects (as returned by twisted's listenXX).
+ This allows NAT to be traversed from incoming packets.
+
+ Currently the only implementation of this class is the UPnP Mapper, which
+ can map UDP and TCP ports, if an UPnP Device exists.
+ """
+ def __init__(self):
+ raise NotImplementedError("Cannot instantiate the class")
+
+ def map(self, port):
+ """
+ Create a mapping for the given twisted's port object.
+
+ The deferred will call back with a tuple (extaddr, extport):
+ - extaddr: The ip string of the external ip address of this host
+ - extport: the external port number used to map the given Port object
+
+ When called multiple times with the same Port,
+ callback with the existing mapping.
+
+ @param port: The port object to map
+ @type port: a L{twisted.internet.interfaces.IListeningPort} object
+ @return: A deferred called with the above defined tuple
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ raise NotImplementedError
+
+ def info(self, port):
+ """
+ Returns the existing mapping for the given port object. That means map()
+ has to be called before.
+
+ @param port: The port object to retreive info from
+ @type port: a L{twisted.internet.interfaces.IListeningPort} object
+ @raise ValueError: When there is no such existing mapping
+ @return: a tuple (extaddress, extport).
+ @see: L{map() function<map>}
+ """
+ raise NotImplementedError
+
+ def unmap(self, port):
+ """
+ Remove an existing mapping for the given twisted's port object.
+
+ @param port: The port object to unmap
+ @type port: a L{twisted.internet.interfaces.IListeningPort} object
+ @return: A deferred called with None
+ @rtype: L{twisted.internet.defer.Deferred}
+ @raise ValueError: When there is no such existing mapping
+ """
+ raise NotImplementedError
+
+ def get_port_mappings(self):
+ """
+ Returns a deferred that will be called with a dictionnary of the
+ existing mappings.
+
+ The dictionnary structure is the following:
+ - Keys: tuple (protocol, external_port)
+ - protocol is "TCP" or "UDP".
+ - external_port is the external port number, as see on the
+ WAN side.
+ - Values:tuple (internal_ip, internal_port)
+ - internal_ip is the LAN ip address of the host.
+ - internal_port is the internal port number mapped
+ to external_port.
+
+ @return: A deferred called with the above defined dictionnary
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ raise NotImplementedError
+
+ def _check_valid_port(self, port):
+ """Various Port object validity checks. Raise a ValueError."""
+ if not isinstance(port, BasePort):
+ raise ValueError("expected a Port, got %r"%(port))
+
+ if not port.connected:
+ raise ValueError("Port %r is not listening"%(port))
+
+ loc_addr = port.getHost()
+ if loc_addr.port == 0:
+ raise ValueError("Port %r has port number of 0"%(port))
+
--- /dev/null
+"""
+This package offers ways to retreive ip addresses of the machine, and map ports
+through UPnP devices.
+
+@author: Raphael Slinckx
+@copyright: Copyright 2005
+@license: LGPL
+@contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
+@version: 0.1.0
+"""
+__revision__ = "$id"
+
+from nattraverso.pynupnp.upnp import search_upnp_device, UPnPMapper
+
+def get_external_ip():
+ """
+ Returns a deferred which will be called with the WAN ip address
+ retreived through UPnP. The ip is a string of the form "x.x.x.x"
+
+ @return: A deferred called with the external ip address of this host
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ return search_upnp_device().addCallback(lambda x: x.get_external_ip())
+
+def get_port_mapper():
+ """
+ Returns a deferred which will be called with a L{UPnPMapper} instance.
+ This is a L{nattraverso.portmapper.NATMapper} implementation.
+
+ @return: A deferred called with the L{UPnPMapper} instance.
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ return search_upnp_device().addCallback(lambda x: UPnPMapper(x))
--- /dev/null
+"""
+This module is a SOAP client using twisted's deferreds.
+It uses the SOAPpy package.
+
+@author: Raphael Slinckx
+@copyright: Copyright 2005
+@license: LGPL
+@contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
+@version: 0.1.0
+"""
+
+__revision__ = "$id"
+
+import SOAPpy, logging
+from SOAPpy.Config import Config
+from twisted.web import client, error
+
+#General config
+Config.typed = False
+
+class SoapError(Exception):
+ """
+ This is a SOAP error message, not an HTTP error message.
+
+ The content of this error is a SOAPpy structure representing the
+ SOAP error message.
+ """
+ pass
+
+class SoapProxy:
+ """
+ Proxy for an url to which we send SOAP rpc calls.
+ """
+ def __init__(self, url, prefix):
+ """
+ Init the proxy, it will connect to the given url, using the
+ given soap namespace.
+
+ @param url: The url of the remote host to call
+ @param prefix: The namespace prefix to use, eg.
+ 'urn:schemas-upnp-org:service:WANIPConnection:1'
+ """
+ logging.debug("Soap Proxy: '%s', prefix: '%s'", url, prefix)
+ self._url = url
+ self._prefix = prefix
+
+ def call(self, method, **kwargs):
+ """
+ Call the given remote method with the given arguments, as keywords.
+
+ Returns a deferred, called with SOAPpy structure representing
+ the soap response.
+
+ @param method: The method name to call, eg. 'GetExternalIP'
+ @param kwargs: The parameters of the call, as keywords
+ @return: A deferred called with the external ip address of this host
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ payload = SOAPpy.buildSOAP(method=method, config=Config, namespace=self._prefix, kw=kwargs)
+ # Here begins the nasty hack
+ payload = payload.replace(
+ # Upnp wants s: instead of SOAP-ENV
+ 'SOAP-ENV','s').replace(
+ # Doesn't seem to like these encoding stuff
+ 'xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"', '').replace(
+ 'SOAP-ENC:root="1"', '').replace(
+ # And it wants u: instead of ns1 namespace for arguments..
+ 'ns1','u')
+
+ logging.debug("SOAP Payload:\n%s", payload)
+
+ return client.getPage(self._url, postdata=payload, method="POST",
+ headers={'content-type': 'text/xml', 'SOAPACTION': '%s#%s' % (self._prefix, method)}
+ ).addCallbacks(self._got_page, self._got_error)
+
+ def _got_page(self, result):
+ """
+ The http POST command was successful, we parse the SOAP
+ answer, and return it.
+
+ @param result: the xml content
+ """
+ parsed = SOAPpy.parseSOAPRPC(result)
+
+ logging.debug("SOAP Answer:\n%s", result)
+ logging.debug("SOAP Parsed Answer: %r", parsed)
+
+ return parsed
+
+ def _got_error(self, res):
+ """
+ The HTTP POST command did not succeed, depending on the error type:
+ - it's a SOAP error, we parse it and return a L{SoapError}.
+ - it's another type of error (http, other), we raise it as is
+ """
+ logging.debug("SOAP Error:\n%s", res)
+
+ if isinstance(res.value, error.Error):
+ try:
+ logging.debug("SOAP Error content:\n%s", res.value.response)
+ raise SoapError(SOAPpy.parseSOAPRPC(res.value.response)["detail"])
+ except:
+ raise
+ raise Exception(res.value)
--- /dev/null
+"""
+This module is the heart of the upnp support. Device discover, ip discovery
+and port mappings are implemented here.
+
+@author: Raphael Slinckx
+@author: Anthony Baxter
+@copyright: Copyright 2005
+@license: LGPL
+@contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
+@version: 0.1.0
+"""
+__revision__ = "$id"
+
+import socket, random, urlparse, logging
+
+from twisted.internet import reactor, defer
+from twisted.web import client
+from twisted.internet.protocol import DatagramProtocol
+from twisted.internet.error import CannotListenError
+
+from nattraverso.pynupnp.soap import SoapProxy
+from nattraverso.pynupnp.upnpxml import UPnPXml
+from nattraverso import ipdiscover, portmapper
+
+class UPnPError(Exception):
+ """
+ A generic UPnP error, with a descriptive message as content.
+ """
+ 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
+ L{NATMapper<portmapper.NATMapper>} interface.
+
+ @see: L{NATMapper<portmapper.NATMapper>}
+ """
+
+ def __init__(self, upnp):
+ """
+ Creates the mapper, with the given L{UPnPDevice} instance.
+
+ @param upnp: L{UPnPDevice} instance
+ """
+ self._mapped = {}
+ self._upnp = upnp
+
+ def map(self, port):
+ """
+ See interface
+ """
+ self._check_valid_port(port)
+
+ #Port is already mapped
+ if port in self._mapped:
+ return defer.succeed(self._mapped[port])
+
+ #Trigger a new mapping creation, first fetch local ip.
+ result = ipdiscover.get_local_ip()
+ self._mapped[port] = result
+ return result.addCallback(self._map_got_local_ip, port)
+
+ def info(self, port):
+ """
+ See interface
+ """
+ # If the mapping exists, everything's ok
+ if port in self._mapped:
+ return self._mapped[port]
+ else:
+ raise ValueError('Port %r is not currently mapped'%(port))
+
+ def unmap(self, port):
+ """
+ See interface
+ """
+ if port in self._mapped:
+ existing = self._mapped[port]
+
+ #Pending mapping, queue an unmap,return existing deferred
+ if type(existing) is not tuple:
+ existing.addCallback(lambda x: self.unmap(port))
+ return existing
+
+ #Remove our local mapping
+ del self._mapped[port]
+
+ #Ask the UPnP to remove the mapping
+ extaddr, extport = existing
+ return self._upnp.remove_port_mapping(extport, port.getHost().type)
+ else:
+ raise ValueError('Port %r is not currently mapped'%(port))
+
+ def get_port_mappings(self):
+ """
+ See interface
+ """
+ return self._upnp.get_port_mappings()
+
+ def _map_got_local_ip(self, ip_result, port):
+ """
+ We got the local ip address, retreive the existing port mappings
+ in the device.
+
+ @param ip_result: result of L{ipdiscover.get_local_ip}
+ @param port: a L{twisted.internet.interfaces.IListeningPort} we
+ want to map
+ """
+ local, ip = ip_result
+ return self._upnp.get_port_mappings().addCallback(
+ self._map_got_port_mappings, ip, port)
+
+ def _map_got_port_mappings(self, mappings, ip, port):
+ """
+ We got all the existing mappings in the device, find an unused one
+ and assign it for the requested port.
+
+ @param ip: The local ip of this host "x.x.x.x"
+ @param port: a L{twisted.internet.interfaces.IListeningPort} we
+ want to map
+ @param mappings: result of L{UPnPDevice.get_port_mappings}
+ """
+
+ #Get the requested mapping's info
+ ptype = port.getHost().type
+ intport = port.getHost().port
+
+ for extport in [random.randrange(1025, 65536) for val in range(20)]:
+ # Check if there is an existing mapping, if it does not exist, bingo
+ if not (ptype, extport) in mappings:
+ break
+
+ if (ptype, extport) in mappings:
+ existing = mappings[ptype, extport]
+
+ local_ip, local_port = existing
+ if local_ip == ip and local_port == intport:
+ # Existing binding for this host/port/proto - replace it
+ break
+
+ # Triggers the creation of the mapping on the device
+ result = self._upnp.add_port_mapping(ip, intport, extport, 'pynupnp', ptype)
+
+ # We also need the external IP, so we queue first an
+ # External IP Discovery, then we add the mapping.
+ return result.addCallback(
+ lambda x: self._upnp.get_external_ip()).addCallback(
+ self._port_mapping_added, extport, port)
+
+ def _port_mapping_added(self, extaddr, extport, port):
+ """
+ The port mapping was added in the device, this means::
+
+ Internet NAT LAN
+ |
+ > IP:extaddr |> IP:local ip
+ > Port:extport |> Port:port
+ |
+
+ @param extaddr: The exernal ip address
+ @param extport: The external port as number
+ @param port: The internal port as a
+ L{twisted.internet.interfaces.IListeningPort} object, that has been
+ mapped
+ """
+ self._mapped[port] = (extaddr, extport)
+ return (extaddr, extport)
+
+class UPnPDevice:
+ """
+ Represents an UPnP device, with the associated infos, and remote methods.
+ """
+ def __init__(self, soap_proxy, info):
+ """
+ Build the device, with the given SOAP proxy, and the meta-infos.
+
+ @param soap_proxy: an initialized L{SoapProxy} to the device
+ @param info: a dictionnary of various infos concerning the
+ device extracted with L{UPnPXml}
+ """
+ self._soap_proxy = soap_proxy
+ self._info = info
+
+ def get_external_ip(self):
+ """
+ Triggers an external ip discovery on the upnp device. Returns
+ a deferred called with the external ip of this host.
+
+ @return: A deferred called with the ip address, as "x.x.x.x"
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ result = self._soap_proxy.call('GetExternalIPAddress')
+ result.addCallback(self._on_external_ip)
+ return result
+
+ def get_port_mappings(self):
+ """
+ Retreive the existing port mappings
+
+ @see: L{portmapper.NATMapper.get_port_mappings}
+ @return: A deferred called with the dictionnary as defined
+ in the interface L{portmapper.NATMapper.get_port_mappings}
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ return self._get_port_mapping()
+
+ def add_port_mapping(self, local_ip, intport, extport, desc, proto, lease=0):
+ """
+ Add a port mapping in the upnp device. Returns a deferred.
+
+ @param local_ip: the LAN ip of this host as "x.x.x.x"
+ @param intport: the internal port number
+ @param extport: the external port number
+ @param desc: the description of this mapping (string)
+ @param proto: "UDP" or "TCP"
+ @param lease: The duration of the lease in (mili)seconds(??)
+ @return: A deferred called with None when the mapping is done
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ result = self._soap_proxy.call('AddPortMapping', NewRemoteHost="",
+ NewExternalPort=extport,
+ NewProtocol=proto,
+ NewInternalPort=intport,
+ NewInternalClient=local_ip,
+ NewEnabled=1,
+ NewPortMappingDescription=desc,
+ NewLeaseDuration=lease)
+
+ return result.addCallbacks(self._on_port_mapping_added,
+ self._on_no_port_mapping_added)
+
+ def remove_port_mapping(self, extport, proto):
+ """
+ Remove an existing port mapping on the device. Returns a deferred
+
+ @param extport: the external port number associated to the mapping
+ to be removed
+ @param proto: either "UDP" or "TCP"
+ @return: A deferred called with None when the mapping is done
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ result = self._soap_proxy.call('DeletePortMapping', NewRemoteHost="",
+ NewExternalPort=extport,
+ NewProtocol=proto)
+
+ return result.addCallbacks(self._on_port_mapping_removed,
+ self._on_no_port_mapping_removed)
+
+ # Private --------
+ def _on_external_ip(self, res):
+ """
+ Called when we received the external ip address from the device.
+
+ @param res: the SOAPpy structure of the result
+ @return: the external ip string, as "x.x.x.x"
+ """
+ logging.debug("Got external ip struct: %r", res)
+ return res['NewExternalIPAddress']
+
+ def _get_port_mapping(self, mapping_id=0, mappings=None):
+ """
+ Fetch the existing mappings starting at index
+ "mapping_id" from the device.
+
+ To retreive all the mappings call this without parameters.
+
+ @param mapping_id: The index of the mapping to start fetching from
+ @param mappings: the dictionnary of already fetched mappings
+ @return: A deferred called with the existing mappings when all have been
+ retreived, see L{get_port_mappings}
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ if mappings == None:
+ mappings = {}
+
+ result = self._soap_proxy.call('GetGenericPortMappingEntry',
+ NewPortMappingIndex=mapping_id)
+ return result.addCallbacks(
+ lambda x: self._on_port_mapping_received(x, mapping_id+1, mappings),
+ lambda x: self._on_no_port_mapping_received( x, mappings))
+
+ def _on_port_mapping_received(self, response, mapping_id, mappings):
+ """
+ Called we we receive a single mapping from the device.
+
+ @param response: a SOAPpy structure, representing the device's answer
+ @param mapping_id: The index of the next mapping in the device
+ @param mappings: the already fetched mappings, see L{get_port_mappings}
+ @return: A deferred called with the existing mappings when all have been
+ retreived, see L{get_port_mappings}
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ logging.debug("Got mapping struct: %r", response)
+ mappings[
+ response['NewProtocol'], response['NewExternalPort']
+ ] = (response['NewInternalClient'], response['NewInternalPort'])
+ return self._get_port_mapping(mapping_id, mappings)
+
+ def _on_no_port_mapping_received(self, failure, mappings):
+ """
+ Called when we have no more port mappings to retreive, or an
+ error occured while retreiving them.
+
+ Either we have a "SpecifiedArrayIndexInvalid" SOAP error, and that's ok,
+ it just means we have finished. If it returns some other error, then we
+ fail with an UPnPError.
+
+ @param mappings: the already retreived mappings
+ @param failure: the failure
+ @return: The existing mappings as defined in L{get_port_mappings}
+ @raise UPnPError: When we got any other error
+ than "SpecifiedArrayIndexInvalid"
+ """
+ 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]))
+
+
+ def _on_port_mapping_added(self, response):
+ """
+ The port mapping was successfully added, return None to the deferred.
+ """
+ return None
+
+ def _on_no_port_mapping_added(self, failure):
+ """
+ Called when the port mapping could not be added. Immediately
+ raise an UPnPError, with the SOAPpy structure inside.
+
+ @raise UPnPError: When the port mapping could not be added
+ """
+ raise UPnPError(failure.value.args[0])
+
+ def _on_port_mapping_removed(self, response):
+ """
+ The port mapping was successfully removed, return None to the deferred.
+ """
+ return None
+
+ def _on_no_port_mapping_removed(self, failure):
+ """
+ Called when the port mapping could not be removed. Immediately
+ raise an UPnPError, with the SOAPpy structure inside.
+
+ @raise UPnPError: When the port mapping could not be deleted
+ """
+ raise UPnPError(failure.value.args[0])
+
+# UPNP multicast address, port and request string
+_UPNP_MCAST = '239.255.255.250'
+_UPNP_PORT = 1900
+_UPNP_SEARCH_REQUEST = """M-SEARCH * HTTP/1.1\r
+Host:%s:%s\r
+ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r
+Man:"ssdp:discover"\r
+MX:3\r
+\r
+""" % (_UPNP_MCAST, _UPNP_PORT)
+
+class UPnPProtocol(DatagramProtocol, object):
+ """
+ The UPnP Device discovery udp multicast twisted protocol.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ Init the protocol, no parameters needed.
+ """
+ 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
+
+ # Public methods
+ def search_device(self):
+ """
+ Triggers a UPnP device discovery.
+
+ The returned deferred will be called with the L{UPnPDevice} that has
+ been found in the LAN.
+
+ @return: A deferred called with the detected L{UPnPDevice} instance.
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ self._discovery = defer.Deferred()
+
+ attempt = 0
+ mcast = None
+ while True:
+ try:
+ 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.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):
+ """
+ This is private, handle the multicast answer from the upnp device.
+ """
+ logging.debug("Got UPNP multicast search answer:\n%s", dgram)
+
+ #This is an HTTP response
+ response, message = dgram.split('\r\n', 1)
+
+ # Prepare status line
+ version, status, textstatus = response.split(None, 2)
+
+ if not version.startswith('HTTP') or self._control_url != None:
+ 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."""
+ hdict = {}
+ body = ''
+ remaining = message
+ while remaining:
+ line, remaining = remaining.split('\r\n', 1)
+ line = line.strip()
+ if not line:
+ body = remaining
+ break
+ key, val = line.split(':', 1)
+ key = key.lower()
+ hdict.setdefault(key, []).append(val.strip())
+ return hdict, body
+
+ headers, body = parse_discovery_response(message)
+
+ if not 'location' in headers:
+ self._on_discovery_failed(
+ UPnPError(
+ "No location header in response to M-SEARCH!: %r"%headers))
+ return
+
+ loc = headers['location'][0]
+ result = client.getPage(url=loc)
+ result.addCallback(self._on_gateway_response, loc).addErrback(
+ self._on_discovery_failed)
+
+ def _on_gateway_response(self, body, loc):
+ """
+ Called with the UPnP device XML description fetched via HTTP.
+
+ If the device has suitable services for ip discovery and port mappings,
+ the callback returned in L{search_device} is called with
+ the discovered L{UPnPDevice}.
+
+ @raise UPnPError: When no suitable service has been
+ found in the description, or another error occurs.
+ @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 if we have a base url, if not consider location as base url
+ urlbase = upnpinfo.urlbase
+ if urlbase == None:
+ urlbase = loc
+
+ # 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"))
+ 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
+
+ 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
+
+ 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())
--- /dev/null
+"""
+This module parse an UPnP device's XML definition in an Object.
+
+@author: Raphael Slinckx
+@copyright: Copyright 2005
+@license: LGPL
+@contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
+@version: 0.1.0
+"""
+
+__revision__ = "$id"
+
+from xml.dom import minidom
+import logging
+
+# Allowed UPnP services to use when mapping ports/external addresses
+WANSERVICES = ['urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'urn:schemas-upnp-org:service:WANPPPConnection:1']
+
+class UPnPXml:
+ """
+ This objects parses the XML definition, and stores the useful
+ results in attributes.
+
+ The device infos dictionnary may contain the following keys:
+ - friendlyname: A friendly name to call the device.
+ - manufacturer: A manufacturer name for the device.
+
+ Here are the different attributes:
+ - deviceinfos: A dictionnary of device infos as defined above.
+ - controlurl: The control url, this is the url to use when sending SOAP
+ requests to the device, relative to the base url.
+ - wanservice: The WAN service to be used, one of the L{WANSERVICES}
+ - urlbase: The base url to use when talking in SOAP to the device.
+
+ The full url to use is obtained by urljoin(urlbase, controlurl)
+ """
+
+ def __init__(self, xml):
+ """
+ Parse the given XML string for UPnP infos. This creates the attributes
+ when they are found, or None if no value was found.
+
+ @param xml: a xml string to parse
+ """
+ logging.debug("Got UPNP Xml description:\n%s", xml)
+ doc = minidom.parseString(xml)
+
+ # Fetch various device info
+ self.deviceinfos = {}
+ try:
+ attributes = {
+ 'friendlyname':'friendlyName',
+ 'manufacturer' : 'manufacturer'
+ }
+ device = doc.getElementsByTagName('device')[0]
+ for name, tag in attributes.iteritems():
+ try:
+ self.deviceinfos[name] = device.getElementsByTagName(
+ tag)[0].firstChild.datas.encode('utf-8')
+ except:
+ pass
+ except:
+ pass
+
+ # Fetch device control url
+ self.controlurl = None
+ self.wanservice = None
+
+ for service in doc.getElementsByTagName('service'):
+ try:
+ stype = service.getElementsByTagName(
+ 'serviceType')[0].firstChild.data.encode('utf-8')
+ if stype in WANSERVICES:
+ self.controlurl = service.getElementsByTagName(
+ 'controlURL')[0].firstChild.data.encode('utf-8')
+ self.wanservice = stype
+ break
+ except:
+ pass
+
+ # Find base url
+ self.urlbase = None
+ try:
+ self.urlbase = doc.getElementsByTagName(
+ 'URLBase')[0].firstChild.data.encode('utf-8')
+ except:
+ pass
+
--- /dev/null
+"""
+Various utility functions used in the nattraverso package.
+
+@author: Raphael Slinckx
+@copyright: Copyright 2005
+@license: LGPL
+@contact: U{raphael@slinckx.net<mailto:raphael@slinckx.net>}
+@version: 0.1.0
+"""
+__revision__ = "$id"
+
+def is_rfc1918_ip(ip):
+ """
+ Checks if the given ip address is a rfc1918 one.
+
+ @param ip: The ip address to test
+ @type ip: a string "x.x.x.x"
+ @return: True if it's a LAN address, False otherwise
+ """
+ if isinstance(ip, basestring):
+ ip = _ip_to_number(ip)
+
+ for net, mask in _nets:
+ if ip&mask == net:
+ return True
+
+ return False
+
+def is_bogus_ip(ip):
+ """
+ Checks if the given ip address is bogus, i.e. 0.0.0.0 or 127.0.0.1.
+
+ @param ip: The ip address to test
+ @type ip: a string "x.x.x.x"
+ @return: True if it's bogus, False otherwise
+ """
+ return ip.startswith('0.') or ip.startswith('127.')
+
+def _ip_to_number(ipstr):
+ """
+ Translate a string ip address to a packed number.
+
+ @param ipstr: the ip address to transform
+ @type ipstr: a string "x.x.x.x"
+ @return: an int32 number representing the ip address
+ """
+ net = [ int(digit) for digit in ipstr.split('.') ] + [ 0, 0, 0 ]
+ net = net[:4]
+ return ((((((0L+net[0])<<8) + net[1])<<8) + net[2])<<8) +net[3]
+
+# List of rfc1918 net/mask
+_rfc1918_networks = [('127', 8), ('192.168', 16), ('10', 8), ('172.16', 12)]
+# Machine readable form of the above
+_nets = [(_ip_to_number(net), (2L**32 -1)^(2L**(32-mask)-1))
+ for net, mask in _rfc1918_networks]
+
from twisted.internet import defer, reactor
from twisted.web import server, resource
from twisted.python import log
+from nattraverso import portmapper, ipdiscover
import bitcoin.p2p, bitcoin.getwork, bitcoin.data
from util import db, expiring_dict, jsonrpc, variable, deferral, math
print ' ...success!'
print
+ @defer.inlineCallbacks
+ def upnp_thread():
+ while True:
+ try:
+ is_lan, lan_ip = yield ipdiscover.get_local_ip()
+ if not is_lan:
+ continue
+ pm = yield portmapper.get_port_mapper()
+ yield pm._upnp.add_port_mapping(lan_ip, args.net.P2P_PORT, args.net.P2P_PORT, 'p2pool', 'TCP')
+ except:
+ log.err()
+ yield deferral.sleep(random.expovariate(1/120))
+
+ if args.upnp:
+ upnp_thread()
+
# start listening for workers with a JSON-RPC server
print 'Listening for workers on port %i...' % (args.worker_port,)
parser.add_argument('-l', '--low-bandwidth',
help='trade lower bandwidth usage for higher latency (reduced efficiency)',
action='store_true', default=False, dest='low_bandwidth')
+ parser.add_argument('--disable-upnp',
+ help='''don't attempt to forward port 9333 (19333 for testnet) from the WAN to this computer using UPnP''',
+ action='store_false', default=True, dest='upnp')
worker_group = parser.add_argument_group('worker interface')
worker_group.add_argument('-w', '--worker-port', metavar='PORT',