You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
352 lines
13 KiB
352 lines
13 KiB
# 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 knownnodes |
|
import queues |
|
import state |
|
import tr |
|
from bmconfigparser import BMConfigParser |
|
from debug import logger |
|
from network import BMConnectionPool, 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: |
|
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") |
|
try: |
|
self.extPort = BMConfigParser().getint('bitmessagesettings', 'extport') |
|
except: |
|
self.extPort = 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 = BMConfigParser().getint('bitmessagesettings', 'port') |
|
|
|
while state.shutdown == 0 and BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): |
|
if time.time() - lastSent > self.sendSleep and not self.routers: |
|
try: |
|
self.sendSearchRouter() |
|
except: |
|
pass |
|
lastSent = time.time() |
|
try: |
|
while state.shutdown == 0 and BMConfigParser().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: |
|
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: |
|
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: |
|
pass |
|
try: |
|
self.sock.close() |
|
except: |
|
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: |
|
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) |
|
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 |
|
BMConfigParser().set('bitmessagesettings', 'extport', str(extPort)) |
|
BMConfigParser().save() |
|
break |
|
except UPnPError: |
|
logger.debug("UPnP error: ", exc_info=True) |
|
|
|
def deletePortMapping(self, router): |
|
"""Delete a port mapping""" |
|
router.DeletePortMapping(router.extPort, 'TCP')
|
|
|