Beispiel #1
0
    def test_soap_scpd(self):
        from shtoom.soapsucks import SOAPRequestFactory, BeautifulSax
        ae = self.assertEquals
        ar = self.assertRaises
        schema = "urn:schemas-upnp-org:service:WANIPConnection:1"
        soap = SOAPRequestFactory('http://127.0.0.1:5533/', schema)
        soap.setSCPD(canned_scpd)
        request = soap.GetGenericPortMappingEntry(NewPortMappingIndex=12)
        for k, v in request.headers.items():
            if k.lower() == 'soapaction':
                ae(v, '"%s#GetGenericPortMappingEntry"'%schema)
        body = request.get_data()
        bs = BeautifulSax(body)
        meth = bs.fetch('u:GetGenericPortMappingEntry')
        ae(len(meth), 1)
        meth = meth[0]
        ae(meth['xmlns:u'], schema)
        # strip stupid whitespace text nodes
        meth.contents = [ x for x in meth.contents if str(x).strip() ]
        ae(len(meth.contents), 1)
        m = meth.contents[0]
        ae(m.name, 'NewPortMappingIndex')
        ae(len(m.contents), 1)
        ae(str(m.contents[0]),'12')

        ar(NameError, soap.GetNonExistentMethod, )
        ar(TypeError, soap.GetGenericPortMappingEntry, a=1, b=2)
        ar(TypeError, soap.GetGenericPortMappingEntry,
                                                NewPortMappingIndex=1, b=2)
Beispiel #2
0
    def test_soap_scpd(self):
        from shtoom.soapsucks import SOAPRequestFactory, BeautifulSax
        ae = self.assertEquals
        ar = self.assertRaises
        schema = "urn:schemas-upnp-org:service:WANIPConnection:1"
        soap = SOAPRequestFactory('http://127.0.0.1:5533/', schema)
        soap.setSCPD(canned_scpd)
        request = soap.GetGenericPortMappingEntry(NewPortMappingIndex=12)
        for k, v in request.headers.items():
            if k.lower() == 'soapaction':
                ae(v, '"%s#GetGenericPortMappingEntry"' % schema)
        body = request.get_data()
        bs = BeautifulSax(body)
        meth = bs.fetch('u:GetGenericPortMappingEntry')
        ae(len(meth), 1)
        meth = meth[0]
        ae(meth['xmlns:u'], schema)
        # strip stupid whitespace text nodes
        meth.contents = [x for x in meth.contents if str(x).strip()]
        ae(len(meth.contents), 1)
        m = meth.contents[0]
        ae(m.name, 'NewPortMappingIndex')
        ae(len(m.contents), 1)
        ae(str(m.contents[0]), '12')

        ar(
            NameError,
            soap.GetNonExistentMethod,
        )
        ar(TypeError, soap.GetGenericPortMappingEntry, a=1, b=2)
        ar(TypeError,
           soap.GetGenericPortMappingEntry,
           NewPortMappingIndex=1,
           b=2)
Beispiel #3
0
 def handleWanServiceDesc(self, body):
     log.msg("got WANServiceDesc from %s"%(self.urlbase,), system='UPnP')
     data = body.read()
     self.soap = SOAPRequestFactory(self.controlURL,
                         "urn:schemas-upnp-org:service:WANIPConnection:1")
     self.soap.setSCPD(data)
     self.completedDiscovery()
Beispiel #4
0
    def test_soaprequest(self):
        from shtoom.soapsucks import SOAPRequestFactory, BeautifulSax
        ae = self.assertEqual
        schema = "urn:schemas-upnp-org:service:WANIPConnection:1"
        s = SOAPRequestFactory('http://127.0.0.1:5533/', schema)
        request = s.GetGenericPortMappingEntry(NewPortMappingIndex=12)

        for k, v in request.headers.items():
            if k.lower() == 'soapaction':
                ae(v, '"%s#GetGenericPortMappingEntry"' % schema)
        #old body = request.get_data()
        body = request.data
        bs = BeautifulSax(body)
        meth = bs.fetch('u:GetGenericPortMappingEntry')
        ae(len(meth), 1)
        meth = meth[0]
        ae(meth['xmlns:u'], schema)
        # strip stupid whitespace text nodes
        meth.contents = [x for x in meth.contents if str(x).strip()]
        ae(len(meth.contents), 1)
        m = meth.contents[0]
        ae(m.name, 'NewPortMappingIndex')
        ae(len(m.contents), 1)
        ae(str(m.contents[0]), '12')

        request = s.GetTotallyBogusRequest(a='hello', b='goodbye', c=None)
        for k, v in request.headers.items():
            if k.lower() == 'soapaction':
                ae(v, '"%s#GetTotallyBogusRequest"' % schema)
        #old body = request.get_data()
        body = request.data
        bs = BeautifulSax(body)
        meth = bs.fetch('u:GetTotallyBogusRequest')
        ae(len(meth), 1)
        meth = meth[0]
        ae(meth['xmlns:u'], schema)
        # strip stupid whitespace text nodes
        meth.contents = [x for x in meth.contents if str(x).strip()]
        ae(len(meth.contents), 3)
        for m in meth.contents:
            # Argument ordering doesn't matter
            if m.name == 'a':
                ae(len(m.contents), 1)
                ae(str(m.contents[0]), 'hello')
            elif m.name == 'b':
                ae(len(m.contents), 1)
                ae(str(m.contents[0]), 'goodbye')
            elif m.name == 'c':
                ae(len(m.contents), 0)
            else:
                # XXX
                ae('test failed', '%s not one of a,b,c' % (m.name))
        #old ae(request.get_host(), '127.0.0.1:5533')
        ae(request.host, '127.0.0.1:5533')
Beispiel #5
0
 def handleWanServiceDesc(self, body):
     log.msg("got WANServiceDesc from %s"%(self.urlbase,), system='UPnP')
     data = body.read()
     self.soap = SOAPRequestFactory(self.controlURL,
                         "urn:schemas-upnp-org:service:WANIPConnection:1")
     self.soap.setSCPD(data)
     self.completedDiscovery()
Beispiel #6
0
class UPnPProtocol(DatagramProtocol, object):

    def __init__(self, *args, **kwargs):
        self.controlURL = None
        self.upnpInfo = {}
        self.urlbase = None
        self.gotSearchResponse = False
        super(UPnPProtocol,self).__init__(*args, **kwargs)

    def datagramReceived(self, dgram, address):
        response, message = dgram.split('\r\n', 1)
        version, status, textstatus = response.split(None, 2)
        if not version.startswith('HTTP'):
            return
        if self.controlURL:
            return
        log.msg("got a response from %s, status %s "%(address, status),
                                        system='UPnP')
        if status == "200":
            self.gotSearchResponse = True
            if self.upnpTimeout:
                self.upnpTimeout.cancel()
                self.upnpTimeout = None
            self.handleSearchResponse(message)

    def handleSearchResponse(self, message):
        headers, body = self.parseSearchResponse(message)
        loc = headers.get('location')
        if not loc:
            log.msg("No location header in response to M-SEARCH!",
                                                            system='UPnP')
            return
        loc = loc[0]
        d = urlopen(loc)
        log.msg("found a UPnP device at %s"%loc, system="UPnP")
        d.addCallback(self.handleIGDeviceResponse, loc).addErrback(log.err)

    def parseSearchResponse(self, message):
        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

    def discoverUPnP(self):
        "Discover UPnP devices. Returns a Deferred"
        from twisted.internet import reactor, defer
        self._discDef = defer.Deferred()
        search = cannedUPnPSearch()
        try:
            self.transport.write(search, (UPNP_MCAST, UPNP_PORT))
            self.transport.write(search, (UPNP_MCAST, UPNP_PORT))
            self.transport.write(search, (UPNP_MCAST, UPNP_PORT))
        except socket.error:
            del self._discDef
            return defer.fail(NoUPnPFound('no network available'))
        self.upnpTimeout = reactor.callLater(6, self.timeoutDiscovery)
        return self._discDef

    def isAvailable(self):
        from twisted.internet import defer
        if hasattr(self, '_discDef'):
            return self._discDef
        elif self.controlURL is not None:
            return defer.succeed(self)
        else:
            return defer.fail(NoUPnPFound())

    def completedDiscovery(self):
        log.msg("discovery completed", system="UPnP")
        if self.upnpTimeout:
            self.upnpTimeout.cancel()
            self.upnpTimeout = None
        if hasattr(self, '_discDef'):
            if self.controlURL is not None:
                d = self._discDef
                del self._discDef
                d.callback(self)

    def failedDiscovery(self, err):
        log.msg("discovery failed", system="UPnP")
        if hasattr(self, '_discDef'):
            if self.controlURL is not None:
                self.controlURL = None
            d = self._discDef
            del self._discDef
            d.callback(NoUPnPFound())

    def timeoutDiscovery(self):
        log.msg("discovery timed out", system="UPnP")
        self.upnpTimeout = None
        if hasattr(self, '_discDef'):
            if self.urlbase is None:
                d = self._discDef
                del self._discDef
                d.callback(NoUPnPFound())

    def listenMulticast(self):
        from twisted.internet import reactor
        from twisted.internet.error import CannotListenError
        attempt = 0
        while True:
            try:
                mcast = reactor.listenMulticast(1900+attempt, self)
                break
            except CannotListenError:
                attempt = random.randint(0,500)
                log.msg("couldn't listen on UPnP port, trying %d"%(
                                    attempt+1900), system='UPnP')
        if attempt != 0:
            log.msg("warning: couldn't listen on std upnp port", system='UPnP')
        mcast.joinGroup('239.255.255.250', socket.INADDR_ANY)

    def handleIGDeviceResponse(self, body, loc):
        from twisted.internet import reactor
        #log.msg("before stupidrandomdelaytoworkaroundbug got an IGDevice from %s"%(loc,), system='UPnP')
        reactor.callLater(0, self.stupidrandomdelaytoworkaroundbug, body, loc)

    def stupidrandomdelaytoworkaroundbug(self, body, loc):
        """
        On Mac (but not on Linux), Twisted seems to drop the ball on
        connecTCP().  The TCP connection gets set up (as confirmed by packet
        trace showing three-part handshake), but the factory object never gets
        buildProtocol().  Eventually (depending on the timeout value), the
        factory object gets its connectionFailed() method called instead.

        Mysteriously, this *always* happens on the urlopen in this method, and
        never on any of the other TCP connections that are made during Shtoom
        setup.  Also mysteriously, inserting this stupidrandomdelay of 4 seconds
        fixes it.

        Surely I should write a minimal test case and then either give the test
        case to the Twisted folks or fix it myself, but some other things are
        way too urgent today.

        The sequence of events that I observed was: connect 1; connect 2;
        connect 3; callback 1; callback 2; connect 4; callback 3; ...timeout 4".

        where connect 1-3 are the "are you an IG device", and the connect 4 is
        this "get WAN service desc".  So maybe in twisted the even of the 3rd
        tcp connection completing is screwing up the state of the 4th tcp
        connection, which has been initiated by hasn't completed yet.
        """
        #log.msg("after stupidrandomrelaytoworkaroundbug, got an IGDevice from %s"%(loc,), system='UPnP')
        if self.controlURL is not None:
            log.msg("already found UPnP, discarding duplicate response",
                                                                system="UPnP")
            # We already got a working one - ignore this one
            return
        data = body.read()
        bs = BeautifulSoap(data)
        manufacturer = bs.first('manufacturer')
        if manufacturer and manufacturer.contents:
            log.msg("you're behind a %s"%(manufacturer.contents[0]),
                                                                system='UPnP')
            self.upnpInfo['manufacturer'] = manufacturer.contents[0]
        friendly = bs.first('friendlyName')
        if friendly:
            self.upnpInfo['friendlyName'] = str(friendly.contents[0])
        urlbase = bs.first('URLBase')
        if urlbase and urlbase.contents:
            self.urlbase = str(urlbase.contents[0])
            log.msg("upnp urlbase is %s"%(self.urlbase), system='UPnP')
        else:
            log.msg("upnp response has no urlbase, falling back to %s"%(loc,), system='UPnP')
            self.urlbase = loc

        wanservices = bs.fetch('service',
            dict(serviceType='urn:schemas-upnp-org:service:WANIPConnection:%'))
        for service in wanservices:
            scpdurl = service.get('SCPDURL')
            controlurl = service.get('controlURL')
            if scpdurl and controlurl:
                break
        else:
            log.msg("upnp response showed no WANIPConnections", system='UPnP')
            if DEBUG:
                print "dump of response", bs
            return

        self.controlURL = urlparse.urljoin(self.urlbase, controlurl)
        log.msg("upnp %r controlURL is %s"%(self, self.controlURL), system='UPnP')
        d = urlopen(urlparse.urljoin(self.urlbase, scpdurl))
        d.addCallback(self.handleWanServiceDesc).addErrback(log.err)

    def handleWanServiceDesc(self, body):
        log.msg("got WANServiceDesc from %s"%(self.urlbase,), system='UPnP')
        data = body.read()
        self.soap = SOAPRequestFactory(self.controlURL,
                            "urn:schemas-upnp-org:service:WANIPConnection:1")
        self.soap.setSCPD(data)
        self.completedDiscovery()

    def getExternalIPAddress(self):
        from twisted.internet import defer
        cd = defer.Deferred()
        req = self.soap.GetExternalIPAddress()
        d = soapenurl(req)
        d.addCallbacks(lambda x: self.cb_gotExternalIPAddress(x, cd),
                       lambda x: self.cb_failedExternalIPAddress(x, cd))
        return cd

    def cb_gotExternalIPAddress(self, res, cd):
        cd.callback(res['NewExternalIPAddress'])

    def cb_failedExternalIPAddress(self, failure, cd):
        err = failure.value.args[0]
        cd.errback(UPnPError("GetGenericPortMappingEntry got %s"%(err)))

    def getPortMappings(self):
        from twisted.internet import defer
        cd = defer.Deferred()
        self.getGenericPortMappingEntry(0, cd)
        return cd

    def getGenericPortMappingEntry(self, nextPMI=0, cd=None, saved=None):
        if saved is None:
            saved = {}
        request = self.soap.GetGenericPortMappingEntry(
                                                NewPortMappingIndex=nextPMI)
        d = soapenurl(request)
        d.addCallbacks(lambda x: self.cb_gotGenericPortMappingEntry(x,
                                                                 nextPMI+1,
                                                                 cd, saved),
                       lambda x: self.cb_failedGenericPortMappingEntry(x,
                                                                    cd, saved))

    def cb_gotGenericPortMappingEntry(self, response, nextPMI, cd, saved):
        saved[response['NewProtocol'], response['NewExternalPort']] = response
        self.getGenericPortMappingEntry(nextPMI, cd, saved)

    def cb_failedGenericPortMappingEntry(self, failure, cd, saved):
        err = failure.value.args[0]
        # Some routers (reported on a Netgear DG834) return "Invalid Action"
        # instead of SpecifiedArrayIndexInvalid. This is double-plus bogus.
        if err == "SpecifiedArrayIndexInvalid":
            cd.callback(saved)
        else:
            cd.errback(UPnPError("GetGenericPortMappingEntry got %s"%(err)))

    def addPortMapping(self, intport, extport, desc, proto='UDP', lease=0):
        "add a port mapping. returns a deferred"
        from nat import getLocalIPAddress
        from twisted.internet import defer
        cd = defer.Deferred()
        d = getLocalIPAddress()
        d.addCallback(lambda locIP: self._cbAddPortMapping(intport, extport,
                                                           desc, proto, lease,
                                                           locIP, cd))
        return cd

    def _cbAddPortMapping(self, iport, eport, desc, proto, lease, locip, cd):
        request = self.soap.AddPortMapping(NewRemoteHost=None,
                                           NewExternalPort=eport,
                                           NewProtocol=proto,
                                           NewInternalPort=iport,
                                           NewInternalClient=locip,
                                           NewEnabled=1,
                                           NewPortMappingDescription=desc,
                                           NewLeaseDuration=lease)
        d = soapenurl(request)
        d.addCallbacks(lambda x,cd=cd:self.cb_gotAddPortMapping(x,cd),
                       lambda x,cd=cd:self.cb_failedAddPortMapping(x,cd))

    def cb_gotAddPortMapping(self, response, compdef):
        log.msg('AddPortMapping ok', system='UPnP')
        compdef.callback(None)

    def cb_failedAddPortMapping(self, failure, compdef):
        err = failure.value.args[0]
        log.err('AddPortMapping failed with: %s'%(err), system='UPnP')
        compdef.errback(UPnPError(err))

    def deletePortMapping(self, extport, proto='UDP'):
        "remove a port mapping"
        from twisted.internet import defer
        cd = defer.Deferred()
        request = self.soap.DeletePortMapping(NewRemoteHost=None,
                                              NewExternalPort=extport,
                                              NewProtocol=proto)
        d = soapenurl(request)
        d.addCallbacks(lambda x,cd=cd:self.cb_gotDeletePortMapping(x,cd),
                       lambda x,cd=cd:self.cb_failedDeletePortMapping(x,cd))
        return cd


    def cb_gotDeletePortMapping(self, response, compdef):
        log.msg('DeletePortMapping ok', system='UPnP')
        compdef.callback(None)

    def cb_failedDeletePortMapping(self, failure, compdef):
        err = failure.value.args[0]
        log.err('DeletePortMapping failed with: %s'%(err), system='UPnP')
        compdef.errback(UPnPError(err))

    def soapCall(self, name, **kwargs):
        request = getattr(self.soap, name)(**kwargs)
        d = soapenurl(request)
        return d
Beispiel #7
0
class UPnPProtocol(DatagramProtocol, object):

    def __init__(self, *args, **kwargs):
        self.controlURL = None
        self.upnpInfo = {}
        self.urlbase = None
        self.gotSearchResponse = False
        super(UPnPProtocol,self).__init__(*args, **kwargs)

    def datagramReceived(self, dgram, address):
        response, message = dgram.split('\r\n', 1)
        version, status, textstatus = response.split(None, 2)
        if not version.startswith('HTTP'):
            return
        if self.controlURL:
            return
        log.msg("got a response from %s, status %s "%(address, status),
                                        system='UPnP')
        if status == "200":
            self.gotSearchResponse = True
            if self.upnpTimeout:
                self.upnpTimeout.cancel()
                self.upnpTimeout = None
            self.handleSearchResponse(message)

    def handleSearchResponse(self, message):
        headers, body = self.parseSearchResponse(message)
        loc = headers.get('location')
        if not loc:
            log.msg("No location header in response to M-SEARCH!",
                                                            system='UPnP')
            return
        loc = loc[0]
        d = urlopen(loc)
        log.msg("found a UPnP device at %s"%loc, system="UPnP")
        d.addCallback(self.handleIGDeviceResponse, loc).addErrback(log.err)

    def parseSearchResponse(self, message):
        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

    def discoverUPnP(self):
        """Discover UPnP devices. Returns a Deferred"""
        from twisted.internet import reactor, defer
        self._discDef = defer.Deferred()
        search = cannedUPnPSearch()
        try:
            self.transport.write(search, (UPNP_MCAST, UPNP_PORT))
            self.transport.write(search, (UPNP_MCAST, UPNP_PORT))
            self.transport.write(search, (UPNP_MCAST, UPNP_PORT))
        except socket.error:
            del self._discDef
            return defer.fail(NoUPnPFound('no network available'))
        self.upnpTimeout = reactor.callLater(6, self.timeoutDiscovery)
        return self._discDef

    def isAvailable(self):
        from twisted.internet import defer
        if hasattr(self, '_discDef'):
            return self._discDef
        elif self.controlURL is not None:
            return defer.succeed(self)
        else:
            return defer.fail(NoUPnPFound())

    def completedDiscovery(self):
        log.msg("discovery completed", system="UPnP")
        if self.upnpTimeout:
            self.upnpTimeout.cancel()
            self.upnpTimeout = None
        if hasattr(self, '_discDef'):
            if self.controlURL is not None:
                d = self._discDef
                del self._discDef
                d.callback(self)

    def failedDiscovery(self, err):
        log.msg("discovery failed", system="UPnP")
        if hasattr(self, '_discDef'):
            if self.controlURL is not None:
                self.controlURL = None
            d = self._discDef
            del self._discDef
            d.callback(NoUPnPFound())

    def timeoutDiscovery(self):
        log.msg("discovery timed out", system="UPnP")
        self.upnpTimeout = None
        if hasattr(self, '_discDef'):
            if self.urlbase is None:
                d = self._discDef
                del self._discDef
                d.callback(NoUPnPFound())

    def listenMulticast(self):
        from twisted.internet import reactor
        from twisted.internet.error import CannotListenError
        attempt = 0
        while True:
            try:
                mcast = reactor.listenMulticast(1900+attempt, self)
                break
            except CannotListenError:
                attempt = random.randint(0,500)
                log.msg("couldn't listen on UPnP port, trying %d"%(
                                    attempt+1900), system='UPnP')
        if attempt != 0:
            log.msg("warning: couldn't listen on std upnp port", system='UPnP')
        mcast.joinGroup('239.255.255.250', socket.INADDR_ANY)

    def handleIGDeviceResponse(self, body, loc):
        from twisted.internet import reactor
        #log.msg("before stupidrandomdelaytoworkaroundbug got an IGDevice from %s"%(loc,), system='UPnP')
        reactor.callLater(0, self.stupidrandomdelaytoworkaroundbug, body, loc)

    def stupidrandomdelaytoworkaroundbug(self, body, loc):
        """
        On Mac (but not on Linux), Twisted seems to drop the ball on
        connecTCP().  The TCP connection gets set up (as confirmed by packet
        trace showing three-part handshake), but the factory object never gets
        buildProtocol().  Eventually (depending on the timeout value), the
        factory object gets its connectionFailed() method called instead.

        Mysteriously, this *always* happens on the urlopen in this method, and
        never on any of the other TCP connections that are made during Shtoom
        setup.  Also mysteriously, inserting this stupidrandomdelay of 4 seconds
        fixes it.

        Surely I should write a minimal test case and then either give the test
        case to the Twisted folks or fix it myself, but some other things are
        way too urgent today.

        The sequence of events that I observed was: connect 1; connect 2;
        connect 3; callback 1; callback 2; connect 4; callback 3; ...timeout 4".

        where connect 1-3 are the "are you an IG device", and the connect 4 is
        this "get WAN service desc".  So maybe in twisted the even of the 3rd
        tcp connection completing is screwing up the state of the 4th tcp
        connection, which has been initiated by hasn't completed yet.
        """
        #log.msg("after stupidrandomrelaytoworkaroundbug, got an IGDevice from %s"%(loc,), system='UPnP')
        if self.controlURL is not None:
            log.msg("already found UPnP, discarding duplicate response",
                                                                system="UPnP")
            # We already got a working one - ignore this one
            return
        data = body.read()
        bs = BeautifulSoap(data)
        manufacturer = bs.first('manufacturer')
        if manufacturer and manufacturer.contents:
            log.msg("you're behind a %s"%(manufacturer.contents[0]),
                                                                system='UPnP')
            self.upnpInfo['manufacturer'] = manufacturer.contents[0]
        friendly = bs.first('friendlyName')
        if friendly:
            self.upnpInfo['friendlyName'] = str(friendly.contents[0])
        urlbase = bs.first('URLBase')
        if urlbase and urlbase.contents:
            self.urlbase = str(urlbase.contents[0])
            log.msg("upnp urlbase is %s"%(self.urlbase), system='UPnP')
        else:
            log.msg("upnp response has no urlbase, falling back to %s"%(loc,), system='UPnP')
            self.urlbase = loc

        wanservices = bs.fetch('service',
            dict(serviceType='urn:schemas-upnp-org:service:WANIPConnection:%'))
        for service in wanservices:
            scpdurl = service.get('SCPDURL')
            controlurl = service.get('controlURL')
            if scpdurl and controlurl:
                break
        else:
            log.msg("upnp response showed no WANIPConnections", system='UPnP')
            if DEBUG:
                print("dump of response", bs)
            return

        self.controlURL = urllib.parse.urljoin(self.urlbase, controlurl)
        log.msg("upnp %r controlURL is %s"%(self, self.controlURL), system='UPnP')
        d = urlopen(urllib.parse.urljoin(self.urlbase, scpdurl))
        d.addCallback(self.handleWanServiceDesc).addErrback(log.err)

    def handleWanServiceDesc(self, body):
        log.msg("got WANServiceDesc from %s"%(self.urlbase,), system='UPnP')
        data = body.read()
        self.soap = SOAPRequestFactory(self.controlURL,
                            "urn:schemas-upnp-org:service:WANIPConnection:1")
        self.soap.setSCPD(data)
        self.completedDiscovery()

    def getExternalIPAddress(self):
        from twisted.internet import defer
        cd = defer.Deferred()
        req = self.soap.GetExternalIPAddress()
        d = soapenurl(req)
        d.addCallbacks(lambda x: self.cb_gotExternalIPAddress(x, cd),
                       lambda x: self.cb_failedExternalIPAddress(x, cd))
        return cd

    def cb_gotExternalIPAddress(self, res, cd):
        cd.callback(res['NewExternalIPAddress'])

    def cb_failedExternalIPAddress(self, failure, cd):
        err = failure.value.args[0]
        cd.errback(UPnPError("GetGenericPortMappingEntry got %s"%(err)))

    def getPortMappings(self):
        from twisted.internet import defer
        cd = defer.Deferred()
        self.getGenericPortMappingEntry(0, cd)
        return cd

    def getGenericPortMappingEntry(self, nextPMI=0, cd=None, saved=None):
        if saved is None:
            saved = {}
        request = self.soap.GetGenericPortMappingEntry(
                                                NewPortMappingIndex=nextPMI)
        d = soapenurl(request)
        d.addCallbacks(lambda x: self.cb_gotGenericPortMappingEntry(x,
                                                                 nextPMI+1,
                                                                 cd, saved),
                       lambda x: self.cb_failedGenericPortMappingEntry(x,
                                                                    cd, saved))

    def cb_gotGenericPortMappingEntry(self, response, nextPMI, cd, saved):
        saved[response['NewProtocol'], response['NewExternalPort']] = response
        self.getGenericPortMappingEntry(nextPMI, cd, saved)

    def cb_failedGenericPortMappingEntry(self, failure, cd, saved):
        err = failure.value.args[0]
        # Some routers (reported on a Netgear DG834) return "Invalid Action"
        # instead of SpecifiedArrayIndexInvalid. This is double-plus bogus.
        if err == "SpecifiedArrayIndexInvalid":
            cd.callback(saved)
        else:
            cd.errback(UPnPError("GetGenericPortMappingEntry got %s"%(err)))

    def addPortMapping(self, intport, extport, desc, proto='UDP', lease=0):
        "add a port mapping. returns a deferred"
        from .nat import getLocalIPAddress
        from twisted.internet import defer
        cd = defer.Deferred()
        d = getLocalIPAddress()
        d.addCallback(lambda locIP: self._cbAddPortMapping(intport, extport,
                                                           desc, proto, lease,
                                                           locIP, cd))
        return cd

    def _cbAddPortMapping(self, iport, eport, desc, proto, lease, locip, cd):
        request = self.soap.AddPortMapping(NewRemoteHost=None,
                                           NewExternalPort=eport,
                                           NewProtocol=proto,
                                           NewInternalPort=iport,
                                           NewInternalClient=locip,
                                           NewEnabled=1,
                                           NewPortMappingDescription=desc,
                                           NewLeaseDuration=lease)
        d = soapenurl(request)
        d.addCallbacks(lambda x,cd=cd:self.cb_gotAddPortMapping(x,cd),
                       lambda x,cd=cd:self.cb_failedAddPortMapping(x,cd))

    def cb_gotAddPortMapping(self, response, compdef):
        log.msg('AddPortMapping ok', system='UPnP')
        compdef.callback(None)

    def cb_failedAddPortMapping(self, failure, compdef):
        err = failure.value.args[0]
        log.err('AddPortMapping failed with: %s'%(err), system='UPnP')
        compdef.errback(UPnPError(err))

    def deletePortMapping(self, extport, proto='UDP'):
        "remove a port mapping"
        from twisted.internet import defer
        cd = defer.Deferred()
        request = self.soap.DeletePortMapping(NewRemoteHost=None,
                                              NewExternalPort=extport,
                                              NewProtocol=proto)
        d = soapenurl(request)
        d.addCallbacks(lambda x,cd=cd:self.cb_gotDeletePortMapping(x,cd),
                       lambda x,cd=cd:self.cb_failedDeletePortMapping(x,cd))
        return cd


    def cb_gotDeletePortMapping(self, response, compdef):
        log.msg('DeletePortMapping ok', system='UPnP')
        compdef.callback(None)

    def cb_failedDeletePortMapping(self, failure, compdef):
        err = failure.value.args[0]
        log.err('DeletePortMapping failed with: %s'%(err), system='UPnP')
        compdef.errback(UPnPError(err))

    def soapCall(self, name, **kwargs):
        request = getattr(self.soap, name)(**kwargs)
        d = soapenurl(request)
        return d