def request(self, zc: Zeroconf, timeout: float) -> bool: now = time.time() delay = 0.2 next_ = now + delay last = now + timeout try: zc.add_listener(self, None) while self.address is None: if last <= now: # Timeout return False if next_ <= now: out = DNSOutgoing(_FLAGS_QR_QUERY) out.add_question(DNSQuestion(self.name, _TYPE_A, _CLASS_IN)) zc.send(out) next_ = now + delay delay *= 2 time.sleep(min(next_, last) - now) now = time.time() finally: zc.remove_listener(self) return True
def test_register_and_lookup_type_by_uppercase_name(self): # instantiate a zeroconf instance zc = Zeroconf(interfaces=['127.0.0.1']) type_ = "_mylowertype._tcp.local." name = "Home" registration_name = "%s.%s" % (name, type_) info = ServiceInfo( type_, name=registration_name, server="random123.local.", addresses=[socket.inet_pton(socket.AF_INET, "1.2.3.4")], port=80, properties={"version": "1.0"}, ) zc.register_service(info) _clear_cache(zc) info = ServiceInfo(type_, registration_name) info.load_from_cache(zc) assert info.addresses == [] out = r.DNSOutgoing(const._FLAGS_QR_QUERY) out.add_question( r.DNSQuestion(type_.upper(), const._TYPE_PTR, const._CLASS_IN)) zc.send(out) time.sleep(0.5) info = ServiceInfo(type_, registration_name) info.load_from_cache(zc) assert info.addresses == [socket.inet_pton(socket.AF_INET, "1.2.3.4")] assert info.properties == {b"version": b"1.0"} zc.close()
def test_invalid_packets_ignored_and_does_not_cause_loop_exception(): """Ensure an invalid packet cannot cause the loop to collapse.""" zc = Zeroconf(interfaces=['127.0.0.1']) generated = r.DNSOutgoing(0) packet = generated.packets()[0] packet = packet[:8] + b'deadbeef' + packet[8:] parsed = r.DNSIncoming(packet) assert parsed.valid is False mock_out = unittest.mock.Mock() mock_out.packets = lambda: [packet] zc.send(mock_out) generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) entry = r.DNSText( "didnotcrashincoming._crash._tcp.local.", const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, 500, b'path=/~paulsm/', ) assert isinstance(entry, r.DNSText) assert isinstance(entry, r.DNSRecord) assert isinstance(entry, r.DNSEntry) generated.add_answer_at_time(entry, 0) zc.send(generated) time.sleep(0.2) zc.close() assert zc.cache.get(entry) is not None
def test_sending_unicast(): """Test sending unicast response.""" zc = Zeroconf(interfaces=['127.0.0.1']) generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) entry = r.DNSText( "didnotcrashincoming._crash._tcp.local.", const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, 500, b'path=/~paulsm/', ) generated.add_answer_at_time(entry, 0) zc.send(generated, "2001:db8::1", const._MDNS_PORT) # https://www.iana.org/go/rfc3849 time.sleep(0.2) assert zc.cache.get(entry) is None zc.send(generated, "198.51.100.0", const._MDNS_PORT) # Documentation (TEST-NET-2) time.sleep(0.2) assert zc.cache.get(entry) is None zc.send(generated) time.sleep(0.2) assert zc.cache.get(entry) is not None zc.close()
def request(self, zc: zeroconf.Zeroconf, timeout: float) -> bool: now = time.time() delay = 0.2 next_ = now + delay last = now + timeout try: zc.add_listener( self, zeroconf.DNSQuestion(self.name, zeroconf._TYPE_ANY, zeroconf._CLASS_IN), ) while self.address is None: if last <= now: # Timeout return False if next_ <= now: out = zeroconf.DNSOutgoing(zeroconf._FLAGS_QR_QUERY) out.add_question( zeroconf.DNSQuestion( self.name, zeroconf._TYPE_A, zeroconf._CLASS_IN ) ) out.add_answer_at_time( zc.cache.get_by_details( self.name, zeroconf._TYPE_A, zeroconf._CLASS_IN ), now, ) zc.send(out) next_ = now + delay delay *= 2 zc.wait(min(next_, last) - now) now = time.time() finally: zc.remove_listener(self) return True
def test_integration(): service_added = Event() service_removed = Event() unexpected_ttl = Event() got_query = Event() type_ = "_http._tcp.local." registration_name = "xxxyyy.%s" % type_ def on_service_state_change(zeroconf, service_type, state_change, name): if name == registration_name: if state_change is ServiceStateChange.Added: service_added.set() elif state_change is ServiceStateChange.Removed: service_removed.set() zeroconf_browser = Zeroconf(interfaces=["127.0.0.1"]) # we are going to monkey patch the zeroconf send to check packet sizes old_send = zeroconf_browser.send time_offset = 0 def current_time_millis(): """Current system time in milliseconds""" return time.time() * 1000 + time_offset * 1000 expected_ttl = r._DNS_TTL # needs to be a list so that we can modify it in our phony send nbr_queries = [0, None] def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT): """Sends an outgoing packet.""" pout = r.DNSIncoming(out.packet()) for answer in pout.answers: nbr_queries[0] += 1 if not answer.ttl > expected_ttl / 2: unexpected_ttl.set() got_query.set() old_send(out, addr=addr, port=port) # monkey patch the zeroconf send zeroconf_browser.send = send # monkey patch the zeroconf current_time_millis r.current_time_millis = current_time_millis service_added = Event() service_removed = Event() browser = ServiceBrowser(zeroconf_browser, type_, [on_service_state_change]) zeroconf_registrar = Zeroconf(interfaces=["127.0.0.1"]) desc = {"path": "/~paulsm/"} info = ServiceInfo( type_, registration_name, socket.inet_aton("10.0.1.2"), 80, 0, 0, desc, "ash-2.local.", ) zeroconf_registrar.register_service(info) try: service_added.wait(1) assert service_added.is_set() sleep_count = 0 while nbr_queries[0] < 50: time_offset += expected_ttl / 4 zeroconf_browser.notify_all() sleep_count += 1 got_query.wait(1) got_query.clear() assert not unexpected_ttl.is_set() # Don't remove service, allow close() to cleanup finally: zeroconf_registrar.close() service_removed.wait(1) assert service_removed.is_set() browser.cancel() zeroconf_browser.close()
def test_ttl(self): # instantiate a zeroconf instance zc = Zeroconf(interfaces=["127.0.0.1"]) # service definition type_ = "_test-srvc-type._tcp.local." name = "xxxyyy" registration_name = "%s.%s" % (name, type_) desc = {"path": "/~paulsm/"} info = ServiceInfo( type_, registration_name, socket.inet_aton("10.0.1.2"), 80, 0, 0, desc, "ash-2.local.", ) # we are going to monkey patch the zeroconf send to check packet sizes old_send = zc.send # needs to be a list so that we can modify it in our phony send nbr_answers = [0, None] nbr_additionals = [0, None] nbr_authorities = [0, None] def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT): """Sends an outgoing packet.""" for answer, time_ in out.answers: nbr_answers[0] += 1 assert answer.ttl == expected_ttl for answer in out.additionals: nbr_additionals[0] += 1 assert answer.ttl == expected_ttl for answer in out.authorities: nbr_authorities[0] += 1 assert answer.ttl == expected_ttl old_send(out, addr=addr, port=port) # monkey patch the zeroconf send zc.send = send # register service with default TTL expected_ttl = r._DNS_TTL zc.register_service(info) assert (nbr_answers[0] == 12 and nbr_additionals[0] == 0 and nbr_authorities[0] == 3) nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # query query = r.DNSOutgoing(r._FLAGS_QR_QUERY | r._FLAGS_AA) query.add_question(r.DNSQuestion(info.type, r._TYPE_PTR, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.name, r._TYPE_SRV, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.name, r._TYPE_TXT, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.server, r._TYPE_A, r._CLASS_IN)) zc.handle_query(query, r._MDNS_ADDR, r._MDNS_PORT) assert (nbr_answers[0] == 4 and nbr_additionals[0] == 1 and nbr_authorities[0] == 0) nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # unregister expected_ttl = 0 zc.unregister_service(info) assert (nbr_answers[0] == 12 and nbr_additionals[0] == 0 and nbr_authorities[0] == 0) nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # register service with custom TTL expected_ttl = r._DNS_TTL * 2 assert expected_ttl != r._DNS_TTL zc.register_service(info, ttl=expected_ttl) assert (nbr_answers[0] == 12 and nbr_additionals[0] == 0 and nbr_authorities[0] == 3) nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # query query = r.DNSOutgoing(r._FLAGS_QR_QUERY | r._FLAGS_AA) query.add_question(r.DNSQuestion(info.type, r._TYPE_PTR, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.name, r._TYPE_SRV, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.name, r._TYPE_TXT, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.server, r._TYPE_A, r._CLASS_IN)) zc.handle_query(query, r._MDNS_ADDR, r._MDNS_PORT) assert (nbr_answers[0] == 4 and nbr_additionals[0] == 1 and nbr_authorities[0] == 0) nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # unregister expected_ttl = 0 zc.unregister_service(info) assert (nbr_answers[0] == 12 and nbr_additionals[0] == 0 and nbr_authorities[0] == 0) nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0
def test_lots_of_names(self): # instantiate a zeroconf instance zc = Zeroconf(interfaces=["127.0.0.1"]) # create a bunch of servers type_ = "_my-service._tcp.local." name = "a wonderful service" server_count = 300 self.generate_many_hosts(zc, type_, name, server_count) # verify that name changing works self.verify_name_change(zc, type_, name, server_count) # we are going to monkey patch the zeroconf send to check packet sizes old_send = zc.send # needs to be a list so that we can modify it in our phony send longest_packet = [0, None] def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT): """Sends an outgoing packet.""" packet = out.packet() if longest_packet[0] < len(packet): longest_packet[0] = len(packet) longest_packet[1] = out old_send(out, addr=addr, port=port) # monkey patch the zeroconf send zc.send = send # dummy service callback def on_service_state_change(zeroconf, service_type, state_change, name): pass # start a browser browser = ServiceBrowser(zc, type_, [on_service_state_change]) # wait until the browse request packet has maxed out in size sleep_count = 0 while sleep_count < 100 and longest_packet[ 0] < r._MAX_MSG_ABSOLUTE - 100: sleep_count += 1 time.sleep(0.1) browser.cancel() time.sleep(0.5) import zeroconf zeroconf.log.debug("sleep_count %d, sized %d", sleep_count, longest_packet[0]) # now the browser has sent at least one request, verify the size assert longest_packet[0] <= r._MAX_MSG_ABSOLUTE assert longest_packet[0] >= r._MAX_MSG_ABSOLUTE - 100 # mock zeroconf's logger warning() and debug() from mock import patch patch_warn = patch("zeroconf.log.warning") patch_debug = patch("zeroconf.log.debug") mocked_log_warn = patch_warn.start() mocked_log_debug = patch_debug.start() # now that we have a long packet in our possession, let's verify the # exception handling. out = longest_packet[1] out.data.append(b"\0" * 1000) # mock the zeroconf logger and check for the correct logging backoff call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count # try to send an oversized packet zc.send(out) assert mocked_log_warn.call_count == call_counts[0] + 1 assert mocked_log_debug.call_count == call_counts[0] zc.send(out) assert mocked_log_warn.call_count == call_counts[0] + 1 assert mocked_log_debug.call_count == call_counts[0] + 1 # force a receive of an oversized packet packet = out.packet() s = zc._respond_sockets[0] # mock the zeroconf logger and check for the correct logging backoff call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count # force receive on oversized packet s.sendto(packet, 0, (r._MDNS_ADDR, r._MDNS_PORT)) s.sendto(packet, 0, (r._MDNS_ADDR, r._MDNS_PORT)) time.sleep(2.0) zeroconf.log.debug( "warn %d debug %d was %s", mocked_log_warn.call_count, mocked_log_debug.call_count, call_counts, ) assert mocked_log_debug.call_count > call_counts[0] # close our zeroconf which will close the sockets zc.close() # pop the big chunk off the end of the data and send on a closed socket out.data.pop() zc._GLOBAL_DONE = False # mock the zeroconf logger and check for the correct logging backoff call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count # send on a closed socket (force a socket error) zc.send(out) zeroconf.log.debug( "warn %d debug %d was %s", mocked_log_warn.call_count, mocked_log_debug.call_count, call_counts, ) assert mocked_log_warn.call_count > call_counts[0] assert mocked_log_debug.call_count > call_counts[0] zc.send(out) zeroconf.log.debug( "warn %d debug %d was %s", mocked_log_warn.call_count, mocked_log_debug.call_count, call_counts, ) assert mocked_log_debug.call_count > call_counts[0] + 2 mocked_log_warn.stop() mocked_log_debug.stop()
def test_lots_of_names(self): # instantiate a zeroconf instance zc = Zeroconf(interfaces=['127.0.0.1']) # create a bunch of servers type_ = "_my-service._tcp.local." name = 'a wonderful service' server_count = 300 self.generate_many_hosts(zc, type_, name, server_count) # verify that name changing works self.verify_name_change(zc, type_, name, server_count) # we are going to monkey patch the zeroconf send to check packet sizes old_send = zc.send longest_packet_len = 0 longest_packet = None # type: Optional[r.DNSOutgoing] def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT): """Sends an outgoing packet.""" packet = out.packet() nonlocal longest_packet_len, longest_packet if longest_packet_len < len(packet): longest_packet_len = len(packet) longest_packet = out old_send(out, addr=addr, port=port) # monkey patch the zeroconf send setattr(zc, "send", send) # dummy service callback def on_service_state_change(zeroconf, service_type, state_change, name): pass # start a browser browser = ServiceBrowser(zc, type_, [on_service_state_change]) # wait until the browse request packet has maxed out in size sleep_count = 0 while sleep_count < 100 and longest_packet_len < r._MAX_MSG_ABSOLUTE - 100: sleep_count += 1 time.sleep(0.1) browser.cancel() time.sleep(0.5) import zeroconf zeroconf.log.debug('sleep_count %d, sized %d', sleep_count, longest_packet_len) # now the browser has sent at least one request, verify the size assert longest_packet_len <= r._MAX_MSG_ABSOLUTE assert longest_packet_len >= r._MAX_MSG_ABSOLUTE - 100 # mock zeroconf's logger warning() and debug() from unittest.mock import patch patch_warn = patch('zeroconf.log.warning') patch_debug = patch('zeroconf.log.debug') mocked_log_warn = patch_warn.start() mocked_log_debug = patch_debug.start() # now that we have a long packet in our possession, let's verify the # exception handling. out = longest_packet assert out is not None out.data.append(b'\0' * 1000) # mock the zeroconf logger and check for the correct logging backoff call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count # try to send an oversized packet zc.send(out) assert mocked_log_warn.call_count == call_counts[0] + 1 assert mocked_log_debug.call_count == call_counts[0] zc.send(out) assert mocked_log_warn.call_count == call_counts[0] + 1 assert mocked_log_debug.call_count == call_counts[0] + 1 # force a receive of an oversized packet packet = out.packet() s = zc._respond_sockets[0] # mock the zeroconf logger and check for the correct logging backoff call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count # force receive on oversized packet s.sendto(packet, 0, (r._MDNS_ADDR, r._MDNS_PORT)) s.sendto(packet, 0, (r._MDNS_ADDR, r._MDNS_PORT)) time.sleep(2.0) zeroconf.log.debug( 'warn %d debug %d was %s', mocked_log_warn.call_count, mocked_log_debug.call_count, call_counts ) assert mocked_log_debug.call_count > call_counts[0] # close our zeroconf which will close the sockets zc.close() # pop the big chunk off the end of the data and send on a closed socket out.data.pop() zc._GLOBAL_DONE = False # mock the zeroconf logger and check for the correct logging backoff call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count # send on a closed socket (force a socket error) zc.send(out) zeroconf.log.debug( 'warn %d debug %d was %s', mocked_log_warn.call_count, mocked_log_debug.call_count, call_counts ) assert mocked_log_warn.call_count > call_counts[0] assert mocked_log_debug.call_count > call_counts[0] zc.send(out) zeroconf.log.debug( 'warn %d debug %d was %s', mocked_log_warn.call_count, mocked_log_debug.call_count, call_counts ) assert mocked_log_debug.call_count > call_counts[0] + 2 mocked_log_warn.stop() mocked_log_debug.stop()
def test_integration(): service_added = Event() service_removed = Event() unexpected_ttl = Event() got_query = Event() type_ = "_http._tcp.local." registration_name = "xxxyyy.%s" % type_ def on_service_state_change(zeroconf, service_type, state_change, name): if name == registration_name: if state_change is ServiceStateChange.Added: service_added.set() elif state_change is ServiceStateChange.Removed: service_removed.set() zeroconf_browser = Zeroconf(interfaces=['127.0.0.1']) # we are going to monkey patch the zeroconf send to check packet sizes old_send = zeroconf_browser.send time_offset = 0 def current_time_millis(): """Current system time in milliseconds""" return time.time() * 1000 + time_offset * 1000 expected_ttl = r._DNS_TTL # needs to be a list so that we can modify it in our phony send nbr_queries = [0, None] def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT): """Sends an outgoing packet.""" pout = r.DNSIncoming(out.packet()) for answer in pout.answers: nbr_queries[0] += 1 if not answer.ttl > expected_ttl / 2: unexpected_ttl.set() got_query.set() old_send(out, addr=addr, port=port) # monkey patch the zeroconf send zeroconf_browser.send = send # monkey patch the zeroconf current_time_millis r.current_time_millis = current_time_millis service_added = Event() service_removed = Event() browser = ServiceBrowser(zeroconf_browser, type_, [on_service_state_change]) zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1']) desc = {'path': '/~paulsm/'} info = ServiceInfo( type_, registration_name, socket.inet_aton("10.0.1.2"), 80, 0, 0, desc, "ash-2.local.") zeroconf_registrar.register_service(info) try: service_added.wait(1) assert service_added.is_set() sleep_count = 0 while nbr_queries[0] < 50: time_offset += expected_ttl / 4 zeroconf_browser.notify_all() sleep_count += 1 got_query.wait(1) got_query.clear() assert not unexpected_ttl.is_set() # Don't remove service, allow close() to cleanup finally: zeroconf_registrar.close() browser.cancel() zeroconf_browser.close()
def test_ttl(self): # instantiate a zeroconf instance zc = Zeroconf(interfaces=['127.0.0.1']) # service definition type_ = "_test-srvc-type._tcp.local." name = "xxxyyy" registration_name = "%s.%s" % (name, type_) desc = {'path': '/~paulsm/'} info = ServiceInfo( type_, registration_name, socket.inet_aton("10.0.1.2"), 80, 0, 0, desc, "ash-2.local.") # we are going to monkey patch the zeroconf send to check packet sizes old_send = zc.send # needs to be a list so that we can modify it in our phony send nbr_answers = [0, None] nbr_additionals = [0, None] nbr_authorities = [0, None] def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT): """Sends an outgoing packet.""" for answer, time_ in out.answers: nbr_answers[0] += 1 assert answer.ttl == expected_ttl for answer in out.additionals: nbr_additionals[0] += 1 assert answer.ttl == expected_ttl for answer in out.authorities: nbr_authorities[0] += 1 assert answer.ttl == expected_ttl old_send(out, addr=addr, port=port) # monkey patch the zeroconf send zc.send = send # register service with default TTL expected_ttl = r._DNS_TTL zc.register_service(info) assert nbr_answers[0] == 12 and nbr_additionals[0] == 0 and nbr_authorities[0] == 3 nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # query query = r.DNSOutgoing(r._FLAGS_QR_QUERY | r._FLAGS_AA) query.add_question(r.DNSQuestion(info.type, r._TYPE_PTR, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.name, r._TYPE_SRV, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.name, r._TYPE_TXT, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.server, r._TYPE_A, r._CLASS_IN)) zc.handle_query(query, r._MDNS_ADDR, r._MDNS_PORT) assert nbr_answers[0] == 4 and nbr_additionals[0] == 1 and nbr_authorities[0] == 0 nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # unregister expected_ttl = 0 zc.unregister_service(info) assert nbr_answers[0] == 12 and nbr_additionals[0] == 0 and nbr_authorities[0] == 0 nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # register service with custom TTL expected_ttl = r._DNS_TTL * 2 assert expected_ttl != r._DNS_TTL zc.register_service(info, ttl=expected_ttl) assert nbr_answers[0] == 12 and nbr_additionals[0] == 0 and nbr_authorities[0] == 3 nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # query query = r.DNSOutgoing(r._FLAGS_QR_QUERY | r._FLAGS_AA) query.add_question(r.DNSQuestion(info.type, r._TYPE_PTR, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.name, r._TYPE_SRV, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.name, r._TYPE_TXT, r._CLASS_IN)) query.add_question(r.DNSQuestion(info.server, r._TYPE_A, r._CLASS_IN)) zc.handle_query(query, r._MDNS_ADDR, r._MDNS_PORT) assert nbr_answers[0] == 4 and nbr_additionals[0] == 1 and nbr_authorities[0] == 0 nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0 # unregister expected_ttl = 0 zc.unregister_service(info) assert nbr_answers[0] == 12 and nbr_additionals[0] == 0 and nbr_authorities[0] == 0 nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0
def test_lots_of_names(self): # instantiate a zeroconf instance zc = Zeroconf(interfaces=['127.0.0.1']) # create a bunch of servers type_ = "_my-service._tcp.local." name = 'a wonderful service' server_count = 300 self.generate_many_hosts(zc, type_, name, server_count) # verify that name changing works self.verify_name_change(zc, type_, name, server_count) # we are going to patch the zeroconf send to check packet sizes old_send = zc.send longest_packet_len = 0 longest_packet = None # type: Optional[r.DNSOutgoing] def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT): """Sends an outgoing packet.""" for packet in out.packets(): nonlocal longest_packet_len, longest_packet if longest_packet_len < len(packet): longest_packet_len = len(packet) longest_packet = out old_send(out, addr=addr, port=port) # patch the zeroconf send with unittest.mock.patch.object(zc, "send", send): # dummy service callback def on_service_state_change(zeroconf, service_type, state_change, name): pass # start a browser browser = ServiceBrowser(zc, type_, [on_service_state_change]) # wait until the browse request packet has maxed out in size sleep_count = 0 # we will never get to this large of a packet given the application-layer # splitting of packets, but we still want to track the longest_packet_len # for the debug message below while sleep_count < 100 and longest_packet_len < const._MAX_MSG_ABSOLUTE - 100: sleep_count += 1 time.sleep(0.1) browser.cancel() time.sleep(0.5) import zeroconf zeroconf.log.debug('sleep_count %d, sized %d', sleep_count, longest_packet_len) # now the browser has sent at least one request, verify the size assert longest_packet_len <= const._MAX_MSG_TYPICAL assert longest_packet_len >= const._MAX_MSG_TYPICAL - 100 # mock zeroconf's logger warning() and debug() from unittest.mock import patch patch_warn = patch('zeroconf._logger.log.warning') patch_debug = patch('zeroconf._logger.log.debug') mocked_log_warn = patch_warn.start() mocked_log_debug = patch_debug.start() # now that we have a long packet in our possession, let's verify the # exception handling. out = longest_packet assert out is not None out.data.append(b'\0' * 1000) # mock the zeroconf logger and check for the correct logging backoff call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count # try to send an oversized packet zc.send(out) assert mocked_log_warn.call_count == call_counts[0] zc.send(out) assert mocked_log_warn.call_count == call_counts[0] # mock the zeroconf logger and check for the correct logging backoff call_counts = mocked_log_warn.call_count, mocked_log_debug.call_count # force receive on oversized packet zc.send(out, const._MDNS_ADDR, const._MDNS_PORT) zc.send(out, const._MDNS_ADDR, const._MDNS_PORT) time.sleep(2.0) zeroconf.log.debug( 'warn %d debug %d was %s', mocked_log_warn.call_count, mocked_log_debug.call_count, call_counts, ) assert mocked_log_debug.call_count > call_counts[0] # close our zeroconf which will close the sockets zc.close()
class DynamicResolver(object): def __init__(self): self.zeroconf = Zeroconf(ip_version=4) def _dynamicResponseRequired(self, query): if str(query.name).endswith(domain): return True return False def _doDynamicResponse(self, query): if query.type == dns.SOA: return defer.succeed(([], [], [])) localname = str(query.name)[:-len(domain)] + "local." def browse(localname): services = [] def handler(zeroconf, service_type, name, state_change): if state_change is ServiceStateChange.Added: services.append(name) sb = ServiceBrowser(self.zeroconf, localname, [handler]) time.sleep(timeout) sb.cancel() answers, additional = [], [] for service in services: answers.append( dns.RRHeader(name=localname[:-6] + domain, ttl=ttl, type=dns.PTR, payload=dns.Record_PTR(name=service[:-6] + domain))) #txt_ans, _, _ = txt(service) #srv_ans, _, a_ans = srv(service) #additional += a_ans + txt_ans + srv_ans return answers, [], additional def txt(localname): if localname.endswith('._device-info._tcp.local.'): info = ServiceInfo(localname, localname) info.request(self.zeroconf, timeout * 1000) if not info.text: return [], [], [] else: info = self.zeroconf.get_service_info(localname, localname, timeout * 1000) if info is None: return [], [], [] order = [] i = 0 while i < len(info.text): length = info.text[i] i += 1 kv = info.text[i:i + length].split(b'=') order.append(kv[0]) i += length data = [ b"%s=%s" % (p, info.properties[p]) for p in sorted(info.properties, key=lambda k: order.index(k) if k in order else 1000) ] answers = [ dns.RRHeader(name=localname[:-6] + domain, ttl=ttl, type=dns.TXT, payload=dns.Record_TXT(*data)) ] return answers, [], [] def srv(localname): info = self.zeroconf.get_service_info(localname, localname, timeout * 1000) if info is None: return [], [], [] answers = [ dns.RRHeader(name=localname[:-6] + domain, ttl=ttl, type=dns.SRV, payload=dns.Record_SRV(info.priority, info.weight, info.port, info.server[:-6] + domain)) ] additional = [ dns.RRHeader(name=info.server[:-6] + domain, ttl=ttl, type=dns.A, payload=dns.Record_A( socket.inet_ntop(socket.AF_INET, addr))) for addr in info.addresses ] return answers, [], additional def host(localname): class listener(RecordUpdateListener): def __init__(self): self.addrs = [] self.time = time.time() def update_record(self, zc, now, record): if record.type == _TYPE_A and len(record.address) == 4: self.addrs.append( socket.inet_ntop(socket.AF_INET, record.address)) l = listener() q = DNSQuestion(localname, _TYPE_A, _CLASS_IN) self.zeroconf.add_listener(l, q) out = DNSOutgoing(_FLAGS_QR_QUERY) out.add_question(q) self.zeroconf.send(out) while len(l.addrs) == 0 and time.time() - l.time < timeout: time.sleep(0.1) self.zeroconf.remove_listener(l) answers = [ dns.RRHeader(name=query.name.name, ttl=ttl, type=dns.A, payload=dns.Record_A(addr)) for addr in l.addrs ] return answers, [], [] d = defer.Deferred() if query.type == dns.PTR: d = threads.deferToThread(browse, localname) return d elif query.type == dns.TXT: d = threads.deferToThread(txt, localname) return d elif query.type == dns.SRV: d = threads.deferToThread(srv, localname) return d elif query.type == dns.A: d = threads.deferToThread(host, localname) return d elif query.type != dns.AAAA: print("Unsupported request", query) d.callback(([], [], [])) return d def query(self, query, timeout=None): if self._dynamicResponseRequired(query): return self._doDynamicResponse(query) else: return defer.fail(error.DomainError())