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)
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)
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 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')
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
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