fixed indentation
[p2pool.git] / nattraverso / pynupnp / upnp.py
index ade388d..e3af52a 100644 (file)
@@ -23,348 +23,348 @@ 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
+    """
+    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))
+    """
+    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)
+    """
+    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])
+    """
+    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'
@@ -376,169 +376,169 @@ 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())
+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())