uh oh im bundling the deps

This commit is contained in:
cere 2024-02-21 01:17:59 -05:00
parent ae28da8d60
commit ecca301ceb
584 changed files with 119933 additions and 24 deletions

View 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
"""

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

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

View file

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

View file

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

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

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

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

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