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/_handlers/__init__.py
Normal file
21
resources/lib/deps/zeroconf/_handlers/__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
|
||||
"""
|
BIN
resources/lib/deps/zeroconf/_handlers/answers.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
resources/lib/deps/zeroconf/_handlers/answers.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
35
resources/lib/deps/zeroconf/_handlers/answers.pxd
Normal file
35
resources/lib/deps/zeroconf/_handlers/answers.pxd
Normal file
|
@ -0,0 +1,35 @@
|
|||
|
||||
import cython
|
||||
|
||||
from .._dns cimport DNSRecord
|
||||
from .._protocol.outgoing cimport DNSOutgoing
|
||||
|
||||
|
||||
cdef class QuestionAnswers:
|
||||
|
||||
cdef public dict ucast
|
||||
cdef public dict mcast_now
|
||||
cdef public dict mcast_aggregate
|
||||
cdef public dict mcast_aggregate_last_second
|
||||
|
||||
|
||||
cdef class AnswerGroup:
|
||||
|
||||
cdef public double send_after
|
||||
cdef public double send_before
|
||||
cdef public cython.dict answers
|
||||
|
||||
|
||||
|
||||
|
||||
cdef object _FLAGS_QR_RESPONSE_AA
|
||||
cdef object NAME_GETTER
|
||||
|
||||
cpdef DNSOutgoing construct_outgoing_multicast_answers(cython.dict answers)
|
||||
|
||||
cpdef DNSOutgoing construct_outgoing_unicast_answers(
|
||||
cython.dict answers, bint ucast_source, cython.list questions, object id_
|
||||
)
|
||||
|
||||
@cython.locals(answer=DNSRecord, additionals=cython.set, additional=DNSRecord)
|
||||
cdef void _add_answers_additionals(DNSOutgoing out, cython.dict answers)
|
114
resources/lib/deps/zeroconf/_handlers/answers.py
Normal file
114
resources/lib/deps/zeroconf/_handlers/answers.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
""" 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 operator import attrgetter
|
||||
from typing import Dict, List, Set
|
||||
|
||||
from .._dns import DNSQuestion, DNSRecord
|
||||
from .._protocol.outgoing import DNSOutgoing
|
||||
from ..const import _FLAGS_AA, _FLAGS_QR_RESPONSE
|
||||
|
||||
_AnswerWithAdditionalsType = Dict[DNSRecord, Set[DNSRecord]]
|
||||
|
||||
int_ = int
|
||||
|
||||
|
||||
MULTICAST_DELAY_RANDOM_INTERVAL = (20, 120)
|
||||
|
||||
NAME_GETTER = attrgetter('name')
|
||||
|
||||
_FLAGS_QR_RESPONSE_AA = _FLAGS_QR_RESPONSE | _FLAGS_AA
|
||||
|
||||
float_ = float
|
||||
|
||||
|
||||
class QuestionAnswers:
|
||||
"""A group of answers to a question."""
|
||||
|
||||
__slots__ = ('ucast', 'mcast_now', 'mcast_aggregate', 'mcast_aggregate_last_second')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ucast: _AnswerWithAdditionalsType,
|
||||
mcast_now: _AnswerWithAdditionalsType,
|
||||
mcast_aggregate: _AnswerWithAdditionalsType,
|
||||
mcast_aggregate_last_second: _AnswerWithAdditionalsType,
|
||||
) -> None:
|
||||
"""Initialize a QuestionAnswers."""
|
||||
self.ucast = ucast
|
||||
self.mcast_now = mcast_now
|
||||
self.mcast_aggregate = mcast_aggregate
|
||||
self.mcast_aggregate_last_second = mcast_aggregate_last_second
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of this QuestionAnswers."""
|
||||
return (
|
||||
f'QuestionAnswers(ucast={self.ucast}, mcast_now={self.mcast_now}, '
|
||||
f'mcast_aggregate={self.mcast_aggregate}, '
|
||||
f'mcast_aggregate_last_second={self.mcast_aggregate_last_second})'
|
||||
)
|
||||
|
||||
|
||||
class AnswerGroup:
|
||||
"""A group of answers scheduled to be sent at the same time."""
|
||||
|
||||
__slots__ = ('send_after', 'send_before', 'answers')
|
||||
|
||||
def __init__(self, send_after: float_, send_before: float_, answers: _AnswerWithAdditionalsType) -> None:
|
||||
self.send_after = send_after # Must be sent after this time
|
||||
self.send_before = send_before # Must be sent before this time
|
||||
self.answers = answers
|
||||
|
||||
|
||||
def construct_outgoing_multicast_answers(answers: _AnswerWithAdditionalsType) -> DNSOutgoing:
|
||||
"""Add answers and additionals to a DNSOutgoing."""
|
||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE_AA, True)
|
||||
_add_answers_additionals(out, answers)
|
||||
return out
|
||||
|
||||
|
||||
def construct_outgoing_unicast_answers(
|
||||
answers: _AnswerWithAdditionalsType, ucast_source: bool, questions: List[DNSQuestion], id_: int_
|
||||
) -> DNSOutgoing:
|
||||
"""Add answers and additionals to a DNSOutgoing."""
|
||||
out = DNSOutgoing(_FLAGS_QR_RESPONSE_AA, False, id_)
|
||||
# Adding the questions back when the source is legacy unicast behavior
|
||||
if ucast_source:
|
||||
for question in questions:
|
||||
out.add_question(question)
|
||||
_add_answers_additionals(out, answers)
|
||||
return out
|
||||
|
||||
|
||||
def _add_answers_additionals(out: DNSOutgoing, answers: _AnswerWithAdditionalsType) -> None:
|
||||
# Find additionals and suppress any additionals that are already in answers
|
||||
sending: Set[DNSRecord] = set(answers)
|
||||
# Answers are sorted to group names together to increase the chance
|
||||
# that similar names will end up in the same packet and can reduce the
|
||||
# overall size of the outgoing response via name compression
|
||||
for answer in sorted(answers, key=NAME_GETTER):
|
||||
out.add_answer_at_time(answer, 0)
|
||||
additionals = answers[answer]
|
||||
for additional in additionals:
|
||||
if additional not in sending:
|
||||
out.add_additional_answer(additional)
|
||||
sending.add(additional)
|
Binary file not shown.
|
@ -0,0 +1,27 @@
|
|||
|
||||
import cython
|
||||
|
||||
from .._utils.time cimport current_time_millis, millis_to_seconds
|
||||
from .answers cimport AnswerGroup, construct_outgoing_multicast_answers
|
||||
|
||||
|
||||
cdef bint TYPE_CHECKING
|
||||
cdef tuple MULTICAST_DELAY_RANDOM_INTERVAL
|
||||
cdef object RAND_INT
|
||||
|
||||
cdef class MulticastOutgoingQueue:
|
||||
|
||||
cdef object zc
|
||||
cdef public object queue
|
||||
cdef public object _multicast_delay_random_min
|
||||
cdef public object _multicast_delay_random_max
|
||||
cdef object _additional_delay
|
||||
cdef object _aggregation_delay
|
||||
|
||||
@cython.locals(last_group=AnswerGroup, random_int=cython.uint)
|
||||
cpdef void async_add(self, double now, cython.dict answers)
|
||||
|
||||
@cython.locals(pending=AnswerGroup)
|
||||
cdef void _remove_answers_from_queue(self, cython.dict answers)
|
||||
|
||||
cpdef void async_ready(self)
|
|
@ -0,0 +1,122 @@
|
|||
""" 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 random
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .._utils.time import current_time_millis, millis_to_seconds
|
||||
from .answers import (
|
||||
MULTICAST_DELAY_RANDOM_INTERVAL,
|
||||
AnswerGroup,
|
||||
_AnswerWithAdditionalsType,
|
||||
construct_outgoing_multicast_answers,
|
||||
)
|
||||
|
||||
RAND_INT = random.randint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._core import Zeroconf
|
||||
|
||||
_float = float
|
||||
_int = int
|
||||
|
||||
|
||||
class MulticastOutgoingQueue:
|
||||
"""An outgoing queue used to aggregate multicast responses."""
|
||||
|
||||
__slots__ = (
|
||||
"zc",
|
||||
"queue",
|
||||
"_multicast_delay_random_min",
|
||||
"_multicast_delay_random_max",
|
||||
"_additional_delay",
|
||||
"_aggregation_delay",
|
||||
)
|
||||
|
||||
def __init__(self, zeroconf: 'Zeroconf', additional_delay: _int, max_aggregation_delay: _int) -> None:
|
||||
self.zc = zeroconf
|
||||
self.queue: deque[AnswerGroup] = deque()
|
||||
# Additional delay is used to implement
|
||||
# Protect the network against excessive packet flooding
|
||||
# https://datatracker.ietf.org/doc/html/rfc6762#section-14
|
||||
self._multicast_delay_random_min = MULTICAST_DELAY_RANDOM_INTERVAL[0]
|
||||
self._multicast_delay_random_max = MULTICAST_DELAY_RANDOM_INTERVAL[1]
|
||||
self._additional_delay = additional_delay
|
||||
self._aggregation_delay = max_aggregation_delay
|
||||
|
||||
def async_add(self, now: _float, answers: _AnswerWithAdditionalsType) -> None:
|
||||
"""Add a group of answers with additionals to the outgoing queue."""
|
||||
loop = self.zc.loop
|
||||
if TYPE_CHECKING:
|
||||
assert loop is not None
|
||||
random_int = RAND_INT(self._multicast_delay_random_min, self._multicast_delay_random_max)
|
||||
random_delay = random_int + self._additional_delay
|
||||
send_after = now + random_delay
|
||||
send_before = now + self._aggregation_delay + self._additional_delay
|
||||
if len(self.queue):
|
||||
# If we calculate a random delay for the send after time
|
||||
# that is less than the last group scheduled to go out,
|
||||
# we instead add the answers to the last group as this
|
||||
# allows aggregating additional responses
|
||||
last_group = self.queue[-1]
|
||||
if send_after <= last_group.send_after:
|
||||
last_group.answers.update(answers)
|
||||
return
|
||||
else:
|
||||
loop.call_at(loop.time() + millis_to_seconds(random_delay), self.async_ready)
|
||||
self.queue.append(AnswerGroup(send_after, send_before, answers))
|
||||
|
||||
def _remove_answers_from_queue(self, answers: _AnswerWithAdditionalsType) -> None:
|
||||
"""Remove a set of answers from the outgoing queue."""
|
||||
for pending in self.queue:
|
||||
for record in answers:
|
||||
pending.answers.pop(record, None)
|
||||
|
||||
def async_ready(self) -> None:
|
||||
"""Process anything in the queue that is ready."""
|
||||
zc = self.zc
|
||||
loop = zc.loop
|
||||
if TYPE_CHECKING:
|
||||
assert loop is not None
|
||||
now = current_time_millis()
|
||||
|
||||
if len(self.queue) > 1 and self.queue[0].send_before > now:
|
||||
# There is more than one answer in the queue,
|
||||
# delay until we have to send it (first answer group reaches send_before)
|
||||
loop.call_at(loop.time() + millis_to_seconds(self.queue[0].send_before - now), self.async_ready)
|
||||
return
|
||||
|
||||
answers: _AnswerWithAdditionalsType = {}
|
||||
# Add all groups that can be sent now
|
||||
while len(self.queue) and self.queue[0].send_after <= now:
|
||||
answers.update(self.queue.popleft().answers)
|
||||
|
||||
if len(self.queue):
|
||||
# If there are still groups in the queue that are not ready to send
|
||||
# be sure we schedule them to go out later
|
||||
loop.call_at(loop.time() + millis_to_seconds(self.queue[0].send_after - now), self.async_ready)
|
||||
|
||||
if answers: # pragma: no branch
|
||||
# If we have the same answer scheduled to go out, remove them
|
||||
self._remove_answers_from_queue(answers)
|
||||
zc.async_send(construct_outgoing_multicast_answers(answers))
|
Binary file not shown.
120
resources/lib/deps/zeroconf/_handlers/query_handler.pxd
Normal file
120
resources/lib/deps/zeroconf/_handlers/query_handler.pxd
Normal file
|
@ -0,0 +1,120 @@
|
|||
|
||||
import cython
|
||||
|
||||
from .._cache cimport DNSCache
|
||||
from .._dns cimport DNSAddress, DNSPointer, DNSQuestion, DNSRecord, DNSRRSet
|
||||
from .._history cimport QuestionHistory
|
||||
from .._protocol.incoming cimport DNSIncoming
|
||||
from .._services.info cimport ServiceInfo
|
||||
from .._services.registry cimport ServiceRegistry
|
||||
from .answers cimport (
|
||||
QuestionAnswers,
|
||||
construct_outgoing_multicast_answers,
|
||||
construct_outgoing_unicast_answers,
|
||||
)
|
||||
from .multicast_outgoing_queue cimport MulticastOutgoingQueue
|
||||
|
||||
|
||||
cdef bint TYPE_CHECKING
|
||||
cdef cython.uint _ONE_SECOND, _TYPE_PTR, _TYPE_ANY, _TYPE_A, _TYPE_AAAA, _TYPE_SRV, _TYPE_TXT
|
||||
cdef str _SERVICE_TYPE_ENUMERATION_NAME
|
||||
cdef cython.set _RESPOND_IMMEDIATE_TYPES
|
||||
cdef cython.set _ADDRESS_RECORD_TYPES
|
||||
cdef object IPVersion, _IPVersion_ALL
|
||||
cdef object _TYPE_PTR, _CLASS_IN, _DNS_OTHER_TTL
|
||||
|
||||
cdef unsigned int _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION
|
||||
cdef unsigned int _ANSWER_STRATEGY_POINTER
|
||||
cdef unsigned int _ANSWER_STRATEGY_ADDRESS
|
||||
cdef unsigned int _ANSWER_STRATEGY_SERVICE
|
||||
cdef unsigned int _ANSWER_STRATEGY_TEXT
|
||||
|
||||
cdef list _EMPTY_SERVICES_LIST
|
||||
cdef list _EMPTY_TYPES_LIST
|
||||
|
||||
cdef class _AnswerStrategy:
|
||||
|
||||
cdef public DNSQuestion question
|
||||
cdef public unsigned int strategy_type
|
||||
cdef public list types
|
||||
cdef public list services
|
||||
|
||||
|
||||
cdef class _QueryResponse:
|
||||
|
||||
cdef bint _is_probe
|
||||
cdef cython.list _questions
|
||||
cdef double _now
|
||||
cdef DNSCache _cache
|
||||
cdef cython.dict _additionals
|
||||
cdef cython.set _ucast
|
||||
cdef cython.set _mcast_now
|
||||
cdef cython.set _mcast_aggregate
|
||||
cdef cython.set _mcast_aggregate_last_second
|
||||
|
||||
@cython.locals(record=DNSRecord)
|
||||
cdef void add_qu_question_response(self, cython.dict answers)
|
||||
|
||||
cdef void add_ucast_question_response(self, cython.dict answers)
|
||||
|
||||
@cython.locals(answer=DNSRecord, question=DNSQuestion)
|
||||
cdef void add_mcast_question_response(self, cython.dict answers)
|
||||
|
||||
@cython.locals(maybe_entry=DNSRecord)
|
||||
cdef bint _has_mcast_within_one_quarter_ttl(self, DNSRecord record)
|
||||
|
||||
@cython.locals(maybe_entry=DNSRecord)
|
||||
cdef bint _has_mcast_record_in_last_second(self, DNSRecord record)
|
||||
|
||||
cdef QuestionAnswers answers(self)
|
||||
|
||||
cdef class QueryHandler:
|
||||
|
||||
cdef object zc
|
||||
cdef ServiceRegistry registry
|
||||
cdef DNSCache cache
|
||||
cdef QuestionHistory question_history
|
||||
cdef MulticastOutgoingQueue out_queue
|
||||
cdef MulticastOutgoingQueue out_delay_queue
|
||||
|
||||
@cython.locals(service=ServiceInfo)
|
||||
cdef void _add_service_type_enumeration_query_answers(self, list types, cython.dict answer_set, DNSRRSet known_answers)
|
||||
|
||||
@cython.locals(service=ServiceInfo)
|
||||
cdef void _add_pointer_answers(self, list services, cython.dict answer_set, DNSRRSet known_answers)
|
||||
|
||||
@cython.locals(service=ServiceInfo, dns_address=DNSAddress)
|
||||
cdef void _add_address_answers(self, list services, cython.dict answer_set, DNSRRSet known_answers, cython.uint type_)
|
||||
|
||||
@cython.locals(question_lower_name=str, type_=cython.uint, service=ServiceInfo)
|
||||
cdef cython.dict _answer_question(self, DNSQuestion question, unsigned int strategy_type, list types, list services, DNSRRSet known_answers)
|
||||
|
||||
@cython.locals(
|
||||
msg=DNSIncoming,
|
||||
msgs=list,
|
||||
strategy=_AnswerStrategy,
|
||||
question=DNSQuestion,
|
||||
answer_set=cython.dict,
|
||||
known_answers=DNSRRSet,
|
||||
known_answers_set=cython.set,
|
||||
is_unicast=bint,
|
||||
is_probe=object,
|
||||
now=double
|
||||
)
|
||||
cpdef QuestionAnswers async_response(self, cython.list msgs, cython.bint unicast_source)
|
||||
|
||||
@cython.locals(name=str, question_lower_name=str)
|
||||
cdef list _get_answer_strategies(self, DNSQuestion question)
|
||||
|
||||
@cython.locals(
|
||||
first_packet=DNSIncoming,
|
||||
ucast_source=bint,
|
||||
)
|
||||
cpdef void handle_assembled_query(
|
||||
self,
|
||||
list packets,
|
||||
object addr,
|
||||
object port,
|
||||
object transport,
|
||||
tuple v6_flow_scope
|
||||
)
|
437
resources/lib/deps/zeroconf/_handlers/query_handler.py
Normal file
437
resources/lib/deps/zeroconf/_handlers/query_handler.py
Normal file
|
@ -0,0 +1,437 @@
|
|||
""" 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 typing import TYPE_CHECKING, List, Optional, Set, Tuple, Union, cast
|
||||
|
||||
from .._cache import DNSCache, _UniqueRecordsType
|
||||
from .._dns import DNSAddress, DNSPointer, DNSQuestion, DNSRecord, DNSRRSet
|
||||
from .._protocol.incoming import DNSIncoming
|
||||
from .._services.info import ServiceInfo
|
||||
from .._transport import _WrappedTransport
|
||||
from .._utils.net import IPVersion
|
||||
from ..const import (
|
||||
_ADDRESS_RECORD_TYPES,
|
||||
_CLASS_IN,
|
||||
_DNS_OTHER_TTL,
|
||||
_MDNS_PORT,
|
||||
_ONE_SECOND,
|
||||
_SERVICE_TYPE_ENUMERATION_NAME,
|
||||
_TYPE_A,
|
||||
_TYPE_AAAA,
|
||||
_TYPE_ANY,
|
||||
_TYPE_NSEC,
|
||||
_TYPE_PTR,
|
||||
_TYPE_SRV,
|
||||
_TYPE_TXT,
|
||||
)
|
||||
from .answers import (
|
||||
QuestionAnswers,
|
||||
_AnswerWithAdditionalsType,
|
||||
construct_outgoing_multicast_answers,
|
||||
construct_outgoing_unicast_answers,
|
||||
)
|
||||
|
||||
_RESPOND_IMMEDIATE_TYPES = {_TYPE_NSEC, _TYPE_SRV, *_ADDRESS_RECORD_TYPES}
|
||||
|
||||
_EMPTY_SERVICES_LIST: List[ServiceInfo] = []
|
||||
_EMPTY_TYPES_LIST: List[str] = []
|
||||
|
||||
_IPVersion_ALL = IPVersion.All
|
||||
|
||||
_int = int
|
||||
_str = str
|
||||
|
||||
_ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION = 0
|
||||
_ANSWER_STRATEGY_POINTER = 1
|
||||
_ANSWER_STRATEGY_ADDRESS = 2
|
||||
_ANSWER_STRATEGY_SERVICE = 3
|
||||
_ANSWER_STRATEGY_TEXT = 4
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._core import Zeroconf
|
||||
|
||||
|
||||
class _AnswerStrategy:
|
||||
|
||||
__slots__ = ("question", "strategy_type", "types", "services")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
question: DNSQuestion,
|
||||
strategy_type: _int,
|
||||
types: List[str],
|
||||
services: List[ServiceInfo],
|
||||
) -> None:
|
||||
"""Create an answer strategy."""
|
||||
self.question = question
|
||||
self.strategy_type = strategy_type
|
||||
self.types = types
|
||||
self.services = services
|
||||
|
||||
|
||||
class _QueryResponse:
|
||||
"""A pair for unicast and multicast DNSOutgoing responses."""
|
||||
|
||||
__slots__ = (
|
||||
"_is_probe",
|
||||
"_questions",
|
||||
"_now",
|
||||
"_cache",
|
||||
"_additionals",
|
||||
"_ucast",
|
||||
"_mcast_now",
|
||||
"_mcast_aggregate",
|
||||
"_mcast_aggregate_last_second",
|
||||
)
|
||||
|
||||
def __init__(self, cache: DNSCache, questions: List[DNSQuestion], is_probe: bool, now: float) -> None:
|
||||
"""Build a query response."""
|
||||
self._is_probe = is_probe
|
||||
self._questions = questions
|
||||
self._now = now
|
||||
self._cache = cache
|
||||
self._additionals: _AnswerWithAdditionalsType = {}
|
||||
self._ucast: Set[DNSRecord] = set()
|
||||
self._mcast_now: Set[DNSRecord] = set()
|
||||
self._mcast_aggregate: Set[DNSRecord] = set()
|
||||
self._mcast_aggregate_last_second: Set[DNSRecord] = set()
|
||||
|
||||
def add_qu_question_response(self, answers: _AnswerWithAdditionalsType) -> None:
|
||||
"""Generate a response to a multicast QU query."""
|
||||
for record, additionals in answers.items():
|
||||
self._additionals[record] = additionals
|
||||
if self._is_probe:
|
||||
self._ucast.add(record)
|
||||
if not self._has_mcast_within_one_quarter_ttl(record):
|
||||
self._mcast_now.add(record)
|
||||
elif not self._is_probe:
|
||||
self._ucast.add(record)
|
||||
|
||||
def add_ucast_question_response(self, answers: _AnswerWithAdditionalsType) -> None:
|
||||
"""Generate a response to a unicast query."""
|
||||
self._additionals.update(answers)
|
||||
self._ucast.update(answers)
|
||||
|
||||
def add_mcast_question_response(self, answers: _AnswerWithAdditionalsType) -> None:
|
||||
"""Generate a response to a multicast query."""
|
||||
self._additionals.update(answers)
|
||||
for answer in answers:
|
||||
if self._is_probe:
|
||||
self._mcast_now.add(answer)
|
||||
continue
|
||||
|
||||
if self._has_mcast_record_in_last_second(answer):
|
||||
self._mcast_aggregate_last_second.add(answer)
|
||||
continue
|
||||
|
||||
if len(self._questions) == 1:
|
||||
question = self._questions[0]
|
||||
if question.type in _RESPOND_IMMEDIATE_TYPES:
|
||||
self._mcast_now.add(answer)
|
||||
continue
|
||||
|
||||
self._mcast_aggregate.add(answer)
|
||||
|
||||
def answers(
|
||||
self,
|
||||
) -> QuestionAnswers:
|
||||
"""Return answer sets that will be queued."""
|
||||
ucast = {r: self._additionals[r] for r in self._ucast}
|
||||
mcast_now = {r: self._additionals[r] for r in self._mcast_now}
|
||||
mcast_aggregate = {r: self._additionals[r] for r in self._mcast_aggregate}
|
||||
mcast_aggregate_last_second = {r: self._additionals[r] for r in self._mcast_aggregate_last_second}
|
||||
return QuestionAnswers(ucast, mcast_now, mcast_aggregate, mcast_aggregate_last_second)
|
||||
|
||||
def _has_mcast_within_one_quarter_ttl(self, record: DNSRecord) -> bool:
|
||||
"""Check to see if a record has been mcasted recently.
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc6762#section-5.4
|
||||
When receiving a question with the unicast-response bit set, a
|
||||
responder SHOULD usually respond with a unicast packet directed back
|
||||
to the querier. However, if the responder has not multicast that
|
||||
record recently (within one quarter of its TTL), then the responder
|
||||
SHOULD instead multicast the response so as to keep all the peer
|
||||
caches up to date
|
||||
"""
|
||||
if TYPE_CHECKING:
|
||||
record = cast(_UniqueRecordsType, record)
|
||||
maybe_entry = self._cache.async_get_unique(record)
|
||||
return bool(maybe_entry is not None and maybe_entry.is_recent(self._now))
|
||||
|
||||
def _has_mcast_record_in_last_second(self, record: DNSRecord) -> bool:
|
||||
"""Check if an answer was seen in the last second.
|
||||
Protect the network against excessive packet flooding
|
||||
https://datatracker.ietf.org/doc/html/rfc6762#section-14
|
||||
"""
|
||||
if TYPE_CHECKING:
|
||||
record = cast(_UniqueRecordsType, record)
|
||||
maybe_entry = self._cache.async_get_unique(record)
|
||||
return bool(maybe_entry is not None and self._now - maybe_entry.created < _ONE_SECOND)
|
||||
|
||||
|
||||
class QueryHandler:
|
||||
"""Query the ServiceRegistry."""
|
||||
|
||||
__slots__ = ("zc", "registry", "cache", "question_history", "out_queue", "out_delay_queue")
|
||||
|
||||
def __init__(self, zc: 'Zeroconf') -> None:
|
||||
"""Init the query handler."""
|
||||
self.zc = zc
|
||||
self.registry = zc.registry
|
||||
self.cache = zc.cache
|
||||
self.question_history = zc.question_history
|
||||
self.out_queue = zc.out_queue
|
||||
self.out_delay_queue = zc.out_delay_queue
|
||||
|
||||
def _add_service_type_enumeration_query_answers(
|
||||
self, types: List[str], answer_set: _AnswerWithAdditionalsType, known_answers: DNSRRSet
|
||||
) -> None:
|
||||
"""Provide an answer to a service type enumeration query.
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc6763#section-9
|
||||
"""
|
||||
for stype in types:
|
||||
dns_pointer = DNSPointer(
|
||||
_SERVICE_TYPE_ENUMERATION_NAME, _TYPE_PTR, _CLASS_IN, _DNS_OTHER_TTL, stype, 0.0
|
||||
)
|
||||
if not known_answers.suppresses(dns_pointer):
|
||||
answer_set[dns_pointer] = set()
|
||||
|
||||
def _add_pointer_answers(
|
||||
self, services: List[ServiceInfo], answer_set: _AnswerWithAdditionalsType, known_answers: DNSRRSet
|
||||
) -> None:
|
||||
"""Answer PTR/ANY question."""
|
||||
for service in services:
|
||||
# Add recommended additional answers according to
|
||||
# https://tools.ietf.org/html/rfc6763#section-12.1.
|
||||
dns_pointer = service._dns_pointer(None)
|
||||
if known_answers.suppresses(dns_pointer):
|
||||
continue
|
||||
answer_set[dns_pointer] = {
|
||||
service._dns_service(None),
|
||||
service._dns_text(None),
|
||||
*service._get_address_and_nsec_records(None),
|
||||
}
|
||||
|
||||
def _add_address_answers(
|
||||
self,
|
||||
services: List[ServiceInfo],
|
||||
answer_set: _AnswerWithAdditionalsType,
|
||||
known_answers: DNSRRSet,
|
||||
type_: _int,
|
||||
) -> None:
|
||||
"""Answer A/AAAA/ANY question."""
|
||||
for service in services:
|
||||
answers: List[DNSAddress] = []
|
||||
additionals: Set[DNSRecord] = set()
|
||||
seen_types: Set[int] = set()
|
||||
for dns_address in service._dns_addresses(None, _IPVersion_ALL):
|
||||
seen_types.add(dns_address.type)
|
||||
if dns_address.type != type_:
|
||||
additionals.add(dns_address)
|
||||
elif not known_answers.suppresses(dns_address):
|
||||
answers.append(dns_address)
|
||||
missing_types: Set[int] = _ADDRESS_RECORD_TYPES - seen_types
|
||||
if answers:
|
||||
if missing_types:
|
||||
assert service.server is not None, "Service server must be set for NSEC record."
|
||||
additionals.add(service._dns_nsec(list(missing_types), None))
|
||||
for answer in answers:
|
||||
answer_set[answer] = additionals
|
||||
elif type_ in missing_types:
|
||||
assert service.server is not None, "Service server must be set for NSEC record."
|
||||
answer_set[service._dns_nsec(list(missing_types), None)] = set()
|
||||
|
||||
def _answer_question(
|
||||
self,
|
||||
question: DNSQuestion,
|
||||
strategy_type: _int,
|
||||
types: List[str],
|
||||
services: List[ServiceInfo],
|
||||
known_answers: DNSRRSet,
|
||||
) -> _AnswerWithAdditionalsType:
|
||||
"""Answer a question."""
|
||||
answer_set: _AnswerWithAdditionalsType = {}
|
||||
|
||||
if strategy_type == _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION:
|
||||
self._add_service_type_enumeration_query_answers(types, answer_set, known_answers)
|
||||
elif strategy_type == _ANSWER_STRATEGY_POINTER:
|
||||
self._add_pointer_answers(services, answer_set, known_answers)
|
||||
elif strategy_type == _ANSWER_STRATEGY_ADDRESS:
|
||||
self._add_address_answers(services, answer_set, known_answers, question.type)
|
||||
elif strategy_type == _ANSWER_STRATEGY_SERVICE:
|
||||
# Add recommended additional answers according to
|
||||
# https://tools.ietf.org/html/rfc6763#section-12.2.
|
||||
service = services[0]
|
||||
dns_service = service._dns_service(None)
|
||||
if not known_answers.suppresses(dns_service):
|
||||
answer_set[dns_service] = service._get_address_and_nsec_records(None)
|
||||
elif strategy_type == _ANSWER_STRATEGY_TEXT: # pragma: no branch
|
||||
service = services[0]
|
||||
dns_text = service._dns_text(None)
|
||||
if not known_answers.suppresses(dns_text):
|
||||
answer_set[dns_text] = set()
|
||||
|
||||
return answer_set
|
||||
|
||||
def async_response( # pylint: disable=unused-argument
|
||||
self, msgs: List[DNSIncoming], ucast_source: bool
|
||||
) -> Optional[QuestionAnswers]:
|
||||
"""Deal with incoming query packets. Provides a response if possible.
|
||||
|
||||
This function must be run in the event loop as it is not
|
||||
threadsafe.
|
||||
"""
|
||||
strategies: List[_AnswerStrategy] = []
|
||||
for msg in msgs:
|
||||
for question in msg._questions:
|
||||
strategies.extend(self._get_answer_strategies(question))
|
||||
|
||||
if not strategies:
|
||||
# We have no way to answer the question because we have
|
||||
# nothing in the ServiceRegistry that matches or we do not
|
||||
# understand the question.
|
||||
return None
|
||||
|
||||
is_probe = False
|
||||
msg = msgs[0]
|
||||
questions = msg._questions
|
||||
# Only decode known answers if we are not a probe and we have
|
||||
# at least one answer strategy
|
||||
answers: List[DNSRecord] = []
|
||||
for msg in msgs:
|
||||
if msg.is_probe():
|
||||
is_probe = True
|
||||
else:
|
||||
answers.extend(msg.answers())
|
||||
|
||||
query_res = _QueryResponse(self.cache, questions, is_probe, msg.now)
|
||||
known_answers = DNSRRSet(answers)
|
||||
known_answers_set: Optional[Set[DNSRecord]] = None
|
||||
now = msg.now
|
||||
for strategy in strategies:
|
||||
question = strategy.question
|
||||
is_unicast = question.unique # unique and unicast are the same flag
|
||||
if not is_unicast:
|
||||
if known_answers_set is None: # pragma: no branch
|
||||
known_answers_set = known_answers.lookup_set()
|
||||
self.question_history.add_question_at_time(question, now, known_answers_set)
|
||||
answer_set = self._answer_question(
|
||||
question, strategy.strategy_type, strategy.types, strategy.services, known_answers
|
||||
)
|
||||
if not ucast_source and is_unicast:
|
||||
query_res.add_qu_question_response(answer_set)
|
||||
continue
|
||||
if ucast_source:
|
||||
query_res.add_ucast_question_response(answer_set)
|
||||
# We always multicast as well even if its a unicast
|
||||
# source as long as we haven't done it recently (75% of ttl)
|
||||
query_res.add_mcast_question_response(answer_set)
|
||||
|
||||
return query_res.answers()
|
||||
|
||||
def _get_answer_strategies(
|
||||
self,
|
||||
question: DNSQuestion,
|
||||
) -> List[_AnswerStrategy]:
|
||||
"""Collect strategies to answer a question."""
|
||||
name = question.name
|
||||
question_lower_name = name.lower()
|
||||
type_ = question.type
|
||||
strategies: List[_AnswerStrategy] = []
|
||||
|
||||
if type_ == _TYPE_PTR and question_lower_name == _SERVICE_TYPE_ENUMERATION_NAME:
|
||||
types = self.registry.async_get_types()
|
||||
if types:
|
||||
strategies.append(
|
||||
_AnswerStrategy(
|
||||
question, _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION, types, _EMPTY_SERVICES_LIST
|
||||
)
|
||||
)
|
||||
return strategies
|
||||
|
||||
if type_ in (_TYPE_PTR, _TYPE_ANY):
|
||||
services = self.registry.async_get_infos_type(question_lower_name)
|
||||
if services:
|
||||
strategies.append(
|
||||
_AnswerStrategy(question, _ANSWER_STRATEGY_POINTER, _EMPTY_TYPES_LIST, services)
|
||||
)
|
||||
|
||||
if type_ in (_TYPE_A, _TYPE_AAAA, _TYPE_ANY):
|
||||
services = self.registry.async_get_infos_server(question_lower_name)
|
||||
if services:
|
||||
strategies.append(
|
||||
_AnswerStrategy(question, _ANSWER_STRATEGY_ADDRESS, _EMPTY_TYPES_LIST, services)
|
||||
)
|
||||
|
||||
if type_ in (_TYPE_SRV, _TYPE_TXT, _TYPE_ANY):
|
||||
service = self.registry.async_get_info_name(question_lower_name)
|
||||
if service is not None:
|
||||
if type_ in (_TYPE_SRV, _TYPE_ANY):
|
||||
strategies.append(
|
||||
_AnswerStrategy(question, _ANSWER_STRATEGY_SERVICE, _EMPTY_TYPES_LIST, [service])
|
||||
)
|
||||
if type_ in (_TYPE_TXT, _TYPE_ANY):
|
||||
strategies.append(
|
||||
_AnswerStrategy(question, _ANSWER_STRATEGY_TEXT, _EMPTY_TYPES_LIST, [service])
|
||||
)
|
||||
|
||||
return strategies
|
||||
|
||||
def handle_assembled_query(
|
||||
self,
|
||||
packets: List[DNSIncoming],
|
||||
addr: _str,
|
||||
port: _int,
|
||||
transport: _WrappedTransport,
|
||||
v6_flow_scope: Union[Tuple[()], Tuple[int, int]],
|
||||
) -> None:
|
||||
"""Respond to a (re)assembled query.
|
||||
|
||||
If the protocol recieved packets with the TC bit set, it will
|
||||
wait a bit for the rest of the packets and only call
|
||||
handle_assembled_query once it has a complete set of packets
|
||||
or the timer expires. If the TC bit is not set, a single
|
||||
packet will be in packets.
|
||||
"""
|
||||
first_packet = packets[0]
|
||||
ucast_source = port != _MDNS_PORT
|
||||
question_answers = self.async_response(packets, ucast_source)
|
||||
if question_answers is None:
|
||||
return
|
||||
if question_answers.ucast:
|
||||
questions = first_packet._questions
|
||||
id_ = first_packet.id
|
||||
out = construct_outgoing_unicast_answers(question_answers.ucast, ucast_source, questions, id_)
|
||||
# When sending unicast, only send back the reply
|
||||
# via the same socket that it was recieved from
|
||||
# as we know its reachable from that socket
|
||||
self.zc.async_send(out, addr, port, v6_flow_scope, transport)
|
||||
if question_answers.mcast_now:
|
||||
self.zc.async_send(construct_outgoing_multicast_answers(question_answers.mcast_now))
|
||||
if question_answers.mcast_aggregate:
|
||||
self.out_queue.async_add(first_packet.now, question_answers.mcast_aggregate)
|
||||
if question_answers.mcast_aggregate_last_second:
|
||||
# https://datatracker.ietf.org/doc/html/rfc6762#section-14
|
||||
# If we broadcast it in the last second, we have to delay
|
||||
# at least a second before we send it again
|
||||
self.out_delay_queue.async_add(first_packet.now, question_answers.mcast_aggregate_last_second)
|
Binary file not shown.
42
resources/lib/deps/zeroconf/_handlers/record_manager.pxd
Normal file
42
resources/lib/deps/zeroconf/_handlers/record_manager.pxd
Normal file
|
@ -0,0 +1,42 @@
|
|||
|
||||
import cython
|
||||
|
||||
from .._cache cimport DNSCache
|
||||
from .._dns cimport DNSQuestion, DNSRecord
|
||||
from .._protocol.incoming cimport DNSIncoming
|
||||
from .._updates cimport RecordUpdateListener
|
||||
from .._utils.time cimport current_time_millis
|
||||
|
||||
|
||||
cdef cython.float _DNS_PTR_MIN_TTL
|
||||
cdef cython.uint _TYPE_PTR
|
||||
cdef object _ADDRESS_RECORD_TYPES
|
||||
cdef object RecordUpdate
|
||||
cdef bint TYPE_CHECKING
|
||||
cdef object _TYPE_PTR
|
||||
|
||||
|
||||
cdef class RecordManager:
|
||||
|
||||
cdef public object zc
|
||||
cdef public DNSCache cache
|
||||
cdef public cython.set listeners
|
||||
|
||||
cpdef void async_updates(self, object now, object records)
|
||||
|
||||
cpdef void async_updates_complete(self, bint notify)
|
||||
|
||||
@cython.locals(
|
||||
cache=DNSCache,
|
||||
record=DNSRecord,
|
||||
answers=cython.list,
|
||||
maybe_entry=DNSRecord,
|
||||
)
|
||||
cpdef void async_updates_from_response(self, DNSIncoming msg)
|
||||
|
||||
cpdef void async_add_listener(self, RecordUpdateListener listener, object question)
|
||||
|
||||
cpdef void async_remove_listener(self, RecordUpdateListener listener)
|
||||
|
||||
@cython.locals(question=DNSQuestion, record=DNSRecord)
|
||||
cdef void _async_update_matching_records(self, RecordUpdateListener listener, cython.list questions)
|
215
resources/lib/deps/zeroconf/_handlers/record_manager.py
Normal file
215
resources/lib/deps/zeroconf/_handlers/record_manager.py
Normal file
|
@ -0,0 +1,215 @@
|
|||
""" 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 typing import TYPE_CHECKING, List, Optional, Set, Tuple, Union, cast
|
||||
|
||||
from .._cache import _UniqueRecordsType
|
||||
from .._dns import DNSQuestion, DNSRecord
|
||||
from .._logger import log
|
||||
from .._protocol.incoming import DNSIncoming
|
||||
from .._record_update import RecordUpdate
|
||||
from .._updates import RecordUpdateListener
|
||||
from .._utils.time import current_time_millis
|
||||
from ..const import _ADDRESS_RECORD_TYPES, _DNS_PTR_MIN_TTL, _TYPE_PTR
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._core import Zeroconf
|
||||
|
||||
_float = float
|
||||
|
||||
|
||||
class RecordManager:
|
||||
"""Process records into the cache and notify listeners."""
|
||||
|
||||
__slots__ = ("zc", "cache", "listeners")
|
||||
|
||||
def __init__(self, zeroconf: 'Zeroconf') -> None:
|
||||
"""Init the record manager."""
|
||||
self.zc = zeroconf
|
||||
self.cache = zeroconf.cache
|
||||
self.listeners: Set[RecordUpdateListener] = set()
|
||||
|
||||
def async_updates(self, now: _float, records: List[RecordUpdate]) -> None:
|
||||
"""Used to notify listeners of new information that has updated
|
||||
a record.
|
||||
|
||||
This method must be called before the cache is updated.
|
||||
|
||||
This method will be run in the event loop.
|
||||
"""
|
||||
for listener in self.listeners:
|
||||
listener.async_update_records(self.zc, now, records)
|
||||
|
||||
def async_updates_complete(self, notify: bool) -> None:
|
||||
"""Used to notify listeners of new information that has updated
|
||||
a record.
|
||||
|
||||
This method must be called after the cache is updated.
|
||||
|
||||
This method will be run in the event loop.
|
||||
"""
|
||||
for listener in self.listeners:
|
||||
listener.async_update_records_complete()
|
||||
if notify:
|
||||
self.zc.async_notify_all()
|
||||
|
||||
def async_updates_from_response(self, msg: DNSIncoming) -> None:
|
||||
"""Deal with incoming response packets. All answers
|
||||
are held in the cache, and listeners are notified.
|
||||
|
||||
This function must be run in the event loop as it is not
|
||||
threadsafe.
|
||||
"""
|
||||
updates: List[RecordUpdate] = []
|
||||
address_adds: List[DNSRecord] = []
|
||||
other_adds: List[DNSRecord] = []
|
||||
removes: Set[DNSRecord] = set()
|
||||
now = msg.now
|
||||
unique_types: Set[Tuple[str, int, int]] = set()
|
||||
cache = self.cache
|
||||
answers = msg.answers()
|
||||
|
||||
for record in answers:
|
||||
# Protect zeroconf from records that can cause denial of service.
|
||||
#
|
||||
# We enforce a minimum TTL for PTR records to avoid
|
||||
# ServiceBrowsers generating excessive queries refresh queries.
|
||||
# Apple uses a 15s minimum TTL, however we do not have the same
|
||||
# level of rate limit and safe guards so we use 1/4 of the recommended value.
|
||||
record_type = record.type
|
||||
record_ttl = record.ttl
|
||||
if record_ttl and record_type == _TYPE_PTR and record_ttl < _DNS_PTR_MIN_TTL:
|
||||
log.debug(
|
||||
"Increasing effective ttl of %s to minimum of %s to protect against excessive refreshes.",
|
||||
record,
|
||||
_DNS_PTR_MIN_TTL,
|
||||
)
|
||||
record.set_created_ttl(record.created, _DNS_PTR_MIN_TTL)
|
||||
|
||||
if record.unique: # https://tools.ietf.org/html/rfc6762#section-10.2
|
||||
unique_types.add((record.name, record_type, record.class_))
|
||||
|
||||
if TYPE_CHECKING:
|
||||
record = cast(_UniqueRecordsType, record)
|
||||
|
||||
maybe_entry = cache.async_get_unique(record)
|
||||
if not record.is_expired(now):
|
||||
if maybe_entry is not None:
|
||||
maybe_entry.reset_ttl(record)
|
||||
else:
|
||||
if record_type in _ADDRESS_RECORD_TYPES:
|
||||
address_adds.append(record)
|
||||
else:
|
||||
other_adds.append(record)
|
||||
updates.append(RecordUpdate(record, maybe_entry))
|
||||
# This is likely a goodbye since the record is
|
||||
# expired and exists in the cache
|
||||
elif maybe_entry is not None:
|
||||
updates.append(RecordUpdate(record, maybe_entry))
|
||||
removes.add(record)
|
||||
|
||||
if unique_types:
|
||||
cache.async_mark_unique_records_older_than_1s_to_expire(unique_types, answers, now)
|
||||
|
||||
if updates:
|
||||
self.async_updates(now, updates)
|
||||
# The cache adds must be processed AFTER we trigger
|
||||
# the updates since we compare existing data
|
||||
# with the new data and updating the cache
|
||||
# ahead of update_record will cause listeners
|
||||
# to miss changes
|
||||
#
|
||||
# We must process address adds before non-addresses
|
||||
# otherwise a fetch of ServiceInfo may miss an address
|
||||
# because it thinks the cache is complete
|
||||
#
|
||||
# The cache is processed under the context manager to ensure
|
||||
# that any ServiceBrowser that is going to call
|
||||
# zc.get_service_info will see the cached value
|
||||
# but ONLY after all the record updates have been
|
||||
# processsed.
|
||||
new = False
|
||||
if other_adds or address_adds:
|
||||
new = cache.async_add_records(address_adds)
|
||||
if cache.async_add_records(other_adds):
|
||||
new = True
|
||||
# Removes are processed last since
|
||||
# ServiceInfo could generate an un-needed query
|
||||
# because the data was not yet populated.
|
||||
if removes:
|
||||
cache.async_remove_records(removes)
|
||||
if updates:
|
||||
self.async_updates_complete(new)
|
||||
|
||||
def async_add_listener(
|
||||
self, listener: RecordUpdateListener, question: Optional[Union[DNSQuestion, List[DNSQuestion]]]
|
||||
) -> None:
|
||||
"""Adds a listener for a given question. The listener will have
|
||||
its update_record method called when information is available to
|
||||
answer the question(s).
|
||||
|
||||
This function is not thread-safe and must be called in the eventloop.
|
||||
"""
|
||||
if not isinstance(listener, RecordUpdateListener):
|
||||
log.error( # type: ignore[unreachable]
|
||||
"listeners passed to async_add_listener must inherit from RecordUpdateListener;"
|
||||
" In the future this will fail"
|
||||
)
|
||||
|
||||
self.listeners.add(listener)
|
||||
|
||||
if question is None:
|
||||
return
|
||||
|
||||
questions = [question] if isinstance(question, DNSQuestion) else question
|
||||
self._async_update_matching_records(listener, questions)
|
||||
|
||||
def _async_update_matching_records(
|
||||
self, listener: RecordUpdateListener, questions: List[DNSQuestion]
|
||||
) -> None:
|
||||
"""Calls back any existing entries in the cache that answer the question.
|
||||
|
||||
This function must be run from the event loop.
|
||||
"""
|
||||
now = current_time_millis()
|
||||
records: List[RecordUpdate] = [
|
||||
RecordUpdate(record, None)
|
||||
for question in questions
|
||||
for record in self.cache.async_entries_with_name(question.name)
|
||||
if not record.is_expired(now) and question.answered_by(record)
|
||||
]
|
||||
if not records:
|
||||
return
|
||||
listener.async_update_records(self.zc, now, records)
|
||||
listener.async_update_records_complete()
|
||||
self.zc.async_notify_all()
|
||||
|
||||
def async_remove_listener(self, listener: RecordUpdateListener) -> None:
|
||||
"""Removes a listener.
|
||||
|
||||
This function is not threadsafe and must be called in the eventloop.
|
||||
"""
|
||||
try:
|
||||
self.listeners.remove(listener)
|
||||
self.zc.async_notify_all()
|
||||
except ValueError as e:
|
||||
log.exception('Failed to remove listener: %r', e)
|
Loading…
Add table
Add a link
Reference in a new issue