| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -51,18 +51,18 @@ def on_service_state_change( | |||
| 51 | 51 | parser.add_argument('--debug', action='store_true') | |
| 52 | 52 | parser.add_argument('--find', action='store_true', help='Browse all available services') | |
| 53 | 53 | version_group = parser.add_mutually_exclusive_group() | |
| 54 | - version_group.add_argument('--v6', action='store_true') | ||
| 55 | 54 | version_group.add_argument('--v6-only', action='store_true') | |
| 55 | + version_group.add_argument('--v4-only', action='store_true') | ||
| 56 | 56 | args = parser.parse_args() | |
| 57 | 57 | ||
| 58 | 58 | if args.debug: | |
| 59 | 59 | logging.getLogger('zeroconf').setLevel(logging.DEBUG) | |
| 60 | - if args.v6: | ||
| 61 | - ip_version = IPVersion.All | ||
| 62 | - elif args.v6_only: | ||
| 60 | + if args.v6_only: | ||
| 63 | 61 | ip_version = IPVersion.V6Only | |
| 64 | - else: | ||
| 62 | + elif args.v4_only: | ||
| 65 | 63 | ip_version = IPVersion.V4Only | |
| 64 | + else: | ||
| 65 | + ip_version = IPVersion.All | ||
| 66 | 66 | ||
| 67 | 67 | zeroconf = Zeroconf(ip_version=ip_version) | |
| 68 | 68 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -32,6 +32,14 @@ cdef object _IPVersion_V4Only_value | |||
| 32 | 32 | cdef cython.set _ADDRESS_RECORD_TYPES | |
| 33 | 33 | ||
| 34 | 34 | cdef bint TYPE_CHECKING | |
| 35 | + cdef bint IPADDRESS_SUPPORTS_SCOPE_ID | ||
| 36 | + | ||
| 37 | + cdef _get_ip_address_object_from_record(DNSAddress record) | ||
| 38 | + | ||
| 39 | + @cython.locals(address_str=str) | ||
| 40 | + cdef _str_without_scope_id(object addr) | ||
| 41 | + | ||
| 42 | + cdef _ip_bytes_and_scope_to_address(object addr, object scope_id) | ||
| 35 | 43 | ||
| 36 | 44 | cdef class ServiceInfo(RecordUpdateListener): | |
| 37 | 45 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -22,6 +22,7 @@ | |||
| 22 | 22 | ||
| 23 | 23 | import asyncio | |
| 24 | 24 | import random | |
| 25 | + import sys | ||
| 25 | 26 | from functools import lru_cache | |
| 26 | 27 | from ipaddress import IPv4Address, IPv6Address, _BaseAddress, ip_address | |
| 27 | 28 | from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union, cast | |
@@ -78,12 +79,15 @@ | |||
| 78 | 79 | # the A/AAAA/SRV records for a host. | |
| 79 | 80 | _AVOID_SYNC_DELAY_RANDOM_INTERVAL = (20, 120) | |
| 80 | 81 | ||
| 82 | + bytes_ = bytes | ||
| 81 | 83 | float_ = float | |
| 82 | 84 | int_ = int | |
| 83 | 85 | ||
| 84 | 86 | DNS_QUESTION_TYPE_QU = DNSQuestionType.QU | |
| 85 | 87 | DNS_QUESTION_TYPE_QM = DNSQuestionType.QM | |
| 86 | 88 | ||
| 89 | + IPADDRESS_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0) | ||
| 90 | + | ||
| 87 | 91 | if TYPE_CHECKING: | |
| 88 | 92 | from .._core import Zeroconf | |
| 89 | 93 | ||
@@ -110,6 +114,29 @@ def _cached_ip_addresses(address: Union[str, bytes, int]) -> Optional[Union[IPv4 | |||
| 110 | 114 | _cached_ip_addresses_wrapper = _cached_ip_addresses | |
| 111 | 115 | ||
| 112 | 116 | ||
| 117 | + def _get_ip_address_object_from_record(record: DNSAddress) -> Optional[Union[IPv4Address, IPv6Address]]: | ||
| 118 | + """Get the IP address object from the record.""" | ||
| 119 | + if IPADDRESS_SUPPORTS_SCOPE_ID and record.type == _TYPE_AAAA and record.scope_id is not None: | ||
| 120 | + return _ip_bytes_and_scope_to_address(record.address, record.scope_id) | ||
| 121 | + return _cached_ip_addresses_wrapper(record.address) | ||
| 122 | + | ||
| 123 | + | ||
| 124 | + def _ip_bytes_and_scope_to_address(address: bytes_, scope: int_) -> Optional[Union[IPv4Address, IPv6Address]]: | ||
| 125 | + """Convert the bytes and scope to an IP address object.""" | ||
| 126 | + base_address = _cached_ip_addresses_wrapper(address) | ||
| 127 | + if base_address is not None and base_address.is_link_local: | ||
| 128 | + return _cached_ip_addresses_wrapper(f"{base_address}%{scope}") | ||
| 129 | + return base_address | ||
| 130 | + | ||
| 131 | + | ||
| 132 | + def _str_without_scope_id(addr: Union[IPv4Address, IPv6Address]) -> str: | ||
| 133 | + """Return the string representation of the address without the scope id.""" | ||
| 134 | + if IPADDRESS_SUPPORTS_SCOPE_ID and addr.version == 6: | ||
| 135 | + address_str = str(addr) | ||
| 136 | + return address_str.partition('%')[0] | ||
| 137 | + return str(addr) | ||
| 138 | + | ||
| 139 | + | ||
| 113 | 140 | class ServiceInfo(RecordUpdateListener): | |
| 114 | 141 | """Service information. | |
| 115 | 142 | ||
@@ -177,6 +204,7 @@ def __init__( | |||
| 177 | 204 | raise TypeError("addresses and parsed_addresses cannot be provided together") | |
| 178 | 205 | if not type_.endswith(service_type_name(name, strict=False)): | |
| 179 | 206 | raise BadTypeInNameException | |
| 207 | + self.interface_index = interface_index | ||
| 180 | 208 | self.text = b'' | |
| 181 | 209 | self.type = type_ | |
| 182 | 210 | self._name = name | |
@@ -199,7 +227,6 @@ def __init__( | |||
| 199 | 227 | self._set_properties(properties) | |
| 200 | 228 | self.host_ttl = host_ttl | |
| 201 | 229 | self.other_ttl = other_ttl | |
| 202 | - self.interface_index = interface_index | ||
| 203 | 230 | self._new_records_futures: Optional[Set[asyncio.Future]] = None | |
| 204 | 231 | self._dns_address_cache: Optional[List[DNSAddress]] = None | |
| 205 | 232 | self._dns_pointer_cache: Optional[DNSPointer] = None | |
@@ -243,7 +270,10 @@ def addresses(self, value: List[bytes]) -> None: | |||
| 243 | 270 | self._get_address_and_nsec_records_cache = None | |
| 244 | 271 | ||
| 245 | 272 | for address in value: | |
| 246 | - addr = _cached_ip_addresses_wrapper(address) | ||
| 273 | + if IPADDRESS_SUPPORTS_SCOPE_ID and len(address) == 16 and self.interface_index is not None: | ||
| 274 | + addr = _ip_bytes_and_scope_to_address(address, self.interface_index) | ||
| 275 | + else: | ||
| 276 | + addr = _cached_ip_addresses_wrapper(address) | ||
| 247 | 277 | if addr is None: | |
| 248 | 278 | raise TypeError( | |
| 249 | 279 | "Addresses must either be IPv4 or IPv6 strings, bytes, or integers;" | |
@@ -322,10 +352,10 @@ def ip_addresses_by_version( | |||
| 322 | 352 | ||
| 323 | 353 | def _ip_addresses_by_version_value( | |
| 324 | 354 | self, version_value: int_ | |
| 325 | - ) -> Union[List[IPv4Address], List[IPv6Address], List[_BaseAddress]]: | ||
| 355 | + ) -> Union[List[IPv4Address], List[IPv6Address]]: | ||
| 326 | 356 | """Backend for addresses_by_version that uses the raw value.""" | |
| 327 | 357 | if version_value == _IPVersion_All_value: | |
| 328 | - return [*self._ipv4_addresses, *self._ipv6_addresses] | ||
| 358 | + return [*self._ipv4_addresses, *self._ipv6_addresses] # type: ignore[return-value] | ||
| 329 | 359 | if version_value == _IPVersion_V4Only_value: | |
| 330 | 360 | return self._ipv4_addresses | |
| 331 | 361 | return self._ipv6_addresses | |
@@ -339,7 +369,7 @@ def parsed_addresses(self, version: IPVersion = IPVersion.All) -> List[str]: | |||
| 339 | 369 | This means the first address will always be the most recently added | |
| 340 | 370 | address of the given IP version. | |
| 341 | 371 | """ | |
| 342 | - return [str(addr) for addr in self._ip_addresses_by_version_value(version.value)] | ||
| 372 | + return [_str_without_scope_id(addr) for addr in self._ip_addresses_by_version_value(version.value)] | ||
| 343 | 373 | ||
| 344 | 374 | def parsed_scoped_addresses(self, version: IPVersion = IPVersion.All) -> List[str]: | |
| 345 | 375 | """Equivalent to parsed_addresses, with the exception that IPv6 Link-Local | |
@@ -351,12 +381,7 @@ def parsed_scoped_addresses(self, version: IPVersion = IPVersion.All) -> List[st | |||
| 351 | 381 | This means the first address will always be the most recently added | |
| 352 | 382 | address of the given IP version. | |
| 353 | 383 | """ | |
| 354 | - if self.interface_index is None: | ||
| 355 | - return self.parsed_addresses(version) | ||
| 356 | - return [ | ||
| 357 | - f"{addr}%{self.interface_index}" if addr.version == 6 and addr.is_link_local else str(addr) | ||
| 358 | - for addr in self._ip_addresses_by_version_value(version.value) | ||
| 359 | - ] | ||
| 384 | + return [str(addr) for addr in self._ip_addresses_by_version_value(version.value)] | ||
| 360 | 385 | ||
| 361 | 386 | def _set_properties(self, properties: Dict[Union[str, bytes], Optional[Union[str, bytes]]]) -> None: | |
| 362 | 387 | """Sets properties and text of this info from a dictionary""" | |
@@ -421,8 +446,8 @@ def _get_ip_addresses_from_cache_lifo( | |||
| 421 | 446 | for record in self._get_address_records_from_cache_by_type(zc, type): | |
| 422 | 447 | if record.is_expired(now): | |
| 423 | 448 | continue | |
| 424 | - ip_addr = _cached_ip_addresses_wrapper(record.address) | ||
| 425 | - if ip_addr is not None: | ||
| 449 | + ip_addr = _get_ip_address_object_from_record(record) | ||
| 450 | + if ip_addr is not None and ip_addr not in address_list: | ||
| 426 | 451 | address_list.append(ip_addr) | |
| 427 | 452 | address_list.reverse() # Reverse to get LIFO order | |
| 428 | 453 | return address_list | |
@@ -471,7 +496,7 @@ def _process_record_threadsafe(self, zc: 'Zeroconf', record: DNSRecord, now: flo | |||
| 471 | 496 | dns_address_record = record | |
| 472 | 497 | if TYPE_CHECKING: | |
| 473 | 498 | assert isinstance(dns_address_record, DNSAddress) | |
| 474 | - ip_addr = _cached_ip_addresses_wrapper(dns_address_record.address) | ||
| 499 | + ip_addr = _get_ip_address_object_from_record(dns_address_record) | ||
| 475 | 500 | if ip_addr is None: | |
| 476 | 501 | log.warning( | |
| 477 | 502 | "Encountered invalid address while processing %s: %s", | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -7,6 +7,7 @@ | |||
| 7 | 7 | import logging | |
| 8 | 8 | import os | |
| 9 | 9 | import socket | |
| 10 | + import sys | ||
| 10 | 11 | import threading | |
| 11 | 12 | import unittest | |
| 12 | 13 | from ipaddress import ip_address | |
@@ -538,6 +539,7 @@ def test_multiple_addresses(): | |||
| 538 | 539 | assert info.addresses == [address, address] | |
| 539 | 540 | assert info.parsed_addresses() == [address_parsed, address_parsed] | |
| 540 | 541 | assert info.parsed_scoped_addresses() == [address_parsed, address_parsed] | |
| 542 | + ipaddress_supports_scope_id = sys.version_info >= (3, 9, 0) | ||
| 541 | 543 | ||
| 542 | 544 | if has_working_ipv6() and not os.environ.get('SKIP_IPV6'): | |
| 543 | 545 | address_v6_parsed = "2001:db8::1" | |
@@ -576,30 +578,79 @@ def test_multiple_addresses(): | |||
| 576 | 578 | assert info.ip_addresses_by_version(r.IPVersion.All) == [ | |
| 577 | 579 | ip_address(address), | |
| 578 | 580 | ip_address(address_v6), | |
| 579 | - ip_address(address_v6_ll), | ||
| 581 | + ip_address(address_v6_ll_scoped_parsed) | ||
| 582 | + if ipaddress_supports_scope_id | ||
| 583 | + else ip_address(address_v6_ll), | ||
| 580 | 584 | ] | |
| 581 | 585 | assert info.addresses_by_version(r.IPVersion.V4Only) == [address] | |
| 582 | 586 | assert info.ip_addresses_by_version(r.IPVersion.V4Only) == [ip_address(address)] | |
| 583 | 587 | assert info.addresses_by_version(r.IPVersion.V6Only) == [address_v6, address_v6_ll] | |
| 584 | 588 | assert info.ip_addresses_by_version(r.IPVersion.V6Only) == [ | |
| 585 | 589 | ip_address(address_v6), | |
| 586 | - ip_address(address_v6_ll), | ||
| 590 | + ip_address(address_v6_ll_scoped_parsed) | ||
| 591 | + if ipaddress_supports_scope_id | ||
| 592 | + else ip_address(address_v6_ll), | ||
| 587 | 593 | ] | |
| 588 | 594 | assert info.parsed_addresses() == [address_parsed, address_v6_parsed, address_v6_ll_parsed] | |
| 589 | 595 | assert info.parsed_addresses(r.IPVersion.V4Only) == [address_parsed] | |
| 590 | 596 | assert info.parsed_addresses(r.IPVersion.V6Only) == [address_v6_parsed, address_v6_ll_parsed] | |
| 591 | 597 | assert info.parsed_scoped_addresses() == [ | |
| 592 | 598 | address_parsed, | |
| 593 | 599 | address_v6_parsed, | |
| 594 | - address_v6_ll_scoped_parsed, | ||
| 600 | + address_v6_ll_scoped_parsed if ipaddress_supports_scope_id else address_v6_ll_parsed, | ||
| 595 | 601 | ] | |
| 596 | 602 | assert info.parsed_scoped_addresses(r.IPVersion.V4Only) == [address_parsed] | |
| 597 | 603 | assert info.parsed_scoped_addresses(r.IPVersion.V6Only) == [ | |
| 598 | 604 | address_v6_parsed, | |
| 599 | - address_v6_ll_scoped_parsed, | ||
| 605 | + address_v6_ll_scoped_parsed if ipaddress_supports_scope_id else address_v6_ll_parsed, | ||
| 600 | 606 | ] | |
| 601 | 607 | ||
| 602 | 608 | ||
| 609 | + @unittest.skipIf(sys.version_info < (3, 9, 0), 'Requires newer python') | ||
| 610 | + def test_scoped_addresses_from_cache(): | ||
| 611 | + type_ = "_http._tcp.local." | ||
| 612 | + registration_name = f"scoped.{type_}" | ||
| 613 | + zeroconf = r.Zeroconf(interfaces=['127.0.0.1']) | ||
| 614 | + host = "scoped.local." | ||
| 615 | + | ||
| 616 | + zeroconf.cache.async_add_records( | ||
| 617 | + [ | ||
| 618 | + r.DNSPointer( | ||
| 619 | + type_, | ||
| 620 | + const._TYPE_PTR, | ||
| 621 | + const._CLASS_IN | const._CLASS_UNIQUE, | ||
| 622 | + 120, | ||
| 623 | + registration_name, | ||
| 624 | + ), | ||
| 625 | + r.DNSService( | ||
| 626 | + registration_name, | ||
| 627 | + const._TYPE_SRV, | ||
| 628 | + const._CLASS_IN | const._CLASS_UNIQUE, | ||
| 629 | + 120, | ||
| 630 | + 0, | ||
| 631 | + 0, | ||
| 632 | + 80, | ||
| 633 | + host, | ||
| 634 | + ), | ||
| 635 | + r.DNSAddress( | ||
| 636 | + host, | ||
| 637 | + const._TYPE_AAAA, | ||
| 638 | + const._CLASS_IN | const._CLASS_UNIQUE, | ||
| 639 | + 120, | ||
| 640 | + socket.inet_pton(socket.AF_INET6, "fe80::52e:c2f2:bc5f:e9c6"), | ||
| 641 | + scope_id=12, | ||
| 642 | + ), | ||
| 643 | + ] | ||
| 644 | + ) | ||
| 645 | + | ||
| 646 | + # New kwarg way | ||
| 647 | + info = ServiceInfo(type_, registration_name) | ||
| 648 | + info.load_from_cache(zeroconf) | ||
| 649 | + assert info.parsed_scoped_addresses() == ["fe80::52e:c2f2:bc5f:e9c6%12"] | ||
| 650 | + assert info.ip_addresses_by_version(r.IPVersion.V6Only) == [ip_address("fe80::52e:c2f2:bc5f:e9c6%12")] | ||
| 651 | + zeroconf.close() | ||
| 652 | + | ||
| 653 | + | ||
| 603 | 654 | # This test uses asyncio because it needs to access the cache directly | |
| 604 | 655 | # which is not threadsafe | |
| 605 | 656 | @pytest.mark.asyncio | |
| Back | FazBrowse Home | New Git URL |
0 commit comments