From f9c6ce9bd6bfc210a278158806fc0ed8db1ddeed Mon Sep 17 00:00:00 2001 From: Jack Robison Date: Tue, 31 Jul 2018 11:23:21 -0400 Subject: [PATCH] miniupnpc fallback --- txupnp/scpd.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++- txupnp/upnp.py | 18 ++++++- 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/txupnp/scpd.py b/txupnp/scpd.py index d50fc21..4f80c05 100644 --- a/txupnp/scpd.py +++ b/txupnp/scpd.py @@ -1,6 +1,6 @@ import logging from collections import OrderedDict -from twisted.internet import defer +from twisted.internet import defer, threads from twisted.web.client import Agent, HTTPConnectionPool import treq from treq.client import HTTPClient @@ -318,3 +318,125 @@ class SCPDCommandRunner(object): def SetDefaultConnectionService(NewDefaultConnectionService): """Returns (None)""" raise NotImplementedError() + + +class UPnPFallback(object): + def __init__(self): + try: + import miniupnpc + self._upnp = miniupnpc.UPnP() + self.available = True + except ImportError: + self._upnp = None + self.available = False + + @defer.inlineCallbacks + def discover(self): + if not self.available: + raise NotImplementedError() + devices = yield threads.deferToThread(self._upnp.discover) + if devices: + device_url = yield threads.deferToThread(self._upnp.selectigd) + else: + device_url = None + + defer.returnValue(devices > 0) + + @return_types(none) + def AddPortMapping(self, NewRemoteHost, NewExternalPort, NewProtocol, NewInternalPort, NewInternalClient, + NewEnabled, NewPortMappingDescription, NewLeaseDuration): + """Returns None""" + if not self.available: + raise NotImplementedError() + return threads.deferToThread(self._upnp.addportmapping, NewExternalPort, NewProtocol, NewInternalPort, + NewInternalClient, NewPortMappingDescription, NewLeaseDuration) + + def GetNATRSIPStatus(self): + """Returns (NewRSIPAvailable, NewNATEnabled)""" + raise NotImplementedError() + + @return_types(none_or_str, int, str, int, str, bool, str, int) + def GetGenericPortMappingEntry(self, NewPortMappingIndex): + """ + Returns (NewRemoteHost, NewExternalPort, NewProtocol, NewInternalPort, NewInternalClient, NewEnabled, + NewPortMappingDescription, NewLeaseDuration) + """ + if not self.available: + raise NotImplementedError() + return threads.deferToThread(self._upnp.getgenericportmapping, NewPortMappingIndex) + + @return_types(int, str, bool, str, int) + def GetSpecificPortMappingEntry(self, NewRemoteHost, NewExternalPort, NewProtocol): + """Returns (NewInternalPort, NewInternalClient, NewEnabled, NewPortMappingDescription, NewLeaseDuration)""" + if not self.available: + raise NotImplementedError() + return threads.deferToThread(self._upnp.getspecificportmapping, NewExternalPort, NewProtocol) + + def SetConnectionType(self, NewConnectionType): + """Returns None""" + raise NotImplementedError() + + @return_types(str) + def GetExternalIPAddress(self): + """Returns (NewExternalIPAddress)""" + if not self.available: + raise NotImplementedError() + return threads.deferToThread(self._upnp.externalipaddress) + + def GetConnectionTypeInfo(self): + """Returns (NewConnectionType, NewPossibleConnectionTypes)""" + raise NotImplementedError() + + @return_types(str, str, int) + def GetStatusInfo(self): + """Returns (NewConnectionStatus, NewLastConnectionError, NewUptime)""" + if not self.available: + raise NotImplementedError() + return threads.deferToThread(self._upnp.statusinfo) + + def ForceTermination(self): + """Returns None""" + raise NotImplementedError() + + @return_types(none) + def DeletePortMapping(self, NewRemoteHost, NewExternalPort, NewProtocol): + """Returns None""" + if not self.available: + raise NotImplementedError() + return threads.deferToThread(self._upnp.deleteportmapping, NewExternalPort, NewProtocol) + + def RequestConnection(self): + """Returns None""" + raise NotImplementedError() + + def GetCommonLinkProperties(self): + """Returns (NewWANAccessType, NewLayer1UpstreamMaxBitRate, NewLayer1DownstreamMaxBitRate, NewPhysicalLinkStatus)""" + raise NotImplementedError() + + def GetTotalBytesSent(self): + """Returns (NewTotalBytesSent)""" + raise NotImplementedError() + + def GetTotalBytesReceived(self): + """Returns (NewTotalBytesReceived)""" + raise NotImplementedError() + + def GetTotalPacketsSent(self): + """Returns (NewTotalPacketsSent)""" + raise NotImplementedError() + + def GetTotalPacketsReceived(self): + """Returns (NewTotalPacketsReceived)""" + raise NotImplementedError() + + def X_GetICSStatistics(self): + """Returns (TotalBytesSent, TotalBytesReceived, TotalPacketsSent, TotalPacketsReceived, Layer1DownstreamMaxBitRate, Uptime)""" + raise NotImplementedError() + + def GetDefaultConnectionService(self): + """Returns (NewDefaultConnectionService)""" + raise NotImplementedError() + + def SetDefaultConnectionService(self, NewDefaultConnectionService): + """Returns (None)""" + raise NotImplementedError() diff --git a/txupnp/upnp.py b/txupnp/upnp.py index e101b1e..6bff78c 100644 --- a/txupnp/upnp.py +++ b/txupnp/upnp.py @@ -3,15 +3,18 @@ import json from twisted.internet import defer from txupnp.fault import UPnPError from txupnp.soap import SOAPServiceManager +from txupnp.scpd import UPnPFallback from txupnp.util import DeferredDict log = logging.getLogger(__name__) class UPnP(object): - def __init__(self, reactor): + def __init__(self, reactor, miniupnpc_fallback=True): self._reactor = reactor + self._miniupnpc_fallback = miniupnpc_fallback self.soap_manager = SOAPServiceManager(reactor) + self.miniupnpc_runner = None @property def lan_address(self): @@ -22,6 +25,8 @@ class UPnP(object): try: return self.soap_manager.get_runner() except UPnPError as err: + if self._miniupnpc_fallback and self.miniupnpc_runner: + return self.miniupnpc_runner log.warning("upnp is not available: %s", err) def m_search(self, address, timeout=30, max_devices=2): @@ -44,11 +49,20 @@ class UPnP(object): yield self.soap_manager.discover_services(timeout=timeout, max_devices=max_devices) found = True except defer.TimeoutError: - log.warning("failed to find upnp gateway") found = False finally: if not keep_listening: self.soap_manager.sspd_factory.disconnect() + if not self.commands: + log.debug("trying miniupnpc fallback") + fallback = UPnPFallback() + success = yield fallback.discover() + if success: + log.info("successfully started miniupnpc fallback") + self.miniupnpc_runner = fallback + found = True + if not found: + log.warning("failed to find upnp gateway") defer.returnValue(found) def get_external_ip(self):