2018-07-27 01:49:33 +02:00
|
|
|
import logging
|
2018-07-30 23:48:20 +02:00
|
|
|
import json
|
2018-07-27 01:49:33 +02:00
|
|
|
from twisted.internet import defer
|
|
|
|
from txupnp.fault import UPnPError
|
2018-07-29 04:08:24 +02:00
|
|
|
from txupnp.soap import SOAPServiceManager
|
2018-07-31 17:23:21 +02:00
|
|
|
from txupnp.scpd import UPnPFallback
|
2018-07-27 01:49:33 +02:00
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2018-07-29 04:08:24 +02:00
|
|
|
class UPnP(object):
|
2018-09-25 20:52:29 +02:00
|
|
|
def __init__(self, reactor, try_miniupnpc_fallback=True, treq_get=None):
|
2018-07-29 04:08:24 +02:00
|
|
|
self._reactor = reactor
|
2018-08-02 16:02:52 +02:00
|
|
|
self.try_miniupnpc_fallback = try_miniupnpc_fallback
|
2018-09-25 20:52:29 +02:00
|
|
|
self.soap_manager = SOAPServiceManager(reactor, treq_get=treq_get)
|
2018-07-31 17:23:21 +02:00
|
|
|
self.miniupnpc_runner = None
|
2018-08-02 16:02:52 +02:00
|
|
|
self.miniupnpc_igd_url = None
|
2018-07-27 01:49:33 +02:00
|
|
|
|
|
|
|
@property
|
2018-07-29 04:08:24 +02:00
|
|
|
def lan_address(self):
|
|
|
|
return self.soap_manager.lan_address
|
2018-07-27 01:49:33 +02:00
|
|
|
|
|
|
|
@property
|
2018-07-29 04:08:24 +02:00
|
|
|
def commands(self):
|
2018-07-30 23:48:20 +02:00
|
|
|
try:
|
2018-08-08 19:48:24 +02:00
|
|
|
runner = self.soap_manager.get_runner()
|
|
|
|
required_commands = [
|
|
|
|
"GetExternalIPAddress",
|
|
|
|
"AddPortMapping",
|
|
|
|
"GetSpecificPortMappingEntry",
|
|
|
|
"GetGenericPortMappingEntry",
|
|
|
|
"DeletePortMapping"
|
|
|
|
]
|
|
|
|
if all((command in runner._registered_commands for command in required_commands)):
|
|
|
|
return runner
|
|
|
|
raise UPnPError("required commands not found")
|
2018-07-30 23:48:20 +02:00
|
|
|
except UPnPError as err:
|
2018-08-02 16:02:52 +02:00
|
|
|
if self.try_miniupnpc_fallback and self.miniupnpc_runner:
|
2018-07-31 17:23:21 +02:00
|
|
|
return self.miniupnpc_runner
|
2018-07-30 23:48:20 +02:00
|
|
|
log.warning("upnp is not available: %s", err)
|
2018-07-27 01:49:33 +02:00
|
|
|
|
2018-07-29 23:32:14 +02:00
|
|
|
def m_search(self, address, timeout=30, max_devices=2):
|
2018-07-27 01:49:33 +02:00
|
|
|
"""
|
|
|
|
Perform a HTTP over UDP M-SEARCH query
|
|
|
|
|
2018-07-29 04:08:24 +02:00
|
|
|
returns (list) [{
|
|
|
|
'server: <gateway os and version string>
|
2018-07-27 01:49:33 +02:00
|
|
|
'location': <upnp gateway url>,
|
|
|
|
'cache-control': <max age>,
|
|
|
|
'date': <server time>,
|
|
|
|
'usn': <usn>
|
2018-07-29 04:08:24 +02:00
|
|
|
}, ...]
|
2018-07-27 01:49:33 +02:00
|
|
|
"""
|
2018-07-29 23:32:14 +02:00
|
|
|
return self.soap_manager.sspd_factory.m_search(address, timeout=timeout, max_devices=max_devices)
|
2018-07-27 01:49:33 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
2018-08-08 00:39:16 +02:00
|
|
|
def discover(self, timeout=1, max_devices=1, keep_listening=False, try_txupnp=True):
|
|
|
|
found = False
|
|
|
|
if not try_txupnp and not self.try_miniupnpc_fallback:
|
|
|
|
log.warning("nothing left to try")
|
|
|
|
if try_txupnp:
|
|
|
|
try:
|
2018-08-08 01:02:56 +02:00
|
|
|
found = yield self.soap_manager.discover_services(timeout=timeout, max_devices=max_devices)
|
2018-08-08 00:39:16 +02:00
|
|
|
except defer.TimeoutError:
|
|
|
|
found = False
|
|
|
|
finally:
|
|
|
|
if not keep_listening:
|
|
|
|
self.soap_manager.sspd_factory.disconnect()
|
2018-08-08 19:48:24 +02:00
|
|
|
if found:
|
|
|
|
try:
|
|
|
|
runner = self.soap_manager.get_runner()
|
|
|
|
required_commands = [
|
|
|
|
"GetExternalIPAddress",
|
|
|
|
"AddPortMapping",
|
|
|
|
"GetSpecificPortMappingEntry",
|
|
|
|
"GetGenericPortMappingEntry",
|
|
|
|
"DeletePortMapping"
|
|
|
|
]
|
|
|
|
found = all((command in runner._registered_commands for command in required_commands))
|
|
|
|
except UPnPError:
|
|
|
|
found = False
|
2018-08-02 16:02:52 +02:00
|
|
|
if not found and self.try_miniupnpc_fallback:
|
|
|
|
found = yield self.start_miniupnpc_fallback()
|
|
|
|
defer.returnValue(found)
|
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def start_miniupnpc_fallback(self):
|
|
|
|
found = False
|
2018-08-07 19:53:35 +02:00
|
|
|
if not self.miniupnpc_runner:
|
2018-07-31 17:23:21 +02:00
|
|
|
log.debug("trying miniupnpc fallback")
|
|
|
|
fallback = UPnPFallback()
|
|
|
|
success = yield fallback.discover()
|
2018-08-02 16:02:52 +02:00
|
|
|
self.miniupnpc_igd_url = fallback.device_url
|
2018-07-31 17:23:21 +02:00
|
|
|
if success:
|
|
|
|
log.info("successfully started miniupnpc fallback")
|
|
|
|
self.miniupnpc_runner = fallback
|
|
|
|
found = True
|
|
|
|
if not found:
|
2018-08-02 16:02:52 +02:00
|
|
|
log.warning("failed to find upnp gateway using miniupnpc fallback")
|
2018-07-30 23:48:20 +02:00
|
|
|
defer.returnValue(found)
|
2018-07-27 01:49:33 +02:00
|
|
|
|
|
|
|
def get_external_ip(self):
|
|
|
|
return self.commands.GetExternalIPAddress()
|
|
|
|
|
2018-08-07 19:53:35 +02:00
|
|
|
def add_port_mapping(self, external_port, protocol, internal_port, lan_address, description):
|
2018-07-27 01:49:33 +02:00
|
|
|
return self.commands.AddPortMapping(
|
2018-08-01 23:57:27 +02:00
|
|
|
NewRemoteHost="", NewExternalPort=external_port, NewProtocol=protocol,
|
2018-07-27 01:49:33 +02:00
|
|
|
NewInternalPort=internal_port, NewInternalClient=lan_address,
|
2018-08-07 19:53:35 +02:00
|
|
|
NewEnabled=1, NewPortMappingDescription=description, NewLeaseDuration=""
|
2018-07-27 01:49:33 +02:00
|
|
|
)
|
|
|
|
|
2018-08-21 18:10:11 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-07-27 01:49:33 +02:00
|
|
|
def get_port_mapping_by_index(self, index):
|
2018-08-21 18:10:11 +02:00
|
|
|
try:
|
|
|
|
redirect = yield self.commands.GetGenericPortMappingEntry(NewPortMappingIndex=index)
|
|
|
|
defer.returnValue(redirect)
|
|
|
|
except UPnPError:
|
|
|
|
defer.returnValue(None)
|
2018-07-27 01:49:33 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def get_redirects(self):
|
|
|
|
redirects = []
|
|
|
|
cnt = 0
|
2018-08-21 18:10:11 +02:00
|
|
|
redirect = yield self.get_port_mapping_by_index(cnt)
|
|
|
|
while redirect:
|
|
|
|
redirects.append(redirect)
|
|
|
|
cnt += 1
|
|
|
|
redirect = yield self.get_port_mapping_by_index(cnt)
|
2018-07-27 01:49:33 +02:00
|
|
|
defer.returnValue(redirects)
|
|
|
|
|
2018-07-30 23:48:20 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-07-27 01:49:33 +02:00
|
|
|
def get_specific_port_mapping(self, external_port, protocol):
|
2018-07-29 04:08:24 +02:00
|
|
|
"""
|
|
|
|
:param external_port: (int) external port to listen on
|
|
|
|
:param protocol: (str) 'UDP' | 'TCP'
|
|
|
|
:return: (int) <internal port>, (str) <lan ip>, (bool) <enabled>, (str) <description>, (int) <lease time>
|
|
|
|
"""
|
2018-07-30 23:48:20 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
result = yield self.commands.GetSpecificPortMappingEntry(
|
|
|
|
NewRemoteHost=None, NewExternalPort=external_port, NewProtocol=protocol
|
|
|
|
)
|
|
|
|
defer.returnValue(result)
|
2018-08-21 18:10:11 +02:00
|
|
|
except UPnPError:
|
|
|
|
defer.returnValue(None)
|
2018-07-27 01:49:33 +02:00
|
|
|
|
2018-08-02 15:38:00 +02:00
|
|
|
def delete_port_mapping(self, external_port, protocol, new_remote_host=""):
|
2018-07-29 04:08:24 +02:00
|
|
|
"""
|
|
|
|
:param external_port: (int) external port to listen on
|
|
|
|
:param protocol: (str) 'UDP' | 'TCP'
|
|
|
|
:return: None
|
|
|
|
"""
|
2018-07-27 01:49:33 +02:00
|
|
|
return self.commands.DeletePortMapping(
|
2018-08-02 15:38:00 +02:00
|
|
|
NewRemoteHost=new_remote_host, NewExternalPort=external_port, NewProtocol=protocol
|
2018-07-27 01:49:33 +02:00
|
|
|
)
|
2018-07-29 04:08:24 +02:00
|
|
|
|
|
|
|
def get_rsip_nat_status(self):
|
|
|
|
"""
|
|
|
|
:return: (bool) NewRSIPAvailable, (bool) NewNATEnabled
|
|
|
|
"""
|
|
|
|
return self.commands.GetNATRSIPStatus()
|
|
|
|
|
|
|
|
def get_status_info(self):
|
|
|
|
"""
|
|
|
|
:return: (str) NewConnectionStatus, (str) NewLastConnectionError, (int) NewUptime
|
|
|
|
"""
|
|
|
|
return self.commands.GetStatusInfo()
|
|
|
|
|
|
|
|
def get_connection_type_info(self):
|
|
|
|
"""
|
|
|
|
:return: (str) NewConnectionType (str), NewPossibleConnectionTypes (str)
|
|
|
|
"""
|
|
|
|
return self.commands.GetConnectionTypeInfo()
|
2018-07-30 23:48:20 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
2018-08-07 19:53:35 +02:00
|
|
|
def get_next_mapping(self, port, protocol, description, internal_port=None):
|
2018-07-30 23:48:20 +02:00
|
|
|
if protocol not in ["UDP", "TCP"]:
|
|
|
|
raise UPnPError("unsupported protocol: {}".format(protocol))
|
2018-08-07 19:53:35 +02:00
|
|
|
internal_port = internal_port or port
|
|
|
|
redirect_tups = yield self.get_redirects()
|
|
|
|
redirects = {
|
|
|
|
"%i:%s" % (ext_port, proto): (int_host, int_port, desc)
|
|
|
|
for (ext_host, ext_port, proto, int_port, int_host, enabled, desc, lease) in redirect_tups
|
|
|
|
}
|
|
|
|
while ("%i:%s" % (port, protocol)) in redirects:
|
|
|
|
int_host, int_port, _ = redirects["%i:%s" % (port, protocol)]
|
|
|
|
if int_host == self.lan_address and int_port == internal_port:
|
|
|
|
break
|
|
|
|
port += 1
|
|
|
|
|
|
|
|
yield self.add_port_mapping( # set one up
|
|
|
|
port, protocol, internal_port, self.lan_address, description
|
2018-07-30 23:48:20 +02:00
|
|
|
)
|
|
|
|
defer.returnValue(port)
|
|
|
|
|
2018-08-01 22:29:00 +02:00
|
|
|
def get_debug_info(self, include_gateway_xml=False):
|
2018-07-30 23:48:20 +02:00
|
|
|
def default_byte(x):
|
|
|
|
if isinstance(x, bytes):
|
|
|
|
return x.decode()
|
|
|
|
return x
|
2018-07-31 22:53:08 +02:00
|
|
|
return json.dumps({
|
2018-08-01 22:29:00 +02:00
|
|
|
'txupnp': self.soap_manager.debug(include_gateway_xml=include_gateway_xml),
|
2018-08-02 16:02:52 +02:00
|
|
|
'miniupnpc_igd_url': self.miniupnpc_igd_url
|
2018-07-31 22:53:08 +02:00
|
|
|
},
|
|
|
|
indent=2, default=default_byte
|
|
|
|
)
|