test ssdp
This commit is contained in:
parent
1cfe84dcef
commit
e9757666ab
7 changed files with 170 additions and 25 deletions
|
@ -9,7 +9,7 @@ jobs:
|
||||||
name: "mypy"
|
name: "mypy"
|
||||||
before_install:
|
before_install:
|
||||||
- pip install mypy lxml
|
- pip install mypy lxml
|
||||||
- pip install -e .
|
- pip install -e .[test]
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- mypy . --txt-report . --scripts-are-modules; cat index.txt; rm index.txt
|
- mypy . --txt-report . --scripts-are-modules; cat index.txt; rm index.txt
|
||||||
|
@ -19,7 +19,7 @@ jobs:
|
||||||
name: "Unit Tests w/ Python 3.7"
|
name: "Unit Tests w/ Python 3.7"
|
||||||
before_install:
|
before_install:
|
||||||
- pip install pylint coverage
|
- pip install pylint coverage
|
||||||
- pip install -e .
|
- pip install -e .[test]
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- HOME=/tmp coverage run --source=aioupnp -m unittest -v
|
- HOME=/tmp coverage run --source=aioupnp -m unittest -v
|
||||||
|
|
|
@ -271,7 +271,6 @@ class Gateway:
|
||||||
name, param_types, return_types, inputs, outputs, soap_socket)
|
name, param_types, return_types, inputs, outputs, soap_socket)
|
||||||
setattr(command, "__doc__", current.__doc__)
|
setattr(command, "__doc__", current.__doc__)
|
||||||
setattr(self.commands, command.method, command)
|
setattr(self.commands, command.method, command)
|
||||||
|
|
||||||
self._registered_commands[command.method] = service.serviceType
|
self._registered_commands[command.method] = service.serviceType
|
||||||
log.debug("registered %s::%s", service.serviceType, command.method)
|
log.debug("registered %s::%s", service.serviceType, command.method)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import re
|
import re
|
||||||
import socket
|
|
||||||
import binascii
|
import binascii
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
@ -45,8 +44,6 @@ class SSDPProtocol(MulticastProtocol):
|
||||||
a, s = t[0], t[1]
|
a, s = t[0], t[1]
|
||||||
if (address == a) and (s in [packet.st, "upnp:rootdevice"]):
|
if (address == a) and (s in [packet.st, "upnp:rootdevice"]):
|
||||||
f: Future = t[2]
|
f: Future = t[2]
|
||||||
# h: asyncio.Handle = t[3]
|
|
||||||
# h.cancel()
|
|
||||||
if f not in set_futures:
|
if f not in set_futures:
|
||||||
set_futures.append(f)
|
set_futures.append(f)
|
||||||
if not f.done():
|
if not f.done():
|
||||||
|
@ -68,7 +65,6 @@ class SSDPProtocol(MulticastProtocol):
|
||||||
for datagram in datagrams:
|
for datagram in datagrams:
|
||||||
packet = SSDPDatagram(SSDPDatagram._M_SEARCH, datagram)
|
packet = SSDPDatagram(SSDPDatagram._M_SEARCH, datagram)
|
||||||
assert packet.st is not None
|
assert packet.st is not None
|
||||||
# h = asyncio.get_running_loop().call_later(timeout, fut.cancel)
|
|
||||||
self._pending_searches.append((address, packet.st, fut))
|
self._pending_searches.append((address, packet.st, fut))
|
||||||
packets.append(packet)
|
packets.append(packet)
|
||||||
self.send_many_m_searches(address, packets),
|
self.send_many_m_searches(address, packets),
|
||||||
|
@ -108,18 +104,19 @@ class SSDPProtocol(MulticastProtocol):
|
||||||
# return
|
# return
|
||||||
|
|
||||||
|
|
||||||
async def listen_ssdp(lan_address: str, gateway_address: str, ssdp_socket: socket.socket = None,
|
async def listen_ssdp(lan_address: str, gateway_address: str, loop=None,
|
||||||
ignored: typing.Set[str] = None, unicast: bool = False) -> typing.Tuple[DatagramTransport,
|
ignored: typing.Set[str] = None, unicast: bool = False) -> typing.Tuple[DatagramTransport,
|
||||||
SSDPProtocol, str, str]:
|
SSDPProtocol, str, str]:
|
||||||
loop = asyncio.get_event_loop_policy().get_event_loop()
|
loop = loop or asyncio.get_event_loop_policy().get_event_loop()
|
||||||
try:
|
try:
|
||||||
sock = ssdp_socket or SSDPProtocol.create_multicast_socket(lan_address)
|
sock = SSDPProtocol.create_multicast_socket(lan_address)
|
||||||
listen_result: typing.Tuple = await loop.create_datagram_endpoint(
|
listen_result: typing.Tuple = await loop.create_datagram_endpoint(
|
||||||
lambda: SSDPProtocol(SSDP_IP_ADDRESS, lan_address, ignored, unicast), sock=sock
|
lambda: SSDPProtocol(SSDP_IP_ADDRESS, lan_address, ignored, unicast), sock=sock
|
||||||
)
|
)
|
||||||
transport: DatagramTransport = listen_result[0]
|
transport: DatagramTransport = listen_result[0]
|
||||||
protocol: SSDPProtocol = listen_result[1]
|
protocol: SSDPProtocol = listen_result[1]
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
print(err)
|
||||||
raise UPnPError(err)
|
raise UPnPError(err)
|
||||||
try:
|
try:
|
||||||
protocol.join_group(protocol.multicast_address, protocol.bind_address)
|
protocol.join_group(protocol.multicast_address, protocol.bind_address)
|
||||||
|
@ -132,24 +129,25 @@ async def listen_ssdp(lan_address: str, gateway_address: str, ssdp_socket: socke
|
||||||
|
|
||||||
|
|
||||||
async def m_search(lan_address: str, gateway_address: str, datagram_args: OrderedDict, timeout: int = 1,
|
async def m_search(lan_address: str, gateway_address: str, datagram_args: OrderedDict, timeout: int = 1,
|
||||||
ssdp_socket: socket.socket = None, ignored: typing.Set[str] = None,
|
loop=None, ignored: typing.Set[str] = None,
|
||||||
unicast: bool = False) -> SSDPDatagram:
|
unicast: bool = False) -> SSDPDatagram:
|
||||||
transport, protocol, gateway_address, lan_address = await listen_ssdp(
|
transport, protocol, gateway_address, lan_address = await listen_ssdp(
|
||||||
lan_address, gateway_address, ssdp_socket, ignored, unicast
|
lan_address, gateway_address, loop, ignored, unicast
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return await protocol.m_search(address=gateway_address, timeout=timeout, datagrams=[datagram_args])
|
return await asyncio.wait_for(
|
||||||
|
protocol.m_search(address=gateway_address, timeout=timeout, datagrams=[datagram_args]), timeout
|
||||||
|
)
|
||||||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||||||
raise UPnPError("M-SEARCH for {}:{} timed out".format(gateway_address, SSDP_PORT))
|
raise UPnPError("M-SEARCH for {}:{} timed out".format(gateway_address, SSDP_PORT))
|
||||||
finally:
|
finally:
|
||||||
protocol.disconnect()
|
protocol.disconnect()
|
||||||
|
|
||||||
|
|
||||||
async def _fuzzy_m_search(lan_address: str, gateway_address: str, timeout: int = 30,
|
async def _fuzzy_m_search(lan_address: str, gateway_address: str, timeout: int = 30, loop=None,
|
||||||
ssdp_socket: socket.socket = None,
|
|
||||||
ignored: typing.Set[str] = None, unicast: bool = False) -> typing.List[OrderedDict]:
|
ignored: typing.Set[str] = None, unicast: bool = False) -> typing.List[OrderedDict]:
|
||||||
transport, protocol, gateway_address, lan_address = await listen_ssdp(
|
transport, protocol, gateway_address, lan_address = await listen_ssdp(
|
||||||
lan_address, gateway_address, ssdp_socket, ignored, unicast
|
lan_address, gateway_address, loop, ignored, unicast
|
||||||
)
|
)
|
||||||
packet_args = list(packet_generator())
|
packet_args = list(packet_generator())
|
||||||
batch_size = 2
|
batch_size = 2
|
||||||
|
@ -168,17 +166,15 @@ async def _fuzzy_m_search(lan_address: str, gateway_address: str, timeout: int =
|
||||||
raise UPnPError("M-SEARCH for {}:{} timed out".format(gateway_address, SSDP_PORT))
|
raise UPnPError("M-SEARCH for {}:{} timed out".format(gateway_address, SSDP_PORT))
|
||||||
|
|
||||||
|
|
||||||
async def fuzzy_m_search(lan_address: str, gateway_address: str, timeout: int = 30,
|
async def fuzzy_m_search(lan_address: str, gateway_address: str, timeout: int = 30, loop=None,
|
||||||
ssdp_socket: socket.socket = None,
|
|
||||||
ignored: typing.Set[str] = None, unicast: bool = False) -> typing.Tuple[OrderedDict,
|
ignored: typing.Set[str] = None, unicast: bool = False) -> typing.Tuple[OrderedDict,
|
||||||
SSDPDatagram]:
|
SSDPDatagram]:
|
||||||
# we don't know which packet the gateway replies to, so send small batches at a time
|
# we don't know which packet the gateway replies to, so send small batches at a time
|
||||||
|
args_to_try = await _fuzzy_m_search(lan_address, gateway_address, timeout, loop, ignored, unicast)
|
||||||
args_to_try = await _fuzzy_m_search(lan_address, gateway_address, timeout, ssdp_socket, ignored, unicast)
|
|
||||||
# check the args in the batch that got a reply one at a time to see which one worked
|
# check the args in the batch that got a reply one at a time to see which one worked
|
||||||
for args in args_to_try:
|
for args in args_to_try:
|
||||||
try:
|
try:
|
||||||
packet = await m_search(lan_address, gateway_address, args, 3, ignored=ignored, unicast=unicast)
|
packet = await m_search(lan_address, gateway_address, args, 3, loop=loop, ignored=ignored, unicast=unicast)
|
||||||
return args, packet
|
return args, packet
|
||||||
except UPnPError:
|
except UPnPError:
|
||||||
continue
|
continue
|
||||||
|
|
60
aioupnp/protocols/test_common.py
Normal file
60
aioupnp/protocols/test_common.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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
|
||||||
|
def mock_datagram_endpoint_factory(loop, expected_addr, replies=None, delay_reply=0.0, sent_packets=None):
|
||||||
|
sent_packets = sent_packets if sent_packets is not None else []
|
||||||
|
replies = replies or {}
|
||||||
|
|
||||||
|
def sendto(p: asyncio.DatagramProtocol):
|
||||||
|
def _sendto(data, addr):
|
||||||
|
sent_packets.append(data)
|
||||||
|
if (data, addr) in replies:
|
||||||
|
loop.call_later(delay_reply, p.datagram_received, replies[(data, addr)], (expected_addr, 1900))
|
||||||
|
return _sendto
|
||||||
|
|
||||||
|
async def create_datagram_endpoint(proto_lam, sock=None):
|
||||||
|
protocol = proto_lam()
|
||||||
|
transport = asyncio.DatagramTransport(extra={'socket': mock_sock})
|
||||||
|
transport.close = lambda: mock_sock.close()
|
||||||
|
mock_sock.sendto = sendto(protocol)
|
||||||
|
transport.sendto = mock_sock.sendto
|
||||||
|
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_DGRAM
|
||||||
|
mock_sock.fileno = lambda: 7
|
||||||
|
|
||||||
|
mock_socket.return_value = mock_sock
|
||||||
|
loop.create_datagram_endpoint = create_datagram_endpoint
|
||||||
|
yield
|
86
aioupnp/protocols/test_ssdp.py
Normal file
86
aioupnp/protocols/test_ssdp.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
from collections import OrderedDict
|
||||||
|
from aioupnp.fault import UPnPError
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class TestSSDP(TestBase):
|
||||||
|
packet_args = list(packet_generator())
|
||||||
|
byte_packets = [SSDPDatagram("M-SEARCH", p).encode().encode() for p in packet_args]
|
||||||
|
|
||||||
|
successful_args = OrderedDict([
|
||||||
|
("HOST", "239.255.255.250:1900"),
|
||||||
|
("MAN", "ssdp:discover"),
|
||||||
|
("MX", 1),
|
||||||
|
("ST", "urn:schemas-upnp-org:device:WANDevice:1")
|
||||||
|
])
|
||||||
|
query_packet = SSDPDatagram("M-SEARCH", successful_args)
|
||||||
|
|
||||||
|
reply_args = 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:22222222-3333-4444-5555-666666666666::urn:schemas-upnp-org:device:WANDevice:1")
|
||||||
|
])
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
sent = []
|
||||||
|
|
||||||
|
with mock_datagram_endpoint_factory(self.loop, "10.0.0.1", replies=replies, sent_packets=sent):
|
||||||
|
reply = await m_search("10.0.0.2", "10.0.0.1", self.successful_args, timeout=1, loop=self.loop, unicast=True)
|
||||||
|
|
||||||
|
self.assertEqual(reply.encode(), self.reply_packet.encode())
|
||||||
|
self.assertListEqual(sent, [self.query_packet.encode().encode()])
|
||||||
|
|
||||||
|
with self.assertRaises(UPnPError):
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
sent = []
|
||||||
|
|
||||||
|
with mock_datagram_endpoint_factory(self.loop, "10.0.0.1", replies=replies, sent_packets=sent):
|
||||||
|
reply = await m_search("10.0.0.2", "10.0.0.1", self.successful_args, timeout=1, loop=self.loop)
|
||||||
|
|
||||||
|
self.assertEqual(reply.encode(), self.reply_packet.encode())
|
||||||
|
self.assertListEqual(sent, [self.query_packet.encode().encode()])
|
||||||
|
|
||||||
|
with self.assertRaises(UPnPError):
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
with self.assertRaises(UPnPError):
|
||||||
|
with mock_datagram_endpoint_factory(self.loop, "10.0.0.1", sent_packets=sent):
|
||||||
|
await fuzzy_m_search("10.0.0.2", "10.0.0.1", 1, self.loop)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
sent = []
|
||||||
|
|
||||||
|
with mock_datagram_endpoint_factory(self.loop, "10.0.0.1", replies=replies, sent_packets=sent):
|
||||||
|
args, reply = await fuzzy_m_search("10.0.0.2", "10.0.0.1", 1, self.loop)
|
||||||
|
|
||||||
|
self.assertEqual(reply.encode(), self.reply_packet.encode())
|
||||||
|
self.assertEqual(args, self.successful_args)
|
|
@ -61,8 +61,7 @@ class UPnP:
|
||||||
@classmethod
|
@classmethod
|
||||||
@cli
|
@cli
|
||||||
async def m_search(cls, lan_address: str = '', gateway_address: str = '', timeout: int = 1,
|
async def m_search(cls, lan_address: str = '', gateway_address: str = '', timeout: int = 1,
|
||||||
igd_args: OrderedDict = None, interface_name: str = 'default',
|
igd_args: OrderedDict = None, interface_name: str = 'default') -> Dict:
|
||||||
ssdp_socket: socket.socket = None) -> Dict:
|
|
||||||
try:
|
try:
|
||||||
lan_address, gateway_address = cls.get_lan_and_gateway(lan_address, gateway_address, interface_name)
|
lan_address, gateway_address = cls.get_lan_and_gateway(lan_address, gateway_address, interface_name)
|
||||||
assert gateway_address and lan_address
|
assert gateway_address and lan_address
|
||||||
|
@ -70,10 +69,10 @@ class UPnP:
|
||||||
raise UPnPError("failed to get lan and gateway addresses for interface \"%s\": %s" % (interface_name,
|
raise UPnPError("failed to get lan and gateway addresses for interface \"%s\": %s" % (interface_name,
|
||||||
str(err)))
|
str(err)))
|
||||||
if not igd_args:
|
if not igd_args:
|
||||||
igd_args, datagram = await fuzzy_m_search(lan_address, gateway_address, timeout, ssdp_socket)
|
igd_args, datagram = await fuzzy_m_search(lan_address, gateway_address, timeout)
|
||||||
else:
|
else:
|
||||||
igd_args = OrderedDict(igd_args)
|
igd_args = OrderedDict(igd_args)
|
||||||
datagram = await m_search(lan_address, gateway_address, igd_args, timeout, ssdp_socket)
|
datagram = await m_search(lan_address, gateway_address, igd_args, timeout)
|
||||||
return {
|
return {
|
||||||
'lan_address': lan_address,
|
'lan_address': lan_address,
|
||||||
'gateway_address': gateway_address,
|
'gateway_address': gateway_address,
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -27,4 +27,9 @@ setup(
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'netifaces',
|
'netifaces',
|
||||||
],
|
],
|
||||||
|
extras_require={
|
||||||
|
'test': (
|
||||||
|
'mock',
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue