NatTraversal.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. # someday: http://files.dns-sd.org/draft-nat-port-mapping.txt
  2. # today: http://www.upnp.org/
  3. import os
  4. import sys
  5. import Queue
  6. import socket
  7. import random
  8. import logging
  9. import urlparse
  10. if os.name == 'nt':
  11. import pywintypes
  12. import win32com.client
  13. from BTL import defer
  14. from BitTorrent.platform import os_version
  15. from BTL.sparse_set import SparseSet
  16. from BTL.exceptions import str_exc
  17. from BitTorrent.RawServer_twisted import Handler
  18. from BitTorrent.BeautifulSupe import BeautifulSupe
  19. from BTL.yielddefer import launch_coroutine, wrap_task
  20. from BTL.HostIP import get_host_ip, get_deferred_host_ip
  21. import twisted.copyright
  22. from twisted.internet import reactor
  23. from urllib import FancyURLopener, addinfourl
  24. from urllib2 import URLError, HTTPError, Request
  25. from httplib import BadStatusLine, HTTPResponse
  26. import BTL.stackthreading as threading
  27. nat_logger = logging.getLogger('NatTraversal')
  28. nat_logger.setLevel(logging.WARNING)
  29. def UnsupportedWarning(s):
  30. nat_logger.warning("NAT Traversal warning " + ("(%s: %s)." % (os_version, s)))
  31. def UPNPError(s):
  32. nat_logger.error("UPnP ERROR: " + ("(%s: %s)." % (os_version, s)))
  33. class UPnPException(Exception):
  34. pass
  35. class NATEventLoop(threading.Thread):
  36. def __init__(self):
  37. threading.Thread.__init__(self)
  38. self.queue = Queue.Queue()
  39. self.killswitch = defer.DeferredEvent()
  40. def ignore(*a, **kw):
  41. pass
  42. self.killswitch.addCallback(ignore)
  43. self.setDaemon(True)
  44. def run(self):
  45. while not self.killswitch.isSet():
  46. (f, a, kw) = self.queue.get()
  47. try:
  48. nat_logger.debug("NATEventLoop Event: %s" % f.__name__)
  49. f(*a, **kw)
  50. nat_logger.debug("NATEventLoop Event: %s finished." % f.__name__)
  51. except:
  52. # sys can be none during interpritter shutdown
  53. if sys is None:
  54. break
  55. nat_logger.exception("Error in NATEventLoop for %s" % str(f.__name__))
  56. class NatTraverser(object):
  57. def __init__(self, rawserver):
  58. self.rawserver = rawserver
  59. self.register_requests = []
  60. self.unregister_requests = []
  61. self.list_requests = []
  62. self.service = None
  63. self.services = []
  64. self.current_service = 0
  65. if self.rawserver.config['upnp']:
  66. if os.name == 'nt':
  67. self.services.append(WindowsUPnP)
  68. self.services.append(ManualUPnP)
  69. self.event_loop = NATEventLoop()
  70. self.event_loop.start()
  71. self.resume_init_services()
  72. def add_task(self, f, *a, **kw):
  73. self.event_loop.queue.put((f, a, kw))
  74. def init_services(self):
  75. # this loop is a little funny so a service can resume the init if it fails later
  76. if not self.rawserver.config['upnp']:
  77. return
  78. while self.current_service < len(self.services):
  79. service = self.services[self.current_service]
  80. self.current_service += 1
  81. try:
  82. nat_logger.info("Trying: %s" % service.__name__)
  83. service(self)
  84. break
  85. except Exception, e:
  86. nat_logger.warning(str_exc(e))
  87. else:
  88. e = "Unable to detect any UPnP services"
  89. UnsupportedWarning(e)
  90. self._cancel_queue(e)
  91. def resume_init_services(self):
  92. self.add_task(self.init_services)
  93. def attach_service(self, service):
  94. nat_logger.info("Using: %s" % type(service).__name__)
  95. self.service = service
  96. self.add_task(self._flush_queue)
  97. def detach_service(self, service):
  98. if service != self.service:
  99. nat_logger.error("Service: %s is not in use!" % type(service).__name__)
  100. return
  101. nat_logger.info("Detached: %s" % type(service).__name__)
  102. self.service = None
  103. def _flush_queue(self):
  104. if self.service:
  105. for mapping in self.register_requests:
  106. self.add_task(self.service.safe_register_port, mapping)
  107. self.register_requests = []
  108. for request in self.unregister_requests:
  109. # unregisters can block, because they occur at shutdown
  110. self.service.unregister_port(*request)
  111. self.unregister_requests = []
  112. for request in self.list_requests:
  113. self.add_task(self._list_ports, request)
  114. self.list_requests = []
  115. def _cancel_queue(self, e):
  116. for mapping in self.register_requests:
  117. mapping.d.errback(Exception(e))
  118. self.register_requests = []
  119. # can't run or cancel blocking removes
  120. self.unregister_requests = []
  121. for request in self.list_requests:
  122. request.errback(Exception(e))
  123. self.list_requests = []
  124. def _gen_deferred(self):
  125. return defer.ThreadableDeferred(reactor.callFromThread)
  126. def register_port(self, external_port, internal_port, protocol,
  127. host = None, service_name = None, remote_host=''):
  128. mapping = UPnPPortMapping(external_port, internal_port, protocol,
  129. host, service_name, remote_host)
  130. mapping.d = self._gen_deferred()
  131. self.register_requests.append(mapping)
  132. self.add_task(self._flush_queue)
  133. return mapping.d
  134. def unregister_port(self, external_port, protocol):
  135. self.unregister_requests.append((external_port, protocol))
  136. # unregisters can block, because they occur at shutdown
  137. self._flush_queue()
  138. def _list_ports(self, d):
  139. matches = self.service._list_ports()
  140. d.callback(matches)
  141. def list_ports(self):
  142. d = self._gen_deferred()
  143. self.list_requests.append(d)
  144. self.add_task(self._flush_queue)
  145. return d
  146. class NATBase(object):
  147. def safe_register_port(self, new_mapping):
  148. # check for the host now, while we're in the thread and before
  149. # we need to read it.
  150. new_mapping.populate_host()
  151. nat_logger.info("You asked for: " + str(new_mapping))
  152. new_mapping.original_external_port = new_mapping.external_port
  153. mappings = self._list_ports()
  154. used_ports = []
  155. for mapping in mappings:
  156. # only consider ports which match the same protocol
  157. if mapping.protocol == new_mapping.protocol:
  158. # look for exact matches
  159. if (mapping.host == new_mapping.host and
  160. mapping.internal_port == new_mapping.internal_port):
  161. # the service name could not match, that's ok.
  162. new_mapping.d.callback(mapping.external_port)
  163. nat_logger.info("Already effectively mapped: " + str(mapping))
  164. return
  165. # otherwise, add it to the list of used external ports
  166. used_ports.append(mapping.external_port)
  167. used_ports.sort()
  168. used_ports = SparseSet(used_ports)
  169. all_ports = SparseSet()
  170. all_ports.add(1024, 65535)
  171. free_ports = all_ports - used_ports
  172. new_mapping.external_port = random.choice(free_ports)
  173. nat_logger.info("I'll give you: " + str(new_mapping))
  174. self.register_port(new_mapping)
  175. def register_port(self, port):
  176. pass
  177. def unregister_port(self, external_port, protocol):
  178. pass
  179. def _list_ports(self):
  180. pass
  181. class UPnPPortMapping(object):
  182. def __init__(self, external_port, internal_port, protocol,
  183. host = None, service_name = None, remote_host=''):
  184. self.external_port = int(external_port)
  185. self.internal_port = int(internal_port)
  186. self.protocol = protocol
  187. self.host = host
  188. self.remote_host = ''
  189. self.service_name = service_name
  190. self.d = defer.Deferred()
  191. def populate_host(self):
  192. # throw out '' or None or ints, also look for semi-valid IPs
  193. if not isinstance(self.host, str) or self.host.count('.') < 3:
  194. self.host = get_host_ip()
  195. def __str__(self):
  196. if not self.remote_host:
  197. remote = 'external'
  198. else:
  199. remote = self.remote_host
  200. return "%s %s %s:%d %s:%d" % (self.service_name, self.protocol,
  201. self.remote_host,
  202. self.external_port,
  203. self.host, self.internal_port)
  204. def VerifySOAPResponse(request, response):
  205. if response.code != 200:
  206. raise HTTPError(request.get_full_url(),
  207. response.code, str(response.msg) + " (unexpected SOAP response code)",
  208. response.info(), response)
  209. data = response.read()
  210. bs = BeautifulSupe(data)
  211. # On Matt's Linksys WRT54G rev 4 v.1.0 I saw u: instead of m:
  212. # and ignoring that caused the router to crash
  213. soap_response = bs.scour("m:", "Response")
  214. if not soap_response:
  215. raise HTTPError(request.get_full_url(),
  216. response.code, str(response.msg) +
  217. " (incorrect SOAP response method)",
  218. response.info(), response)
  219. return soap_response[0]
  220. def SOAPResponseToDict(soap_response):
  221. result = {}
  222. for tag in soap_response.child_elements():
  223. value = None
  224. if tag.contents:
  225. value = str(tag.contents[0])
  226. result[tag.name] = value
  227. return result
  228. def SOAPErrorToString(response):
  229. if not isinstance(response, Exception):
  230. data = response.read()
  231. bs = BeautifulSupe(data)
  232. error = bs.first('errorDescription')
  233. if error:
  234. return str(error.contents[0])
  235. return str(response)
  236. _urlopener = None
  237. def urlopen_custom(req, rawserver):
  238. global _urlopener
  239. if not _urlopener:
  240. opener = FancyURLopener()
  241. _urlopener = opener
  242. #remove User-Agent
  243. del _urlopener.addheaders[:]
  244. if not isinstance(req, str):
  245. #for header in r.headers:
  246. # _urlopener.addheaders.append((header, r.headers[header]))
  247. #return _urlopener.open(r.get_full_url(), r.data)
  248. # All this has to be done manually, since httplib and urllib 1 and 2
  249. # add headers to the request that some routers do not accept.
  250. # A minimal, functional request includes the headers:
  251. # Content-Length
  252. # Soapaction
  253. # I have found the following to be specifically disallowed:
  254. # User-agent
  255. # Connection
  256. # Accept-encoding
  257. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  258. (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(req.get_full_url())
  259. if not scheme.startswith("http"):
  260. raise ValueError("UPnP URL scheme is not http: " + req.get_full_url())
  261. if len(path) == 0:
  262. path = '/'
  263. if netloc.count(":") > 0:
  264. host, port = netloc.split(':', 1)
  265. try:
  266. port = int(port)
  267. except:
  268. raise ValueError("UPnP URL port is not int: " + req.get_full_url())
  269. else:
  270. host = netloc
  271. port = 80
  272. header_str = ''
  273. data = ''
  274. method = ''
  275. header_str = " " + path + " HTTP/1.0\r\n"
  276. if req.has_data():
  277. method = 'POST'
  278. header_str = method + header_str
  279. header_str += "Content-Length: " + str(len(req.data)) + "\r\n"
  280. data = req.data + "\r\n"
  281. else:
  282. method = 'GET'
  283. header_str = method + header_str
  284. header_str += "Host: " + host + ":" + str(port) + "\r\n"
  285. for header in req.headers:
  286. header_str += header + ": " + str(req.headers[header]) + "\r\n"
  287. header_str += "\r\n"
  288. data = header_str + data
  289. try:
  290. rawserver.add_pending_connection(host)
  291. s.connect((host, port))
  292. finally:
  293. rawserver.remove_pending_connection(host)
  294. s.send(data)
  295. r = HTTPResponse(s, method=method)
  296. r.begin()
  297. r.recv = r.read
  298. fp = socket._fileobject(r)
  299. resp = addinfourl(fp, r.msg, req.get_full_url())
  300. resp.code = r.status
  301. resp.msg = r.reason
  302. return resp
  303. return _urlopener.open(req)
  304. class ManualUPnP(NATBase, Handler):
  305. upnp_addr = ('239.255.255.250', 1900)
  306. search_string = ('M-SEARCH * HTTP/1.1\r\n' +
  307. 'Host:239.255.255.250:1900\r\n' +
  308. 'ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n' +
  309. 'Man:"ssdp:discover"\r\n' +
  310. 'MX:3\r\n' +
  311. '\r\n')
  312. # if you think for one second that I'm going to implement SOAP in any fashion, you're crazy
  313. get_mapping_template = ('<?xml version="1.0"?>' +
  314. '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"' +
  315. 's:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
  316. '<s:Body>' +
  317. '<u:GetGenericPortMappingEntry xmlns:u=' +
  318. '"urn:schemas-upnp-org:service:WANIPConnection:1">' +
  319. '<NewPortMappingIndex>%d</NewPortMappingIndex>' +
  320. '</u:GetGenericPortMappingEntry>' +
  321. '</s:Body>' +
  322. '</s:Envelope>')
  323. add_mapping_template = ('<?xml version="1.0"?>' +
  324. '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle=' +
  325. '"http://schemas.xmlsoap.org/soap/encoding/">' +
  326. '<s:Body>' +
  327. '<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">' +
  328. '<NewEnabled>1</NewEnabled>' +
  329. '<NewRemoteHost>%s</NewRemoteHost>' +
  330. '<NewLeaseDuration>0</NewLeaseDuration>' +
  331. '<NewInternalPort>%d</NewInternalPort>' +
  332. '<NewExternalPort>%d</NewExternalPort>' +
  333. '<NewProtocol>%s</NewProtocol>' +
  334. '<NewInternalClient>%s</NewInternalClient>' +
  335. '<NewPortMappingDescription>%s</NewPortMappingDescription>' +
  336. '</u:AddPortMapping>' +
  337. '</s:Body>' +
  338. '</s:Envelope>')
  339. delete_mapping_template = ('<?xml version="1.0"?>' +
  340. '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle=' +
  341. '"http://schemas.xmlsoap.org/soap/encoding/">' +
  342. '<s:Body>' +
  343. '<u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">' +
  344. '<NewRemoteHost></NewRemoteHost>' +
  345. '<NewExternalPort>%d</NewExternalPort>' +
  346. '<NewProtocol>%s</NewProtocol>' +
  347. '</u:DeletePortMapping>' +
  348. '</s:Body>' +
  349. '</s:Envelope>')
  350. def _pretify(self, body):
  351. # I actually found a router that needed one tag per line
  352. body = body.replace('><', '>\r\n<')
  353. # don't add newlines in the middle of empty tags (like NewRemoteHost)
  354. body = body.replace('>\r\n</', '></')
  355. body = body.encode('utf-8')
  356. return body
  357. def _build_get_mapping_request(self, pmi):
  358. body = (self.get_mapping_template % (pmi))
  359. body = self._pretify(body)
  360. headers = {'SOAPAction': '"urn:schemas-upnp-org:service:WANIPConnection:1#' +
  361. 'GetGenericPortMappingEntry"'}
  362. return Request(self.controlURL, body, headers)
  363. def _build_add_mapping_request(self, mapping):
  364. body = (self.add_mapping_template % (mapping.remote_host,
  365. mapping.internal_port,
  366. mapping.external_port,
  367. mapping.protocol,
  368. mapping.host,
  369. mapping.service_name))
  370. body = self._pretify(body)
  371. headers = {'SOAPAction': '"urn:schemas-upnp-org:service:WANIPConnection:1#' +
  372. 'AddPortMapping"'}
  373. return Request(self.controlURL, body, headers)
  374. def _build_delete_mapping_request(self, external_port, protocol):
  375. body = (self.delete_mapping_template % (external_port, protocol))
  376. body = self._pretify(body)
  377. headers = {'SOAPAction': '"urn:schemas-upnp-org:service:WANIPConnection:1#' +
  378. 'DeletePortMapping"'}
  379. return Request(self.controlURL, body, headers)
  380. def __init__(self, traverser):
  381. NATBase.__init__(self)
  382. self.controlURL = None
  383. self.transport = None
  384. self.traverser = traverser
  385. self.rawserver = traverser.rawserver
  386. # this service can only be provided if rawserver supports multicast
  387. if not hasattr(self.rawserver, "create_multicastsocket"):
  388. raise AttributeError("RawServer does not support create_multicastsocket!")
  389. reactor.callFromThread(launch_coroutine,
  390. wrap_task(reactor.callLater),
  391. self.begin_discovery)
  392. def begin_discovery(self):
  393. # bind to an available port, and join the multicast group
  394. df = get_deferred_host_ip()
  395. yield df
  396. hostip = df.getResult()
  397. for p in xrange(self.upnp_addr[1], self.upnp_addr[1]+50):
  398. try:
  399. # Original RawServer cannot do this!
  400. s = self.rawserver.create_multicastsocket(p, hostip)
  401. self.transport = s
  402. self.rawserver.start_listening_multicast(s, self)
  403. df = s.listening_port.joinGroup(self.upnp_addr[0],
  404. socket.INADDR_ANY)
  405. yield df
  406. result = df.getResult()
  407. # blargh
  408. if twisted.copyright.version >= '2.4.0':
  409. success = None
  410. # ACKKKK..K. Prevents "Unhandled error in Deferred"
  411. if df._debugInfo is not None:
  412. df._debugInfo.failResult = None
  413. else:
  414. success = 1
  415. if result is success:
  416. break
  417. elif isinstance(result, twisted.python.failure.Failure):
  418. # HACK. If the failure contains a 'No such device' error
  419. # then we abort the discovery because this error denotes
  420. # that the peer is not connected to the network.
  421. if hasattr( result.value, "__getitem__" ) and \
  422. result.value[2] == 19:
  423. yield 0 # abort discovery.
  424. else:
  425. # I suppose keep trying on different ports, but why would
  426. # joinGroup fail?
  427. self.transport = None
  428. x = s.listening_port.stopListening()
  429. if isinstance(x, defer.Deferred):
  430. yield x
  431. x.getResult()
  432. except socket.error, e:
  433. # may look weird, but spin the event loop once on failure
  434. yield defer.succeed(True)
  435. if not self.transport:
  436. # resume init services, because we couldn't bind to a port
  437. self.traverser.resume_init_services()
  438. else:
  439. self.transport.sendto(self.search_string, 0, self.upnp_addr)
  440. self.transport.sendto(self.search_string, 0, self.upnp_addr)
  441. reactor.callLater(6, self._discovery_timedout)
  442. def _discovery_timedout(self):
  443. if self.transport:
  444. nat_logger.warning("Discovery timed out")
  445. self.rawserver.stop_listening_multicast(self.transport)
  446. self.transport = None
  447. # resume init services, because we know we've failed
  448. self.traverser.resume_init_services()
  449. def register_port(self, mapping):
  450. request = self._build_add_mapping_request(mapping)
  451. try:
  452. response = urlopen_custom(request, self.rawserver)
  453. response = VerifySOAPResponse(request, response)
  454. mapping.d.callback(mapping.external_port)
  455. nat_logger.info("registered: " + str(mapping))
  456. except Exception, e: #HTTPError, URLError, BadStatusLine, you name it.
  457. error = SOAPErrorToString(e)
  458. mapping.d.errback(Exception(error))
  459. def unregister_port(self, external_port, protocol):
  460. request = self._build_delete_mapping_request(external_port, protocol)
  461. try:
  462. response = urlopen_custom(request, self.rawserver)
  463. response = VerifySOAPResponse(request, response)
  464. nat_logger.info("unregisterd: %s, %s" % (external_port, protocol))
  465. except Exception, e: #HTTPError, URLError, BadStatusLine, you name it.
  466. error = SOAPErrorToString(e)
  467. nat_logger.error(error)
  468. def data_came_in(self, addr, datagram):
  469. if self.transport is None:
  470. return
  471. try:
  472. statusline, response = datagram.split('\r\n', 1)
  473. except ValueError, e:
  474. nat_logger.error(str_exc(e) + ": " + str(datagram))
  475. # resume init services, because the data is unknown
  476. self.traverser.resume_init_services()
  477. return
  478. httpversion, statuscode, reasonline = statusline.split(None, 2)
  479. if (not httpversion.startswith('HTTP')) or (statuscode != '200'):
  480. return
  481. headers = response.split('\r\n')
  482. location = None
  483. for header in headers:
  484. prefix = 'location:'
  485. if header.lower().startswith(prefix):
  486. location = header[len(prefix):]
  487. location = location.strip()
  488. if location:
  489. self.rawserver.stop_listening_multicast(self.transport)
  490. self.transport = None
  491. self.traverser.add_task(self._got_location, location)
  492. def _got_location(self, location):
  493. if self.controlURL is not None:
  494. return
  495. URLBase = location
  496. for i in xrange(5): # retry
  497. try:
  498. data = urlopen_custom(location, self.rawserver).read()
  499. except IOError:
  500. nat_logger.warning("urlopen_custom timeout")
  501. except:
  502. nat_logger.warning("urlopen_custom error", exc_info=sys.exc_info())
  503. else:
  504. break
  505. else:
  506. nat_logger.warning("urlopen_custom error. giving up.")
  507. return
  508. try:
  509. bs = BeautifulSupe(data)
  510. except: # xml.parsers.expat.ExpatError, maybe others
  511. #open("wtf.xml", 'wb').write(data)
  512. nat_logger.warning("XML parse error", exc_info=sys.exc_info())
  513. return
  514. URLBase_tag = bs.first('URLBase')
  515. if URLBase_tag and URLBase_tag.contents:
  516. URLBase = str(URLBase_tag.contents[0])
  517. wanservices = bs.fetch('service',
  518. dict(serviceType=
  519. 'urn:schemas-upnp-org:service:WANIPConnection:'))
  520. wanservices += bs.fetch('service',
  521. dict(serviceType=
  522. 'urn:schemas-upnp-org:service:WANPPPConnection:'))
  523. for service in wanservices:
  524. controlURL = service.get('controlURL')
  525. if controlURL:
  526. self.controlURL = urlparse.urljoin(URLBase, controlURL)
  527. break
  528. if self.controlURL is None:
  529. # resume init services, because we know we've failed
  530. self.traverser.resume_init_services()
  531. return
  532. # attach service, so the queue gets flushed
  533. self.traverser.attach_service(self)
  534. def _list_ports(self):
  535. mappings = []
  536. _mappings_dict = {}
  537. if self.controlURL is None:
  538. raise UPnPException("ManualUPnP is not prepared")
  539. while True:
  540. request = self._build_get_mapping_request(len(mappings))
  541. try:
  542. response = urlopen_custom(request, self.rawserver)
  543. soap_response = VerifySOAPResponse(request, response)
  544. results = SOAPResponseToDict(soap_response)
  545. mapping = UPnPPortMapping(results['NewExternalPort'], results['NewInternalPort'],
  546. results['NewProtocol'], results['NewInternalClient'],
  547. results['NewPortMappingDescription'])
  548. ports = (results['NewExternalPort'], results['NewInternalPort'])
  549. if ports in _mappings_dict:
  550. # duplicate response, stop searching (because the router is clearly insane)
  551. break
  552. mappings.append(mapping)
  553. _mappings_dict[ports] = 1
  554. except URLError, e:
  555. # SpecifiedArrayIndexInvalid, for example
  556. break
  557. except (HTTPError, BadStatusLine, socket.error):
  558. nat_logger.error("list_ports failed with:", exc_info=sys.exc_info())
  559. break
  560. return mappings
  561. class WindowsUPnPException(UPnPException):
  562. def __init__(self, msg, *args):
  563. msg += " (%s)" % os_version
  564. a = [msg] + list(args)
  565. UPnPException.__init__(self, *a)
  566. class WindowsUPnP(NATBase):
  567. def __init__(self, traverser):
  568. NATBase.__init__(self)
  569. self.upnpnat = None
  570. self.port_collection = None
  571. self.traverser = traverser
  572. win32com.client.pythoncom.CoInitialize()
  573. try:
  574. self.upnpnat = win32com.client.Dispatch("HNetCfg.NATUPnP")
  575. except pywintypes.com_error, e:
  576. if (e[2][5] == -2147221005):
  577. raise WindowsUPnPException("invalid class string")
  578. else:
  579. raise
  580. try:
  581. self.port_collection = self.upnpnat.StaticPortMappingCollection
  582. if self.port_collection is None:
  583. raise WindowsUPnPException("none port_collection")
  584. except pywintypes.com_error, e:
  585. #if e[1].lower() == "exception occurred.":
  586. if (e[2][5] == -2147221164):
  587. # I think this is Class Not Registered.
  588. # Happens on Windows 98 after the XP ICS wizard has been run
  589. raise WindowsUPnPException("exception occurred, class not registered")
  590. else:
  591. raise
  592. # attach service, so the queue gets flushed
  593. self.traverser.attach_service(self)
  594. def register_port(self, mapping):
  595. try:
  596. self.port_collection.Add(mapping.external_port, mapping.protocol,
  597. mapping.internal_port, mapping.host,
  598. True, mapping.service_name)
  599. nat_logger.info("registered: " + str(mapping))
  600. mapping.d.callback(mapping.external_port)
  601. except pywintypes.com_error, e:
  602. # host == 'fake' or address already bound
  603. #if (e[2][5] == -2147024726):
  604. # host == '', or I haven't a clue
  605. #e.args[0] == -2147024894
  606. #mapping.d.errback(e)
  607. # detach self so the queue isn't flushed
  608. self.traverser.detach_service(self)
  609. if hasattr(mapping, 'original_external_port'):
  610. mapping.external_port = mapping.original_external_port
  611. del mapping.original_external_port
  612. # push this mapping back on the queue
  613. self.traverser.register_requests.append(mapping)
  614. # resume init services, because we know we've failed
  615. self.traverser.resume_init_services()
  616. def unregister_port(self, external_port, protocol):
  617. try:
  618. self.port_collection.Remove(external_port, protocol)
  619. nat_logger.info("unregisterd: %s, %s" % (external_port, protocol))
  620. except pywintypes.com_error, e:
  621. if (e[2][5] == -2147352567):
  622. UPNPError("Port %d:%s not bound" % (external_port, protocol))
  623. elif (e[2][5] == -2147221008):
  624. UPNPError("Port %d:%s is bound and is not ours to remove" % (external_port, protocol))
  625. elif (e[2][5] == -2147024894):
  626. UPNPError("Port %d:%s not bound (2)" % (external_port, protocol))
  627. else:
  628. raise
  629. def _list_ports(self):
  630. mappings = []
  631. try:
  632. for mp in self.port_collection:
  633. mapping = UPnPPortMapping(mp.ExternalPort, mp.InternalPort, mp.Protocol,
  634. mp.InternalClient, mp.Description)
  635. mappings.append(mapping)
  636. except pywintypes.com_error, e:
  637. # it's the "for mp in self.port_collection" iter that can throw
  638. # an exception.
  639. # com_error: (-2147220976, 'The owner of the PerUser subscription is
  640. # not logged on to the system specified',
  641. # None, None)
  642. pass
  643. return mappings