Skip to content

Commit 405f547

Browse files
authored
feat: add support for sending to a specific addr and port with ServiceInfo.async_request and ServiceInfo.request (#1192)
1 parent d713a45 commit 405f547

2 files changed

Lines changed: 109 additions & 4 deletions

File tree

src/zeroconf/_services/info.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
_DNS_OTHER_TTL,
5656
_FLAGS_QR_QUERY,
5757
_LISTENER_TIME,
58+
_MDNS_PORT,
5859
_TYPE_A,
5960
_TYPE_AAAA,
6061
_TYPE_NSEC,
@@ -616,7 +617,12 @@ def _is_complete(self) -> bool:
616617
return bool(self.text is not None and (self._ipv4_addresses or self._ipv6_addresses))
617618

618619
def request(
619-
self, zc: 'Zeroconf', timeout: float, question_type: Optional[DNSQuestionType] = None
620+
self,
621+
zc: 'Zeroconf',
622+
timeout: float,
623+
question_type: Optional[DNSQuestionType] = None,
624+
addr: Optional[str] = None,
625+
port: int = _MDNS_PORT,
620626
) -> bool:
621627
"""Returns true if the service could be discovered on the
622628
network, and updates this object with details discovered.
@@ -628,13 +634,29 @@ def request(
628634
assert zc.loop is not None and zc.loop.is_running()
629635
if zc.loop == get_running_loop():
630636
raise RuntimeError("Use AsyncServiceInfo.async_request from the event loop")
631-
return bool(run_coro_with_timeout(self.async_request(zc, timeout, question_type), zc.loop, timeout))
637+
return bool(
638+
run_coro_with_timeout(
639+
self.async_request(zc, timeout, question_type, addr, port), zc.loop, timeout
640+
)
641+
)
632642

633643
async def async_request(
634-
self, zc: 'Zeroconf', timeout: float, question_type: Optional[DNSQuestionType] = None
644+
self,
645+
zc: 'Zeroconf',
646+
timeout: float,
647+
question_type: Optional[DNSQuestionType] = None,
648+
addr: Optional[str] = None,
649+
port: int = _MDNS_PORT,
635650
) -> bool:
636651
"""Returns true if the service could be discovered on the
637652
network, and updates this object with details discovered.
653+
654+
This method will be run in the event loop.
655+
656+
Passing addr and port is optional, and will default to the
657+
mDNS multicast address and port. This is useful for directing
658+
requests to a specific host that may be able to respond across
659+
subnets.
638660
"""
639661
if not zc.started:
640662
await zc.async_wait_for_start()
@@ -658,7 +680,7 @@ async def async_request(
658680
first_request = False
659681
if not out.questions:
660682
return self.load_from_cache(zc)
661-
zc.async_send(out)
683+
zc.async_send(out, addr, port)
662684
next_ = now + delay
663685
delay *= 2
664686
next_ += random.randint(*_AVOID_SYNC_DELAY_RANDOM_INTERVAL)

tests/services/test_info.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,89 @@ async def test_port_changes_are_seen():
956956
await aiozc.async_close()
957957

958958

959+
@pytest.mark.asyncio
960+
async def test_port_changes_are_seen_with_directed_request():
961+
"""Test that port changes are seen by async_request with a directed request."""
962+
type_ = "_http._tcp.local."
963+
registration_name = "multiarec.%s" % type_
964+
desc = {'path': '/~paulsm/'}
965+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
966+
host = "multahost.local."
967+
968+
# New kwarg way
969+
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
970+
generated.add_answer_at_time(
971+
r.DNSNsec(
972+
registration_name,
973+
const._TYPE_NSEC,
974+
const._CLASS_IN | const._CLASS_UNIQUE,
975+
const._DNS_OTHER_TTL,
976+
registration_name,
977+
[const._TYPE_AAAA],
978+
),
979+
0,
980+
)
981+
generated.add_answer_at_time(
982+
r.DNSService(
983+
registration_name,
984+
const._TYPE_SRV,
985+
const._CLASS_IN | const._CLASS_UNIQUE,
986+
10000,
987+
0,
988+
0,
989+
80,
990+
host,
991+
),
992+
0,
993+
)
994+
generated.add_answer_at_time(
995+
r.DNSAddress(
996+
host,
997+
const._TYPE_A,
998+
const._CLASS_IN,
999+
10000,
1000+
b'\x7f\x00\x00\x01',
1001+
),
1002+
0,
1003+
)
1004+
generated.add_answer_at_time(
1005+
r.DNSText(
1006+
registration_name,
1007+
const._TYPE_TXT,
1008+
const._CLASS_IN | const._CLASS_UNIQUE,
1009+
10000,
1010+
b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==',
1011+
),
1012+
0,
1013+
)
1014+
await aiozc.zeroconf.async_wait_for_start()
1015+
await asyncio.sleep(0)
1016+
aiozc.zeroconf.handle_response(r.DNSIncoming(generated.packets()[0]))
1017+
1018+
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
1019+
generated.add_answer_at_time(
1020+
r.DNSService(
1021+
registration_name,
1022+
const._TYPE_SRV,
1023+
const._CLASS_IN | const._CLASS_UNIQUE,
1024+
10000,
1025+
90,
1026+
90,
1027+
81,
1028+
host,
1029+
),
1030+
0,
1031+
)
1032+
aiozc.zeroconf.handle_response(r.DNSIncoming(generated.packets()[0]))
1033+
1034+
info = ServiceInfo(type_, registration_name, 80, 10, 10, desc, host)
1035+
await info.async_request(aiozc.zeroconf, timeout=200, addr="127.0.0.1", port=5353)
1036+
assert info.port == 81
1037+
assert info.priority == 90
1038+
assert info.weight == 90
1039+
await aiozc.async_close()
1040+
1041+
9591042
@pytest.mark.asyncio
9601043
async def test_ipv4_changes_are_seen():
9611044
"""Test that ipv4 changes are seen by async_request."""

0 commit comments

Comments
 (0)