Skip to content

Commit 410f3f1

Browse files
committed
Rework exposing IPv6 addresses on ServiceInfo
* Return backward compatibility for ServiceInfo.addresses by making it return V4 addresses only * Add ServiceInfo.parsed_addresses for convenient access to addresses * Raise TypeError if addresses are not provided as bytes (otherwise an ugly assertion error is raised when sending) * Add more IPv6 unit tests
1 parent aae7fd3 commit 410f3f1

3 files changed

Lines changed: 82 additions & 16 deletions

File tree

examples/self_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
print("1. Testing registration of a service...")
2020
desc = {'version': '0.10', 'a': 'test value', 'b': 'another value'}
2121
addresses = [socket.inet_aton("127.0.0.1")]
22+
expected = {'127.0.0.1'}
2223
if socket.has_ipv6:
2324
addresses.append(socket.inet_pton(socket.AF_INET6, '::1'))
25+
expected.add('::1')
2426
info = ServiceInfo(
2527
"_http._tcp.local.",
2628
"My Service Name._http._tcp.local.",
@@ -37,6 +39,7 @@
3739
print("3. Testing query of own service...")
3840
queried_info = r.get_service_info("_http._tcp.local.", "My Service Name._http._tcp.local.")
3941
assert queried_info
42+
assert set(queried_info.parsed_addresses()) == expected
4043
print(" Getting self: %s" % (queried_info,))
4144
print(" Query done.")
4245
print("4. Testing unregister of service information...")

test_zeroconf.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,23 @@ def test_good_service_names(self):
536536

537537
r.service_type_name('_one_two._tcp.local.', allow_underscores=True)
538538

539+
def test_invalid_addresses(self):
540+
type_ = "_test-srvc-type._tcp.local."
541+
name = "xxxyyy"
542+
registration_name = "%s.%s" % (name, type_)
543+
544+
bad = ('127.0.0.1', '::1', 42)
545+
for addr in bad:
546+
self.assertRaisesRegex(
547+
TypeError,
548+
'Addresses must be bytes',
549+
ServiceInfo,
550+
type_,
551+
registration_name,
552+
port=80,
553+
addresses=[addr],
554+
)
555+
539556

540557
class TestDnsIncoming(unittest.TestCase):
541558
def test_incoming_exception_handling(self):
@@ -844,6 +861,9 @@ def update_service(self, zeroconf, type, name):
844861
assert info.properties[b'prop_blank'] == properties['prop_blank']
845862
assert info.properties[b'prop_true'] is True
846863
assert info.properties[b'prop_false'] is False
864+
assert info.addresses == addresses[:1] # no V6 by default
865+
all_addresses = info.addresses_by_version(r.IPVersion.All)
866+
assert all_addresses == addresses, all_addresses
847867

848868
info = zeroconf_browser.get_service_info(subtype, registration_name)
849869
assert info is not None
@@ -1039,7 +1059,8 @@ def test_multiple_addresses():
10391059
type_ = "_http._tcp.local."
10401060
registration_name = "xxxyyy.%s" % type_
10411061
desc = {'path': '/~paulsm/'}
1042-
address = socket.inet_aton("10.0.1.2")
1062+
address_parsed = "10.0.1.2"
1063+
address = socket.inet_aton(address_parsed)
10431064

10441065
# Old way
10451066
info = ServiceInfo(type_, registration_name, address, 80, 0, 0, desc, "ash-2.local.")
@@ -1059,6 +1080,11 @@ def test_multiple_addresses():
10591080
assert info.address is None
10601081
assert info.addresses == []
10611082

1083+
info.addresses = [address2]
1084+
1085+
assert info.address == address2
1086+
assert info.addresses == [address2]
1087+
10621088
# Compatibility way
10631089
info = ServiceInfo(type_, registration_name, [address, address], 80, 0, 0, desc, "ash-2.local.")
10641090

@@ -1072,12 +1098,16 @@ def test_multiple_addresses():
10721098
assert info.addresses == [address, address]
10731099

10741100
if socket.has_ipv6:
1075-
address_v6 = socket.inet_pton(socket.AF_INET6, "2001:db8::1")
1101+
address_v6_parsed = "2001:db8::1"
1102+
address_v6 = socket.inet_pton(socket.AF_INET6, address_v6_parsed)
10761103
info = ServiceInfo(type_, registration_name, [address, address_v6], 80, 0, 0, desc, "ash-2.local.")
1077-
assert info.addresses == [address, address_v6]
1104+
assert info.addresses == [address]
10781105
assert info.addresses_by_version(r.IPVersion.All) == [address, address_v6]
10791106
assert info.addresses_by_version(r.IPVersion.V4Only) == [address]
10801107
assert info.addresses_by_version(r.IPVersion.V6Only) == [address_v6]
1108+
assert info.parsed_addresses() == [address_parsed, address_v6_parsed]
1109+
assert info.parsed_addresses(r.IPVersion.V4Only) == [address_parsed]
1110+
assert info.parsed_addresses(r.IPVersion.V6Only) == [address_v6_parsed]
10811111

10821112

10831113
def test_ptr_optimization():

zeroconf.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,15 +1498,21 @@ def __init__(
14981498
self.type = type_
14991499
self.name = name
15001500
if addresses is not None:
1501-
self.addresses = addresses
1501+
self._addresses = addresses
15021502
elif address is not None:
15031503
warnings.warn("address is deprecated, use addresses instead", DeprecationWarning)
15041504
if isinstance(address, list):
1505-
self.addresses = address
1505+
self._addresses = address
15061506
else:
1507-
self.addresses = [address]
1507+
self._addresses = [address]
15081508
else:
1509-
self.addresses = []
1509+
self._addresses = []
1510+
# This results in an ugly error when registering, better check now
1511+
invalid = [a for a in self._addresses
1512+
if not isinstance(a, bytes) or len(a) not in (4, 16)]
1513+
if invalid:
1514+
raise TypeError('Addresses must be bytes, got %s. Hint: convert string addresses '
1515+
'with socket.inet_pton' % invalid)
15101516
self.port = port
15111517
self.weight = weight
15121518
self.priority = priority
@@ -1524,6 +1530,7 @@ def __init__(
15241530
def address(self):
15251531
warnings.warn("ServiceInfo.address is deprecated, use addresses instead", DeprecationWarning)
15261532
try:
1533+
# Return the first V4 address for compatibility
15271534
return self.addresses[0]
15281535
except IndexError:
15291536
return None
@@ -1532,9 +1539,27 @@ def address(self):
15321539
def address(self, value):
15331540
warnings.warn("ServiceInfo.address is deprecated, use addresses instead", DeprecationWarning)
15341541
if value is None:
1535-
self.addresses = []
1542+
self._addresses = []
15361543
else:
1537-
self.addresses = [value]
1544+
self._addresses = [value]
1545+
1546+
@property
1547+
def addresses(self):
1548+
"""IPv4 addresses of this service.
1549+
1550+
Only IPv4 addresses are returned for backward compatibility.
1551+
Use :meth:`addresses_by_version` or :meth:`parsed_addresses` to
1552+
include IPv6 addresses as well.
1553+
"""
1554+
return self.addresses_by_version(IPVersion.V4Only)
1555+
1556+
@addresses.setter
1557+
def addresses(self, value):
1558+
"""Replace the addresses list.
1559+
1560+
This replaces all currently stored addresses, both IPv4 and IPv6.
1561+
"""
1562+
self._addresses = value
15381563

15391564
@property
15401565
def properties(self) -> ServicePropertiesType:
@@ -1543,11 +1568,19 @@ def properties(self) -> ServicePropertiesType:
15431568
def addresses_by_version(self, version: IPVersion) -> List[bytes]:
15441569
"""List addresses matching IP version."""
15451570
if version == IPVersion.V4Only:
1546-
return [addr for addr in self.addresses if not _is_v6_address(addr)]
1571+
return [addr for addr in self._addresses if not _is_v6_address(addr)]
15471572
elif version == IPVersion.V6Only:
1548-
return list(filter(_is_v6_address, self.addresses))
1573+
return list(filter(_is_v6_address, self._addresses))
15491574
else:
1550-
return self.addresses
1575+
return self._addresses
1576+
1577+
def parsed_addresses(self, version: IPVersion = IPVersion.All) -> List[str]:
1578+
"""List addresses in their parsed string form."""
1579+
result = self.addresses_by_version(version)
1580+
return [
1581+
socket.inet_ntop(socket.AF_INET6 if _is_v6_address(addr) else socket.AF_INET, addr)
1582+
for addr in result
1583+
]
15511584

15521585
def _set_properties(self, properties: Union[bytes, ServicePropertiesType]):
15531586
"""Sets properties and text of this info from a dictionary"""
@@ -1625,8 +1658,8 @@ def update_record(self, zc: 'Zeroconf', now: float, record: DNSRecord) -> None:
16251658
assert isinstance(record, DNSAddress)
16261659
# if record.name == self.name:
16271660
if record.name == self.server:
1628-
if record.address not in self.addresses:
1629-
self.addresses.append(record.address)
1661+
if record.address not in self._addresses:
1662+
self._addresses.append(record.address)
16301663
elif record.type == _TYPE_SRV:
16311664
assert isinstance(record, DNSService)
16321665
if record.name == self.name:
@@ -1660,12 +1693,12 @@ def request(self, zc: 'Zeroconf', timeout: float) -> bool:
16601693
if cached:
16611694
self.update_record(zc, now, cached)
16621695

1663-
if self.server is not None and self.text is not None and self.addresses:
1696+
if self.server is not None and self.text is not None and self._addresses:
16641697
return True
16651698

16661699
try:
16671700
zc.add_listener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1668-
while self.server is None or self.text is None or not self.addresses:
1701+
while self.server is None or self.text is None or not self._addresses:
16691702
if last <= now:
16701703
return False
16711704
if next_ <= now:

0 commit comments

Comments
 (0)