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