diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3070fb..f7aa4bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/) with
regard to the json-rpc api. As we're currently pre-1.0 release, we
-can and probably will change functionality and break backwards compatability
+can and probably will change functionality and break backwards compatibility
at anytime.
## [Unreleased]
diff --git a/README.md b/README.md
index e5b1432..c1f4858 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@
Cisco CGA4131COM
Linksys WRT1200AC
Netgear Nighthawk X4 AC2350
+ ASUS RT-N66U
## Installation
diff --git a/aioupnp/commands.py b/aioupnp/commands.py
index 5e49630..d202d22 100644
--- a/aioupnp/commands.py
+++ b/aioupnp/commands.py
@@ -1,9 +1,135 @@
-from typing import Tuple, Union
+import logging
+import time
+import typing
+from typing import Tuple, Union, List
+from aioupnp.protocols.scpd import scpd_post
+log = logging.getLogger(__name__)
none_or_str = Union[None, str]
+return_type_lambas = {
+ Union[None, str]: lambda x: x if x is not None and str(x).lower() not in ['none', 'nil'] else None
+}
+
+
+def safe_type(t):
+ if t is typing.Tuple:
+ return tuple
+ if t is typing.List:
+ return list
+ if t is typing.Dict:
+ return dict
+ if t is typing.Set:
+ return set
+ return t
+
+
+class SOAPCommand:
+ def __init__(self, gateway_address: str, service_port: int, control_url: str, service_id: bytes, method: str,
+ param_types: dict, return_types: dict, param_order: list, return_order: list, loop=None) -> None:
+ self.gateway_address = gateway_address
+ self.service_port = service_port
+ self.control_url = control_url
+ self.service_id = service_id
+ self.method = method
+ self.param_types = param_types
+ self.param_order = param_order
+ self.return_types = return_types
+ self.return_order = return_order
+ self.loop = loop
+ self._requests: typing.List = []
+
+ async def __call__(self, **kwargs) -> typing.Union[None, typing.Dict, typing.List, typing.Tuple]:
+ if set(kwargs.keys()) != set(self.param_types.keys()):
+ raise Exception("argument mismatch: %s vs %s" % (kwargs.keys(), self.param_types.keys()))
+ soap_kwargs = {n: safe_type(self.param_types[n])(kwargs[n]) for n in self.param_types.keys()}
+ response, xml_bytes, err = await scpd_post(
+ self.control_url, self.gateway_address, self.service_port, self.method, self.param_order,
+ self.service_id, self.loop, **soap_kwargs
+ )
+ if err is not None:
+ self._requests.append((soap_kwargs, xml_bytes, None, err, time.time()))
+ raise err
+ if not response:
+ result = None
+ else:
+ recast_result = tuple([safe_type(self.return_types[n])(response.get(n)) for n in self.return_order])
+ if len(recast_result) == 1:
+ result = recast_result[0]
+ else:
+ result = recast_result
+ self._requests.append((soap_kwargs, xml_bytes, result, None, time.time()))
+ return result
class SOAPCommands:
+ """
+ Type annotated wrappers for common UPnP SOAP functions
+
+ A SOAPCommands object has its command attributes overridden during device discovery with SOAPCommand objects
+ for the commands implemented by the gateway.
+
+ SOAPCommand will use the typing annotations provided here to properly cast the types of arguments and results
+ to their expected types.
+ """
+
+ SOAP_COMMANDS = [
+ 'AddPortMapping',
+ 'GetNATRSIPStatus',
+ 'GetGenericPortMappingEntry',
+ 'GetSpecificPortMappingEntry',
+ 'SetConnectionType',
+ 'GetExternalIPAddress',
+ 'GetConnectionTypeInfo',
+ 'GetStatusInfo',
+ 'ForceTermination',
+ 'DeletePortMapping',
+ 'RequestConnection',
+ 'GetCommonLinkProperties',
+ 'GetTotalBytesSent',
+ 'GetTotalBytesReceived',
+ 'GetTotalPacketsSent',
+ 'GetTotalPacketsReceived',
+ 'X_GetICSStatistics',
+ 'GetDefaultConnectionService',
+ 'NewDefaultConnectionService',
+ 'NewEnabledForInternet',
+ 'SetDefaultConnectionService',
+ 'SetEnabledForInternet',
+ 'GetEnabledForInternet',
+ 'NewActiveConnectionIndex',
+ 'GetMaximumActiveConnections',
+ 'GetActiveConnections'
+ ]
+
+ def __init__(self):
+ self._registered = set()
+
+ def register(self, base_ip: bytes, port: int, name: str, control_url: str,
+ service_type: bytes, inputs: List, outputs: List, loop=None) -> None:
+ if name not in self.SOAP_COMMANDS or name in self._registered:
+ raise AttributeError(name)
+ current = getattr(self, name)
+ annotations = current.__annotations__
+ return_types = annotations.get('return', None)
+ if return_types:
+ if hasattr(return_types, '__args__'):
+ return_types = tuple([return_type_lambas.get(a, a) for a in return_types.__args__])
+ elif isinstance(return_types, type):
+ return_types = (return_types,)
+ return_types = {r: t for r, t in zip(outputs, return_types)}
+ param_types = {}
+ for param_name, param_type in annotations.items():
+ if param_name == "return":
+ continue
+ param_types[param_name] = param_type
+ command = SOAPCommand(
+ base_ip.decode(), port, control_url, service_type,
+ name, param_types, return_types, inputs, outputs, loop=loop
+ )
+ setattr(command, "__doc__", current.__doc__)
+ setattr(self, command.method, command)
+ self._registered.add(command.method)
+
@staticmethod
async def AddPortMapping(NewRemoteHost: str, NewExternalPort: int, NewProtocol: str, NewInternalPort: int,
NewInternalClient: str, NewEnabled: int, NewPortMappingDescription: str,
diff --git a/aioupnp/device.py b/aioupnp/device.py
index 7d385c0..fe01bbd 100644
--- a/aioupnp/device.py
+++ b/aioupnp/device.py
@@ -6,45 +6,23 @@ log = logging.getLogger(__name__)
class CaseInsensitive:
def __init__(self, **kwargs) -> None:
- not_evaluated = {}
for k, v in kwargs.items():
- if k.startswith("_"):
- not_evaluated[k] = v
- continue
- try:
+ if not k.startswith("_"):
getattr(self, k)
setattr(self, k, v)
- except AttributeError as err:
- not_evaluated[k] = v
- if not_evaluated:
- log.debug("%s did not apply kwargs: %s", self.__class__.__name__, not_evaluated)
-
- def _get_attr_name(self, case_insensitive: str) -> str:
- for k, v in self.__dict__.items():
- if k.lower() == case_insensitive.lower():
- return k
- raise AttributeError(case_insensitive)
def __getattr__(self, item):
- if item in self.__dict__:
- return self.__dict__[item]
- for k, v in self.__class__.__dict__.items():
+ for k in self.__class__.__dict__.keys():
if k.lower() == item.lower():
- if k not in self.__dict__:
- self.__dict__[k] = v
- return v
+ return self.__dict__.get(k)
raise AttributeError(item)
def __setattr__(self, item, value):
- if item in self.__dict__:
- self.__dict__[item] = value
- return
- to_update = None
- for k, v in self.__dict__.items():
+ for k, v in self.__class__.__dict__.items():
if k.lower() == item.lower():
- to_update = k
- break
- self.__dict__[to_update or item] = value
+ self.__dict__[k] = value
+ return
+ raise AttributeError(item)
def as_dict(self) -> dict:
return {
diff --git a/aioupnp/gateway.py b/aioupnp/gateway.py
index 02242c8..4310123 100644
--- a/aioupnp/gateway.py
+++ b/aioupnp/gateway.py
@@ -9,7 +9,6 @@ from aioupnp.commands import SOAPCommands
from aioupnp.device import Device, Service
from aioupnp.protocols.ssdp import fuzzy_m_search, m_search
from aioupnp.protocols.scpd import scpd_get
-from aioupnp.protocols.soap import SOAPCommand
from aioupnp.serialization.ssdp import SSDPDatagram
from aioupnp.util import flatten_keys
from aioupnp.fault import UPnPError
@@ -156,9 +155,8 @@ class Gateway:
}
@classmethod
- async def _discover_gateway(cls, lan_address: str, gateway_address: str, timeout: int = 30,
- igd_args: OrderedDict = None, ssdp_socket: socket.socket = None,
- soap_socket: socket.socket = None, unicast: bool = False):
+ async def _discover_gateway(cls, lan_address: str, gateway_address: str, timeout: int=30,
+ igd_args: OrderedDict=None, loop=None, unicast: bool=False):
ignored: set = set()
required_commands = [
'AddPortMapping',
@@ -167,16 +165,17 @@ class Gateway:
]
while True:
if not igd_args:
- m_search_args, datagram = await asyncio.wait_for(fuzzy_m_search(lan_address, gateway_address, timeout, ssdp_socket,
- ignored, unicast), timeout)
+ m_search_args, datagram = await asyncio.wait_for(
+ fuzzy_m_search(lan_address, gateway_address, timeout, loop, ignored, unicast),
+ timeout
+ )
else:
m_search_args = OrderedDict(igd_args)
- datagram = await m_search(lan_address, gateway_address, igd_args, timeout, ssdp_socket, ignored,
- unicast)
+ datagram = await m_search(lan_address, gateway_address, igd_args, timeout, loop, ignored, unicast)
try:
gateway = cls(datagram, m_search_args, lan_address, gateway_address)
log.debug('get gateway descriptor %s', datagram.location)
- await gateway.discover_commands(soap_socket)
+ await gateway.discover_commands(loop)
requirements_met = all([required in gateway._registered_commands for required in required_commands])
if not requirements_met:
not_met = [
@@ -196,17 +195,15 @@ class Gateway:
@classmethod
async def discover_gateway(cls, lan_address: str, gateway_address: str, timeout: int = 30,
- igd_args: OrderedDict = None, ssdp_socket: socket.socket = None,
- soap_socket: socket.socket = None, unicast: bool = None):
+ igd_args: OrderedDict = None, loop=None, unicast: bool = None):
if unicast is not None:
- return await cls._discover_gateway(lan_address, gateway_address, timeout, igd_args, ssdp_socket,
- soap_socket, unicast=unicast)
+ return await cls._discover_gateway(lan_address, gateway_address, timeout, igd_args, loop)
done, pending = await asyncio.wait([
cls._discover_gateway(
- lan_address, gateway_address, timeout, igd_args, ssdp_socket, soap_socket, unicast=True
+ lan_address, gateway_address, timeout, igd_args, loop, unicast=True
),
cls._discover_gateway(
- lan_address, gateway_address, timeout, igd_args, ssdp_socket, soap_socket, unicast=False
+ lan_address, gateway_address, timeout, igd_args, loop, unicast=False
)], return_when=asyncio.tasks.FIRST_COMPLETED
)
for task in list(pending):
@@ -214,8 +211,8 @@ class Gateway:
result = list(done)[0].result()
return result
- async def discover_commands(self, soap_socket: socket.socket = None):
- response, xml_bytes, get_err = await scpd_get(self.path.decode(), self.base_ip.decode(), self.port)
+ async def discover_commands(self, loop=None):
+ response, xml_bytes, get_err = await scpd_get(self.path.decode(), self.base_ip.decode(), self.port, loop=loop)
self._xml_response = xml_bytes
if get_err is not None:
raise get_err
@@ -230,9 +227,9 @@ class Gateway:
else:
self._device = Device(self._devices, self._services)
for service_type in self.services.keys():
- await self.register_commands(self.services[service_type], soap_socket)
+ await self.register_commands(self.services[service_type], loop)
- async def register_commands(self, service: Service, soap_socket: socket.socket = None):
+ async def register_commands(self, service: Service, loop=None):
if not service.SCPDURL:
raise UPnPError("no scpd url")
@@ -252,27 +249,10 @@ class Gateway:
for name, inputs, outputs in action_list:
try:
- current = getattr(self.commands, name)
- annotations = current.__annotations__
- return_types = annotations.get('return', None)
- if return_types:
- if hasattr(return_types, '__args__'):
- return_types = tuple([return_type_lambas.get(a, a) for a in return_types.__args__])
- elif isinstance(return_types, type):
- return_types = (return_types, )
- return_types = {r: t for r, t in zip(outputs, return_types)}
- param_types = {}
- for param_name, param_type in annotations.items():
- if param_name == "return":
- continue
- param_types[param_name] = param_type
- command = SOAPCommand(
- self.base_ip.decode(), self.port, service.controlURL, service.serviceType.encode(),
- name, param_types, return_types, inputs, outputs, soap_socket)
- setattr(command, "__doc__", current.__doc__)
- setattr(self.commands, command.method, command)
- self._registered_commands[command.method] = service.serviceType
- log.debug("registered %s::%s", service.serviceType, command.method)
+ self.commands.register(self.base_ip, self.port, name, service.controlURL, service.serviceType.encode(),
+ inputs, outputs, loop)
+ self._registered_commands[name] = service.serviceType
+ log.debug("registered %s::%s", service.serviceType, name)
except AttributeError:
s = self._unsupported_actions.get(service.serviceType, [])
s.append(name)
diff --git a/aioupnp/protocols/scpd.py b/aioupnp/protocols/scpd.py
index 30c26d2..5b61212 100644
--- a/aioupnp/protocols/scpd.py
+++ b/aioupnp/protocols/scpd.py
@@ -1,5 +1,4 @@
import logging
-import socket
import typing
import re
from collections import OrderedDict
@@ -64,7 +63,7 @@ class SCPDHTTPClientProtocol(Protocol):
for i, line in enumerate(self.response_buff.split(b'\r\n')):
if not line: # we hit the blank line between the headers and the body
if i == (len(self.response_buff.split(b'\r\n')) - 1):
- continue # the body is still yet to be written
+ return # the body is still yet to be written
if not self._got_headers:
self._headers, self._response_code, self._response_msg = parse_headers(
b'\r\n'.join(self.response_buff.split(b'\r\n')[:i])
@@ -82,17 +81,17 @@ class SCPDHTTPClientProtocol(Protocol):
else:
self.finished.set_exception(
UPnPError(
- "too many bytes written to response (%i vs %i expected)" % (
- len(body), self._content_length
- )
+ "too many bytes written to response (%i vs %i expected)" % (
+ len(body), self._content_length
+ )
)
)
return
-async def scpd_get(control_url: str, address: str, port: int) -> typing.Tuple[typing.Dict, bytes,
- typing.Optional[Exception]]:
- loop = asyncio.get_event_loop_policy().get_event_loop()
+async def scpd_get(control_url: str, address: str, port: int, loop=None) -> typing.Tuple[typing.Dict, bytes,
+ typing.Optional[Exception]]:
+ loop = loop or asyncio.get_event_loop_policy().get_event_loop()
finished: asyncio.Future = asyncio.Future()
packet = serialize_scpd_get(control_url, address)
transport, protocol = await loop.create_connection(
@@ -105,6 +104,9 @@ async def scpd_get(control_url: str, address: str, port: int) -> typing.Tuple[ty
except asyncio.TimeoutError:
error = UPnPError("get request timed out")
body = b''
+ except UPnPError as err:
+ error = err
+ body = protocol.response_buff
finally:
transport.close()
if not error:
@@ -116,21 +118,22 @@ async def scpd_get(control_url: str, address: str, port: int) -> typing.Tuple[ty
async def scpd_post(control_url: str, address: str, port: int, method: str, param_names: list, service_id: bytes,
- soap_socket: socket.socket = None, **kwargs) -> typing.Tuple[typing.Dict, bytes,
- typing.Optional[Exception]]:
- loop = asyncio.get_event_loop_policy().get_event_loop()
+ loop=None, **kwargs) -> typing.Tuple[typing.Dict, bytes, typing.Optional[Exception]]:
+ loop = loop or asyncio.get_event_loop_policy().get_event_loop()
finished: asyncio.Future = asyncio.Future()
packet = serialize_soap_post(method, param_names, service_id, address.encode(), control_url.encode(), **kwargs)
transport, protocol = await loop.create_connection(
lambda : SCPDHTTPClientProtocol(
packet, finished, soap_method=method, soap_service_id=service_id.decode(),
- ), address, port, sock=soap_socket
+ ), address, port
)
assert isinstance(protocol, SCPDHTTPClientProtocol)
try:
body, response_code, response_msg = await asyncio.wait_for(finished, 1.0)
except asyncio.TimeoutError:
return {}, b'', UPnPError("Timeout")
+ except UPnPError as err:
+ return {}, protocol.response_buff, err
finally:
transport.close()
try:
diff --git a/aioupnp/protocols/soap.py b/aioupnp/protocols/soap.py
deleted file mode 100644
index f230590..0000000
--- a/aioupnp/protocols/soap.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import logging
-import socket
-import asyncio
-import typing
-import time
-from aioupnp.protocols.scpd import scpd_post
-from aioupnp.fault import UPnPError
-
-log = logging.getLogger(__name__)
-
-
-def safe_type(t):
- if t is typing.Tuple:
- return tuple
- if t is typing.List:
- return list
- if t is typing.Dict:
- return dict
- if t is typing.Set:
- return set
- return t
-
-
-class SOAPCommand:
- def __init__(self, gateway_address: str, service_port: int, control_url: str, service_id: bytes, method: str,
- param_types: dict, return_types: dict, param_order: list, return_order: list,
- soap_socket: socket.socket = None) -> None:
- self.gateway_address = gateway_address
- self.service_port = service_port
- self.control_url = control_url
- self.service_id = service_id
- self.method = method
- self.param_types = param_types
- self.param_order = param_order
- self.return_types = return_types
- self.return_order = return_order
- self.soap_socket = soap_socket
- self._requests: typing.List = []
-
- async def __call__(self, **kwargs) -> typing.Union[None, typing.Dict, typing.List, typing.Tuple]:
- if set(kwargs.keys()) != set(self.param_types.keys()):
- raise Exception("argument mismatch: %s vs %s" % (kwargs.keys(), self.param_types.keys()))
- soap_kwargs = {n: safe_type(self.param_types[n])(kwargs[n]) for n in self.param_types.keys()}
- response, xml_bytes, err = await scpd_post(
- self.control_url, self.gateway_address, self.service_port, self.method, self.param_order,
- self.service_id, self.soap_socket, **soap_kwargs
- )
- if err is not None:
- self._requests.append((soap_kwargs, xml_bytes, None, err, time.time()))
- raise err
- if not response:
- result = None
- else:
- recast_result = tuple([safe_type(self.return_types[n])(response.get(n)) for n in self.return_order])
- if len(recast_result) == 1:
- result = recast_result[0]
- else:
- result = recast_result
- self._requests.append((soap_kwargs, xml_bytes, result, None, time.time()))
- return result
diff --git a/aioupnp/upnp.py b/aioupnp/upnp.py
index fe50a62..8c86cd9 100644
--- a/aioupnp/upnp.py
+++ b/aioupnp/upnp.py
@@ -11,7 +11,7 @@ from aioupnp.fault import UPnPError
from aioupnp.gateway import Gateway
from aioupnp.util import get_gateway_and_lan_addresses
from aioupnp.protocols.ssdp import m_search, fuzzy_m_search
-from aioupnp.protocols.soap import SOAPCommand
+from aioupnp.commands import SOAPCommand
from aioupnp.serialization.ssdp import SSDPDatagram
log = logging.getLogger(__name__)
@@ -47,14 +47,13 @@ class UPnP:
@classmethod
async def discover(cls, lan_address: str = '', gateway_address: str = '', timeout: int = 30,
- igd_args: OrderedDict = None, interface_name: str = 'default',
- ssdp_socket: socket.socket = None, soap_socket: socket.socket = None):
+ igd_args: OrderedDict = None, interface_name: str = 'default', loop=None):
try:
lan_address, gateway_address = cls.get_lan_and_gateway(lan_address, gateway_address, interface_name)
except Exception as err:
raise UPnPError("failed to get lan and gateway addresses: %s" % str(err))
gateway = await Gateway.discover_gateway(
- lan_address, gateway_address, timeout, igd_args, ssdp_socket, soap_socket
+ lan_address, gateway_address, timeout, igd_args, loop
)
return cls(lan_address, gateway_address, gateway)
@@ -342,21 +341,31 @@ class UPnP:
@classmethod
def run_cli(cls, method, igd_args: OrderedDict, lan_address: str = '', gateway_address: str = '', timeout: int = 30,
interface_name: str = 'default', kwargs: dict = None) -> None:
+ """
+ :param method: the command name
+ :param igd_args: ordered case sensitive M-SEARCH headers, if provided all headers to be used must be provided
+ :param lan_address: the ip address of the local interface
+ :param gateway_address: the ip address of the gateway
+ :param timeout: timeout, in seconds
+ :param interface_name: name of the network interface, the default is aliased to 'default'
+ :param kwargs: keyword arguments for the command
+ """
kwargs = kwargs or {}
igd_args = igd_args
timeout = int(timeout)
loop = asyncio.get_event_loop_policy().get_event_loop()
fut: asyncio.Future = asyncio.Future()
- async def wrapper():
- if method == 'm_search':
+ async def wrapper(): # wrap the upnp setup and call of the command in a coroutine
+
+ if method == 'm_search': # if we're only m_searching don't do any device discovery
fn = lambda *_a, **_kw: cls.m_search(
lan_address, gateway_address, timeout, igd_args, interface_name
)
- else:
+ else: # automatically discover the gateway
try:
u = await cls.discover(
- lan_address, gateway_address, timeout, igd_args, interface_name
+ lan_address, gateway_address, timeout, igd_args, interface_name, loop=loop
)
except UPnPError as err:
fut.set_exception(err)
@@ -366,7 +375,7 @@ class UPnP:
else:
fut.set_exception(UPnPError("\"%s\" is not a recognized command" % method))
return
- try:
+ try: # call the command
result = await fn(**{k: fn.__annotations__[k](v) for k, v in kwargs.items()})
fut.set_result(result)
except UPnPError as err:
diff --git a/mypy.ini b/mypy.ini
index 5787c65..be20a6c 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,4 +1,7 @@
[mypy]
python_version = 3.7
mypy_path=stubs
-cache_dir=/dev/null
\ No newline at end of file
+cache_dir=/dev/null
+
+[mypy-tests]
+ignore_errors=true
diff --git a/tests/protocols/__init__.py b/tests/protocols/__init__.py
new file mode 100644
index 0000000..dd299fc
--- /dev/null
+++ b/tests/protocols/__init__.py
@@ -0,0 +1,113 @@
+import asyncio
+import unittest
+from unittest.case import _Outcome
+
+try:
+ from asyncio.runners import _cancel_all_tasks
+except ImportError:
+ # this is only available in py3.7
+ def _cancel_all_tasks(loop):
+ pass
+
+
+class TestBase(unittest.TestCase):
+ # Implementation inspired by discussion:
+ # https://bugs.python.org/issue32972
+
+ async def asyncSetUp(self):
+ pass
+
+ async def asyncTearDown(self):
+ pass
+
+ async def doAsyncCleanups(self):
+ pass
+
+ def run(self, result=None):
+ orig_result = result
+ if result is None:
+ result = self.defaultTestResult()
+ startTestRun = getattr(result, 'startTestRun', None)
+ if startTestRun is not None:
+ startTestRun()
+
+ result.startTest(self)
+
+ testMethod = getattr(self, self._testMethodName)
+ if (getattr(self.__class__, "__unittest_skip__", False) or
+ getattr(testMethod, "__unittest_skip__", False)):
+ # If the class or method was skipped.
+ try:
+ skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
+ or getattr(testMethod, '__unittest_skip_why__', ''))
+ self._addSkip(result, self, skip_why)
+ finally:
+ result.stopTest(self)
+ return
+ expecting_failure_method = getattr(testMethod,
+ "__unittest_expecting_failure__", False)
+ expecting_failure_class = getattr(self,
+ "__unittest_expecting_failure__", False)
+ expecting_failure = expecting_failure_class or expecting_failure_method
+ outcome = _Outcome(result)
+ try:
+ self._outcome = outcome
+
+ loop = asyncio.new_event_loop()
+ try:
+ asyncio.set_event_loop(loop)
+ loop.set_debug(True)
+
+ with outcome.testPartExecutor(self):
+ self.setUp()
+ loop.run_until_complete(self.asyncSetUp())
+ if outcome.success:
+ outcome.expecting_failure = expecting_failure
+ with outcome.testPartExecutor(self, isTest=True):
+ possible_coroutine = testMethod()
+ if asyncio.iscoroutine(possible_coroutine):
+ loop.run_until_complete(possible_coroutine)
+ outcome.expecting_failure = False
+ with outcome.testPartExecutor(self):
+ loop.run_until_complete(self.asyncTearDown())
+ self.tearDown()
+ finally:
+ try:
+ _cancel_all_tasks(loop)
+ loop.run_until_complete(loop.shutdown_asyncgens())
+ finally:
+ asyncio.set_event_loop(None)
+ loop.close()
+
+ self.doCleanups()
+
+ for test, reason in outcome.skipped:
+ self._addSkip(result, test, reason)
+ self._feedErrorsToResult(result, outcome.errors)
+ if outcome.success:
+ if expecting_failure:
+ if outcome.expectedFailure:
+ self._addExpectedFailure(result, outcome.expectedFailure)
+ else:
+ self._addUnexpectedSuccess(result)
+ else:
+ result.addSuccess(self)
+ return result
+ finally:
+ result.stopTest(self)
+ if orig_result is None:
+ stopTestRun = getattr(result, 'stopTestRun', None)
+ if stopTestRun is not None:
+ stopTestRun()
+
+ # explicitly break reference cycles:
+ # outcome.errors -> frame -> outcome -> outcome.errors
+ # outcome.expectedFailure -> frame -> outcome -> outcome.expectedFailure
+ outcome.errors.clear()
+ outcome.expectedFailure = None
+
+ # clear the outcome, no more needed
+ self._outcome = None
+
+ def setUp(self):
+ self.loop = asyncio.get_event_loop_policy().get_event_loop()
diff --git a/aioupnp/protocols/test_common.py b/tests/protocols/mocks.py
similarity index 52%
rename from aioupnp/protocols/test_common.py
rename to tests/protocols/mocks.py
index 972c8fc..52f1d5e 100644
--- a/aioupnp/protocols/test_common.py
+++ b/tests/protocols/mocks.py
@@ -1,26 +1,7 @@
import asyncio
-import inspect
import contextlib
import socket
import mock
-import unittest
-
-
-def async_test(f):
- def wrapper(*args, **kwargs):
- if inspect.iscoroutinefunction(f):
- future = f(*args, **kwargs)
- else:
- coroutine = asyncio.coroutine(f)
- future = coroutine(*args, **kwargs)
- asyncio.get_event_loop().run_until_complete(future)
-
- return wrapper
-
-
-class TestBase(unittest.TestCase):
- def setUp(self):
- self.loop = asyncio.get_event_loop_policy().get_event_loop()
@contextlib.contextmanager
@@ -58,3 +39,39 @@ def mock_datagram_endpoint_factory(loop, expected_addr, replies=None, delay_repl
mock_socket.return_value = mock_sock
loop.create_datagram_endpoint = create_datagram_endpoint
yield
+
+@contextlib.contextmanager
+def mock_tcp_endpoint_factory(loop, replies=None, delay_reply=0.0, sent_packets=None):
+ sent_packets = sent_packets if sent_packets is not None else []
+ replies = replies or {}
+
+ def write(p: asyncio.Protocol):
+ def _write(data):
+ sent_packets.append(data)
+ if data in replies:
+ loop.call_later(delay_reply, p.data_received, replies[data])
+ return _write
+
+ async def create_connection(protocol_factory, host=None, port=None):
+ protocol = protocol_factory()
+ transport = asyncio.Transport(extra={'socket': mock_sock})
+ transport.close = lambda: mock_sock.close()
+ mock_sock.write = write(protocol)
+ transport.write = mock_sock.write
+ protocol.connection_made(transport)
+ return transport, protocol
+
+ with mock.patch('socket.socket') as mock_socket:
+ mock_sock = mock.Mock(spec=socket.socket)
+ mock_sock.setsockopt = lambda *_: None
+ mock_sock.bind = lambda *_: None
+ mock_sock.setblocking = lambda *_: None
+ mock_sock.getsockname = lambda: "0.0.0.0"
+ mock_sock.getpeername = lambda: ""
+ mock_sock.close = lambda: None
+ mock_sock.type = socket.SOCK_STREAM
+ mock_sock.fileno = lambda: 7
+
+ mock_socket.return_value = mock_sock
+ loop.create_connection = create_connection
+ yield
diff --git a/tests/protocols/test_gateway.py b/tests/protocols/test_gateway.py
new file mode 100644
index 0000000..462a97f
--- /dev/null
+++ b/tests/protocols/test_gateway.py
@@ -0,0 +1,61 @@
+from aioupnp.fault import UPnPError
+from aioupnp.protocols.scpd import scpd_post, scpd_get
+from . import TestBase
+from .mocks import mock_tcp_endpoint_factory
+from collections import OrderedDict
+from aioupnp.gateway import Gateway
+from aioupnp.serialization.ssdp import SSDPDatagram
+
+class TestDiscoverCommands(TestBase):
+ gateway_address = "10.0.0.1"
+ soap_port = 49152
+ m_search_args = OrderedDict([
+ ("HOST", "239.255.255.250:1900"),
+ ("MAN", "ssdp:discover"),
+ ("MX", 1),
+ ("ST", "urn:schemas-upnp-org:device:WANDevice:1")
+ ])
+ reply = SSDPDatagram("OK", OrderedDict([
+ ("CACHE_CONTROL", "max-age=1800"),
+ ("LOCATION", "http://10.0.0.1:49152/InternetGatewayDevice.xml"),
+ ("SERVER", "Linux, UPnP/1.0, DIR-890L Ver 1.20"),
+ ("ST", "urn:schemas-upnp-org:device:WANDevice:1"),
+ ("USN", "uuid:11111111-2222-3333-4444-555555555555::urn:schemas-upnp-org:device:WANDevice:1")
+ ]))
+
+ replies = {
+ b'GET /InternetGatewayDevice.xml HTTP/1.1\r\nAccept-Encoding: gzip\r\nHost: 10.0.0.1\r\nConnection: Close\r\n\r\n': b"HTTP/1.1 200 OK\r\nServer: WebServer\r\nDate: Thu, 11 Oct 2018 22:16:16 GMT\r\nContent-Type: text/xml\r\nContent-Length: 3921\r\nLast-Modified: Thu, 09 Aug 2018 12:41:07 GMT\r\nConnection: close\r\n\r\n\n\n\t\n\t\t1\n\t\t0\n\t\n\thttp://10.0.0.1:49152\n\t\n\t\turn:schemas-upnp-org:device:InternetGatewayDevice:1\n\t\tWireless Broadband Router\n\t\tD-Link Corporation\n\t\thttp://www.dlink.com\n\t\tD-Link Router\n\t\tD-Link Router\n\t\tDIR-890L\n\t\thttp://www.dlink.com\n\t\t120\n\t\tuuid:11111111-2222-3333-4444-555555555555\n\t\t\n\t\t\t\n\t\t\t\timage/gif\n\t\t\t\t118\n\t\t\t\t119\n\t\t\t\t8\n\t\t\t\t/ligd.gif\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\turn:schemas-microsoft-com:service:OSInfo:1\n\t\t\t\turn:microsoft-com:serviceId:OSInfo1\n\t\t\t\t/soap.cgi?service=OSInfo1\n\t\t\t\t/gena.cgi?service=OSInfo1\n\t\t\t\t/OSInfo.xml\n\t\t\t\n\t\t\t\n\t\t\t\turn:schemas-upnp-org:service:Layer3Forwarding:1\n\t\t\t\turn:upnp-org:serviceId:L3Forwarding1\n\t\t\t\t/soap.cgi?service=L3Forwarding1\n\t\t\t\t/gena.cgi?service=L3Forwarding1\n\t\t\t\t/Layer3Forwarding.xml\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\turn:schemas-upnp-org:device:WANDevice:1\n\t\t\t\tWANDevice\n\t\t\t\tD-Link\n\t\t\t\thttp://www.dlink.com\n\t\t\t\tWANDevice\n\t\t\t\tDIR-890L\n\t\t\t\t1\n\t\t\t\thttp://www.dlink.com\n\t\t\t\t120\n\t\t\t\tuuid:11111111-2222-3333-4444-555555555555\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\turn:schemas-upnp-org:service:WANCommonInterfaceConfig:1\n\t\t\t\t\t\turn:upnp-org:serviceId:WANCommonIFC1\n\t\t\t\t\t\t/soap.cgi?service=WANCommonIFC1\n\t\t\t\t\t\t/gena.cgi?service=WANCommonIFC1\n\t\t\t\t\t\t/WANCommonInterfaceConfig.xml\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\turn:schemas-upnp-org:device:WANConnectionDevice:1\n\t\t\t\t\t\tWANConnectionDevice\n\t\t\t\t\t\tD-Link\n\t\t\t\t\t\thttp://www.dlink.com\n\t\t\t\t\t\tWanConnectionDevice\n\t\t\t\t\t\tDIR-890L\n\t\t\t\t\t\t1\n\t\t\t\t\t\thttp://www.dlink.com\n\t\t\t\t\t\t120\n\t\t\t\t\t\tuuid:11111111-2222-3333-4444-555555555555\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\turn:schemas-upnp-org:service:WANEthernetLinkConfig:1\n\t\t\t\t\t\t\t\turn:upnp-org:serviceId:WANEthLinkC1\n\t\t\t\t\t\t\t\t/soap.cgi?service=WANEthLinkC1\n\t\t\t\t\t\t\t\t/gena.cgi?service=WANEthLinkC1\n\t\t\t\t\t\t\t\t/WANEthernetLinkConfig.xml\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\turn:schemas-upnp-org:service:WANIPConnection:1\n\t\t\t\t\t\t\t\turn:upnp-org:serviceId:WANIPConn1\n\t\t\t\t\t\t\t\t/soap.cgi?service=WANIPConn1\n\t\t\t\t\t\t\t\t/gena.cgi?service=WANIPConn1\n\t\t\t\t\t\t\t\t/WANIPConnection.xml\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\thttp://10.0.0.1\n\t\n\n",
+ b'GET /OSInfo.xml HTTP/1.1\r\nAccept-Encoding: gzip\r\nHost: 10.0.0.1\r\nConnection: Close\r\n\r\n': b"HTTP/1.1 200 OK\r\nServer: WebServer\r\nDate: Thu, 11 Oct 2018 22:16:16 GMT\r\nContent-Type: text/xml\r\nContent-Length: 219\r\nLast-Modified: Thu, 09 Aug 2018 12:41:07 GMT\r\nConnection: close\r\n\r\n\n\n\t\n\t\t1\n\t\t0\n\t\n\t\n\t\n\t\n\t\n\n",
+ b'GET /Layer3Forwarding.xml HTTP/1.1\r\nAccept-Encoding: gzip\r\nHost: 10.0.0.1\r\nConnection: Close\r\n\r\n': b"HTTP/1.1 200 OK\r\nServer: WebServer\r\nDate: Thu, 11 Oct 2018 22:16:16 GMT\r\nContent-Type: text/xml\r\nContent-Length: 920\r\nLast-Modified: Thu, 09 Aug 2018 12:41:07 GMT\r\nConnection: close\r\n\r\n\n\n\t\n\t\t1\n\t\t0\n\t\n\t\n\t\t\n\t\t\tGetDefaultConnectionService\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tNewDefaultConnectionService\n\t\t\t\t\tout\n\t\t\t\t\tDefaultConnectionService\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\tSetDefaultConnectionService\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tNewDefaultConnectionService\n\t\t\t\t\tin\n\t\t\t\t\tDefaultConnectionService\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\tDefaultConnectionService\n\t\t\tstring\n\t\t\n\t\n\n",
+ b'GET /WANCommonInterfaceConfig.xml HTTP/1.1\r\nAccept-Encoding: gzip\r\nHost: 10.0.0.1\r\nConnection: Close\r\n\r\n': b"HTTP/1.1 200 OK\r\nServer: WebServer\r\nDate: Thu, 11 Oct 2018 22:16:16 GMT\r\nContent-Type: text/xml\r\nContent-Length: 5343\r\nLast-Modified: Thu, 09 Aug 2018 12:41:07 GMT\r\nConnection: close\r\n\r\n\r\n\r\n\t\r\n\t\t1\r\n\t\t0\r\n\t\r\n\t\r\n\t\t\r\n\t\t\tGetCommonLinkProperties\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewWANAccessType\r\n\t\t\t\t\tout\r\n\t\t\t\t\tWANAccessType\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewLayer1UpstreamMaxBitRate\r\n\t\t\t\t\tout\r\n\t\t\t\t\tLayer1UpstreamMaxBitRate\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewLayer1DownstreamMaxBitRate\r\n\t\t\t\t\tout\r\n\t\t\t\t\tLayer1DownstreamMaxBitRate\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewPhysicalLinkStatus\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPhysicalLinkStatus\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tGetTotalBytesSent\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewTotalBytesSent\r\n\t\t\t\t\tout\r\n\t\t\t\t\tTotalBytesSent\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tGetTotalBytesReceived\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewTotalBytesReceived\r\n\t\t\t\t\tout\r\n\t\t\t\t\tTotalBytesReceived\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tGetTotalPacketsSent\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewTotalPacketsSent\r\n\t\t\t\t\tout\r\n\t\t\t\t\tTotalPacketsSent\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tGetTotalPacketsReceived\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewTotalPacketsReceived\r\n\t\t\t\t\tout\r\n\t\t\t\t\tTotalPacketsReceived\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tX_GetICSStatistics\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tTotalBytesSent\r\n\t\t\t\t\tout\r\n\t\t\t\t\tTotalBytesSent\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tTotalBytesReceived\r\n\t\t\t\t\tout\r\n\t\t\t\t\tTotalBytesReceived\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tTotalPacketsSent\r\n\t\t\t\t\tout\r\n\t\t\t\t\tTotalPacketsSent\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tTotalPacketsReceived\r\n\t\t\t\t\tout\r\n\t\t\t\t\tTotalPacketsReceived\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tLayer1DownstreamMaxBitRate\r\n\t\t\t\t\tout\r\n\t\t\t\t\tLayer1DownstreamMaxBitRate\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tUptime\r\n\t\t\t\t\tout\r\n\t\t\t\t\tX_Uptime\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\r\n\t\r\n\t\t\r\n\t\t\tWANAccessType\r\n\t\t\tstring\r\n\t\t\t\r\n\t\t\t\tDSL\r\n\t\t\t\tPOTS\r\n\t\t\t\tCable\r\n\t\t\t\tEthernet\r\n\t\t\t\tOther\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tLayer1UpstreamMaxBitRate\r\n\t\t\tui4\r\n\t\t\r\n\t\t\r\n\t\t\tLayer1DownstreamMaxBitRate\r\n\t\t\tui4\r\n\t\t\r\n\t\t\r\n\t\t\tPhysicalLinkStatus\r\n\t\t\tstring\r\n\t\t\t\r\n\t\t\t\tUp\r\n\t\t\t\tDown\r\n\t\t\t\tInitializing\r\n\t\t\t\tUnavailable\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tWANAccessProvider\r\n\t\t\tstring\r\n\t\t\r\n\t\t\r\n\t\t\tMaximumActiveConnections\r\n\t\t\tui2\r\n\t\t\t\r\n\t\t\t\t1\r\n\t\t\t\t\r\n\t\t\t\t1\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tTotalBytesSent\r\n\t\t\tui4\r\n\t\t\r\n\t\t\r\n\t\t\tTotalBytesReceived\r\n\t\t\tui4\r\n\t\t\r\n\t\t\r\n\t\t\tTotalPacketsSent\r\n\t\t\tui4\r\n\t\t\r\n\t\t\r\n\t\t\tTotalPacketsReceived\r\n\t\t\tui4\r\n\t\t\r\n\t\t\r\n\t\t\tX_PersonalFirewallEnabled\r\n\t\t\tboolean\r\n\t\t\r\n\t\t\r\n\t\t\tX_Uptime\r\n\t\t\tui4\r\n\t\t\r\n\t\r\n\r\n",
+ b'GET /WANEthernetLinkConfig.xml HTTP/1.1\r\nAccept-Encoding: gzip\r\nHost: 10.0.0.1\r\nConnection: Close\r\n\r\n': b"HTTP/1.1 200 OK\r\nServer: WebServer\r\nDate: Thu, 11 Oct 2018 22:16:16 GMT\r\nContent-Type: text/xml\r\nContent-Length: 773\r\nLast-Modified: Thu, 09 Aug 2018 12:41:07 GMT\r\nConnection: close\r\n\r\n\n\n\t\n\t\t1\n\t\t0\n\t\n\t\n\t\t\n\t\t\tGetEthernetLinkStatus\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tNewEthernetLinkStatus\n\t\t\t\t\tout\n\t\t\t\t\tEthernetLinkStatus\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\tEthernetLinkStatus\n\t\t\tstring\n\t\t\t\n\t\t\t\tUp\n\t\t\t\tDown\n\t\t\t\tUnavailable\n\t\t\t\n\t\t\n\t\n\n",
+ b'GET /WANIPConnection.xml HTTP/1.1\r\nAccept-Encoding: gzip\r\nHost: 10.0.0.1\r\nConnection: Close\r\n\r\n': b"HTTP/1.1 200 OK\r\nServer: WebServer\r\nDate: Thu, 11 Oct 2018 22:16:16 GMT\r\nContent-Type: text/xml\r\nContent-Length: 12078\r\nLast-Modified: Thu, 09 Aug 2018 12:41:07 GMT\r\nConnection: close\r\n\r\n\r\n\r\n\t\r\n\t\t1\r\n\t\t0\r\n\t\r\n\t\r\n\t\t\r\n\t\t\tSetConnectionType\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewConnectionType\r\n\t\t\t\t\tin\r\n\t\t\t\t\tConnectionType\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t \r\n\t\t\r\n\t\t\tGetConnectionTypeInfo\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewConnectionType\r\n\t\t\t\t\tout\r\n\t\t\t\t\tConnectionType\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewPossibleConnectionTypes\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPossibleConnectionTypes\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tRequestConnection\r\n\t\t\r\n\t\t\r\n\t\t\tForceTermination\r\n\t\t\r\n\t\t\r\n\t\t\tGetStatusInfo\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewConnectionStatus\r\n\t\t\t\t\tout\r\n\t\t\t\t\tConnectionStatus\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewLastConnectionError\r\n\t\t\t\t\tout\r\n\t\t\t\t\tLastConnectionError\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewUptime\r\n\t\t\t\t\tout\r\n\t\t\t\t\tUptime\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tGetNATRSIPStatus\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewRSIPAvailable\r\n\t\t\t\t\tout\r\n\t\t\t\t\tRSIPAvailable\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewNATEnabled\r\n\t\t\t\t\tout\r\n\t\t\t\t\tNATEnabled\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tGetGenericPortMappingEntry\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewPortMappingIndex\r\n\t\t\t\t\tin\r\n\t\t\t\t\tPortMappingNumberOfEntries\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewRemoteHost\r\n\t\t\t\t\tout\r\n\t\t\t\t\tRemoteHost\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewExternalPort\r\n\t\t\t\t\tout\r\n\t\t\t\t\tExternalPort\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewProtocol\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPortMappingProtocol\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewInternalPort\r\n\t\t\t\t\tout\r\n\t\t\t\t\tInternalPort\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewInternalClient\r\n\t\t\t\t\tout\r\n\t\t\t\t\tInternalClient\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewEnabled\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPortMappingEnabled\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewPortMappingDescription\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPortMappingDescription\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewLeaseDuration\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPortMappingLeaseDuration\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tGetSpecificPortMappingEntry\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewRemoteHost\r\n\t\t\t\t\tin\r\n\t\t\t\t\tRemoteHost\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewExternalPort\r\n\t\t\t\t\tin\r\n\t\t\t\t\tExternalPort\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewProtocol\r\n\t\t\t\t\tin\r\n\t\t\t\t\tPortMappingProtocol\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewInternalPort\r\n\t\t\t\t\tout\r\n\t\t\t\t\tInternalPort\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewInternalClient\r\n\t\t\t\t\tout\r\n\t\t\t\t\tInternalClient\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewEnabled\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPortMappingEnabled\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewPortMappingDescription\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPortMappingDescription\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewLeaseDuration\r\n\t\t\t\t\tout\r\n\t\t\t\t\tPortMappingLeaseDuration\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tAddPortMapping\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewRemoteHost\r\n\t\t\t\t\tin\r\n\t\t\t\t\tRemoteHost\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewExternalPort\r\n\t\t\t\t\tin\r\n\t\t\t\t\tExternalPort\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewProtocol\r\n\t\t\t\t\tin\r\n\t\t\t\t\tPortMappingProtocol\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewInternalPort\r\n\t\t\t\t\tin\r\n\t\t\t\t\tInternalPort\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewInternalClient\r\n\t\t\t\t\tin\r\n\t\t\t\t\tInternalClient\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewEnabled\r\n\t\t\t\t\tin\r\n\t\t\t\t\tPortMappingEnabled\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewPortMappingDescription\r\n\t\t\t\t\tin\r\n\t\t\t\t\tPortMappingDescription\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewLeaseDuration\r\n\t\t\t\t\tin\r\n\t\t\t\t\tPortMappingLeaseDuration\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tDeletePortMapping\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewRemoteHost\r\n\t\t\t\t\tin\r\n\t\t\t\t\tRemoteHost\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewExternalPort\r\n\t\t\t\t\tin\r\n\t\t\t\t\tExternalPort\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewProtocol\r\n\t\t\t\t\tin\r\n\t\t\t\t\tPortMappingProtocol\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tGetExternalIPAddress\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\tNewExternalIPAddress\r\n\t\t\t\t\tout\r\n\t\t\t\t\tExternalIPAddress\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\r\n\t\r\n\t\t\r\n\t\t\tConnectionType\r\n\t\t\tstring\r\n\t\t\tUnconfigured\r\n\t\t\r\n\t\t\r\n\t\t\tPossibleConnectionTypes\r\n\t\t\tstring\r\n\t\t\t\r\n\t\t\t\tUnconfigured\r\n\t\t\t\tIP_Routed\r\n\t\t\t\tIP_Bridged\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tConnectionStatus\r\n\t\t\tstring\r\n\t\t\tUnconfigured\r\n\t\t\t\r\n\t\t\t\tUnconfigured\r\n\t\t\t\tConnecting\r\n\t\t\t\tAuthenticating\r\n\t\t\t\tPendingDisconnect\r\n\t\t\t\tDisconnecting\r\n\t\t\t\tDisconnected\r\n\t\t\t\tConnected\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tUptime\r\n\t\t\tui4\r\n\t\t\t0\r\n\t\t\t\r\n\t\t\t\t0\r\n\t\t\t\t\r\n\t\t\t\t1\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tRSIPAvailable\r\n\t\tboolean\r\n\t\t\t0\r\n\t\t\r\n\t\t\r\n\t\t\tNATEnabled\r\n\t\t\tboolean\r\n\t\t\t1\r\n\t\t \r\n\t\t\r\n\t\t\tX_Name\r\n\t\t\tstring\r\n\t\t\r\n\t\t\r\n\t\t\tLastConnectionError\r\n\t\t\tstring\r\n\t\t\tERROR_NONE\r\n\t\t\t\r\n\t\t\t\tERROR_NONE\r\n\t\t\t\tERROR_ISP_TIME_OUT\r\n\t\t\t\tERROR_COMMAND_ABORTED\r\n\t\t\t\tERROR_NOT_ENABLED_FOR_INTERNET\r\n\t\t\t\tERROR_BAD_PHONE_NUMBER\r\n\t\t\t\tERROR_USER_DISCONNECT\r\n\t\t\t\tERROR_ISP_DISCONNECT\r\n\t\t\t\tERROR_IDLE_DISCONNECT\r\n\t\t\t\tERROR_FORCED_DISCONNECT\r\n\t\t\t\tERROR_SERVER_OUT_OF_RESOURCES\r\n\t\t\t\tERROR_RESTRICTED_LOGON_HOURS\r\n\t\t\t\tERROR_ACCOUNT_DISABLED\r\n\t\t\t\tERROR_ACCOUNT_EXPIRED\r\n\t\t\t\tERROR_PASSWORD_EXPIRED\r\n\t\t\t\tERROR_AUTHENTICATION_FAILURE\r\n\t\t\t\tERROR_NO_DIALTONE\r\n\t\t\t\tERROR_NO_CARRIER\r\n\t\t\t\tERROR_NO_ANSWER\r\n\t\t\t\tERROR_LINE_BUSY\r\n\t\t\t\tERROR_UNSUPPORTED_BITSPERSECOND\r\n\t\t\t\tERROR_TOO_MANY_LINE_ERRORS\r\n\t\t\t\tERROR_IP_CONFIGURATION\r\n\t\t\t\tERROR_UNKNOWN\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tExternalIPAddress\r\n\t\t\tstring\r\n\t\t\r\n\t\t\r\n\t\t\tRemoteHost\r\n\t\t\tstring\r\n\t\t\r\n\t\t\r\n\t\t\tExternalPort\r\n\t\t\tui2\r\n\t\t\r\n\t\t\r\n\t\t\tInternalPort\r\n\t\t\tui2\r\n\t\t\r\n\t\t\r\n\t\t\tPortMappingProtocol\r\n\t\t\tstring\r\n\t\t\t\r\n\t\t\t\tTCP\r\n\t\t\t\tUDP\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\tInternalClient\r\n\t\t\tstring\r\n\t\t\r\n\t\t\r\n\t\t\tPortMappingDescription\r\n\t\t\tstring\r\n\t\t\r\n\t\t\r\n\t\t\tPortMappingEnabled\r\n\t\t\tboolean\r\n\t\t\r\n\t\t\r\n\t\t\tPortMappingLeaseDuration\r\n\t\t\tui4\r\n\t\t\r\n\t\t\r\n\t\t\tPortMappingNumberOfEntries\r\n\t\t\tui2\r\n\t\t\r\n\t\r\n\r\n"
+ }
+
+ expected_commands = {
+ 'GetDefaultConnectionService': 'urn:schemas-upnp-org:service:Layer3Forwarding:1',
+ 'SetDefaultConnectionService': 'urn:schemas-upnp-org:service:Layer3Forwarding:1',
+ 'GetCommonLinkProperties': 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
+ 'GetTotalBytesSent': 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
+ 'GetTotalBytesReceived': 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
+ 'GetTotalPacketsSent': 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
+ 'GetTotalPacketsReceived': 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
+ 'X_GetICSStatistics': 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
+ 'SetConnectionType': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'GetConnectionTypeInfo': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'RequestConnection': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'ForceTermination': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'GetStatusInfo': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'GetNATRSIPStatus': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'GetGenericPortMappingEntry': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'GetSpecificPortMappingEntry': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'AddPortMapping': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'DeletePortMapping': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'GetExternalIPAddress': 'urn:schemas-upnp-org:service:WANIPConnection:1'
+ }
+
+ async def test_discover_commands(self):
+ with mock_tcp_endpoint_factory(self.loop, self.replies):
+ gateway = Gateway(self.reply, self.m_search_args, '10.0.0.2', self.gateway_address)
+ await gateway.discover_commands(self.loop)
+ self.assertDictEqual(self.expected_commands, gateway._registered_commands)
diff --git a/tests/protocols/test_scpd.py b/tests/protocols/test_scpd.py
new file mode 100644
index 0000000..5ba0af9
--- /dev/null
+++ b/tests/protocols/test_scpd.py
@@ -0,0 +1,228 @@
+from aioupnp.fault import UPnPError
+from aioupnp.protocols.scpd import scpd_post, scpd_get
+from . import TestBase
+from .mocks import mock_tcp_endpoint_factory
+
+
+class TestSCPDGet(TestBase):
+ path, lan_address, port = '/IGDdevicedesc_brlan0.xml', '10.1.10.1', 49152
+ get_request = b'GET /IGDdevicedesc_brlan0.xml HTTP/1.1\r\n' \
+ b'Accept-Encoding: gzip\r\nHost: 10.1.10.1\r\nConnection: Close\r\n\r\n'
+
+ response = b"HTTP/1.1 200 OK\r\n" \
+ b"CONTENT-LENGTH: 2972\r\n" \
+ b"CONTENT-TYPE: text/xml\r\n" \
+ b"DATE: Thu, 18 Oct 2018 01:20:23 GMT\r\n" \
+ b"LAST-MODIFIED: Fri, 28 Sep 2018 18:35:48 GMT\r\n" \
+ b"SERVER: Linux/3.14.28-Prod_17.2, UPnP/1.0, Portable SDK for UPnP devices/1.6.22\r\n" \
+ b"X-User-Agent: redsonic\r\n" \
+ b"CONNECTION: close\r\n" \
+ b"\r\n" \
+ b"\n\n\n1\n0\n\n\nurn:schemas-upnp-org:device:InternetGatewayDevice:1\nCGA4131COM\nCisco\nhttp://www.cisco.com/\nCGA4131COM\nCGA4131COM\nCGA4131COM\nhttp://www.cisco.com\n\nuuid:11111111-2222-3333-4444-555555555556\nCGA4131COM\n\n\nurn:schemas-upnp-org:service:Layer3Forwarding:1\nurn:upnp-org:serviceId:L3Forwarding1\n/Layer3ForwardingSCPD.xml\n/upnp/control/Layer3Forwarding\n/upnp/event/Layer3Forwarding\n\n\n\n\nurn:schemas-upnp-org:device:WANDevice:1\nWANDevice:1\nCisco\nhttp://www.cisco.com/\nCGA4131COM\nCGA4131COM\nCGA4131COM\nhttp://www.cisco.com\n\nuuid:ebf5a0a0-1dd1-11b2-a92f-603d266f9915\nCGA4131COM\n\n\nurn:schemas-upnp-org:service:WANCommonInterfaceConfig:1\nurn:upnp-org:serviceId:WANCommonIFC1\n/WANCommonInterfaceConfigSCPD.xml\n/upnp/control/WANCommonInterfaceConfig0\n/upnp/event/WANCommonInterfaceConfig0\n\n\n\n \n urn:schemas-upnp-org:device:WANConnectionDevice:1\n WANConnectionDevice:1\n Cisco\n http://www.cisco.com/\n CGA4131COM\n CGA4131COM\n CGA4131COM\n http://www.cisco.com\n \n uuid:11111111-2222-3333-4444-555555555555\n CGA4131COM\n \n \n urn:schemas-upnp-org:service:WANIPConnection:1\n urn:upnp-org:serviceId:WANIPConn1\n /WANIPConnectionServiceSCPD.xml\n /upnp/control/WANIPConnection0\n /upnp/event/WANIPConnection0\n \n \n \n\n\n\nhttp://10.1.10.1/\n\n"
+
+ bad_xml = b"\n\n\n1\n0\n\n\nurn:schemas-upnp-org:device:InternetGatewayDevice:1\nCGA4131COM\nCisco\nhttp://www.cisco.com/\nCGA4131COM\nCGA4131COM\nCGA4131COM\nhttp://www.cisco.com\n\nuuid:11111111-2222-3333-4444-555555555556\nCGA4131COM\n\n\nurn:schemas-upnp-org:service:Layer3Forwarding:1\nurn:upnp-org:serviceId:L3Forwarding1\n/Layer3ForwardingSCPD.xml\n/upnp/control/Layer3Forwarding\n/upnp/event/Layer3Forwarding\n\n\n\n\nurn:schemas-upnp-org:device:WANDevice:1\nWANDevice:1\nCisco\nhttp://www.cisco.com/\nCGA4131COM\nCGA4131COM\nCGA4131COM\nhttp://www.cisco.com\n\nuuid:ebf5a0a0-1dd1-11b2-a92f-603d266f9915\nCGA4131COM\n\n\nurn:schemas-upnp-org:service:WANCommonInterfaceConfig:1\nurn:upnp-org:serviceId:WANCommonIFC1\n/WANCommonInterfaceConfigSCPD.xml\n/upnp/control/WANCommonInterfaceConfig0\n/upnp/event/WANCommonInterfaceConfig0\n\n\n\n \n urn:schemas-upnp-org:device:WANConnectionDevice:1\n WANConnectionDevice:1\n Cisco\n http://www.cisco.com/\n CGA4131COM\n CGA4131COM\n CGA4131COM\n http://www.cisco.com\n \n uuid:11111111-2222-3333-4444-555555555555\n CGA4131COM\n \n \n urn:schemas-upnp-org:service:WANIPConnection:1\n urn:upnp-org:serviceId:WANIPConn1\n /WANIPConnectionServiceSCPD.xml\n /upnp/control/WANIPConnection0\n /upnp/event/WANIPConnection0\n \n \n \n\n\n\nhttp://10.1.10.1/\n/root>\n"
+ bad_response = b"HTTP/1.1 200 OK\r\n" \
+ b"CONTENT-LENGTH: 2971\r\n" \
+ b"CONTENT-TYPE: text/xml\r\n" \
+ b"DATE: Thu, 18 Oct 2018 01:20:23 GMT\r\n" \
+ b"LAST-MODIFIED: Fri, 28 Sep 2018 18:35:48 GMT\r\n" \
+ b"SERVER: Linux/3.14.28-Prod_17.2, UPnP/1.0, Portable SDK for UPnP devices/1.6.22\r\n" \
+ b"X-User-Agent: redsonic\r\n" \
+ b"CONNECTION: close\r\n" \
+ b"\r\n" \
+ b"%s" % bad_xml
+
+ expected_parsed = {
+ 'specVersion': {'major': '1', 'minor': '0'},
+ 'device': {
+ 'deviceType': 'urn:schemas-upnp-org:device:InternetGatewayDevice:1',
+ 'friendlyName': 'CGA4131COM',
+ 'manufacturer': 'Cisco',
+ 'manufacturerURL': 'http://www.cisco.com/',
+ 'modelDescription': 'CGA4131COM',
+ 'modelName': 'CGA4131COM',
+ 'modelNumber': 'CGA4131COM',
+ 'modelURL': 'http://www.cisco.com',
+ 'UDN': 'uuid:11111111-2222-3333-4444-555555555556',
+ 'UPC': 'CGA4131COM',
+ 'serviceList': {
+ 'service': {
+ 'serviceType': 'urn:schemas-upnp-org:service:Layer3Forwarding:1',
+ 'serviceId': 'urn:upnp-org:serviceId:L3Forwarding1',
+ 'SCPDURL': '/Layer3ForwardingSCPD.xml',
+ 'controlURL': '/upnp/control/Layer3Forwarding',
+ 'eventSubURL': '/upnp/event/Layer3Forwarding'
+ }
+ },
+ 'deviceList': {
+ 'device': {
+ 'deviceType': 'urn:schemas-upnp-org:device:WANDevice:1',
+ 'friendlyName': 'WANDevice:1',
+ 'manufacturer': 'Cisco',
+ 'manufacturerURL': 'http://www.cisco.com/',
+ 'modelDescription': 'CGA4131COM',
+ 'modelName': 'CGA4131COM',
+ 'modelNumber': 'CGA4131COM',
+ 'modelURL': 'http://www.cisco.com',
+ 'UDN': 'uuid:ebf5a0a0-1dd1-11b2-a92f-603d266f9915',
+ 'UPC': 'CGA4131COM',
+ 'serviceList': {
+ 'service': {
+ 'serviceType': 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
+ 'serviceId': 'urn:upnp-org:serviceId:WANCommonIFC1',
+ 'SCPDURL': '/WANCommonInterfaceConfigSCPD.xml',
+ 'controlURL': '/upnp/control/WANCommonInterfaceConfig0',
+ 'eventSubURL': '/upnp/event/WANCommonInterfaceConfig0'
+ }
+ },
+ 'deviceList': {
+ 'device': {
+ 'deviceType': 'urn:schemas-upnp-org:device:WANConnectionDevice:1',
+ 'friendlyName': 'WANConnectionDevice:1',
+ 'manufacturer': 'Cisco',
+ 'manufacturerURL': 'http://www.cisco.com/',
+ 'modelDescription': 'CGA4131COM',
+ 'modelName': 'CGA4131COM',
+ 'modelNumber': 'CGA4131COM',
+ 'modelURL': 'http://www.cisco.com',
+ 'UDN': 'uuid:11111111-2222-3333-4444-555555555555',
+ 'UPC': 'CGA4131COM',
+ 'serviceList': {
+ 'service': {
+ 'serviceType': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'serviceId': 'urn:upnp-org:serviceId:WANIPConn1',
+ 'SCPDURL': '/WANIPConnectionServiceSCPD.xml',
+ 'controlURL': '/upnp/control/WANIPConnection0',
+ 'eventSubURL': '/upnp/event/WANIPConnection0'
+ }
+ }
+ }
+ }
+ }
+ },
+ 'presentationURL': 'http://10.1.10.1/'
+ }
+ }
+
+ async def test_scpd_get(self):
+ sent = []
+ replies = {self.get_request: self.response}
+ with mock_tcp_endpoint_factory(self.loop, replies, sent_packets=sent):
+ result, raw, err = await scpd_get(self.path, self.lan_address, self.port, self.loop)
+ self.assertEqual(None, err)
+ self.assertDictEqual(self.expected_parsed, result)
+
+ async def test_scpd_get_timeout(self):
+ sent = []
+ replies = {}
+ with mock_tcp_endpoint_factory(self.loop, replies, sent_packets=sent):
+ result, raw, err = await scpd_get(self.path, self.lan_address, self.port, self.loop)
+ self.assertTrue(isinstance(err, UPnPError))
+ self.assertDictEqual({}, result)
+ self.assertEqual(b'', raw)
+
+ async def test_scpd_get_bad_xml(self):
+ sent = []
+ replies = {self.get_request: self.bad_response}
+ with mock_tcp_endpoint_factory(self.loop, replies, sent_packets=sent):
+ result, raw, err = await scpd_get(self.path, self.lan_address, self.port, self.loop)
+ self.assertDictEqual({}, result)
+ self.assertEqual(self.bad_xml, raw)
+ self.assertTrue(isinstance(err, UPnPError))
+ self.assertTrue(str(err).startswith('no element found'))
+
+ async def test_scpd_get_overrun_content_length(self):
+ sent = []
+ replies = {self.get_request: self.bad_response + b'\r\n'}
+ with mock_tcp_endpoint_factory(self.loop, replies, sent_packets=sent):
+ result, raw, err = await scpd_get(self.path, self.lan_address, self.port, self.loop)
+ self.assertDictEqual({}, result)
+ self.assertEqual(self.bad_response + b'\r\n', raw)
+ self.assertTrue(isinstance(err, UPnPError))
+ self.assertTrue(str(err).startswith('too many bytes written'))
+
+
+class TestSCPDPost(TestBase):
+ param_names: list = []
+ kwargs: dict = {}
+ method, gateway_address, port = "GetExternalIPAddress", '10.0.0.1', 49152
+ st, lan_address, path = b'urn:schemas-upnp-org:service:WANIPConnection:1', '10.0.0.2', '/soap.cgi?service=WANIPConn1'
+ post_bytes = b'POST /soap.cgi?service=WANIPConn1 HTTP/1.1\r\n' \
+ b'Host: 10.0.0.1\r\nUser-Agent: python3/aioupnp, UPnP/1.0, MiniUPnPc/1.9\r\n' \
+ b'Content-Length: 285\r\nContent-Type: text/xml\r\n' \
+ b'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress"\r\n' \
+ b'Connection: Close\r\nCache-Control: no-cache\r\nPragma: no-cache\r\n\r\n' \
+ b'\r\n' \
+ b'' \
+ b'\r\n'
+
+ bad_envelope = b"s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n\r\n11.22.33.44\r\n\r\n "
+ envelope = b"\n\r\n11.22.33.44\r\n\r\n "
+
+ post_response = b"HTTP/1.1 200 OK\r\n" \
+ b"CONTENT-LENGTH: 340\r\n" \
+ b"CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n" \
+ b"DATE: Thu, 18 Oct 2018 01:20:23 GMT\r\n" \
+ b"EXT:\r\n" \
+ b"SERVER: Linux/3.14.28-Prod_17.2, UPnP/1.0, Portable SDK for UPnP devices/1.6.22\r\n" \
+ b"X-User-Agent: redsonic\r\n" \
+ b"\r\n" \
+ b"%s" % envelope
+
+ bad_envelope_response = b"HTTP/1.1 200 OK\r\n" \
+ b"CONTENT-LENGTH: 339\r\n" \
+ b"CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n" \
+ b"DATE: Thu, 18 Oct 2018 01:20:23 GMT\r\n" \
+ b"EXT:\r\n" \
+ b"SERVER: Linux/3.14.28-Prod_17.2, UPnP/1.0, Portable SDK for UPnP devices/1.6.22\r\n" \
+ b"X-User-Agent: redsonic\r\n" \
+ b"\r\n" \
+ b"%s" % bad_envelope
+
+ async def test_scpd_post(self):
+ sent = []
+ replies = {self.post_bytes: self.post_response}
+ with mock_tcp_endpoint_factory(self.loop, replies, sent_packets=sent):
+ result, raw, err = await scpd_post(
+ self.path, self.gateway_address, self.port, self.method, self.param_names, self.st, self.loop
+ )
+ self.assertEqual(None, err)
+ self.assertEqual(self.envelope, raw)
+ self.assertDictEqual({'NewExternalIPAddress': '11.22.33.44'}, result)
+
+ async def test_scpd_post_timeout(self):
+ sent = []
+ replies = {}
+ with mock_tcp_endpoint_factory(self.loop, replies, sent_packets=sent):
+ result, raw, err = await scpd_post(
+ self.path, self.gateway_address, self.port, self.method, self.param_names, self.st, self.loop
+ )
+ self.assertTrue(isinstance(err, UPnPError))
+ self.assertTrue(str(err).startswith('Timeout'))
+ self.assertEqual(b'', raw)
+ self.assertDictEqual({}, result)
+
+ async def test_scpd_post_bad_xml_response(self):
+ sent = []
+ replies = {self.post_bytes: self.bad_envelope_response}
+ with mock_tcp_endpoint_factory(self.loop, replies, sent_packets=sent):
+ result, raw, err = await scpd_post(
+ self.path, self.gateway_address, self.port, self.method, self.param_names, self.st, self.loop
+ )
+ self.assertTrue(isinstance(err, UPnPError))
+ self.assertTrue(str(err).startswith('no element found'))
+ self.assertEqual(self.bad_envelope, raw)
+ self.assertDictEqual({}, result)
+
+ async def test_scpd_post_overrun_response(self):
+ sent = []
+ replies = {self.post_bytes: self.post_response + b'\r\n'}
+ with mock_tcp_endpoint_factory(self.loop, replies, sent_packets=sent):
+ result, raw, err = await scpd_post(
+ self.path, self.gateway_address, self.port, self.method, self.param_names, self.st, self.loop
+ )
+ self.assertTrue(isinstance(err, UPnPError))
+ self.assertTrue(str(err).startswith('too many bytes written'))
+ self.assertEqual(self.post_response + b'\r\n', raw)
+ self.assertDictEqual({}, result)
diff --git a/aioupnp/protocols/test_ssdp.py b/tests/protocols/test_ssdp.py
similarity index 95%
rename from aioupnp/protocols/test_ssdp.py
rename to tests/protocols/test_ssdp.py
index 5a42330..79df31b 100644
--- a/aioupnp/protocols/test_ssdp.py
+++ b/tests/protocols/test_ssdp.py
@@ -4,7 +4,8 @@ from aioupnp.protocols.m_search_patterns import packet_generator
from aioupnp.serialization.ssdp import SSDPDatagram
from aioupnp.constants import SSDP_IP_ADDRESS
from aioupnp.protocols.ssdp import fuzzy_m_search, m_search
-from aioupnp.protocols.test_common import TestBase, async_test, mock_datagram_endpoint_factory
+from . import TestBase
+from .mocks import mock_datagram_endpoint_factory
class TestSSDP(TestBase):
@@ -28,7 +29,6 @@ class TestSSDP(TestBase):
])
reply_packet = SSDPDatagram("OK", reply_args)
- @async_test
async def test_m_search_reply_unicast(self):
replies = {
(self.query_packet.encode().encode(), ("10.0.0.1", 1900)): self.reply_packet.encode().encode()
@@ -45,7 +45,6 @@ class TestSSDP(TestBase):
with mock_datagram_endpoint_factory(self.loop, "10.0.0.1", replies=replies):
await m_search("10.0.0.2", "10.0.0.1", self.successful_args, timeout=1, loop=self.loop, unicast=False)
- @async_test
async def test_m_search_reply_multicast(self):
replies = {
(self.query_packet.encode().encode(), (SSDP_IP_ADDRESS, 1900)): self.reply_packet.encode().encode()
@@ -62,7 +61,6 @@ class TestSSDP(TestBase):
with mock_datagram_endpoint_factory(self.loop, "10.0.0.1", replies=replies):
await m_search("10.0.0.2", "10.0.0.1", self.successful_args, timeout=1, loop=self.loop, unicast=True)
- @async_test
async def test_packets_sent_fuzzy_m_search(self):
sent = []
@@ -72,7 +70,6 @@ class TestSSDP(TestBase):
self.assertListEqual(sent, self.byte_packets)
- @async_test
async def test_packets_fuzzy_m_search(self):
replies = {
(self.query_packet.encode().encode(), (SSDP_IP_ADDRESS, 1900)): self.reply_packet.encode().encode()
diff --git a/tests/serialization/__init__.py b/tests/serialization/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/aioupnp/serialization/test_scpd.py b/tests/serialization/test_scpd.py
similarity index 65%
rename from aioupnp/serialization/test_scpd.py
rename to tests/serialization/test_scpd.py
index efbae77..5617fec 100644
--- a/aioupnp/serialization/test_scpd.py
+++ b/tests/serialization/test_scpd.py
@@ -1,5 +1,7 @@
import unittest
from aioupnp.serialization.scpd import serialize_scpd_get, deserialize_scpd_get_response
+from aioupnp.device import Device
+from aioupnp.util import get_dict_val_case_insensitive
class TestSCPDSerialization(unittest.TestCase):
@@ -16,7 +18,7 @@ class TestSCPDSerialization(unittest.TestCase):
b"X-User-Agent: redsonic\r\n" \
b"CONNECTION: close\r\n" \
b"\r\n" \
- b"\n\n\n1\n0\n\n\nurn:schemas-upnp-org:device:InternetGatewayDevice:1\nCGA4131COM\nCisco\nhttp://www.cisco.com/\nCGA4131COM\nCGA4131COM\nCGA4131COM\nhttp://www.cisco.com\n\nuuid:11111111-2222-3333-4444-555555555556\nCGA4131COM\n\n\nurn:schemas-upnp-org:service:Layer3Forwarding:1\nurn:upnp-org:serviceId:L3Forwarding1\n/Layer3ForwardingSCPD.xml\n/upnp/control/Layer3Forwarding\n/upnp/event/Layer3Forwarding\n\n\n\n\nurn:schemas-upnp-org:device:WANDevice:1\nWANDevice:1\nCisco\nhttp://www.cisco.com/\nCGA4131COM\nCGA4131COM\nCGA4131COM\nhttp://www.cisco.com\n\nuuid:ebf5a0a0-1dd1-11b2-a92f-603d266f9915\nCGA4131COM\n\n\nurn:schemas-upnp-org:service:WANCommonInterfaceConfig:1\nurn:upnp-org:serviceId:WANCommonIFC1\n/WANCommonInterfaceConfigSCPD.xml\n/upnp/control/WANCommonInterfaceConfig0\n/upnp/event/WANCommonInterfaceConfig0\n\n\n\n \n urn:schemas-upnp-org:device:WANConnectionDevice:1\n WANConnectionDevice:1\n Cisco\n http://www.cisco.com/\n CGA4131COM\n CGA4131COM\n CGA4131COM\n http://www.cisco.com\n \n uuid:11111111-2222-3333-4444-555555555555\n CGA4131COM\n \n \n urn:schemas-upnp-org:service:WANIPConnection:1\n urn:upnp-org:serviceId:WANIPConn1\n /WANIPConnectionServiceSCPD.xml\n /upnp/control/WANIPConnection0\n /upnp/event/WANIPConnection0\n \n \n \n\n\n\nhttp://10.1.10.1/\n\n"
+ b"\n\n\n1\n0\n\n\nurn:schemas-upnp-org:device:InternetGatewayDevice:1\nCGA4131COM\nCisco\nhttp://www.cisco.com/\nCGA4131COM\nCGA4131COM\nCGA4131COM\nhttp://www.cisco.com\n\nuuid:11111111-2222-3333-4444-555555555556\nCGA4131COM\n\n\nurn:schemas-upnp-org:service:Layer3Forwarding:1\nurn:upnp-org:serviceId:L3Forwarding1\n/Layer3ForwardingSCPD.xml\n/upnp/control/Layer3Forwarding\n/upnp/event/Layer3Forwarding\n\n\n\n\nurn:schemas-upnp-org:device:WANDevice:1\nWANDevice:1\nCisco\nhttp://www.cisco.com/\nCGA4131COM\nCGA4131COM\nCGA4131COM\nhttp://www.cisco.com\n\nuuid:11111111-2222-3333-4444-555555555556\nCGA4131COM\n\n\nurn:schemas-upnp-org:service:WANCommonInterfaceConfig:1\nurn:upnp-org:serviceId:WANCommonIFC1\n/WANCommonInterfaceConfigSCPD.xml\n/upnp/control/WANCommonInterfaceConfig0\n/upnp/event/WANCommonInterfaceConfig0\n\n\n\n \n urn:schemas-upnp-org:device:WANConnectionDevice:1\n WANConnectionDevice:1\n Cisco\n http://www.cisco.com/\n CGA4131COM\n CGA4131COM\n CGA4131COM\n http://www.cisco.com\n \n uuid:11111111-2222-3333-4444-555555555555\n CGA4131COM\n \n \n urn:schemas-upnp-org:service:WANIPConnection:1\n urn:upnp-org:serviceId:WANIPConn1\n /WANIPConnectionServiceSCPD.xml\n /upnp/control/WANIPConnection0\n /upnp/event/WANIPConnection0\n \n \n \n\n\n\nhttp://10.1.10.1/\n\n"
expected_parsed = {
'specVersion': {'major': '1', 'minor': '0'},
@@ -50,7 +52,7 @@ class TestSCPDSerialization(unittest.TestCase):
'modelName': 'CGA4131COM',
'modelNumber': 'CGA4131COM',
'modelURL': 'http://www.cisco.com',
- 'UDN': 'uuid:ebf5a0a0-1dd1-11b2-a92f-603d266f9915',
+ 'UDN': 'uuid:11111111-2222-3333-4444-555555555556',
'UPC': 'CGA4131COM',
'serviceList': {
'service': {
@@ -98,3 +100,76 @@ class TestSCPDSerialization(unittest.TestCase):
def test_deserialize_blank(self):
self.assertDictEqual(deserialize_scpd_get_response(b''), {})
+
+ def test_deserialize_to_device_object(self):
+ devices = []
+ services = []
+ device = Device(devices, services, **get_dict_val_case_insensitive(self.expected_parsed, "device"))
+ expected_result = {
+ 'deviceType': 'urn:schemas-upnp-org:device:InternetGatewayDevice:1',
+ 'friendlyName': 'CGA4131COM',
+ 'manufacturer': 'Cisco',
+ 'manufacturerURL': 'http://www.cisco.com/',
+ 'modelDescription': 'CGA4131COM',
+ 'modelName': 'CGA4131COM',
+ 'modelNumber': 'CGA4131COM',
+ 'modelURL': 'http://www.cisco.com',
+ 'udn': 'uuid:11111111-2222-3333-4444-555555555556',
+ 'upc': 'CGA4131COM',
+ 'serviceList': {
+ 'service': {
+ 'serviceType': 'urn:schemas-upnp-org:service:Layer3Forwarding:1',
+ 'serviceId': 'urn:upnp-org:serviceId:L3Forwarding1',
+ 'SCPDURL': '/Layer3ForwardingSCPD.xml',
+ 'controlURL': '/upnp/control/Layer3Forwarding',
+ 'eventSubURL': '/upnp/event/Layer3Forwarding'
+ }
+ },
+ 'deviceList': {
+ 'device': {
+ 'deviceType': 'urn:schemas-upnp-org:device:WANDevice:1',
+ 'friendlyName': 'WANDevice:1',
+ 'manufacturer': 'Cisco',
+ 'manufacturerURL': 'http://www.cisco.com/',
+ 'modelDescription': 'CGA4131COM',
+ 'modelName': 'CGA4131COM',
+ 'modelNumber': 'CGA4131COM',
+ 'modelURL': 'http://www.cisco.com',
+ 'UDN': 'uuid:11111111-2222-3333-4444-555555555556',
+ 'UPC': 'CGA4131COM',
+ 'serviceList': {
+ 'service': {
+ 'serviceType': 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
+ 'serviceId': 'urn:upnp-org:serviceId:WANCommonIFC1',
+ 'SCPDURL': '/WANCommonInterfaceConfigSCPD.xml',
+ 'controlURL': '/upnp/control/WANCommonInterfaceConfig0',
+ 'eventSubURL': '/upnp/event/WANCommonInterfaceConfig0'
+ }
+ },
+ 'deviceList': {
+ 'device': {
+ 'deviceType': 'urn:schemas-upnp-org:device:WANConnectionDevice:1',
+ 'friendlyName': 'WANConnectionDevice:1',
+ 'manufacturer': 'Cisco',
+ 'manufacturerURL': 'http://www.cisco.com/',
+ 'modelDescription': 'CGA4131COM',
+ 'modelName': 'CGA4131COM',
+ 'modelNumber': 'CGA4131COM',
+ 'modelURL': 'http://www.cisco.com',
+ 'UDN': 'uuid:11111111-2222-3333-4444-555555555555',
+ 'UPC': 'CGA4131COM',
+ 'serviceList': {
+ 'service': {
+ 'serviceType': 'urn:schemas-upnp-org:service:WANIPConnection:1',
+ 'serviceId': 'urn:upnp-org:serviceId:WANIPConn1',
+ 'SCPDURL': '/WANIPConnectionServiceSCPD.xml',
+ 'controlURL': '/upnp/control/WANIPConnection0',
+ 'eventSubURL': '/upnp/event/WANIPConnection0'
+ }
+ }
+ }
+ }
+ }
+ }, 'presentationURL': 'http://10.1.10.1/'
+ }
+ self.assertDictEqual(expected_result, device.as_dict())
diff --git a/aioupnp/serialization/test_soap.py b/tests/serialization/test_soap.py
similarity index 64%
rename from aioupnp/serialization/test_soap.py
rename to tests/serialization/test_soap.py
index 487cf84..a238ef5 100644
--- a/aioupnp/serialization/test_soap.py
+++ b/tests/serialization/test_soap.py
@@ -1,4 +1,5 @@
import unittest
+from aioupnp.fault import UPnPError
from aioupnp.serialization.soap import serialize_soap_post, deserialize_soap_post_response
@@ -27,6 +28,16 @@ class TestSOAPSerialization(unittest.TestCase):
b"\r\n" \
b"\n\r\n11.22.33.44\r\n\r\n "
+ error_response = b"HTTP/1.1 500 Internal Server Error\r\n" \
+ b"Server: WebServer\r\n" \
+ b"Date: Thu, 11 Oct 2018 22:16:17 GMT\r\n" \
+ b"Connection: close\r\n" \
+ b"CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n" \
+ b"CONTENT-LENGTH: 482 \r\n" \
+ b"EXT:\r\n" \
+ b"\r\n" \
+ b"\n\n\t\n\t\t\n\t\t\ts:Client\n\t\t\tUPnPError\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t713\n\t\t\t\t\tSpecifiedArrayIndexInvalid\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\n"
+
def test_serialize_post(self):
self.assertEqual(serialize_soap_post(
self.method, self.param_names, self.st, self.gateway_address, self.path, **self.kwargs
@@ -37,3 +48,12 @@ class TestSOAPSerialization(unittest.TestCase):
deserialize_soap_post_response(self.post_response, self.method, service_id=self.st.decode()),
{'NewExternalIPAddress': '11.22.33.44'}
)
+
+ def test_raise_from_error_response(self):
+ raised = False
+ try:
+ deserialize_soap_post_response(self.error_response, self.method, service_id=self.st.decode())
+ except UPnPError as err:
+ raised = True
+ self.assertTrue(str(err) == 'SpecifiedArrayIndexInvalid')
+ self.assertTrue(raised)
diff --git a/aioupnp/serialization/test_ssdp.py b/tests/serialization/test_ssdp.py
similarity index 100%
rename from aioupnp/serialization/test_ssdp.py
rename to tests/serialization/test_ssdp.py
diff --git a/tests/test_case_insensitive.py b/tests/test_case_insensitive.py
new file mode 100644
index 0000000..2b40b65
--- /dev/null
+++ b/tests/test_case_insensitive.py
@@ -0,0 +1,45 @@
+import unittest
+from aioupnp.device import CaseInsensitive
+
+
+class TestService(CaseInsensitive):
+ serviceType = None
+ serviceId = None
+ controlURL = None
+ eventSubURL = None
+ SCPDURL = None
+
+
+class TestCaseInsensitive(unittest.TestCase):
+ def test_initialize(self):
+ s = TestService(
+ serviceType="test", serviceId="test id", controlURL="/test", eventSubURL="/test2", SCPDURL="/test3"
+ )
+ self.assertEqual('test', getattr(s, 'serviceType'))
+ self.assertEqual('test', getattr(s, 'servicetype'))
+ self.assertEqual('test', getattr(s, 'SERVICETYPE'))
+
+ s = TestService(
+ servicetype="test", serviceid="test id", controlURL="/test", eventSubURL="/test2", SCPDURL="/test3"
+ )
+ self.assertEqual('test', getattr(s, 'serviceType'))
+ self.assertEqual('test', getattr(s, 'servicetype'))
+ self.assertEqual('test', getattr(s, 'SERVICETYPE'))
+
+ self.assertDictEqual({
+ 'serviceType': 'test',
+ 'serviceId': 'test id',
+ 'controlURL': "/test",
+ 'eventSubURL': "/test2",
+ 'SCPDURL': "/test3"
+ }, s.as_dict())
+
+ def test_set_attr(self):
+ s = TestService(
+ serviceType="test", serviceId="test id", controlURL="/test", eventSubURL="/test2", SCPDURL="/test3"
+ )
+ self.assertEqual('test', getattr(s, 'serviceType'))
+ s.servicetype = 'foo'
+ self.assertEqual('foo', getattr(s, 'serviceType'))
+ self.assertEqual('foo', getattr(s, 'servicetype'))
+ self.assertEqual('foo', getattr(s, 'SERVICETYPE'))