uh oh im bundling the deps
This commit is contained in:
parent
ae28da8d60
commit
ecca301ceb
584 changed files with 119933 additions and 24 deletions
21
resources/lib/deps/zeroconf/_utils/__init__.py
Normal file
21
resources/lib/deps/zeroconf/_utils/__init__.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
137
resources/lib/deps/zeroconf/_utils/asyncio.py
Normal file
137
resources/lib/deps/zeroconf/_utils/asyncio.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import contextlib
|
||||
import sys
|
||||
from typing import Any, Awaitable, Coroutine, Optional, Set
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from async_timeout import timeout as asyncio_timeout
|
||||
else:
|
||||
from asyncio import timeout as asyncio_timeout
|
||||
|
||||
from .._exceptions import EventLoopBlocked
|
||||
from ..const import _LOADED_SYSTEM_TIMEOUT
|
||||
from .time import millis_to_seconds
|
||||
|
||||
# The combined timeouts should be lower than _CLOSE_TIMEOUT + _WAIT_FOR_LOOP_TASKS_TIMEOUT
|
||||
_TASK_AWAIT_TIMEOUT = 1
|
||||
_GET_ALL_TASKS_TIMEOUT = 3
|
||||
_WAIT_FOR_LOOP_TASKS_TIMEOUT = 3 # Must be larger than _TASK_AWAIT_TIMEOUT
|
||||
|
||||
|
||||
def _set_future_none_if_not_done(fut: asyncio.Future) -> None:
|
||||
"""Set a future to None if it is not done."""
|
||||
if not fut.done(): # pragma: no branch
|
||||
fut.set_result(None)
|
||||
|
||||
|
||||
def _resolve_all_futures_to_none(futures: Set[asyncio.Future]) -> None:
|
||||
"""Resolve all futures to None."""
|
||||
for fut in futures:
|
||||
_set_future_none_if_not_done(fut)
|
||||
futures.clear()
|
||||
|
||||
|
||||
async def wait_for_future_set_or_timeout(
|
||||
loop: asyncio.AbstractEventLoop, future_set: Set[asyncio.Future], timeout: float
|
||||
) -> None:
|
||||
"""Wait for a future or timeout (in milliseconds)."""
|
||||
future = loop.create_future()
|
||||
future_set.add(future)
|
||||
handle = loop.call_later(millis_to_seconds(timeout), _set_future_none_if_not_done, future)
|
||||
try:
|
||||
await future
|
||||
finally:
|
||||
handle.cancel()
|
||||
future_set.discard(future)
|
||||
|
||||
|
||||
async def wait_event_or_timeout(event: asyncio.Event, timeout: float) -> None:
|
||||
"""Wait for an event or timeout."""
|
||||
with contextlib.suppress(asyncio.TimeoutError):
|
||||
async with asyncio_timeout(timeout):
|
||||
await event.wait()
|
||||
|
||||
|
||||
async def _async_get_all_tasks(loop: asyncio.AbstractEventLoop) -> Set[asyncio.Task]:
|
||||
"""Return all tasks running."""
|
||||
await asyncio.sleep(0) # flush out any call_soon_threadsafe
|
||||
# If there are multiple event loops running, all_tasks is not
|
||||
# safe EVEN WHEN CALLED FROM THE EVENTLOOP
|
||||
# under PyPy so we have to try a few times.
|
||||
for _ in range(3):
|
||||
with contextlib.suppress(RuntimeError):
|
||||
return asyncio.all_tasks(loop)
|
||||
return set()
|
||||
|
||||
|
||||
async def _wait_for_loop_tasks(wait_tasks: Set[asyncio.Task]) -> None:
|
||||
"""Wait for the event loop thread we started to shutdown."""
|
||||
await asyncio.wait(wait_tasks, timeout=_TASK_AWAIT_TIMEOUT)
|
||||
|
||||
|
||||
async def await_awaitable(aw: Awaitable) -> None:
|
||||
"""Wait on an awaitable and the task it returns."""
|
||||
task = await aw
|
||||
await task
|
||||
|
||||
|
||||
def run_coro_with_timeout(aw: Coroutine, loop: asyncio.AbstractEventLoop, timeout: float) -> Any:
|
||||
"""Run a coroutine with a timeout.
|
||||
|
||||
The timeout should only be used as a safeguard to prevent
|
||||
the program from blocking forever. The timeout should
|
||||
never be expected to be reached during normal operation.
|
||||
|
||||
While not expected during normal operations, the
|
||||
function raises `EventLoopBlocked` if the coroutine takes
|
||||
longer to complete than the timeout.
|
||||
"""
|
||||
try:
|
||||
return asyncio.run_coroutine_threadsafe(aw, loop).result(
|
||||
millis_to_seconds(timeout) + _LOADED_SYSTEM_TIMEOUT
|
||||
)
|
||||
except concurrent.futures.TimeoutError as ex:
|
||||
raise EventLoopBlocked from ex
|
||||
|
||||
|
||||
def shutdown_loop(loop: asyncio.AbstractEventLoop) -> None:
|
||||
"""Wait for pending tasks and stop an event loop."""
|
||||
pending_tasks = set(
|
||||
asyncio.run_coroutine_threadsafe(_async_get_all_tasks(loop), loop).result(_GET_ALL_TASKS_TIMEOUT)
|
||||
)
|
||||
pending_tasks -= {task for task in pending_tasks if task.done()}
|
||||
if pending_tasks:
|
||||
asyncio.run_coroutine_threadsafe(_wait_for_loop_tasks(pending_tasks), loop).result(
|
||||
_WAIT_FOR_LOOP_TASKS_TIMEOUT
|
||||
)
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
|
||||
|
||||
def get_running_loop() -> Optional[asyncio.AbstractEventLoop]:
|
||||
"""Check if an event loop is already running."""
|
||||
with contextlib.suppress(RuntimeError):
|
||||
return asyncio.get_running_loop()
|
||||
return None
|
BIN
resources/lib/deps/zeroconf/_utils/ipaddress.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
resources/lib/deps/zeroconf/_utils/ipaddress.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
14
resources/lib/deps/zeroconf/_utils/ipaddress.pxd
Normal file
14
resources/lib/deps/zeroconf/_utils/ipaddress.pxd
Normal file
|
@ -0,0 +1,14 @@
|
|||
cdef bint TYPE_CHECKING
|
||||
cdef bint IPADDRESS_SUPPORTS_SCOPE_ID
|
||||
|
||||
from .._dns cimport DNSAddress
|
||||
|
||||
|
||||
cpdef get_ip_address_object_from_record(DNSAddress record)
|
||||
|
||||
@cython.locals(address_str=str)
|
||||
cpdef str_without_scope_id(object addr)
|
||||
|
||||
cpdef ip_bytes_and_scope_to_address(object addr, object scope_id)
|
||||
|
||||
cdef object cached_ip_addresses_wrapper
|
134
resources/lib/deps/zeroconf/_utils/ipaddress.py
Normal file
134
resources/lib/deps/zeroconf/_utils/ipaddress.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from ipaddress import AddressValueError, IPv4Address, IPv6Address, NetmaskValueError
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from .._dns import DNSAddress
|
||||
from ..const import _TYPE_AAAA
|
||||
|
||||
bytes_ = bytes
|
||||
int_ = int
|
||||
IPADDRESS_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0)
|
||||
|
||||
|
||||
class ZeroconfIPv4Address(IPv4Address):
|
||||
|
||||
__slots__ = ("_str", "_is_link_local", "_is_unspecified")
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize a new IPv4 address."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._str = super().__str__()
|
||||
self._is_link_local = super().is_link_local
|
||||
self._is_unspecified = super().is_unspecified
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the string representation of the IPv4 address."""
|
||||
return self._str
|
||||
|
||||
@property
|
||||
def is_link_local(self) -> bool:
|
||||
"""Return True if this is a link-local address."""
|
||||
return self._is_link_local
|
||||
|
||||
@property
|
||||
def is_unspecified(self) -> bool:
|
||||
"""Return True if this is an unspecified address."""
|
||||
return self._is_unspecified
|
||||
|
||||
|
||||
class ZeroconfIPv6Address(IPv6Address):
|
||||
|
||||
__slots__ = ("_str", "_is_link_local", "_is_unspecified")
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize a new IPv6 address."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._str = super().__str__()
|
||||
self._is_link_local = super().is_link_local
|
||||
self._is_unspecified = super().is_unspecified
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the string representation of the IPv6 address."""
|
||||
return self._str
|
||||
|
||||
@property
|
||||
def is_link_local(self) -> bool:
|
||||
"""Return True if this is a link-local address."""
|
||||
return self._is_link_local
|
||||
|
||||
@property
|
||||
def is_unspecified(self) -> bool:
|
||||
"""Return True if this is an unspecified address."""
|
||||
return self._is_unspecified
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def _cached_ip_addresses(address: Union[str, bytes, int]) -> Optional[Union[IPv4Address, IPv6Address]]:
|
||||
"""Cache IP addresses."""
|
||||
try:
|
||||
return ZeroconfIPv4Address(address)
|
||||
except (AddressValueError, NetmaskValueError):
|
||||
pass
|
||||
|
||||
try:
|
||||
return ZeroconfIPv6Address(address)
|
||||
except (AddressValueError, NetmaskValueError):
|
||||
return None
|
||||
|
||||
|
||||
cached_ip_addresses_wrapper = _cached_ip_addresses
|
||||
cached_ip_addresses = cached_ip_addresses_wrapper
|
||||
|
||||
|
||||
def get_ip_address_object_from_record(record: DNSAddress) -> Optional[Union[IPv4Address, IPv6Address]]:
|
||||
"""Get the IP address object from the record."""
|
||||
if IPADDRESS_SUPPORTS_SCOPE_ID and record.type == _TYPE_AAAA and record.scope_id is not None:
|
||||
return ip_bytes_and_scope_to_address(record.address, record.scope_id)
|
||||
return cached_ip_addresses_wrapper(record.address)
|
||||
|
||||
|
||||
def ip_bytes_and_scope_to_address(address: bytes_, scope: int_) -> Optional[Union[IPv4Address, IPv6Address]]:
|
||||
"""Convert the bytes and scope to an IP address object."""
|
||||
base_address = cached_ip_addresses_wrapper(address)
|
||||
if base_address is not None and base_address.is_link_local:
|
||||
# Avoid expensive __format__ call by using PyUnicode_Join
|
||||
return cached_ip_addresses_wrapper("".join((str(base_address), "%", str(scope))))
|
||||
return base_address
|
||||
|
||||
|
||||
def str_without_scope_id(addr: Union[IPv4Address, IPv6Address]) -> str:
|
||||
"""Return the string representation of the address without the scope id."""
|
||||
if IPADDRESS_SUPPORTS_SCOPE_ID and addr.version == 6:
|
||||
address_str = str(addr)
|
||||
return address_str.partition('%')[0]
|
||||
return str(addr)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"cached_ip_addresses",
|
||||
"get_ip_address_object_from_record",
|
||||
"ip_bytes_and_scope_to_address",
|
||||
"str_without_scope_id",
|
||||
)
|
177
resources/lib/deps/zeroconf/_utils/name.py
Normal file
177
resources/lib/deps/zeroconf/_utils/name.py
Normal file
|
@ -0,0 +1,177 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Set
|
||||
|
||||
from .._exceptions import BadTypeInNameException
|
||||
from ..const import (
|
||||
_HAS_A_TO_Z,
|
||||
_HAS_ASCII_CONTROL_CHARS,
|
||||
_HAS_ONLY_A_TO_Z_NUM_HYPHEN,
|
||||
_HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE,
|
||||
_LOCAL_TRAILER,
|
||||
_NONTCP_PROTOCOL_LOCAL_TRAILER,
|
||||
_TCP_PROTOCOL_LOCAL_TRAILER,
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def service_type_name(type_: str, *, strict: bool = True) -> str: # pylint: disable=too-many-branches
|
||||
"""
|
||||
Validate a fully qualified service name, instance or subtype. [rfc6763]
|
||||
|
||||
Returns fully qualified service name.
|
||||
|
||||
Domain names used by mDNS-SD take the following forms:
|
||||
|
||||
<sn> . <_tcp|_udp> . local.
|
||||
<Instance> . <sn> . <_tcp|_udp> . local.
|
||||
<sub>._sub . <sn> . <_tcp|_udp> . local.
|
||||
|
||||
1) must end with 'local.'
|
||||
|
||||
This is true because we are implementing mDNS and since the 'm' means
|
||||
multi-cast, the 'local.' domain is mandatory.
|
||||
|
||||
2) local is preceded with either '_udp.' or '_tcp.' unless
|
||||
strict is False
|
||||
|
||||
3) service name <sn> precedes <_tcp|_udp> unless
|
||||
strict is False
|
||||
|
||||
The rules for Service Names [RFC6335] state that they may be no more
|
||||
than fifteen characters long (not counting the mandatory underscore),
|
||||
consisting of only letters, digits, and hyphens, must begin and end
|
||||
with a letter or digit, must not contain consecutive hyphens, and
|
||||
must contain at least one letter.
|
||||
|
||||
The instance name <Instance> and sub type <sub> may be up to 63 bytes.
|
||||
|
||||
The portion of the Service Instance Name is a user-
|
||||
friendly name consisting of arbitrary Net-Unicode text [RFC5198]. It
|
||||
MUST NOT contain ASCII control characters (byte values 0x00-0x1F and
|
||||
0x7F) [RFC20] but otherwise is allowed to contain any characters,
|
||||
without restriction, including spaces, uppercase, lowercase,
|
||||
punctuation -- including dots -- accented characters, non-Roman text,
|
||||
and anything else that may be represented using Net-Unicode.
|
||||
|
||||
:param type_: Type, SubType or service name to validate
|
||||
:return: fully qualified service name (eg: _http._tcp.local.)
|
||||
"""
|
||||
if len(type_) > 256:
|
||||
# https://datatracker.ietf.org/doc/html/rfc6763#section-7.2
|
||||
raise BadTypeInNameException("Full name (%s) must be > 256 bytes" % type_)
|
||||
|
||||
if type_.endswith((_TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)):
|
||||
remaining = type_[: -len(_TCP_PROTOCOL_LOCAL_TRAILER)].split('.')
|
||||
trailer = type_[-len(_TCP_PROTOCOL_LOCAL_TRAILER) :]
|
||||
has_protocol = True
|
||||
elif strict:
|
||||
raise BadTypeInNameException(
|
||||
"Type '%s' must end with '%s' or '%s'"
|
||||
% (type_, _TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)
|
||||
)
|
||||
elif type_.endswith(_LOCAL_TRAILER):
|
||||
remaining = type_[: -len(_LOCAL_TRAILER)].split('.')
|
||||
trailer = type_[-len(_LOCAL_TRAILER) + 1 :]
|
||||
has_protocol = False
|
||||
else:
|
||||
raise BadTypeInNameException(f"Type '{type_}' must end with '{_LOCAL_TRAILER}'")
|
||||
|
||||
if strict or has_protocol:
|
||||
service_name = remaining.pop()
|
||||
if not service_name:
|
||||
raise BadTypeInNameException("No Service name found")
|
||||
|
||||
if len(remaining) == 1 and len(remaining[0]) == 0:
|
||||
raise BadTypeInNameException("Type '%s' must not start with '.'" % type_)
|
||||
|
||||
if service_name[0] != '_':
|
||||
raise BadTypeInNameException("Service name (%s) must start with '_'" % service_name)
|
||||
|
||||
test_service_name = service_name[1:]
|
||||
|
||||
if strict and len(test_service_name) > 15:
|
||||
# https://datatracker.ietf.org/doc/html/rfc6763#section-7.2
|
||||
raise BadTypeInNameException("Service name (%s) must be <= 15 bytes" % test_service_name)
|
||||
|
||||
if '--' in test_service_name:
|
||||
raise BadTypeInNameException("Service name (%s) must not contain '--'" % test_service_name)
|
||||
|
||||
if '-' in (test_service_name[0], test_service_name[-1]):
|
||||
raise BadTypeInNameException(
|
||||
"Service name (%s) may not start or end with '-'" % test_service_name
|
||||
)
|
||||
|
||||
if not _HAS_A_TO_Z.search(test_service_name):
|
||||
raise BadTypeInNameException(
|
||||
"Service name (%s) must contain at least one letter (eg: 'A-Z')" % test_service_name
|
||||
)
|
||||
|
||||
allowed_characters_re = (
|
||||
_HAS_ONLY_A_TO_Z_NUM_HYPHEN if strict else _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE
|
||||
)
|
||||
|
||||
if not allowed_characters_re.search(test_service_name):
|
||||
raise BadTypeInNameException(
|
||||
"Service name (%s) must contain only these characters: "
|
||||
"A-Z, a-z, 0-9, hyphen ('-')%s" % (test_service_name, "" if strict else ", underscore ('_')")
|
||||
)
|
||||
else:
|
||||
service_name = ''
|
||||
|
||||
if remaining and remaining[-1] == '_sub':
|
||||
remaining.pop()
|
||||
if len(remaining) == 0 or len(remaining[0]) == 0:
|
||||
raise BadTypeInNameException("_sub requires a subtype name")
|
||||
|
||||
if len(remaining) > 1:
|
||||
remaining = ['.'.join(remaining)]
|
||||
|
||||
if remaining:
|
||||
length = len(remaining[0].encode('utf-8'))
|
||||
if length > 63:
|
||||
raise BadTypeInNameException("Too long: '%s'" % remaining[0])
|
||||
|
||||
if _HAS_ASCII_CONTROL_CHARS.search(remaining[0]):
|
||||
raise BadTypeInNameException(
|
||||
"Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" % remaining[0]
|
||||
)
|
||||
|
||||
return service_name + trailer
|
||||
|
||||
|
||||
def possible_types(name: str) -> Set[str]:
|
||||
"""Build a set of all possible types from a fully qualified name."""
|
||||
labels = name.split('.')
|
||||
label_count = len(labels)
|
||||
types = set()
|
||||
for count in range(label_count):
|
||||
parts = labels[label_count - count - 4 :]
|
||||
if not parts[0].startswith('_'):
|
||||
break
|
||||
types.add('.'.join(parts))
|
||||
return types
|
||||
|
||||
|
||||
cached_possible_types = lru_cache(maxsize=256)(possible_types)
|
418
resources/lib/deps/zeroconf/_utils/net.py
Normal file
418
resources/lib/deps/zeroconf/_utils/net.py
Normal file
|
@ -0,0 +1,418 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
import enum
|
||||
import errno
|
||||
import ipaddress
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
from typing import Any, List, Optional, Sequence, Tuple, Union, cast
|
||||
|
||||
import ifaddr
|
||||
|
||||
from .._logger import log
|
||||
from ..const import _IPPROTO_IPV6, _MDNS_ADDR, _MDNS_ADDR6, _MDNS_PORT
|
||||
|
||||
|
||||
@enum.unique
|
||||
class InterfaceChoice(enum.Enum):
|
||||
Default = 1
|
||||
All = 2
|
||||
|
||||
|
||||
InterfacesType = Union[Sequence[Union[str, int, Tuple[Tuple[str, int, int], int]]], InterfaceChoice]
|
||||
|
||||
|
||||
@enum.unique
|
||||
class ServiceStateChange(enum.Enum):
|
||||
Added = 1
|
||||
Removed = 2
|
||||
Updated = 3
|
||||
|
||||
|
||||
@enum.unique
|
||||
class IPVersion(enum.Enum):
|
||||
V4Only = 1
|
||||
V6Only = 2
|
||||
All = 3
|
||||
|
||||
|
||||
# utility functions
|
||||
|
||||
|
||||
def _is_v6_address(addr: bytes) -> bool:
|
||||
return len(addr) == 16
|
||||
|
||||
|
||||
def _encode_address(address: str) -> bytes:
|
||||
is_ipv6 = ':' in address
|
||||
address_family = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||
return socket.inet_pton(address_family, address)
|
||||
|
||||
|
||||
def get_all_addresses() -> List[str]:
|
||||
return list({addr.ip for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv4})
|
||||
|
||||
|
||||
def get_all_addresses_v6() -> List[Tuple[Tuple[str, int, int], int]]:
|
||||
# IPv6 multicast uses positive indexes for interfaces
|
||||
# TODO: What about multi-address interfaces?
|
||||
return list(
|
||||
{(addr.ip, iface.index) for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv6}
|
||||
)
|
||||
|
||||
|
||||
def ip6_to_address_and_index(adapters: List[Any], ip: str) -> Tuple[Tuple[str, int, int], int]:
|
||||
if '%' in ip:
|
||||
ip = ip[: ip.index('%')] # Strip scope_id.
|
||||
ipaddr = ipaddress.ip_address(ip)
|
||||
for adapter in adapters:
|
||||
for adapter_ip in adapter.ips:
|
||||
# IPv6 addresses are represented as tuples
|
||||
if isinstance(adapter_ip.ip, tuple) and ipaddress.ip_address(adapter_ip.ip[0]) == ipaddr:
|
||||
return (cast(Tuple[str, int, int], adapter_ip.ip), cast(int, adapter.index))
|
||||
|
||||
raise RuntimeError('No adapter found for IP address %s' % ip)
|
||||
|
||||
|
||||
def interface_index_to_ip6_address(adapters: List[Any], index: int) -> Tuple[str, int, int]:
|
||||
for adapter in adapters:
|
||||
if adapter.index == index:
|
||||
for adapter_ip in adapter.ips:
|
||||
# IPv6 addresses are represented as tuples
|
||||
if isinstance(adapter_ip.ip, tuple):
|
||||
return cast(Tuple[str, int, int], adapter_ip.ip)
|
||||
|
||||
raise RuntimeError('No adapter found for index %s' % index)
|
||||
|
||||
|
||||
def ip6_addresses_to_indexes(
|
||||
interfaces: Sequence[Union[str, int, Tuple[Tuple[str, int, int], int]]]
|
||||
) -> List[Tuple[Tuple[str, int, int], int]]:
|
||||
"""Convert IPv6 interface addresses to interface indexes.
|
||||
|
||||
IPv4 addresses are ignored.
|
||||
|
||||
:param interfaces: List of IP addresses and indexes.
|
||||
:returns: List of indexes.
|
||||
"""
|
||||
result = []
|
||||
adapters = ifaddr.get_adapters()
|
||||
|
||||
for iface in interfaces:
|
||||
if isinstance(iface, int):
|
||||
result.append((interface_index_to_ip6_address(adapters, iface), iface))
|
||||
elif isinstance(iface, str) and ipaddress.ip_address(iface).version == 6:
|
||||
result.append(ip6_to_address_and_index(adapters, iface))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def normalize_interface_choice(
|
||||
choice: InterfacesType, ip_version: IPVersion = IPVersion.V4Only
|
||||
) -> List[Union[str, Tuple[Tuple[str, int, int], int]]]:
|
||||
"""Convert the interfaces choice into internal representation.
|
||||
|
||||
:param choice: `InterfaceChoice` or list of interface addresses or indexes (IPv6 only).
|
||||
:param ip_address: IP version to use (ignored if `choice` is a list).
|
||||
:returns: List of IP addresses (for IPv4) and indexes (for IPv6).
|
||||
"""
|
||||
result: List[Union[str, Tuple[Tuple[str, int, int], int]]] = []
|
||||
if choice is InterfaceChoice.Default:
|
||||
if ip_version != IPVersion.V4Only:
|
||||
# IPv6 multicast uses interface 0 to mean the default
|
||||
result.append((('', 0, 0), 0))
|
||||
if ip_version != IPVersion.V6Only:
|
||||
result.append('0.0.0.0')
|
||||
elif choice is InterfaceChoice.All:
|
||||
if ip_version != IPVersion.V4Only:
|
||||
result.extend(get_all_addresses_v6())
|
||||
if ip_version != IPVersion.V6Only:
|
||||
result.extend(get_all_addresses())
|
||||
if not result:
|
||||
raise RuntimeError(
|
||||
'No interfaces to listen on, check that any interfaces have IP version %s' % ip_version
|
||||
)
|
||||
elif isinstance(choice, list):
|
||||
# First, take IPv4 addresses.
|
||||
result = [i for i in choice if isinstance(i, str) and ipaddress.ip_address(i).version == 4]
|
||||
# Unlike IP_ADD_MEMBERSHIP, IPV6_JOIN_GROUP requires interface indexes.
|
||||
result += ip6_addresses_to_indexes(choice)
|
||||
else:
|
||||
raise TypeError("choice must be a list or InterfaceChoice, got %r" % choice)
|
||||
return result
|
||||
|
||||
|
||||
def disable_ipv6_only_or_raise(s: socket.socket) -> None:
|
||||
"""Make V6 sockets work for both V4 and V6 (required for Windows)."""
|
||||
try:
|
||||
s.setsockopt(_IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
|
||||
except OSError:
|
||||
log.error('Support for dual V4-V6 sockets is not present, use IPVersion.V4 or IPVersion.V6')
|
||||
raise
|
||||
|
||||
|
||||
def set_so_reuseport_if_available(s: socket.socket) -> None:
|
||||
"""Set SO_REUSEADDR on a socket if available."""
|
||||
# SO_REUSEADDR should be equivalent to SO_REUSEPORT for
|
||||
# multicast UDP sockets (p 731, "TCP/IP Illustrated,
|
||||
# Volume 2"), but some BSD-derived systems require
|
||||
# SO_REUSEPORT to be specified explicitly. Also, not all
|
||||
# versions of Python have SO_REUSEPORT available.
|
||||
# Catch OSError and socket.error for kernel versions <3.9 because lacking
|
||||
# SO_REUSEPORT support.
|
||||
if not hasattr(socket, 'SO_REUSEPORT'):
|
||||
return
|
||||
|
||||
try:
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) # pylint: disable=no-member
|
||||
except OSError as err:
|
||||
if err.errno != errno.ENOPROTOOPT:
|
||||
raise
|
||||
|
||||
|
||||
def set_mdns_port_socket_options_for_ip_version(
|
||||
s: socket.socket, bind_addr: Union[Tuple[str], Tuple[str, int, int]], ip_version: IPVersion
|
||||
) -> None:
|
||||
"""Set ttl/hops and loop for mdns port."""
|
||||
if ip_version != IPVersion.V6Only:
|
||||
ttl = struct.pack(b'B', 255)
|
||||
loop = struct.pack(b'B', 1)
|
||||
# OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and
|
||||
# IP_MULTICAST_LOOP socket options as an unsigned char.
|
||||
try:
|
||||
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
|
||||
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop)
|
||||
except OSError as e:
|
||||
if bind_addr[0] != '' or get_errno(e) != errno.EINVAL: # Fails to set on MacOS
|
||||
raise
|
||||
|
||||
if ip_version != IPVersion.V4Only:
|
||||
# However, char doesn't work here (at least on Linux)
|
||||
s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
|
||||
s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, True)
|
||||
|
||||
|
||||
def new_socket(
|
||||
bind_addr: Union[Tuple[str], Tuple[str, int, int]],
|
||||
port: int = _MDNS_PORT,
|
||||
ip_version: IPVersion = IPVersion.V4Only,
|
||||
apple_p2p: bool = False,
|
||||
) -> Optional[socket.socket]:
|
||||
log.debug(
|
||||
'Creating new socket with port %s, ip_version %s, apple_p2p %s and bind_addr %r',
|
||||
port,
|
||||
ip_version,
|
||||
apple_p2p,
|
||||
bind_addr,
|
||||
)
|
||||
socket_family = socket.AF_INET if ip_version == IPVersion.V4Only else socket.AF_INET6
|
||||
s = socket.socket(socket_family, socket.SOCK_DGRAM)
|
||||
|
||||
if ip_version == IPVersion.All:
|
||||
disable_ipv6_only_or_raise(s)
|
||||
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
set_so_reuseport_if_available(s)
|
||||
|
||||
if port == _MDNS_PORT:
|
||||
set_mdns_port_socket_options_for_ip_version(s, bind_addr, ip_version)
|
||||
|
||||
if apple_p2p:
|
||||
# SO_RECV_ANYIF = 0x1104
|
||||
# https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/socket.h
|
||||
s.setsockopt(socket.SOL_SOCKET, 0x1104, 1)
|
||||
|
||||
bind_tup = (bind_addr[0], port, *bind_addr[1:])
|
||||
try:
|
||||
s.bind(bind_tup)
|
||||
except OSError as ex:
|
||||
if ex.errno == errno.EADDRNOTAVAIL:
|
||||
log.warning(
|
||||
'Address not available when binding to %s, ' 'it is expected to happen on some systems',
|
||||
bind_tup,
|
||||
)
|
||||
return None
|
||||
raise
|
||||
log.debug('Created socket %s', s)
|
||||
return s
|
||||
|
||||
|
||||
def add_multicast_member(
|
||||
listen_socket: socket.socket,
|
||||
interface: Union[str, Tuple[Tuple[str, int, int], int]],
|
||||
) -> bool:
|
||||
# This is based on assumptions in normalize_interface_choice
|
||||
is_v6 = isinstance(interface, tuple)
|
||||
err_einval = {errno.EINVAL}
|
||||
if sys.platform == 'win32':
|
||||
# No WSAEINVAL definition in typeshed
|
||||
err_einval |= {cast(Any, errno).WSAEINVAL} # pylint: disable=no-member
|
||||
log.debug('Adding %r (socket %d) to multicast group', interface, listen_socket.fileno())
|
||||
try:
|
||||
if is_v6:
|
||||
try:
|
||||
mdns_addr6_bytes = socket.inet_pton(socket.AF_INET6, _MDNS_ADDR6)
|
||||
except OSError:
|
||||
log.info(
|
||||
'Unable to translate IPv6 address when adding %s to multicast group, '
|
||||
'this can happen if IPv6 is disabled on the system',
|
||||
interface,
|
||||
)
|
||||
return False
|
||||
iface_bin = struct.pack('@I', cast(int, interface[1]))
|
||||
_value = mdns_addr6_bytes + iface_bin
|
||||
listen_socket.setsockopt(_IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, _value)
|
||||
else:
|
||||
_value = socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(cast(str, interface))
|
||||
listen_socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, _value)
|
||||
except OSError as e:
|
||||
_errno = get_errno(e)
|
||||
if _errno == errno.EADDRINUSE:
|
||||
log.info(
|
||||
'Address in use when adding %s to multicast group, '
|
||||
'it is expected to happen on some systems',
|
||||
interface,
|
||||
)
|
||||
return False
|
||||
if _errno == errno.EADDRNOTAVAIL:
|
||||
log.info(
|
||||
'Address not available when adding %s to multicast '
|
||||
'group, it is expected to happen on some systems',
|
||||
interface,
|
||||
)
|
||||
return False
|
||||
if _errno in err_einval:
|
||||
log.info('Interface of %s does not support multicast, ' 'it is expected in WSL', interface)
|
||||
return False
|
||||
if _errno == errno.ENOPROTOOPT:
|
||||
log.info(
|
||||
'Failed to set socket option on %s, this can happen if '
|
||||
'the network adapter is in a disconnected state',
|
||||
interface,
|
||||
)
|
||||
return False
|
||||
if is_v6 and _errno == errno.ENODEV:
|
||||
log.info(
|
||||
'Address in use when adding %s to multicast group, '
|
||||
'it is expected to happen when the device does not have ipv6',
|
||||
interface,
|
||||
)
|
||||
return False
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def new_respond_socket(
|
||||
interface: Union[str, Tuple[Tuple[str, int, int], int]],
|
||||
apple_p2p: bool = False,
|
||||
) -> Optional[socket.socket]:
|
||||
is_v6 = isinstance(interface, tuple)
|
||||
respond_socket = new_socket(
|
||||
ip_version=(IPVersion.V6Only if is_v6 else IPVersion.V4Only),
|
||||
apple_p2p=apple_p2p,
|
||||
bind_addr=cast(Tuple[Tuple[str, int, int], int], interface)[0] if is_v6 else (cast(str, interface),),
|
||||
)
|
||||
if not respond_socket:
|
||||
return None
|
||||
log.debug('Configuring socket %s with multicast interface %s', respond_socket, interface)
|
||||
if is_v6:
|
||||
iface_bin = struct.pack('@I', cast(int, interface[1]))
|
||||
respond_socket.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, iface_bin)
|
||||
else:
|
||||
respond_socket.setsockopt(
|
||||
socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(cast(str, interface))
|
||||
)
|
||||
return respond_socket
|
||||
|
||||
|
||||
def create_sockets(
|
||||
interfaces: InterfacesType = InterfaceChoice.All,
|
||||
unicast: bool = False,
|
||||
ip_version: IPVersion = IPVersion.V4Only,
|
||||
apple_p2p: bool = False,
|
||||
) -> Tuple[Optional[socket.socket], List[socket.socket]]:
|
||||
if unicast:
|
||||
listen_socket = None
|
||||
else:
|
||||
listen_socket = new_socket(ip_version=ip_version, apple_p2p=apple_p2p, bind_addr=('',))
|
||||
|
||||
normalized_interfaces = normalize_interface_choice(interfaces, ip_version)
|
||||
|
||||
# If we are using InterfaceChoice.Default we can use
|
||||
# a single socket to listen and respond.
|
||||
if not unicast and interfaces is InterfaceChoice.Default:
|
||||
for i in normalized_interfaces:
|
||||
add_multicast_member(cast(socket.socket, listen_socket), i)
|
||||
return listen_socket, [cast(socket.socket, listen_socket)]
|
||||
|
||||
respond_sockets = []
|
||||
|
||||
for i in normalized_interfaces:
|
||||
if not unicast:
|
||||
if add_multicast_member(cast(socket.socket, listen_socket), i):
|
||||
respond_socket = new_respond_socket(i, apple_p2p=apple_p2p)
|
||||
else:
|
||||
respond_socket = None
|
||||
else:
|
||||
respond_socket = new_socket(
|
||||
port=0,
|
||||
ip_version=ip_version,
|
||||
apple_p2p=apple_p2p,
|
||||
bind_addr=i[0] if isinstance(i, tuple) else (i,),
|
||||
)
|
||||
|
||||
if respond_socket is not None:
|
||||
respond_sockets.append(respond_socket)
|
||||
|
||||
return listen_socket, respond_sockets
|
||||
|
||||
|
||||
def get_errno(e: Exception) -> int:
|
||||
assert isinstance(e, socket.error)
|
||||
return cast(int, e.args[0])
|
||||
|
||||
|
||||
def can_send_to(ipv6_socket: bool, address: str) -> bool:
|
||||
"""Check if the address type matches the socket type.
|
||||
|
||||
This function does not validate if the address is a valid
|
||||
ipv6 or ipv4 address.
|
||||
"""
|
||||
return ":" in address if ipv6_socket else ":" not in address
|
||||
|
||||
|
||||
def autodetect_ip_version(interfaces: InterfacesType) -> IPVersion:
|
||||
"""Auto detect the IP version when it is not provided."""
|
||||
if isinstance(interfaces, list):
|
||||
has_v6 = any(
|
||||
isinstance(i, int) or (isinstance(i, str) and ipaddress.ip_address(i).version == 6)
|
||||
for i in interfaces
|
||||
)
|
||||
has_v4 = any(isinstance(i, str) and ipaddress.ip_address(i).version == 4 for i in interfaces)
|
||||
if has_v4 and has_v6:
|
||||
return IPVersion.All
|
||||
if has_v6:
|
||||
return IPVersion.V6Only
|
||||
|
||||
return IPVersion.V4Only
|
BIN
resources/lib/deps/zeroconf/_utils/time.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
resources/lib/deps/zeroconf/_utils/time.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
4
resources/lib/deps/zeroconf/_utils/time.pxd
Normal file
4
resources/lib/deps/zeroconf/_utils/time.pxd
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
cpdef double current_time_millis()
|
||||
|
||||
cpdef millis_to_seconds(double millis)
|
42
resources/lib/deps/zeroconf/_utils/time.py
Normal file
42
resources/lib/deps/zeroconf/_utils/time.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
|
||||
import time
|
||||
|
||||
_float = float
|
||||
|
||||
|
||||
def current_time_millis() -> _float:
|
||||
"""Current time in milliseconds.
|
||||
|
||||
The current implemention uses `time.monotonic`
|
||||
but may change in the future.
|
||||
|
||||
The design requires the time to match asyncio.loop.time()
|
||||
"""
|
||||
return time.monotonic() * 1000
|
||||
|
||||
|
||||
def millis_to_seconds(millis: _float) -> _float:
|
||||
"""Convert milliseconds to seconds."""
|
||||
return millis / 1000.0
|
Loading…
Add table
Add a link
Reference in a new issue