diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py index 079ba5bd..c27cc137 100755 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -149,9 +149,6 @@ class Main: # is the application already running? If yes then exit. thisapp = singleton.singleinstance("", daemon) - import upnp - upnp.createPortMapping() - # get curses flag curses = False if '-c' in sys.argv: @@ -210,6 +207,11 @@ class Main: singleListenerThread.setup(selfInitiatedConnections) singleListenerThread.daemon = True # close the main program even if there are threads left singleListenerThread.start() + + if shared.safeConfigGetBoolean('bitmessagesettings','upnp'): + import upnp + upnpThread = upnp.uPnPThread() + upnpThread.start() if daemon == False and shared.safeConfigGetBoolean('bitmessagesettings', 'daemon') == False: if curses == False: diff --git a/src/shared.py b/src/shared.py index 98f46413..e3bfeafd 100644 --- a/src/shared.py +++ b/src/shared.py @@ -384,8 +384,6 @@ def doCleanShutdown(): 'Flushing inventory in memory out to disk. This should normally only take a second...')) flushInventory() - import upnp - upnp.deletePortMapping() # Verify that the objectProcessor has finished exiting. It should have incremented the # shutdown variable from 1 to 2. This must finish before we command the sqlThread to exit. while shutdown == 1: @@ -399,7 +397,12 @@ def doCleanShutdown(): # Wait long enough to guarantee that any running proof of work worker threads will check the # shutdown variable and exit. If the main thread closes before they do then they won't stop. - time.sleep(.25) + time.sleep(.25) + + for thread in threading.enumerate(): + if thread.name == "uPnPThread" or "outgoingSynSender" in thread.name: + logger.debug("Waiting for thread %s", thread.name) + thread.join() if safeConfigGetBoolean('bitmessagesettings','daemon'): logger.info('Clean shutdown complete.') diff --git a/src/upnp.py b/src/upnp.py index e09f6066..ec82caab 100644 --- a/src/upnp.py +++ b/src/upnp.py @@ -1,45 +1,10 @@ # A simple upnp module to forward port for BitMessage # Reference: http://mattscodecave.com/posts/using-python-and-upnp-to-forward-a-port import socket -import httplib -from shared import config - -routers = [] - -def searchRouter(): - SSDP_ADDR = "239.255.255.250" - SSDP_PORT = 1900 - SSDP_MX = 2 - SSDP_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" - - ssdpRequest = "M-SEARCH * HTTP/1.1\r\n" + \ - "HOST: %s:%d\r\n" % (SSDP_ADDR, SSDP_PORT) + \ - "MAN: \"ssdp:discover\"\r\n" + \ - "MX: %d\r\n" % (SSDP_MX, ) + \ - "ST: %s\r\n" % (SSDP_ST, ) + "\r\n" - - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.sendto(ssdpRequest, (SSDP_ADDR, SSDP_PORT)) - routers = [] - sock.settimeout(0.5) - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) - sock.settimeout(2) - logger.debug("Sending UPnP query") - sock.sendto(ssdpRequest, (SSDP_ADDR, SSDP_PORT)) - except: - logger.exception("UPnP sock failed") - try: - while True: - resp,(ip,port) = sock.recvfrom(1000) - if resp is None: - continue - routers.append(Router(resp, ip)) - resp,(ip,port) = sock.recvfrom(1000) - except:pass - - return routers +from struct import unpack, pack +import threading +import time +import shared def createRequestXML(service, action, arguments=[]): from xml.dom.minidom import Document @@ -98,6 +63,8 @@ class Router: path = "" address = None routerPath = None + extPort = None + def __init__(self, ssdpResponse, address): import urllib2 from xml.dom.minidom import parseString @@ -143,6 +110,7 @@ class Router: pass def AddPortMapping(self, externalPort, internalPort, internalClient, protocol, description, leaseDuration = 0, enabled = 1): + from debug import logger resp = self.soapRequest('WANIPConnection:1', 'AddPortMapping', [ ('NewExternalPort', str(externalPort)), ('NewProtocol', protocol), @@ -153,6 +121,7 @@ class Router: ('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): @@ -191,38 +160,90 @@ class Router: raise UPnPError(errinfo[0].childNodes[0].data) return resp -def createPortMappingInternal(router): - from debug import logger +class uPnPThread(threading.Thread): + def __init__ (self): + threading.Thread.__init__(self, name="uPnPThread") + self.localPort = shared.config.getint('bitmessagesettings', 'port') + self.extPort = None + self.routers = [] + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + self.sock.settimeout(10) + self.sendSleep = 60 + + def run(self): + from debug import logger + + logger.debug("Starting UPnP thread") + lastSent = 0 + while shared.shutdown == 0: + if time.time() - lastSent > self.sendSleep and len(self.routers) == 0: + self.sendSearchRouter() + lastSent = time.time() + try: + while shared.shutdown == 0: + resp,(ip,port) = self.sock.recvfrom(1000) + if resp is None: + continue + newRouter = Router(resp, ip) + for router in self.routers: + if router.location == newRouter.location: + break + else: + logger.debug("Found UPnP router at %s", ip) + self.routers.append(newRouter) + self.createPortMapping(newRouter) + break + except socket.timeout as e: + 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) + for router in self.routers: + if router.extPort is not None: + self.deletePortMapping(router) + logger.debug("UPnP thread done") + + def sendSearchRouter(self): + from debug import logger + SSDP_ADDR = "239.255.255.250" + SSDP_PORT = 1900 + SSDP_MX = 2 + SSDP_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + ssdpRequest = "M-SEARCH * HTTP/1.1\r\n" + \ + "HOST: %s:%d\r\n" % (SSDP_ADDR, SSDP_PORT) + \ + "MAN: \"ssdp:discover\"\r\n" + \ + "MX: %d\r\n" % (SSDP_MX, ) + \ + "ST: %s\r\n" % (SSDP_ST, ) + "\r\n" - for i in range(0, 50): try: - routerIP, = unpack('>I', socket.inet_aton(router.address)) - localIP = router.localAddress - localPort = shared.config.getint('bitmessagesettings', 'port') - if i == 0: - extPort = localPort # try same port first - else: - extPort = randint(32767, 65535) - logger.debug("Requesting UPnP mapping for %s:%i on external port %i", localIP, localPort, extPort) - router.AddPortMapping(extPort, localPort, localIP, 'TCP', 'BitMessage') - logger.info("Successfully established UPnP mapping for %s:%i on external port %i", localIP, localPort, extPort) - shared.extPort = extPort - break - except UPnPError: - logger.debug("UPnP error: ", exc_info=True) + logger.debug("Sending UPnP query") + self.sock.sendto(ssdpRequest, (SSDP_ADDR, SSDP_PORT)) + except: + logger.exception("UPnP send query failed") -def createPortMapping(): - from debug import logger - global routers + def createPortMapping(self, router): + from debug import logger + + for i in range(50): + try: + routerIP, = unpack('>I', socket.inet_aton(router.address)) + localIP = router.localAddress + if i == 0: + extPort = self.localPort # try same port first + else: + extPort = randint(32767, 65535) + logger.debug("Requesting UPnP mapping for %s:%i on external port %i", localIP, self.localPort, extPort) + router.AddPortMapping(extPort, self.localPort, localIP, 'TCP', 'BitMessage') + shared.extPort = extPort + break + except UPnPError: + logger.debug("UPnP error: ", exc_info=True) + + def deletePortMapping(self, router): + router.DeletePortMapping(router.extPort, 'TCP') - routers = searchRouter() - logger.debug("Found %i UPnP routers", len(routers)) - for router in routers: - createPortMappingInternal(router) -def deletePortMapping(): - localPort = config.getint('bitmessagesettings', 'port') - for router in routers: - if hasattr(router, "extPort"): - router.DeletePortMapping(router.extPort, 'TCP')