From 273ba7a270c15a5bd5405ff6ad07bffdc0de6a70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 09:52:05 -0600 Subject: [PATCH 01/31] feat: avoid decoding known answers if we have no answers to give --- src/zeroconf/_handlers/query_handler.py | 54 ++++++++++++++++++++++++- src/zeroconf/_record_update.py | 1 - 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 4e74aa5c..44030c87 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -20,13 +20,14 @@ USA """ - -from typing import TYPE_CHECKING, List, Optional, Set, cast +import logging +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 .._history import QuestionHistory from .._protocol.incoming import DNSIncoming +from .._services.info import ServiceInfo from .._services.registry import ServiceRegistry from .._utils.net import IPVersion from ..const import ( @@ -46,6 +47,7 @@ from .answers import QuestionAnswers, _AnswerWithAdditionalsType _RESPOND_IMMEDIATE_TYPES = {_TYPE_NSEC, _TYPE_SRV, *_ADDRESS_RECORD_TYPES} +_LOGGER = logging.getLogger(__name__) _IPVersion_ALL = IPVersion.All @@ -152,6 +154,13 @@ def _has_mcast_record_in_last_second(self, record: DNSRecord) -> bool: return bool(maybe_entry is not None and self._now - maybe_entry.created < _ONE_SECOND) +_ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION = 0 +_ANSWER_STRATEGY_POINTER = 1 +_ANSWER_STRATEGY_ADDRESS = 2 +_ANSWER_STRATEGY_SERVICE = 3 +_ANSWER_STRATEGY_TEXT = 4 + + class QueryHandler: """Query the ServiceRegistry.""" @@ -221,6 +230,40 @@ def _add_address_answers( 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 _get_answer_strategies( + self, + question: DNSQuestion, + ) -> List[Tuple[DNSQuestion, int, Union[List[str], ServiceInfo, List[ServiceInfo]]]]: + """Collect strategies to answer a question.""" + question_lower_name = question.name.lower() + type_ = question.type + strategies: List[Tuple[DNSQuestion, int, Union[List[str], ServiceInfo, List[ServiceInfo]]]] = [] + + if type_ == _TYPE_PTR and question_lower_name == _SERVICE_TYPE_ENUMERATION_NAME: + types = self.registry.async_get_types() + if types: + strategies.append((question, _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION, types)) + + if type_ in (_TYPE_PTR, _TYPE_ANY): + services = self.registry.async_get_infos_type(question_lower_name) + if services: + strategies.append((question, _ANSWER_STRATEGY_POINTER, services)) + + if type_ in (_TYPE_A, _TYPE_AAAA, _TYPE_ANY): + services = self.registry.async_get_infos_server(question_lower_name) + if services: + strategies.append((question, _ANSWER_STRATEGY_ADDRESS, 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((question, _ANSWER_STRATEGY_SERVICE, service)) + if type_ in (_TYPE_TXT, _TYPE_ANY): + strategies.append((question, _ANSWER_STRATEGY_TEXT, service)) + + return strategies + def _answer_question( self, question: DNSQuestion, @@ -279,6 +322,13 @@ def async_response( # pylint: disable=unused-argument query_res = _QueryResponse(self.cache, questions, is_probe, now) known_answers_set: Optional[Set[DNSRecord]] = None + strategies: List[Tuple[DNSQuestion, int, Union[List[str], ServiceInfo, List[ServiceInfo]]]] = [] + for msg in msgs: + for question in msg.questions: + strategies.extend(self._get_answer_strategies(question)) + + _LOGGER.warning("Answering %s with %s", questions, strategies) + for msg in msgs: for question in msg.questions: if not question.unique: # unique and unicast are the same flag diff --git a/src/zeroconf/_record_update.py b/src/zeroconf/_record_update.py index 5a362534..8e0e4bdb 100644 --- a/src/zeroconf/_record_update.py +++ b/src/zeroconf/_record_update.py @@ -26,7 +26,6 @@ class RecordUpdate: - __slots__ = ("new", "old") def __init__(self, new: DNSRecord, old: Optional[DNSRecord] = None): From 5e4f56f2ed352403a19e6a1c659d3cfac0396dae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 09:54:50 -0600 Subject: [PATCH 02/31] feat: avoid decoding known answers if we have no answers to give --- src/zeroconf/_handlers/query_handler.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 44030c87..7e509938 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -46,6 +46,8 @@ ) from .answers import QuestionAnswers, _AnswerWithAdditionalsType +AnswerStrategyType = Tuple[DNSQuestion, int, Union[List[str], ServiceInfo, List[ServiceInfo]]] + _RESPOND_IMMEDIATE_TYPES = {_TYPE_NSEC, _TYPE_SRV, *_ADDRESS_RECORD_TYPES} _LOGGER = logging.getLogger(__name__) @@ -233,11 +235,11 @@ def _add_address_answers( def _get_answer_strategies( self, question: DNSQuestion, - ) -> List[Tuple[DNSQuestion, int, Union[List[str], ServiceInfo, List[ServiceInfo]]]]: + ) -> List[AnswerStrategyType]: """Collect strategies to answer a question.""" question_lower_name = question.name.lower() type_ = question.type - strategies: List[Tuple[DNSQuestion, int, Union[List[str], ServiceInfo, List[ServiceInfo]]]] = [] + strategies: List[AnswerStrategyType] = [] if type_ == _TYPE_PTR and question_lower_name == _SERVICE_TYPE_ENUMERATION_NAME: types = self.registry.async_get_types() @@ -322,7 +324,7 @@ def async_response( # pylint: disable=unused-argument query_res = _QueryResponse(self.cache, questions, is_probe, now) known_answers_set: Optional[Set[DNSRecord]] = None - strategies: List[Tuple[DNSQuestion, int, Union[List[str], ServiceInfo, List[ServiceInfo]]]] = [] + strategies: List[AnswerStrategyType] = [] for msg in msgs: for question in msg.questions: strategies.extend(self._get_answer_strategies(question)) From 2ae8897f143fc4d5acc124f9793219a83f9004c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:02:46 -0600 Subject: [PATCH 03/31] feat: avoid decoding known answers if we have no answers to give --- src/zeroconf/_handlers/query_handler.py | 100 ++++++++++++------------ 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 7e509938..0e0f6a60 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -175,13 +175,13 @@ def __init__(self, registry: ServiceRegistry, cache: DNSCache, question_history: self.question_history = question_history def _add_service_type_enumeration_query_answers( - self, answer_set: _AnswerWithAdditionalsType, known_answers: DNSRRSet + 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 self.registry.async_get_types(): + for stype in types: dns_pointer = DNSPointer( _SERVICE_TYPE_ENUMERATION_NAME, _TYPE_PTR, _CLASS_IN, _DNS_OTHER_TTL, stype, 0.0 ) @@ -189,10 +189,10 @@ def _add_service_type_enumeration_query_answers( answer_set[dns_pointer] = set() def _add_pointer_answers( - self, lower_name: str, answer_set: _AnswerWithAdditionalsType, known_answers: DNSRRSet + self, services: List[ServiceInfo], answer_set: _AnswerWithAdditionalsType, known_answers: DNSRRSet ) -> None: """Answer PTR/ANY question.""" - for service in self.registry.async_get_infos_type(lower_name): + 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) @@ -205,13 +205,13 @@ def _add_pointer_answers( def _add_address_answers( self, - lower_name: str, + services: List[ServiceInfo], answer_set: _AnswerWithAdditionalsType, known_answers: DNSRRSet, type_: _int, ) -> None: """Answer A/AAAA/ANY question.""" - for service in self.registry.async_get_infos_server(lower_name): + for service in services: answers: List[DNSAddress] = [] additionals: Set[DNSRecord] = set() seen_types: Set[int] = set() @@ -269,36 +269,40 @@ def _get_answer_strategies( def _answer_question( self, question: DNSQuestion, + strategy_type: int, + data: Union[List[str], ServiceInfo, List[ServiceInfo]], known_answers: DNSRRSet, ) -> _AnswerWithAdditionalsType: """Answer a question.""" answer_set: _AnswerWithAdditionalsType = {} - question_lower_name = question.name.lower() type_ = question.type - if type_ == _TYPE_PTR and question_lower_name == _SERVICE_TYPE_ENUMERATION_NAME: - self._add_service_type_enumeration_query_answers(answer_set, known_answers) + if strategy_type == _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION: + types = cast(List[str], data) + self._add_service_type_enumeration_query_answers(types, answer_set, known_answers) return answer_set - if type_ in (_TYPE_PTR, _TYPE_ANY): - self._add_pointer_answers(question_lower_name, answer_set, known_answers) + if strategy_type == _ANSWER_STRATEGY_POINTER: + services = cast(List[ServiceInfo], data) + self._add_pointer_answers(services, answer_set, known_answers) - if type_ in (_TYPE_A, _TYPE_AAAA, _TYPE_ANY): - self._add_address_answers(question_lower_name, answer_set, known_answers, type_) + if strategy_type == _ANSWER_STRATEGY_ADDRESS: + services = cast(List[ServiceInfo], data) + self._add_address_answers(services, answer_set, known_answers, type_) - 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): - # Add recommended additional answers according to - # https://tools.ietf.org/html/rfc6763#section-12.2. - dns_service = service._dns_service(None) - if known_answers.suppresses(dns_service) is False: - answer_set[dns_service] = service._get_address_and_nsec_records(None) - if type_ in (_TYPE_TXT, _TYPE_ANY): - dns_text = service._dns_text(None) - if known_answers.suppresses(dns_text) is False: - answer_set[dns_text] = set() + if strategy_type == _ANSWER_STRATEGY_SERVICE: + # Add recommended additional answers according to + # https://tools.ietf.org/html/rfc6763#section-12.2. + service = cast(ServiceInfo, data) + dns_service = service._dns_service(None) + if known_answers.suppresses(dns_service) is False: + answer_set[dns_service] = service._get_address_and_nsec_records(None) + + if strategy_type == _ANSWER_STRATEGY_TEXT: + service = cast(ServiceInfo, data) + dns_text = service._dns_text(None) + if known_answers.suppresses(dns_text) is False: + answer_set[dns_text] = set() return answer_set @@ -315,36 +319,34 @@ def async_response( # pylint: disable=unused-argument msg = msgs[0] questions = msg.questions now = msg.now - for msg in msgs: - if msg.is_probe() is False: - answers.extend(msg.answers()) - else: - is_probe = True - known_answers = DNSRRSet(answers) query_res = _QueryResponse(self.cache, questions, is_probe, now) - known_answers_set: Optional[Set[DNSRecord]] = None strategies: List[AnswerStrategyType] = [] for msg in msgs: + if msg.is_probe() is True: + is_probe = True for question in msg.questions: strategies.extend(self._get_answer_strategies(question)) - _LOGGER.warning("Answering %s with %s", questions, strategies) + if not strategies: + # TODO: return None + return query_res.answers() - for msg in msgs: - for question in msg.questions: - if not question.unique: # unique and unicast are the same flag - if not known_answers_set: # 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, known_answers) - if not ucast_source and question.unique: # unique and unicast are the same flag - 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) + known_answers = DNSRRSet(answers) + known_answers_set: Optional[Set[DNSRecord]] = None + for question, strategy_type, data in strategies: + if not question.unique: # unique and unicast are the same flag + if not known_answers_set: # 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_type, data, known_answers) + if not ucast_source and question.unique: # unique and unicast are the same flag + 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() From e1161b8abc3007d18073b7dfb6b41409153f53c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:13:30 -0600 Subject: [PATCH 04/31] fix: fixes --- src/zeroconf/_handlers/query_handler.pxd | 15 +++++++++++---- src/zeroconf/_handlers/query_handler.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.pxd b/src/zeroconf/_handlers/query_handler.pxd index ff970d76..54f0424c 100644 --- a/src/zeroconf/_handlers/query_handler.pxd +++ b/src/zeroconf/_handlers/query_handler.pxd @@ -18,6 +18,13 @@ 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 class _QueryResponse: cdef bint _is_probe @@ -53,16 +60,16 @@ cdef class QueryHandler: cdef QuestionHistory question_history @cython.locals(service=ServiceInfo) - cdef _add_service_type_enumeration_query_answers(self, cython.dict answer_set, DNSRRSet known_answers) + cdef _add_service_type_enumeration_query_answers(self, list types, cython.dict answer_set, DNSRRSet known_answers) @cython.locals(service=ServiceInfo) - cdef _add_pointer_answers(self, str lower_name, cython.dict answer_set, DNSRRSet known_answers) + cdef _add_pointer_answers(self, list services, cython.dict answer_set, DNSRRSet known_answers) @cython.locals(service=ServiceInfo, dns_address=DNSAddress) - cdef _add_address_answers(self, str lower_name, cython.dict answer_set, DNSRRSet known_answers, cython.uint type_) + cdef _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, DNSRRSet known_answers) + cdef cython.dict _answer_question(self, DNSQuestion question, unsigned int strategy_type, object data, DNSRRSet known_answers) @cython.locals( msg=DNSIncoming, diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 0e0f6a60..ff8e695c 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -269,7 +269,7 @@ def _get_answer_strategies( def _answer_question( self, question: DNSQuestion, - strategy_type: int, + strategy_type: _int, data: Union[List[str], ServiceInfo, List[ServiceInfo]], known_answers: DNSRRSet, ) -> _AnswerWithAdditionalsType: From fc83516507d4030c1335feeb1a30c2c0544bc529 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:21:11 -0600 Subject: [PATCH 05/31] fix: fixes --- src/zeroconf/_handlers/query_handler.pxd | 1 + src/zeroconf/_handlers/query_handler.py | 32 ++++++++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.pxd b/src/zeroconf/_handlers/query_handler.pxd index 54f0424c..9159327e 100644 --- a/src/zeroconf/_handlers/query_handler.pxd +++ b/src/zeroconf/_handlers/query_handler.pxd @@ -77,6 +77,7 @@ cdef class QueryHandler: answer_set=cython.dict, known_answers=DNSRRSet, known_answers_set=cython.set, + is_unicast=bint, is_probe=object, now=object ) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index ff8e695c..c86081a5 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -278,28 +278,43 @@ def _answer_question( type_ = question.type if strategy_type == _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION: - types = cast(List[str], data) + if TYPE_CHECKING: + types = cast("List[str]", data) + else: + types = data self._add_service_type_enumeration_query_answers(types, answer_set, known_answers) return answer_set if strategy_type == _ANSWER_STRATEGY_POINTER: - services = cast(List[ServiceInfo], data) + if TYPE_CHECKING: + services = cast("List[ServiceInfo]", data) + else: + services = data self._add_pointer_answers(services, answer_set, known_answers) if strategy_type == _ANSWER_STRATEGY_ADDRESS: - services = cast(List[ServiceInfo], data) + if TYPE_CHECKING: + services = cast("List[ServiceInfo]", data) + else: + services = data self._add_address_answers(services, answer_set, known_answers, type_) if strategy_type == _ANSWER_STRATEGY_SERVICE: # Add recommended additional answers according to # https://tools.ietf.org/html/rfc6763#section-12.2. - service = cast(ServiceInfo, data) + if TYPE_CHECKING: + service = cast(ServiceInfo, data) + else: + service = data dns_service = service._dns_service(None) if known_answers.suppresses(dns_service) is False: answer_set[dns_service] = service._get_address_and_nsec_records(None) if strategy_type == _ANSWER_STRATEGY_TEXT: - service = cast(ServiceInfo, data) + if TYPE_CHECKING: + service = cast(ServiceInfo, data) + else: + service = data dns_text = service._dns_text(None) if known_answers.suppresses(dns_text) is False: answer_set[dns_text] = set() @@ -335,12 +350,13 @@ def async_response( # pylint: disable=unused-argument known_answers = DNSRRSet(answers) known_answers_set: Optional[Set[DNSRecord]] = None for question, strategy_type, data in strategies: - if not question.unique: # unique and unicast are the same flag - if not known_answers_set: # pragma: no branch + 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_type, data, known_answers) - if not ucast_source and question.unique: # unique and unicast are the same flag + if not ucast_source and is_unicast: # unique and unicast are the same flag query_res.add_qu_question_response(answer_set) continue if ucast_source: From 19d8262638173f18c2ba799dde2dcb560bc40077 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:24:37 -0600 Subject: [PATCH 06/31] fix: fixes --- src/zeroconf/_handlers/query_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index c86081a5..3205be9e 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -329,7 +329,6 @@ def async_response( # pylint: disable=unused-argument This function must be run in the event loop as it is not threadsafe. """ - answers: List[DNSRecord] = [] is_probe = False msg = msgs[0] questions = msg.questions @@ -347,6 +346,11 @@ def async_response( # pylint: disable=unused-argument # TODO: return None return query_res.answers() + answers: List[DNSRecord] = [] + if not is_probe: + for msg in msgs: + answers.extend(msg.answers()) + known_answers = DNSRRSet(answers) known_answers_set: Optional[Set[DNSRecord]] = None for question, strategy_type, data in strategies: From 80003b996fe54bc2d554312e76a9df3df7da1c6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:25:08 -0600 Subject: [PATCH 07/31] fix: fixes --- src/zeroconf/_handlers/query_handler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 3205be9e..b796760e 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -346,6 +346,8 @@ def async_response( # pylint: disable=unused-argument # TODO: return None return query_res.answers() + # Only decode known answers if we are not a probe and we have + # at least one answer strategy answers: List[DNSRecord] = [] if not is_probe: for msg in msgs: From 9f2f1eb944f8ce1cd8b9604015d6c0e7daf7abf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:26:34 -0600 Subject: [PATCH 08/31] fix: fixes --- src/zeroconf/_handlers/query_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index b796760e..5ae4b6dd 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -333,7 +333,6 @@ def async_response( # pylint: disable=unused-argument msg = msgs[0] questions = msg.questions now = msg.now - query_res = _QueryResponse(self.cache, questions, is_probe, now) strategies: List[AnswerStrategyType] = [] for msg in msgs: @@ -342,6 +341,8 @@ def async_response( # pylint: disable=unused-argument for question in msg.questions: strategies.extend(self._get_answer_strategies(question)) + query_res = _QueryResponse(self.cache, questions, is_probe, now) + if not strategies: # TODO: return None return query_res.answers() From 4c84de6d56a797d4507f9bbd5457b41142e6c381 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:27:43 -0600 Subject: [PATCH 09/31] fix: fixes --- src/zeroconf/_handlers/query_handler.py | 68 ++++++++++++------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 5ae4b6dd..25a22015 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -232,40 +232,6 @@ def _add_address_answers( 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 _get_answer_strategies( - self, - question: DNSQuestion, - ) -> List[AnswerStrategyType]: - """Collect strategies to answer a question.""" - question_lower_name = question.name.lower() - type_ = question.type - strategies: List[AnswerStrategyType] = [] - - if type_ == _TYPE_PTR and question_lower_name == _SERVICE_TYPE_ENUMERATION_NAME: - types = self.registry.async_get_types() - if types: - strategies.append((question, _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION, types)) - - if type_ in (_TYPE_PTR, _TYPE_ANY): - services = self.registry.async_get_infos_type(question_lower_name) - if services: - strategies.append((question, _ANSWER_STRATEGY_POINTER, services)) - - if type_ in (_TYPE_A, _TYPE_AAAA, _TYPE_ANY): - services = self.registry.async_get_infos_server(question_lower_name) - if services: - strategies.append((question, _ANSWER_STRATEGY_ADDRESS, 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((question, _ANSWER_STRATEGY_SERVICE, service)) - if type_ in (_TYPE_TXT, _TYPE_ANY): - strategies.append((question, _ANSWER_STRATEGY_TEXT, service)) - - return strategies - def _answer_question( self, question: DNSQuestion, @@ -373,3 +339,37 @@ def async_response( # pylint: disable=unused-argument query_res.add_mcast_question_response(answer_set) return query_res.answers() + + def _get_answer_strategies( + self, + question: DNSQuestion, + ) -> List[AnswerStrategyType]: + """Collect strategies to answer a question.""" + question_lower_name = question.name.lower() + type_ = question.type + strategies: List[AnswerStrategyType] = [] + + if type_ == _TYPE_PTR and question_lower_name == _SERVICE_TYPE_ENUMERATION_NAME: + types = self.registry.async_get_types() + if types: + strategies.append((question, _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION, types)) + + if type_ in (_TYPE_PTR, _TYPE_ANY): + services = self.registry.async_get_infos_type(question_lower_name) + if services: + strategies.append((question, _ANSWER_STRATEGY_POINTER, services)) + + if type_ in (_TYPE_A, _TYPE_AAAA, _TYPE_ANY): + services = self.registry.async_get_infos_server(question_lower_name) + if services: + strategies.append((question, _ANSWER_STRATEGY_ADDRESS, 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((question, _ANSWER_STRATEGY_SERVICE, service)) + if type_ in (_TYPE_TXT, _TYPE_ANY): + strategies.append((question, _ANSWER_STRATEGY_TEXT, service)) + + return strategies From 1f1e76b9e726fbc233aa506c90e6d297a4f5da8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:31:44 -0600 Subject: [PATCH 10/31] fix: make responses optional --- src/zeroconf/_core.py | 2 ++ src/zeroconf/_handlers/query_handler.py | 25 +++++++++++-------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/zeroconf/_core.py b/src/zeroconf/_core.py index 40375484..7e66c5e2 100644 --- a/src/zeroconf/_core.py +++ b/src/zeroconf/_core.py @@ -586,6 +586,8 @@ def handle_assembled_query( now = packets[0].now ucast_source = port != _MDNS_PORT question_answers = self.query_handler.async_response(packets, ucast_source) + if not question_answers: + return if question_answers.ucast: questions = packets[0].questions id_ = packets[0].id diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 25a22015..47402ca5 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -289,37 +289,34 @@ def _answer_question( def async_response( # pylint: disable=unused-argument self, msgs: List[DNSIncoming], ucast_source: bool - ) -> QuestionAnswers: + ) -> 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. """ - is_probe = False - msg = msgs[0] - questions = msg.questions - now = msg.now - strategies: List[AnswerStrategyType] = [] for msg in msgs: - if msg.is_probe() is True: - is_probe = True for question in msg.questions: strategies.extend(self._get_answer_strategies(question)) - query_res = _QueryResponse(self.cache, questions, is_probe, now) - if not strategies: - # TODO: return None - return query_res.answers() + return None + is_probe = False + msg = msgs[0] + questions = msg.questions + now = msg.now # Only decode known answers if we are not a probe and we have # at least one answer strategy answers: List[DNSRecord] = [] - if not is_probe: - for msg in msgs: + for msg in msgs: + if msg.is_probe() is True: + is_probe = True + else: answers.extend(msg.answers()) + query_res = _QueryResponse(self.cache, questions, is_probe, now) known_answers = DNSRRSet(answers) known_answers_set: Optional[Set[DNSRecord]] = None for question, strategy_type, data in strategies: From 488a1df335ad2585c074c4126a87c7dccb43cf8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:43:28 -0600 Subject: [PATCH 11/31] fix: cleanups --- src/zeroconf/_handlers/query_handler.pxd | 16 ++++++++++------ src/zeroconf/_handlers/query_handler.py | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.pxd b/src/zeroconf/_handlers/query_handler.pxd index 9159327e..90d48596 100644 --- a/src/zeroconf/_handlers/query_handler.pxd +++ b/src/zeroconf/_handlers/query_handler.pxd @@ -18,11 +18,11 @@ 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 object _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION +cdef object _ANSWER_STRATEGY_POINTER +cdef object _ANSWER_STRATEGY_ADDRESS +cdef object _ANSWER_STRATEGY_SERVICE +cdef object _ANSWER_STRATEGY_TEXT cdef class _QueryResponse: @@ -73,12 +73,16 @@ cdef class QueryHandler: @cython.locals( msg=DNSIncoming, + msgs=list, question=DNSQuestion, answer_set=cython.dict, known_answers=DNSRRSet, known_answers_set=cython.set, is_unicast=bint, is_probe=object, - now=object + now=float ) cpdef async_response(self, cython.list msgs, cython.bint unicast_source) + + @cython.locals(name=str, question_lower_name=str) + cdef _get_answer_strategies(self, DNSQuestion question) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 47402ca5..a9389b34 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -342,7 +342,8 @@ def _get_answer_strategies( question: DNSQuestion, ) -> List[AnswerStrategyType]: """Collect strategies to answer a question.""" - question_lower_name = question.name.lower() + name = question.name + question_lower_name = name.lower() type_ = question.type strategies: List[AnswerStrategyType] = [] From 9b4d78d4a2060e8d6b98702331969529432d8858 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:52:09 -0600 Subject: [PATCH 12/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.pxd | 14 ++++--- src/zeroconf/_handlers/query_handler.py | 50 +++++++++--------------- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.pxd b/src/zeroconf/_handlers/query_handler.pxd index 90d48596..428274ca 100644 --- a/src/zeroconf/_handlers/query_handler.pxd +++ b/src/zeroconf/_handlers/query_handler.pxd @@ -18,12 +18,14 @@ cdef cython.set _ADDRESS_RECORD_TYPES cdef object IPVersion, _IPVersion_ALL cdef object _TYPE_PTR, _CLASS_IN, _DNS_OTHER_TTL -cdef object _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION -cdef object _ANSWER_STRATEGY_POINTER -cdef object _ANSWER_STRATEGY_ADDRESS -cdef object _ANSWER_STRATEGY_SERVICE -cdef object _ANSWER_STRATEGY_TEXT +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 _QueryResponse: @@ -69,7 +71,7 @@ cdef class QueryHandler: cdef _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, object data, DNSRRSet known_answers) + cdef cython.dict _answer_question(self, DNSQuestion question, unsigned int strategy_type, list types, list services, DNSRRSet known_answers) @cython.locals( msg=DNSIncoming, diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index a9389b34..7f406b2d 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -21,7 +21,7 @@ """ import logging -from typing import TYPE_CHECKING, List, Optional, Set, Tuple, Union, cast +from typing import TYPE_CHECKING, List, Optional, Set, Tuple, cast from .._cache import DNSCache, _UniqueRecordsType from .._dns import DNSAddress, DNSPointer, DNSQuestion, DNSRecord, DNSRRSet @@ -46,11 +46,14 @@ ) from .answers import QuestionAnswers, _AnswerWithAdditionalsType -AnswerStrategyType = Tuple[DNSQuestion, int, Union[List[str], ServiceInfo, List[ServiceInfo]]] +AnswerStrategyType = Tuple[DNSQuestion, int, List[str], List[ServiceInfo]] _RESPOND_IMMEDIATE_TYPES = {_TYPE_NSEC, _TYPE_SRV, *_ADDRESS_RECORD_TYPES} _LOGGER = logging.getLogger(__name__) +_EMPTY_SERVICES_LIST: List[ServiceInfo] = [] +_EMPTY_TYPES_LIST: List[str] = [] + _IPVersion_ALL = IPVersion.All _int = int @@ -236,7 +239,8 @@ def _answer_question( self, question: DNSQuestion, strategy_type: _int, - data: Union[List[str], ServiceInfo, List[ServiceInfo]], + types: List[str], + services: List[ServiceInfo], known_answers: DNSRRSet, ) -> _AnswerWithAdditionalsType: """Answer a question.""" @@ -244,43 +248,25 @@ def _answer_question( type_ = question.type if strategy_type == _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION: - if TYPE_CHECKING: - types = cast("List[str]", data) - else: - types = data self._add_service_type_enumeration_query_answers(types, answer_set, known_answers) return answer_set if strategy_type == _ANSWER_STRATEGY_POINTER: - if TYPE_CHECKING: - services = cast("List[ServiceInfo]", data) - else: - services = data self._add_pointer_answers(services, answer_set, known_answers) if strategy_type == _ANSWER_STRATEGY_ADDRESS: - if TYPE_CHECKING: - services = cast("List[ServiceInfo]", data) - else: - services = data self._add_address_answers(services, answer_set, known_answers, type_) if strategy_type == _ANSWER_STRATEGY_SERVICE: # Add recommended additional answers according to # https://tools.ietf.org/html/rfc6763#section-12.2. - if TYPE_CHECKING: - service = cast(ServiceInfo, data) - else: - service = data + service = services[0] dns_service = service._dns_service(None) if known_answers.suppresses(dns_service) is False: answer_set[dns_service] = service._get_address_and_nsec_records(None) if strategy_type == _ANSWER_STRATEGY_TEXT: - if TYPE_CHECKING: - service = cast(ServiceInfo, data) - else: - service = data + service = services[0] dns_text = service._dns_text(None) if known_answers.suppresses(dns_text) is False: answer_set[dns_text] = set() @@ -319,13 +305,13 @@ def async_response( # pylint: disable=unused-argument query_res = _QueryResponse(self.cache, questions, is_probe, now) known_answers = DNSRRSet(answers) known_answers_set: Optional[Set[DNSRecord]] = None - for question, strategy_type, data in strategies: - is_unicast = question.unique # unique and unicast are the same flag + for question, strategy_type, types, services in strategies: + is_unicast = question.unique is True # 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_type, data, known_answers) + answer_set = self._answer_question(question, strategy_type, types, services, known_answers) if not ucast_source and is_unicast: # unique and unicast are the same flag query_res.add_qu_question_response(answer_set) continue @@ -350,24 +336,26 @@ def _get_answer_strategies( if type_ == _TYPE_PTR and question_lower_name == _SERVICE_TYPE_ENUMERATION_NAME: types = self.registry.async_get_types() if types: - strategies.append((question, _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION, types)) + strategies.append( + (question, _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION, types, _EMPTY_SERVICES_LIST) + ) if type_ in (_TYPE_PTR, _TYPE_ANY): services = self.registry.async_get_infos_type(question_lower_name) if services: - strategies.append((question, _ANSWER_STRATEGY_POINTER, services)) + strategies.append((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((question, _ANSWER_STRATEGY_ADDRESS, services)) + strategies.append((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((question, _ANSWER_STRATEGY_SERVICE, service)) + strategies.append((question, _ANSWER_STRATEGY_SERVICE, _EMPTY_TYPES_LIST, [service])) if type_ in (_TYPE_TXT, _TYPE_ANY): - strategies.append((question, _ANSWER_STRATEGY_TEXT, service)) + strategies.append((question, _ANSWER_STRATEGY_TEXT, _EMPTY_TYPES_LIST, [service])) return strategies From 4c9f7e1d99690802363998a175c500ce06491328 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:57:15 -0600 Subject: [PATCH 13/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.pxd | 1 + src/zeroconf/_handlers/query_handler.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.pxd b/src/zeroconf/_handlers/query_handler.pxd index 428274ca..aeb41511 100644 --- a/src/zeroconf/_handlers/query_handler.pxd +++ b/src/zeroconf/_handlers/query_handler.pxd @@ -76,6 +76,7 @@ cdef class QueryHandler: @cython.locals( msg=DNSIncoming, msgs=list, + strategy=tuple, question=DNSQuestion, answer_set=cython.dict, known_answers=DNSRRSet, diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 7f406b2d..69c32c52 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -204,7 +204,8 @@ def _add_pointer_answers( answer_set[dns_pointer] = { service._dns_service(None), service._dns_text(None), - } | service._get_address_and_nsec_records(None) + *service._get_address_and_nsec_records(None), + } def _add_address_answers( self, @@ -305,12 +306,16 @@ def async_response( # pylint: disable=unused-argument query_res = _QueryResponse(self.cache, questions, is_probe, now) known_answers = DNSRRSet(answers) known_answers_set: Optional[Set[DNSRecord]] = None - for question, strategy_type, types, services in strategies: + for strategy in strategies: + question = strategy[0] is_unicast = question.unique is True # 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) + strategy_type = strategy[1] + types = strategy[2] + services = strategy[3] answer_set = self._answer_question(question, strategy_type, types, services, known_answers) if not ucast_source and is_unicast: # unique and unicast are the same flag query_res.add_qu_question_response(answer_set) From 8102b7e90b50d4e805aadca4623bb88ffeff0f39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 10:59:54 -0600 Subject: [PATCH 14/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.pxd | 10 ++++- src/zeroconf/_handlers/query_handler.py | 57 +++++++++++++++++------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.pxd b/src/zeroconf/_handlers/query_handler.pxd index aeb41511..8c42144c 100644 --- a/src/zeroconf/_handlers/query_handler.pxd +++ b/src/zeroconf/_handlers/query_handler.pxd @@ -27,6 +27,14 @@ 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 @@ -76,7 +84,7 @@ cdef class QueryHandler: @cython.locals( msg=DNSIncoming, msgs=list, - strategy=tuple, + strategy=_AnswerStrategy, question=DNSQuestion, answer_set=cython.dict, known_answers=DNSRRSet, diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 69c32c52..5a053ede 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -21,7 +21,7 @@ """ import logging -from typing import TYPE_CHECKING, List, Optional, Set, Tuple, cast +from typing import TYPE_CHECKING, List, Optional, Set, cast from .._cache import DNSCache, _UniqueRecordsType from .._dns import DNSAddress, DNSPointer, DNSQuestion, DNSRecord, DNSRRSet @@ -46,8 +46,6 @@ ) from .answers import QuestionAnswers, _AnswerWithAdditionalsType -AnswerStrategyType = Tuple[DNSQuestion, int, List[str], List[ServiceInfo]] - _RESPOND_IMMEDIATE_TYPES = {_TYPE_NSEC, _TYPE_SRV, *_ADDRESS_RECORD_TYPES} _LOGGER = logging.getLogger(__name__) @@ -59,6 +57,24 @@ _int = int +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.""" @@ -282,7 +298,7 @@ def async_response( # pylint: disable=unused-argument This function must be run in the event loop as it is not threadsafe. """ - strategies: List[AnswerStrategyType] = [] + strategies: List[_AnswerStrategy] = [] for msg in msgs: for question in msg.questions: strategies.extend(self._get_answer_strategies(question)) @@ -307,16 +323,15 @@ def async_response( # pylint: disable=unused-argument known_answers = DNSRRSet(answers) known_answers_set: Optional[Set[DNSRecord]] = None for strategy in strategies: - question = strategy[0] + question = strategy.question is_unicast = question.unique is True # 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) - strategy_type = strategy[1] - types = strategy[2] - services = strategy[3] - answer_set = self._answer_question(question, strategy_type, types, services, known_answers) + answer_set = self._answer_question( + question, strategy.strategy_type, strategy.types, strategy.services, known_answers + ) if not ucast_source and is_unicast: # unique and unicast are the same flag query_res.add_qu_question_response(answer_set) continue @@ -331,36 +346,46 @@ def async_response( # pylint: disable=unused-argument def _get_answer_strategies( self, question: DNSQuestion, - ) -> List[AnswerStrategyType]: + ) -> List[_AnswerStrategy]: """Collect strategies to answer a question.""" name = question.name question_lower_name = name.lower() type_ = question.type - strategies: List[AnswerStrategyType] = [] + 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( - (question, _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION, types, _EMPTY_SERVICES_LIST) + _AnswerStrategy( + question, _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION, types, _EMPTY_SERVICES_LIST + ) ) if type_ in (_TYPE_PTR, _TYPE_ANY): services = self.registry.async_get_infos_type(question_lower_name) if services: - strategies.append((question, _ANSWER_STRATEGY_POINTER, _EMPTY_TYPES_LIST, 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((question, _ANSWER_STRATEGY_ADDRESS, _EMPTY_TYPES_LIST, 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((question, _ANSWER_STRATEGY_SERVICE, _EMPTY_TYPES_LIST, [service])) + strategies.append( + _AnswerStrategy(question, _ANSWER_STRATEGY_SERVICE, _EMPTY_TYPES_LIST, [service]) + ) if type_ in (_TYPE_TXT, _TYPE_ANY): - strategies.append((question, _ANSWER_STRATEGY_TEXT, _EMPTY_TYPES_LIST, [service])) + strategies.append( + _AnswerStrategy(question, _ANSWER_STRATEGY_TEXT, _EMPTY_TYPES_LIST, [service]) + ) return strategies From ed80a339a77e63bac4f7432a6c5b5c5d5f4eb57b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:02:41 -0600 Subject: [PATCH 15/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 5a053ede..6e10a22e 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -307,9 +307,7 @@ def async_response( # pylint: disable=unused-argument return None is_probe = False - msg = msgs[0] questions = msg.questions - now = msg.now # Only decode known answers if we are not a probe and we have # at least one answer strategy answers: List[DNSRecord] = [] @@ -319,9 +317,11 @@ def async_response( # pylint: disable=unused-argument else: answers.extend(msg.answers()) - query_res = _QueryResponse(self.cache, questions, is_probe, now) + msg = msgs[0] + 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 is True # unique and unicast are the same flag From 2dc8045732012d198b0d3480354cd7417abce04a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:04:23 -0600 Subject: [PATCH 16/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 6e10a22e..293f2ae5 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -57,6 +57,13 @@ _int = int +_ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION = 0 +_ANSWER_STRATEGY_POINTER = 1 +_ANSWER_STRATEGY_ADDRESS = 2 +_ANSWER_STRATEGY_SERVICE = 3 +_ANSWER_STRATEGY_TEXT = 4 + + class _AnswerStrategy: __slots__ = ("question", "strategy_type", "types", "services") @@ -175,13 +182,6 @@ def _has_mcast_record_in_last_second(self, record: DNSRecord) -> bool: return bool(maybe_entry is not None and self._now - maybe_entry.created < _ONE_SECOND) -_ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION = 0 -_ANSWER_STRATEGY_POINTER = 1 -_ANSWER_STRATEGY_ADDRESS = 2 -_ANSWER_STRATEGY_SERVICE = 3 -_ANSWER_STRATEGY_TEXT = 4 - - class QueryHandler: """Query the ServiceRegistry.""" From f1c18eba3d39557dcb35af0eefb028d515aa8719 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:04:39 -0600 Subject: [PATCH 17/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 293f2ae5..29ab3efa 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -20,7 +20,6 @@ USA """ -import logging from typing import TYPE_CHECKING, List, Optional, Set, cast from .._cache import DNSCache, _UniqueRecordsType @@ -47,7 +46,6 @@ from .answers import QuestionAnswers, _AnswerWithAdditionalsType _RESPOND_IMMEDIATE_TYPES = {_TYPE_NSEC, _TYPE_SRV, *_ADDRESS_RECORD_TYPES} -_LOGGER = logging.getLogger(__name__) _EMPTY_SERVICES_LIST: List[ServiceInfo] = [] _EMPTY_TYPES_LIST: List[str] = [] From fb27400467e8ca5be60bc44d4141e24d412efba9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:05:53 -0600 Subject: [PATCH 18/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 29ab3efa..e5c6cc30 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -302,6 +302,9 @@ def async_response( # pylint: disable=unused-argument 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 From f0bb8449a108ff6b89242b47f443e8079e1a98fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:06:21 -0600 Subject: [PATCH 19/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index e5c6cc30..3d3a574c 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -333,7 +333,7 @@ def async_response( # pylint: disable=unused-argument answer_set = self._answer_question( question, strategy.strategy_type, strategy.types, strategy.services, known_answers ) - if not ucast_source and is_unicast: # unique and unicast are the same flag + if not ucast_source and is_unicast: query_res.add_qu_question_response(answer_set) continue if ucast_source: From 1bc70d204eae2f492e7ea3837a8b1a7d7731cef9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:09:04 -0600 Subject: [PATCH 20/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 3d3a574c..7ae1c3df 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -362,6 +362,7 @@ def _get_answer_strategies( 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) From 470fc8355080be57f2996e91a5726fd36185b87f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:10:00 -0600 Subject: [PATCH 21/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 7ae1c3df..c5b59df6 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -266,13 +266,13 @@ def _answer_question( self._add_service_type_enumeration_query_answers(types, answer_set, known_answers) return answer_set - if strategy_type == _ANSWER_STRATEGY_POINTER: + elif strategy_type == _ANSWER_STRATEGY_POINTER: self._add_pointer_answers(services, answer_set, known_answers) - if strategy_type == _ANSWER_STRATEGY_ADDRESS: + elif strategy_type == _ANSWER_STRATEGY_ADDRESS: self._add_address_answers(services, answer_set, known_answers, type_) - if strategy_type == _ANSWER_STRATEGY_SERVICE: + elif strategy_type == _ANSWER_STRATEGY_SERVICE: # Add recommended additional answers according to # https://tools.ietf.org/html/rfc6763#section-12.2. service = services[0] @@ -280,7 +280,7 @@ def _answer_question( if known_answers.suppresses(dns_service) is False: answer_set[dns_service] = service._get_address_and_nsec_records(None) - if strategy_type == _ANSWER_STRATEGY_TEXT: + elif strategy_type == _ANSWER_STRATEGY_TEXT: service = services[0] dns_text = service._dns_text(None) if known_answers.suppresses(dns_text) is False: From 7bc8fb30546199543a07e6e8d8cda4ed71c0d1f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:10:30 -0600 Subject: [PATCH 22/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index c5b59df6..adff2491 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -264,14 +264,10 @@ def _answer_question( if strategy_type == _ANSWER_STRATEGY_SERVICE_TYPE_ENUMERATION: self._add_service_type_enumeration_query_answers(types, answer_set, known_answers) - return answer_set - 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, type_) - elif strategy_type == _ANSWER_STRATEGY_SERVICE: # Add recommended additional answers according to # https://tools.ietf.org/html/rfc6763#section-12.2. @@ -279,7 +275,6 @@ def _answer_question( dns_service = service._dns_service(None) if known_answers.suppresses(dns_service) is False: answer_set[dns_service] = service._get_address_and_nsec_records(None) - elif strategy_type == _ANSWER_STRATEGY_TEXT: service = services[0] dns_text = service._dns_text(None) From a88af110d52a2833fecb29e2588432a4003864bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:11:07 -0600 Subject: [PATCH 23/31] fix: tweaks --- src/zeroconf/_handlers/query_handler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index adff2491..2b115a5f 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -260,14 +260,13 @@ def _answer_question( ) -> _AnswerWithAdditionalsType: """Answer a question.""" answer_set: _AnswerWithAdditionalsType = {} - type_ = question.type 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, type_) + 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. From a55f8052bf1b8a704d98042dfd44894510dedd1e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:32:08 -0600 Subject: [PATCH 24/31] fix: typo in tests --- tests/test_handlers.py | 60 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 13fe3a51..8bd4210e 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -107,6 +107,7 @@ def _process_outgoing_packet(out): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers _process_outgoing_packet(construct_outgoing_multicast_answers(question_answers.mcast_aggregate)) # The additonals should all be suppresed since they are all in the answers section @@ -145,6 +146,7 @@ def _process_outgoing_packet(out): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers _process_outgoing_packet(construct_outgoing_multicast_answers(question_answers.mcast_aggregate)) # There will be one NSEC additional to indicate the lack of AAAA record @@ -244,6 +246,7 @@ def test_ptr_optimization(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -260,6 +263,7 @@ def test_ptr_optimization(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert not question_answers.mcast_aggregate_last_second @@ -305,6 +309,7 @@ def test_any_query_for_ptr(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers mcast_answers = list(question_answers.mcast_aggregate) assert mcast_answers[0].name == type_ assert mcast_answers[0].alias == registration_name # type: ignore[attr-defined] @@ -332,6 +337,7 @@ def test_aaaa_query(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers mcast_answers = list(question_answers.mcast_now) assert mcast_answers[0].address == ipv6_address # type: ignore[attr-defined] # unregister @@ -358,6 +364,7 @@ def test_aaaa_query_upper_case(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers mcast_answers = list(question_answers.mcast_now) assert mcast_answers[0].address == ipv6_address # type: ignore[attr-defined] # unregister @@ -391,6 +398,7 @@ def test_a_and_aaaa_record_fate_sharing(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers additionals = set().union(*question_answers.mcast_now.values()) assert aaaa_record in question_answers.mcast_now assert a_record in additionals @@ -403,6 +411,7 @@ def test_a_and_aaaa_record_fate_sharing(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers additionals = set().union(*question_answers.mcast_now.values()) assert a_record in question_answers.mcast_now assert aaaa_record in additionals @@ -437,6 +446,7 @@ def test_unicast_response(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], True ) + assert question_answers for answers in (question_answers.ucast, question_answers.mcast_aggregate): has_srv = has_txt = has_a = has_aaaa = has_nsec = False nbr_additionals = 0 @@ -486,6 +496,7 @@ async def test_probe_answered_immediately(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_aggregate assert not question_answers.mcast_aggregate_last_second @@ -499,6 +510,7 @@ async def test_probe_answered_immediately(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert question_answers.ucast assert question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -528,6 +540,7 @@ async def test_probe_answered_immediately_with_uppercase_name(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_aggregate assert not question_answers.mcast_aggregate_last_second @@ -541,6 +554,7 @@ async def test_probe_answered_immediately_with_uppercase_name(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert question_answers.ucast assert question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -607,6 +621,7 @@ def _validate_complete_response(answers): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers _validate_complete_response(question_answers.ucast) assert not question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -622,6 +637,7 @@ def _validate_complete_response(answers): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_aggregate assert not question_answers.mcast_aggregate @@ -637,6 +653,7 @@ def _validate_complete_response(answers): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers _validate_complete_response(question_answers.ucast) _validate_complete_response(question_answers.mcast_now) @@ -652,6 +669,7 @@ def _validate_complete_response(answers): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.mcast_now assert not question_answers.mcast_aggregate assert not question_answers.mcast_aggregate_last_second @@ -681,6 +699,7 @@ def test_known_answer_supression(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert question_answers.mcast_aggregate @@ -692,6 +711,7 @@ def test_known_answer_supression(): generated.add_answer_at_time(info.dns_pointer(), now) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -703,6 +723,7 @@ def test_known_answer_supression(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -715,6 +736,7 @@ def test_known_answer_supression(): generated.add_answer_at_time(dns_address, now) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -728,6 +750,7 @@ def test_known_answer_supression(): generated.add_answer_at_time(dns_address, now) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast expected_nsec_record = cast(r.DNSNsec, list(question_answers.mcast_now)[0]) assert const._TYPE_A not in expected_nsec_record.rdtypes @@ -741,6 +764,7 @@ def test_known_answer_supression(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -752,6 +776,7 @@ def test_known_answer_supression(): generated.add_answer_at_time(info.dns_service(), now) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -763,6 +788,7 @@ def test_known_answer_supression(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert question_answers.mcast_aggregate @@ -774,6 +800,7 @@ def test_known_answer_supression(): generated.add_answer_at_time(info.dns_text(), now) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -827,6 +854,7 @@ def test_multi_packet_known_answer_supression(): packets = generated.packets() assert len(packets) > 1 question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -868,6 +896,7 @@ def test_known_answer_supression_service_type_enumeration_query(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert question_answers.mcast_aggregate @@ -898,6 +927,7 @@ def test_known_answer_supression_service_type_enumeration_query(): ) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert not question_answers.mcast_aggregate @@ -938,6 +968,7 @@ def test_upper_case_enumeration_query(): generated.add_question(question) packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now assert question_answers.mcast_aggregate @@ -1000,6 +1031,7 @@ async def test_qu_response_only_sends_additionals_if_sends_answer(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.mcast_now assert not question_answers.mcast_aggregate assert not question_answers.mcast_aggregate_last_second @@ -1024,6 +1056,7 @@ async def test_qu_response_only_sends_additionals_if_sends_answer(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.mcast_now assert not question_answers.mcast_aggregate assert not question_answers.mcast_aggregate_last_second @@ -1047,6 +1080,7 @@ async def test_qu_response_only_sends_additionals_if_sends_answer(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_aggregate assert not question_answers.mcast_aggregate_last_second @@ -1075,6 +1109,7 @@ async def test_qu_response_only_sends_additionals_if_sends_answer(): question_answers = zc.query_handler.async_response( [r.DNSIncoming(packet) for packet in query.packets()], False ) + assert question_answers assert not question_answers.mcast_aggregate assert not question_answers.mcast_aggregate_last_second @@ -1235,8 +1270,22 @@ async def test_questions_query_handler_populates_the_question_history_from_qm_qu now = current_time_millis() _clear_cache(zc) + aiozc.zeroconf.registry.async_add( + ServiceInfo( + "_hap._tcp.local.", + "other._hap._tcp.local.", + 80, + 0, + 0, + {"md": "known"}, + "ash-2.local.", + addresses=[socket.inet_aton("1.2.3.4")], + ) + ) + services = aiozc.zeroconf.registry.async_get_infos_type("_hap._tcp.local.") + assert len(services) == 1 generated = r.DNSOutgoing(const._FLAGS_QR_QUERY) - question = r.DNSQuestion("_hap._tcp._local.", const._TYPE_PTR, const._CLASS_IN) + question = r.DNSQuestion("_hap._tcp.local.", const._TYPE_PTR, const._CLASS_IN) question.unicast = False known_answer = r.DNSPointer( "_hap._tcp.local.", const._TYPE_PTR, const._CLASS_IN, 10000, 'known-to-other._hap._tcp.local.' @@ -1246,9 +1295,10 @@ async def test_questions_query_handler_populates_the_question_history_from_qm_qu now = r.current_time_millis() packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert question_answers assert not question_answers.ucast assert not question_answers.mcast_now - assert not question_answers.mcast_aggregate + assert question_answers.mcast_aggregate assert not question_answers.mcast_aggregate_last_second assert zc.question_history.suppresses(question, now, {known_answer}) @@ -1273,10 +1323,8 @@ async def test_questions_query_handler_does_not_put_qu_questions_in_history(): now = r.current_time_millis() packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) - assert not question_answers.ucast - assert not question_answers.mcast_now - assert not question_answers.mcast_aggregate - assert not question_answers.mcast_aggregate_last_second + assert question_answers + assert not question_answers assert not zc.question_history.suppresses(question, now, {known_answer}) await aiozc.async_close() From a4b8a06fb5bc1b9947174b6d0f420da081c039cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:51:10 -0600 Subject: [PATCH 25/31] fix: more test fixes --- src/zeroconf/_handlers/answers.py | 8 ++++++++ tests/test_handlers.py | 34 +++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/zeroconf/_handlers/answers.py b/src/zeroconf/_handlers/answers.py index 6ba502ac..a2dbd66a 100644 --- a/src/zeroconf/_handlers/answers.py +++ b/src/zeroconf/_handlers/answers.py @@ -59,6 +59,14 @@ def __init__( 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.""" diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 8bd4210e..4537e6b7 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -979,6 +979,19 @@ def test_upper_case_enumeration_query(): zc.close() +def test_enumeration_query_with_no_registered_services(): + zc = Zeroconf(interfaces=['127.0.0.1']) + _clear_cache(zc) + generated = r.DNSOutgoing(const._FLAGS_QR_QUERY) + question = r.DNSQuestion(const._SERVICE_TYPE_ENUMERATION_NAME.upper(), const._TYPE_PTR, const._CLASS_IN) + generated.add_question(question) + packets = generated.packets() + question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) + assert not question_answers + # unregister + zc.close() + + # This test uses asyncio because it needs to access the cache directly # which is not threadsafe @pytest.mark.asyncio @@ -1311,12 +1324,22 @@ async def test_questions_query_handler_does_not_put_qu_questions_in_history(): zc = aiozc.zeroconf now = current_time_millis() _clear_cache(zc) - + info = ServiceInfo( + "_hap._tcp.local.", + "qu._hap._tcp.local.", + 80, + 0, + 0, + {"md": "known"}, + "ash-2.local.", + addresses=[socket.inet_aton("1.2.3.4")], + ) + aiozc.zeroconf.registry.async_add(info) generated = r.DNSOutgoing(const._FLAGS_QR_QUERY) - question = r.DNSQuestion("_hap._tcp._local.", const._TYPE_PTR, const._CLASS_IN) + question = r.DNSQuestion("_hap._tcp.local.", const._TYPE_PTR, const._CLASS_IN) question.unicast = True known_answer = r.DNSPointer( - "_hap._tcp.local.", const._TYPE_PTR, const._CLASS_IN, 10000, 'known-to-other._hap._tcp.local.' + "_hap._tcp.local.", const._TYPE_PTR, const._CLASS_IN, 10000, 'notqu._hap._tcp.local.' ) generated.add_question(question) generated.add_answer_at_time(known_answer, 0) @@ -1324,7 +1347,10 @@ async def test_questions_query_handler_does_not_put_qu_questions_in_history(): packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) assert question_answers - assert not question_answers + assert not question_answers.ucast # has not multicast recently + assert question_answers.mcast_now + assert not question_answers.mcast_aggregate + assert not question_answers.mcast_aggregate_last_second assert not zc.question_history.suppresses(question, now, {known_answer}) await aiozc.async_close() From 1aa886916c9bc4899ea6300de89af0949966b417 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:54:50 -0600 Subject: [PATCH 26/31] fix: more test fixes --- src/zeroconf/_handlers/multicast_outgoing_queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zeroconf/_handlers/multicast_outgoing_queue.py b/src/zeroconf/_handlers/multicast_outgoing_queue.py index 1d398d73..d92f1392 100644 --- a/src/zeroconf/_handlers/multicast_outgoing_queue.py +++ b/src/zeroconf/_handlers/multicast_outgoing_queue.py @@ -77,7 +77,7 @@ def async_add(self, now: _float, answers: _AnswerWithAdditionalsType) -> None: # 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 additonal responses + # allows aggregating additional responses last_group = self.queue[-1] if send_after <= last_group.send_after: last_group.answers.update(answers) From ef7f622f058fd8f4a440bff3d37010f9272fea3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 11:59:32 -0600 Subject: [PATCH 27/31] fix: test repr --- tests/test_handlers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 4537e6b7..1a1066fa 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -1347,6 +1347,7 @@ async def test_questions_query_handler_does_not_put_qu_questions_in_history(): packets = generated.packets() question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False) assert question_answers + assert "qu._hap._tcp.local." in str(question_answers) assert not question_answers.ucast # has not multicast recently assert question_answers.mcast_now assert not question_answers.mcast_aggregate From eb3f8fd9cb955265e75ef5616f49076f65bde444 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 12:09:13 -0600 Subject: [PATCH 28/31] fix: only 5 options --- src/zeroconf/_handlers/query_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zeroconf/_handlers/query_handler.py b/src/zeroconf/_handlers/query_handler.py index 2b115a5f..0af72f4c 100644 --- a/src/zeroconf/_handlers/query_handler.py +++ b/src/zeroconf/_handlers/query_handler.py @@ -274,7 +274,7 @@ def _answer_question( dns_service = service._dns_service(None) if known_answers.suppresses(dns_service) is False: answer_set[dns_service] = service._get_address_and_nsec_records(None) - elif strategy_type == _ANSWER_STRATEGY_TEXT: + elif strategy_type == _ANSWER_STRATEGY_TEXT: # pragma: no branch service = services[0] dns_text = service._dns_text(None) if known_answers.suppresses(dns_text) is False: From b2f4ae7c9defdb665d0375523065448b30f115f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 12:18:01 -0600 Subject: [PATCH 29/31] fix: branching --- src/zeroconf/_handlers/multicast_outgoing_queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zeroconf/_handlers/multicast_outgoing_queue.py b/src/zeroconf/_handlers/multicast_outgoing_queue.py index d92f1392..23288d18 100644 --- a/src/zeroconf/_handlers/multicast_outgoing_queue.py +++ b/src/zeroconf/_handlers/multicast_outgoing_queue.py @@ -116,7 +116,7 @@ def async_ready(self) -> None: # 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: + 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)) From 115ae932224f6593347ea2249ddc20c3a1ce04c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 12:19:38 -0600 Subject: [PATCH 30/31] fix: cleanup --- src/zeroconf/_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zeroconf/_core.py b/src/zeroconf/_core.py index 7e66c5e2..ceb830d2 100644 --- a/src/zeroconf/_core.py +++ b/src/zeroconf/_core.py @@ -583,11 +583,11 @@ def handle_assembled_query( or the timer expires. If the TC bit is not set, a single packet will be in packets. """ - now = packets[0].now ucast_source = port != _MDNS_PORT question_answers = self.query_handler.async_response(packets, ucast_source) if not question_answers: return + now = packets[0].now if question_answers.ucast: questions = packets[0].questions id_ = packets[0].id From d1db9c0fc238a99e12ca06ea665a4497b60b676f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 12:20:04 -0600 Subject: [PATCH 31/31] fix: cleanup --- src/zeroconf/_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zeroconf/_core.py b/src/zeroconf/_core.py index ceb830d2..75a7a7cf 100644 --- a/src/zeroconf/_core.py +++ b/src/zeroconf/_core.py @@ -577,7 +577,7 @@ def handle_assembled_query( ) -> None: """Respond to a (re)assembled query. - If the protocol recieved packets with the TC bit set, it will + If the protocol received 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