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'))