# pylint: disable=too-many-statements,too-many-branches,protected-access,no-self-use """ Complete UPnP port forwarding implementation in separate thread. Reference: http://mattscodecave.com/posts/using-python-and-upnp-to-forward-a-port """ import httplib import socket import time import urllib2 from random import randint from urlparse import urlparse from xml.dom.minidom import Document, parseString import queues import state import tr from bmconfigparser import config from debug import logger from network import BMConnectionPool, knownnodes, StoppableThread from network.node import Peer def createRequestXML(service, action, arguments=None): """Router UPnP requests are XML formatted""" doc = Document() # create the envelope element and set its attributes envelope = doc.createElementNS('', 's:Envelope') envelope.setAttribute('xmlns:s', 'http://schemas.xmlsoap.org/soap/envelope/') envelope.setAttribute('s:encodingStyle', 'http://schemas.xmlsoap.org/soap/encoding/') # create the body element body = doc.createElementNS('', 's:Body') # create the function element and set its attribute fn = doc.createElementNS('', 'u:%s' % action) fn.setAttribute('xmlns:u', 'urn:schemas-upnp-org:service:%s' % service) # setup the argument element names and values # using a list of tuples to preserve order # container for created nodes argument_list = [] # iterate over arguments, create nodes, create text nodes, # append text nodes to nodes, and finally add the ready product # to argument_list if arguments is not None: for k, v in arguments: tmp_node = doc.createElement(k) tmp_text_node = doc.createTextNode(v) tmp_node.appendChild(tmp_text_node) argument_list.append(tmp_node) # append the prepared argument nodes to the function element for arg in argument_list: fn.appendChild(arg) # append function element to the body element body.appendChild(fn) # append body element to envelope element envelope.appendChild(body) # append envelope element to document, making it the root element doc.appendChild(envelope) # our tree is ready, conver it to a string return doc.toxml() class UPnPError(Exception): """Handle a UPnP error""" def __init__(self, message): super(UPnPError, self).__init__() logger.error(message) class Router: # pylint: disable=old-style-class """Encapulate routing""" name = "" path = "" address = None routerPath = None extPort = None def __init__(self, ssdpResponse, address): self.address = address row = ssdpResponse.split('\r\n') header = {} for i in range(1, len(row)): part = row[i].split(': ') if len(part) == 2: header[part[0].lower()] = part[1] try: self.routerPath = urlparse(header['location']) if not self.routerPath or not hasattr(self.routerPath, "hostname"): logger.error("UPnP: no hostname: %s", header['location']) except KeyError: logger.error("UPnP: missing location header") # get the profile xml file and read it into a variable directory = urllib2.urlopen(header['location']).read() # create a DOM object that represents the `directory` document dom = parseString(directory) self.name = dom.getElementsByTagName('friendlyName')[0].childNodes[0].data # find all 'serviceType' elements service_types = dom.getElementsByTagName('serviceType') for service in service_types: if service.childNodes[0].data.find('WANIPConnection') > 0 or \ service.childNodes[0].data.find('WANPPPConnection') > 0: self.path = service.parentNode.getElementsByTagName('controlURL')[0].childNodes[0].data self.upnp_schema = service.childNodes[0].data.split(':')[-2] def AddPortMapping( self, externalPort, internalPort, internalClient, protocol, description, leaseDuration=0, enabled=1, ): # pylint: disable=too-many-arguments """Add UPnP port mapping""" resp = self.soapRequest(self.upnp_schema + ':1', 'AddPortMapping', [ ('NewRemoteHost', ''), ('NewExternalPort', str(externalPort)), ('NewProtocol', protocol), ('NewInternalPort', str(internalPort)), ('NewInternalClient', internalClient), ('NewEnabled', str(enabled)), ('NewPortMappingDescription', str(description)), ('NewLeaseDuration', str(leaseDuration)) ]) self.extPort = externalPort logger.info("Successfully established UPnP mapping for %s:%i on external port %i", internalClient, internalPort, externalPort) return resp def DeletePortMapping(self, externalPort, protocol): """Delete UPnP port mapping""" resp = self.soapRequest(self.upnp_schema + ':1', 'DeletePortMapping', [ ('NewRemoteHost', ''), ('NewExternalPort', str(externalPort)), ('NewProtocol', protocol), ]) logger.info("Removed UPnP mapping on external port %i", externalPort) return resp def GetExternalIPAddress(self): """Get the external address""" resp = self.soapRequest( self.upnp_schema + ':1', 'GetExternalIPAddress') dom = parseString(resp.read()) return dom.getElementsByTagName( 'NewExternalIPAddress')[0].childNodes[0].data def soapRequest(self, service, action, arguments=None): """Make a request to a router""" conn = httplib.HTTPConnection(self.routerPath.hostname, self.routerPath.port) conn.request( 'POST', self.path, createRequestXML(service, action, arguments), { 'SOAPAction': '"urn:schemas-upnp-org:service:%s#%s"' % (service, action), 'Content-Type': 'text/xml' } ) resp = conn.getresponse() conn.close() if resp.status == 500: respData = resp.read() try: dom = parseString(respData) errinfo = dom.getElementsByTagName('errorDescription') if errinfo: logger.error("UPnP error: %s", respData) raise UPnPError(errinfo[0].childNodes[0].data) except: # noqa:E722 raise UPnPError("Unable to parse SOAP error: %s" % (respData)) return resp class uPnPThread(StoppableThread): """Start a thread to handle UPnP activity""" SSDP_ADDR = "239.255.255.250" GOOGLE_DNS = "8.8.8.8" SSDP_PORT = 1900 SSDP_MX = 2 SSDP_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" def __init__(self): super(uPnPThread, self).__init__(name="uPnPThread") self.extPort = config.safeGetInt('bitmessagesettings', 'extport', default=None) self.localIP = self.getLocalIP() self.routers = [] self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind((self.localIP, 0)) self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) self.sock.settimeout(5) self.sendSleep = 60 def run(self): """Start the thread to manage UPnP activity""" logger.debug("Starting UPnP thread") logger.debug("Local IP: %s", self.localIP) lastSent = 0 # wait until asyncore binds so that we know the listening port bound = False while state.shutdown == 0 and not self._stopped and not bound: for s in BMConnectionPool().listeningSockets.values(): if s.is_bound(): bound = True if not bound: time.sleep(1) # pylint: disable=attribute-defined-outside-init self.localPort = config.getint('bitmessagesettings', 'port') while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'): if time.time() - lastSent > self.sendSleep and not self.routers: try: self.sendSearchRouter() except: # noqa:E722 pass lastSent = time.time() try: while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'): resp, (ip, _) = self.sock.recvfrom(1000) if resp is None: continue newRouter = Router(resp, ip) for router in self.routers: if router.routerPath == newRouter.routerPath: break else: logger.debug("Found UPnP router at %s", ip) self.routers.append(newRouter) self.createPortMapping(newRouter) try: self_peer = Peer( newRouter.GetExternalIPAddress(), self.extPort ) except: # noqa:E722 logger.debug('Failed to get external IP') else: with knownnodes.knownNodesLock: knownnodes.addKnownNode( 1, self_peer, is_self=True) queues.UISignalQueue.put(('updateStatusBar', tr._translate( "MainWindow", 'UPnP port mapping established on port %1' ).arg(str(self.extPort)))) break except socket.timeout: pass except: # noqa:E722 logger.error("Failure running UPnP router search.", exc_info=True) for router in self.routers: if router.extPort is None: self.createPortMapping(router) try: self.sock.shutdown(socket.SHUT_RDWR) except: # noqa:E722 pass try: self.sock.close() except: # noqa:E722 pass deleted = False for router in self.routers: if router.extPort is not None: deleted = True self.deletePortMapping(router) if deleted: queues.UISignalQueue.put(('updateStatusBar', tr._translate("MainWindow", 'UPnP port mapping removed'))) logger.debug("UPnP thread done") def getLocalIP(self): """Get the local IP of the node""" s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) s.connect((uPnPThread.GOOGLE_DNS, 1)) return s.getsockname()[0] def sendSearchRouter(self): """Querying for UPnP services""" ssdpRequest = "M-SEARCH * HTTP/1.1\r\n" + \ "HOST: %s:%d\r\n" % (uPnPThread.SSDP_ADDR, uPnPThread.SSDP_PORT) + \ "MAN: \"ssdp:discover\"\r\n" + \ "MX: %d\r\n" % (uPnPThread.SSDP_MX, ) + \ "ST: %s\r\n" % (uPnPThread.SSDP_ST, ) + "\r\n" try: logger.debug("Sending UPnP query") self.sock.sendto(ssdpRequest, (uPnPThread.SSDP_ADDR, uPnPThread.SSDP_PORT)) except: # noqa:E722 logger.exception("UPnP send query failed") def createPortMapping(self, router): """Add a port mapping""" for i in range(50): try: localIP = self.localIP if i == 0: extPort = self.localPort # try same port first elif i == 1 and self.extPort: extPort = self.extPort # try external port from last time next else: extPort = randint(32767, 65535) # nosec B311 logger.debug( "Attempt %i, requesting UPnP mapping for %s:%i on external port %i", i, localIP, self.localPort, extPort) router.AddPortMapping(extPort, self.localPort, localIP, 'TCP', 'BitMessage') self.extPort = extPort config.set('bitmessagesettings', 'extport', str(extPort)) config.save() break except UPnPError: logger.debug("UPnP error: ", exc_info=True) def deletePortMapping(self, router): """Delete a port mapping""" router.DeletePortMapping(router.extPort, 'TCP')