/ src / upnp.py.bak
upnp.py.bak
  1  # pylint: disable=too-many-statements,too-many-branches,protected-access,no-self-use
  2  """
  3  Complete UPnP port forwarding implementation in separate thread.
  4  Reference: http://mattscodecave.com/posts/using-python-and-upnp-to-forward-a-port
  5  """
  6  
  7  import re
  8  import socket
  9  import time
 10  from random import randint
 11  from xml.dom.minidom import Document  # nosec B408
 12  
 13  from defusedxml.minidom import parseString
 14  from six.moves import http_client as httplib
 15  from six.moves.urllib.parse import urlparse
 16  from six.moves.urllib.request import urlopen
 17  
 18  import queues
 19  import state
 20  import tr
 21  from bmconfigparser import config
 22  from debug import logger
 23  from network import StoppableThread, connectionpool, knownnodes
 24  from network.node import Peer
 25  
 26  
 27  def createRequestXML(service, action, arguments=None):
 28      """Router UPnP requests are XML formatted"""
 29  
 30      doc = Document()
 31  
 32      # create the envelope element and set its attributes
 33      envelope = doc.createElementNS('', 's:Envelope')
 34      envelope.setAttribute('xmlns:s', 'http://schemas.xmlsoap.org/soap/envelope/')
 35      envelope.setAttribute('s:encodingStyle', 'http://schemas.xmlsoap.org/soap/encoding/')
 36  
 37      # create the body element
 38      body = doc.createElementNS('', 's:Body')
 39  
 40      # create the function element and set its attribute
 41      fn = doc.createElementNS('', 'u:%s' % action)
 42      fn.setAttribute('xmlns:u', 'urn:schemas-upnp-org:service:%s' % service)
 43  
 44      # setup the argument element names and values
 45      # using a list of tuples to preserve order
 46  
 47      # container for created nodes
 48      argument_list = []
 49  
 50      # iterate over arguments, create nodes, create text nodes,
 51      # append text nodes to nodes, and finally add the ready product
 52      # to argument_list
 53      if arguments is not None:
 54          for k, v in arguments:
 55              tmp_node = doc.createElement(k)
 56              tmp_text_node = doc.createTextNode(v)
 57              tmp_node.appendChild(tmp_text_node)
 58              argument_list.append(tmp_node)
 59  
 60      # append the prepared argument nodes to the function element
 61      for arg in argument_list:
 62          fn.appendChild(arg)
 63  
 64      # append function element to the body element
 65      body.appendChild(fn)
 66  
 67      # append body element to envelope element
 68      envelope.appendChild(body)
 69  
 70      # append envelope element to document, making it the root element
 71      doc.appendChild(envelope)
 72  
 73      # our tree is ready, conver it to a string
 74      return doc.toxml()
 75  
 76  
 77  class UPnPError(Exception):
 78      """Handle a UPnP error"""
 79  
 80      def __init__(self, message):
 81          super(UPnPError, self).__init__()
 82          logger.error(message)
 83  
 84  
 85  class Router:  # pylint: disable=old-style-class
 86      """Encapulate routing"""
 87      name = ""
 88      path = ""
 89      address = None
 90      routerPath = None
 91      extPort = None
 92  
 93      def __init__(self, ssdpResponse, address):
 94  
 95          self.address = address
 96  
 97          row = ssdpResponse.split('\r\n')
 98          header = {}
 99          for i in range(1, len(row)):
100              part = row[i].split(': ')
101              if len(part) == 2:
102                  header[part[0].lower()] = part[1]
103  
104          try:
105              self.routerPath = urlparse(header['location'])
106              if not self.routerPath or not hasattr(self.routerPath, "hostname"):
107                  logger.error("UPnP: no hostname: %s", header['location'])
108          except KeyError:
109              logger.error("UPnP: missing location header")
110  
111          # get the profile xml file and read it into a variable
112          parsed_url = urlparse(header['location'])
113          if parsed_url.scheme not in ['http', 'https']:
114              raise UPnPError("Unsupported URL scheme: %s" % parsed_url.scheme)
115          directory = urlopen(header['location']).read()  # nosec B310
116  
117          # create a DOM object that represents the `directory` document
118          dom = parseString(directory)
119  
120          self.name = dom.getElementsByTagName('friendlyName')[0].childNodes[0].data
121          # find all 'serviceType' elements
122          service_types = dom.getElementsByTagName('serviceType')
123  
124          for service in service_types:
125              if service.childNodes[0].data.find('WANIPConnection') > 0 or \
126                      service.childNodes[0].data.find('WANPPPConnection') > 0:
127                  self.path = service.parentNode.getElementsByTagName('controlURL')[0].childNodes[0].data
128                  self.upnp_schema = re.sub(r'[^A-Za-z0-9:-]', '', service.childNodes[0].data.split(':')[-2])
129  
130      def AddPortMapping(
131              self,
132              externalPort,
133              internalPort,
134              internalClient,
135              protocol,
136              description,
137              leaseDuration=0,
138              enabled=1,
139      ):
140          """Add UPnP port mapping"""
141  
142          resp = self.soapRequest(self.upnp_schema + ':1', 'AddPortMapping', [
143              ('NewRemoteHost', ''),
144              ('NewExternalPort', str(externalPort)),
145              ('NewProtocol', protocol),
146              ('NewInternalPort', str(internalPort)),
147              ('NewInternalClient', internalClient),
148              ('NewEnabled', str(enabled)),
149              ('NewPortMappingDescription', str(description)),
150              ('NewLeaseDuration', str(leaseDuration))
151          ])
152          self.extPort = externalPort
153          logger.info("Successfully established UPnP mapping for %s:%i on external port %i",
154                      internalClient, internalPort, externalPort)
155          return resp
156  
157      def DeletePortMapping(self, externalPort, protocol):
158          """Delete UPnP port mapping"""
159  
160          resp = self.soapRequest(self.upnp_schema + ':1', 'DeletePortMapping', [
161              ('NewRemoteHost', ''),
162              ('NewExternalPort', str(externalPort)),
163              ('NewProtocol', protocol),
164          ])
165          logger.info("Removed UPnP mapping on external port %i", externalPort)
166          return resp
167  
168      def GetExternalIPAddress(self):
169          """Get the external address"""
170  
171          resp = self.soapRequest(
172              self.upnp_schema + ':1', 'GetExternalIPAddress')
173          dom = parseString(resp.read())
174          return dom.getElementsByTagName(
175              'NewExternalIPAddress')[0].childNodes[0].data
176  
177      def soapRequest(self, service, action, arguments=None):
178          """Make a request to a router"""
179  
180          conn = httplib.HTTPConnection(self.routerPath.hostname, self.routerPath.port)
181          conn.request(
182              'POST',
183              self.path,
184              createRequestXML(service, action, arguments),
185              {
186                  'SOAPAction': '"urn:schemas-upnp-org:service:%s#%s"' % (service, action),
187                  'Content-Type': 'text/xml'
188              }
189          )
190          resp = conn.getresponse()
191          conn.close()
192          if resp.status == 500:
193              respData = resp.read()
194              try:
195                  dom = parseString(respData)
196                  errinfo = dom.getElementsByTagName('errorDescription')
197                  if errinfo:
198                      logger.error("UPnP error: %s", respData)
199                      raise UPnPError(errinfo[0].childNodes[0].data)
200              except:  # noqa:E722
201                  raise UPnPError("Unable to parse SOAP error: %s" % (respData))
202          return resp
203  
204  
205  class uPnPThread(StoppableThread):
206      """Start a thread to handle UPnP activity"""
207  
208      SSDP_ADDR = "239.255.255.250"
209      GOOGLE_DNS = "8.8.8.8"
210      SSDP_PORT = 1900
211      SSDP_MX = 2
212      SSDP_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
213  
214      def __init__(self):
215          super(uPnPThread, self).__init__(name="uPnPThread")
216          self.extPort = config.safeGetInt('bitmessagesettings', 'extport', default=None)
217          self.localIP = self.getLocalIP()
218          self.routers = []
219          self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
220          self.sock.bind((self.localIP, 0))
221          self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
222          self.sock.settimeout(5)
223          self.sendSleep = 60
224  
225      def run(self):
226          """Start the thread to manage UPnP activity"""
227  
228          logger.debug("Starting UPnP thread")
229          logger.debug("Local IP: %s", self.localIP)
230          lastSent = 0
231  
232          # wait until asyncore binds so that we know the listening port
233          bound = False
234          while state.shutdown == 0 and not self._stopped and not bound:
235              for s in connectionpool.pool.listeningSockets.values():
236                  if s.is_bound():
237                      bound = True
238              if not bound:
239                  time.sleep(1)
240  
241          # pylint: disable=attribute-defined-outside-init
242          self.localPort = config.getint('bitmessagesettings', 'port')
243  
244          while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'):
245              if time.time() - lastSent > self.sendSleep and not self.routers:
246                  try:
247                      self.sendSearchRouter()
248                  except:  # nosec B110 # noqa:E722 # pylint:disable=bare-except
249                      pass
250                  lastSent = time.time()
251              try:
252                  while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'):
253                      resp, (ip, _) = self.sock.recvfrom(1000)
254                      if resp is None:
255                          continue
256                      newRouter = Router(resp, ip)
257                      for router in self.routers:
258                          if router.routerPath == newRouter.routerPath:
259                              break
260                      else:
261                          logger.debug("Found UPnP router at %s", ip)
262                          self.routers.append(newRouter)
263                          self.createPortMapping(newRouter)
264                          try:
265                              self_peer = Peer(
266                                  newRouter.GetExternalIPAddress(),
267                                  self.extPort
268                              )
269                          except:  # noqa:E722
270                              logger.debug('Failed to get external IP')
271                          else:
272                              with knownnodes.knownNodesLock:
273                                  knownnodes.addKnownNode(
274                                      1, self_peer, is_self=True)
275                          queues.UISignalQueue.put(('updateStatusBar', tr._translate(
276                              "MainWindow", 'UPnP port mapping established on port %1'
277                          ).arg(str(self.extPort))))
278                          break
279              except socket.timeout:
280                  pass
281              except:  # noqa:E722
282                  logger.error("Failure running UPnP router search.", exc_info=True)
283              for router in self.routers:
284                  if router.extPort is None:
285                      self.createPortMapping(router)
286          try:
287              self.sock.shutdown(socket.SHUT_RDWR)
288          except (IOError, OSError):  # noqa:E722
289              pass
290          try:
291              self.sock.close()
292          except (IOError, OSError):  # noqa:E722
293              pass
294          deleted = False
295          for router in self.routers:
296              if router.extPort is not None:
297                  deleted = True
298                  self.deletePortMapping(router)
299          if deleted:
300              queues.UISignalQueue.put(('updateStatusBar', tr._translate("MainWindow", 'UPnP port mapping removed')))
301          logger.debug("UPnP thread done")
302  
303      def getLocalIP(self):
304          """Get the local IP of the node"""
305  
306          s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
307          s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
308          s.connect((uPnPThread.GOOGLE_DNS, 1))
309          return s.getsockname()[0]
310  
311      def sendSearchRouter(self):
312          """Querying for UPnP services"""
313  
314          ssdpRequest = "M-SEARCH * HTTP/1.1\r\n" + \
315              "HOST: %s:%d\r\n" % (uPnPThread.SSDP_ADDR, uPnPThread.SSDP_PORT) + \
316              "MAN: \"ssdp:discover\"\r\n" + \
317              "MX: %d\r\n" % (uPnPThread.SSDP_MX, ) + \
318              "ST: %s\r\n" % (uPnPThread.SSDP_ST, ) + "\r\n"
319  
320          try:
321              logger.debug("Sending UPnP query")
322              self.sock.sendto(ssdpRequest, (uPnPThread.SSDP_ADDR, uPnPThread.SSDP_PORT))
323          except:  # noqa:E722
324              logger.exception("UPnP send query failed")
325  
326      def createPortMapping(self, router):
327          """Add a port mapping"""
328  
329          for i in range(50):
330              try:
331                  localIP = self.localIP
332                  if i == 0:
333                      extPort = self.localPort  # try same port first
334                  elif i == 1 and self.extPort:
335                      extPort = self.extPort  # try external port from last time next
336                  else:
337                      extPort = randint(32767, 65535)  # nosec B311
338                  logger.debug(
339                      "Attempt %i, requesting UPnP mapping for %s:%i on external port %i",
340                      i,
341                      localIP,
342                      self.localPort,
343                      extPort)
344                  router.AddPortMapping(extPort, self.localPort, localIP, 'TCP', 'BitMessage')
345                  self.extPort = extPort
346                  config.set('bitmessagesettings', 'extport', str(extPort))
347                  config.save()
348                  break
349              except UPnPError:
350                  logger.debug("UPnP error: ", exc_info=True)
351  
352      def deletePortMapping(self, router):
353          """Delete a port mapping"""
354          router.DeletePortMapping(router.extPort, 'TCP')