zeroconf 0.152.0__cp313-cp313-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zeroconf/__init__.py +104 -0
- zeroconf/_cache.cp313-win_arm64.pyd +0 -0
- zeroconf/_cache.pxd +96 -0
- zeroconf/_cache.py +316 -0
- zeroconf/_core.py +791 -0
- zeroconf/_dns.cp313-win_arm64.pyd +0 -0
- zeroconf/_dns.pxd +160 -0
- zeroconf/_dns.py +622 -0
- zeroconf/_engine.py +456 -0
- zeroconf/_exceptions.py +54 -0
- zeroconf/_handlers/__init__.py +8 -0
- zeroconf/_handlers/answers.cp313-win_arm64.pyd +0 -0
- zeroconf/_handlers/answers.pxd +34 -0
- zeroconf/_handlers/answers.py +110 -0
- zeroconf/_handlers/multicast_outgoing_queue.cp313-win_arm64.pyd +0 -0
- zeroconf/_handlers/multicast_outgoing_queue.pxd +27 -0
- zeroconf/_handlers/multicast_outgoing_queue.py +115 -0
- zeroconf/_handlers/query_handler.cp313-win_arm64.pyd +0 -0
- zeroconf/_handlers/query_handler.pxd +124 -0
- zeroconf/_handlers/query_handler.py +458 -0
- zeroconf/_handlers/record_manager.cp313-win_arm64.pyd +0 -0
- zeroconf/_handlers/record_manager.pxd +42 -0
- zeroconf/_handlers/record_manager.py +188 -0
- zeroconf/_history.cp313-win_arm64.pyd +0 -0
- zeroconf/_history.pxd +23 -0
- zeroconf/_history.py +93 -0
- zeroconf/_listener.cp313-win_arm64.pyd +0 -0
- zeroconf/_listener.pxd +63 -0
- zeroconf/_listener.py +353 -0
- zeroconf/_logger.py +95 -0
- zeroconf/_protocol/__init__.py +8 -0
- zeroconf/_protocol/incoming.cp313-win_arm64.pyd +0 -0
- zeroconf/_protocol/incoming.pxd +157 -0
- zeroconf/_protocol/incoming.py +571 -0
- zeroconf/_protocol/outgoing.cp313-win_arm64.pyd +0 -0
- zeroconf/_protocol/outgoing.pxd +143 -0
- zeroconf/_protocol/outgoing.py +481 -0
- zeroconf/_record_update.cp313-win_arm64.pyd +0 -0
- zeroconf/_record_update.pxd +12 -0
- zeroconf/_record_update.py +33 -0
- zeroconf/_services/__init__.cp313-win_arm64.pyd +0 -0
- zeroconf/_services/__init__.pxd +11 -0
- zeroconf/_services/__init__.py +63 -0
- zeroconf/_services/browser.cp313-win_arm64.pyd +0 -0
- zeroconf/_services/browser.pxd +122 -0
- zeroconf/_services/browser.py +810 -0
- zeroconf/_services/info.cp313-win_arm64.pyd +0 -0
- zeroconf/_services/info.pxd +197 -0
- zeroconf/_services/info.py +1138 -0
- zeroconf/_services/registry.cp313-win_arm64.pyd +0 -0
- zeroconf/_services/registry.pxd +37 -0
- zeroconf/_services/registry.py +108 -0
- zeroconf/_services/types.py +69 -0
- zeroconf/_transport.py +114 -0
- zeroconf/_updates.cp313-win_arm64.pyd +0 -0
- zeroconf/_updates.pxd +9 -0
- zeroconf/_updates.py +66 -0
- zeroconf/_utils/__init__.py +8 -0
- zeroconf/_utils/asyncio.py +126 -0
- zeroconf/_utils/ipaddress.cp313-win_arm64.pyd +0 -0
- zeroconf/_utils/ipaddress.pxd +16 -0
- zeroconf/_utils/ipaddress.py +148 -0
- zeroconf/_utils/name.py +166 -0
- zeroconf/_utils/net.py +545 -0
- zeroconf/_utils/time.cp313-win_arm64.pyd +0 -0
- zeroconf/_utils/time.pxd +4 -0
- zeroconf/_utils/time.py +26 -0
- zeroconf/asyncio.py +248 -0
- zeroconf/const.py +193 -0
- zeroconf/py.typed +0 -0
- zeroconf-0.152.0.dist-info/METADATA +177 -0
- zeroconf-0.152.0.dist-info/RECORD +74 -0
- zeroconf-0.152.0.dist-info/WHEEL +4 -0
- zeroconf-0.152.0.dist-info/licenses/COPYING +475 -0
zeroconf/__init__.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""A pure python implementation of multicast DNS service discovery.
|
|
2
|
+
|
|
3
|
+
Licensed under LGPL-2.1-or-later; see COPYING for details. This file is
|
|
4
|
+
part of a continuously modified work; modification dates are recorded
|
|
5
|
+
in the project's git history.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._cache import DNSCache # noqa # import needed for backwards compat
|
|
11
|
+
from ._core import Zeroconf
|
|
12
|
+
from ._dns import ( # noqa # import needed for backwards compat
|
|
13
|
+
DNSAddress,
|
|
14
|
+
DNSEntry,
|
|
15
|
+
DNSHinfo,
|
|
16
|
+
DNSNsec,
|
|
17
|
+
DNSPointer,
|
|
18
|
+
DNSQuestion,
|
|
19
|
+
DNSQuestionType,
|
|
20
|
+
DNSRecord,
|
|
21
|
+
DNSService,
|
|
22
|
+
DNSText,
|
|
23
|
+
)
|
|
24
|
+
from ._exceptions import (
|
|
25
|
+
AbstractMethodException,
|
|
26
|
+
BadTypeInNameException,
|
|
27
|
+
Error,
|
|
28
|
+
EventLoopBlocked,
|
|
29
|
+
IncomingDecodeError,
|
|
30
|
+
NamePartTooLongException,
|
|
31
|
+
NonUniqueNameException,
|
|
32
|
+
NotRunningException,
|
|
33
|
+
ServiceNameAlreadyRegistered,
|
|
34
|
+
)
|
|
35
|
+
from ._logger import QuietLogger, log # noqa # import needed for backwards compat
|
|
36
|
+
from ._protocol.incoming import DNSIncoming # noqa # import needed for backwards compat
|
|
37
|
+
from ._protocol.outgoing import DNSOutgoing # noqa # import needed for backwards compat
|
|
38
|
+
from ._record_update import RecordUpdate
|
|
39
|
+
from ._services import ( # noqa # import needed for backwards compat
|
|
40
|
+
ServiceListener,
|
|
41
|
+
ServiceStateChange,
|
|
42
|
+
Signal,
|
|
43
|
+
SignalRegistrationInterface,
|
|
44
|
+
)
|
|
45
|
+
from ._services.browser import ServiceBrowser
|
|
46
|
+
from ._services.info import ( # noqa # import needed for backwards compat
|
|
47
|
+
AddressResolver,
|
|
48
|
+
AddressResolverIPv4,
|
|
49
|
+
AddressResolverIPv6,
|
|
50
|
+
ServiceInfo,
|
|
51
|
+
instance_name_from_service_info,
|
|
52
|
+
)
|
|
53
|
+
from ._services.registry import ( # noqa # import needed for backwards compat
|
|
54
|
+
ServiceRegistry,
|
|
55
|
+
)
|
|
56
|
+
from ._services.types import ZeroconfServiceTypes
|
|
57
|
+
from ._updates import RecordUpdateListener
|
|
58
|
+
from ._utils.name import service_type_name # noqa # import needed for backwards compat
|
|
59
|
+
from ._utils.net import ( # noqa # import needed for backwards compat
|
|
60
|
+
InterfaceChoice,
|
|
61
|
+
InterfacesType,
|
|
62
|
+
IPVersion,
|
|
63
|
+
add_multicast_member,
|
|
64
|
+
autodetect_ip_version,
|
|
65
|
+
create_sockets,
|
|
66
|
+
get_all_addresses,
|
|
67
|
+
get_all_addresses_v6,
|
|
68
|
+
)
|
|
69
|
+
from ._utils.time import ( # noqa # import needed for backwards compat
|
|
70
|
+
current_time_millis,
|
|
71
|
+
millis_to_seconds,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
__author__ = "The python-zeroconf authors" # full history in COPYING and git
|
|
75
|
+
__maintainer__ = "J. Nick Koston <nick@koston.org>"
|
|
76
|
+
__version__ = "0.152.0"
|
|
77
|
+
__license__ = "LGPL"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"AbstractMethodException",
|
|
82
|
+
"BadTypeInNameException",
|
|
83
|
+
"DNSQuestionType",
|
|
84
|
+
# Exceptions
|
|
85
|
+
"Error",
|
|
86
|
+
"EventLoopBlocked",
|
|
87
|
+
"IPVersion",
|
|
88
|
+
"IncomingDecodeError",
|
|
89
|
+
"InterfaceChoice",
|
|
90
|
+
"NamePartTooLongException",
|
|
91
|
+
"NonUniqueNameException",
|
|
92
|
+
"NotRunningException",
|
|
93
|
+
"RecordUpdate",
|
|
94
|
+
"RecordUpdateListener",
|
|
95
|
+
"ServiceBrowser",
|
|
96
|
+
"ServiceInfo",
|
|
97
|
+
"ServiceListener",
|
|
98
|
+
"ServiceNameAlreadyRegistered",
|
|
99
|
+
"ServiceStateChange",
|
|
100
|
+
"Zeroconf",
|
|
101
|
+
"ZeroconfServiceTypes",
|
|
102
|
+
"__version__",
|
|
103
|
+
"current_time_millis",
|
|
104
|
+
]
|
|
Binary file
|
zeroconf/_cache.pxd
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import cython
|
|
2
|
+
|
|
3
|
+
from ._dns cimport (
|
|
4
|
+
DNSAddress,
|
|
5
|
+
DNSEntry,
|
|
6
|
+
DNSHinfo,
|
|
7
|
+
DNSNsec,
|
|
8
|
+
DNSPointer,
|
|
9
|
+
DNSRecord,
|
|
10
|
+
DNSService,
|
|
11
|
+
DNSText,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
cdef object heappop
|
|
15
|
+
cdef object heappush
|
|
16
|
+
cdef object heapify
|
|
17
|
+
|
|
18
|
+
cdef object _UNIQUE_RECORD_TYPES
|
|
19
|
+
cdef unsigned int _TYPE_PTR
|
|
20
|
+
cdef cython.uint _ONE_SECOND
|
|
21
|
+
cdef unsigned int _MIN_SCHEDULED_RECORD_EXPIRATION
|
|
22
|
+
cdef unsigned int _MAX_CACHE_RECORDS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cython.locals(record_cache=dict)
|
|
26
|
+
cdef _remove_key(cython.dict cache, object key, DNSRecord record)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
cdef class DNSCache:
|
|
30
|
+
|
|
31
|
+
cdef public cython.dict cache
|
|
32
|
+
cdef public cython.dict service_cache
|
|
33
|
+
cdef public list _expire_heap
|
|
34
|
+
cdef public dict _expirations
|
|
35
|
+
cdef public unsigned int _total_records
|
|
36
|
+
|
|
37
|
+
cpdef bint async_add_records(self, object entries)
|
|
38
|
+
|
|
39
|
+
cpdef void async_remove_records(self, object entries)
|
|
40
|
+
|
|
41
|
+
@cython.locals(store=cython.dict)
|
|
42
|
+
cpdef DNSRecord async_get_unique(self, DNSRecord entry)
|
|
43
|
+
|
|
44
|
+
@cython.locals(record=DNSRecord, when_record=tuple, when=double)
|
|
45
|
+
cpdef list async_expire(self, double now)
|
|
46
|
+
|
|
47
|
+
@cython.locals(records=cython.dict, record=DNSRecord)
|
|
48
|
+
cpdef list async_all_by_details(self, str name, unsigned int type_, unsigned int class_)
|
|
49
|
+
|
|
50
|
+
cpdef list async_entries_with_name(self, str name)
|
|
51
|
+
|
|
52
|
+
cpdef list async_entries_with_server(self, str name)
|
|
53
|
+
|
|
54
|
+
@cython.locals(cached_entry=DNSRecord, records=dict)
|
|
55
|
+
cpdef DNSRecord get_by_details(self, str name, unsigned int type_, unsigned int class_)
|
|
56
|
+
|
|
57
|
+
@cython.locals(records=cython.dict, entry=DNSRecord)
|
|
58
|
+
cpdef cython.list get_all_by_details(self, str name, unsigned int type_, unsigned int class_)
|
|
59
|
+
|
|
60
|
+
@cython.locals(
|
|
61
|
+
store=cython.dict,
|
|
62
|
+
service_store=cython.dict,
|
|
63
|
+
service_record=DNSService,
|
|
64
|
+
when=object,
|
|
65
|
+
new=bint,
|
|
66
|
+
is_new=bint
|
|
67
|
+
)
|
|
68
|
+
cdef bint _async_add(self, DNSRecord record)
|
|
69
|
+
|
|
70
|
+
@cython.locals(record=DNSRecord, when_record=tuple)
|
|
71
|
+
cdef void _async_evict_oldest(self)
|
|
72
|
+
|
|
73
|
+
@cython.locals(expire_heap_len="unsigned int")
|
|
74
|
+
cdef void _maybe_rebuild_heap(self)
|
|
75
|
+
|
|
76
|
+
@cython.locals(service_record=DNSService)
|
|
77
|
+
cdef void _async_remove(self, DNSRecord record)
|
|
78
|
+
|
|
79
|
+
@cython.locals(record=DNSRecord, created_double=double)
|
|
80
|
+
cpdef void async_mark_unique_records_older_than_1s_to_expire(self, cython.set unique_types, object answers, double now)
|
|
81
|
+
|
|
82
|
+
@cython.locals(entries=dict)
|
|
83
|
+
cpdef list entries_with_name(self, str name)
|
|
84
|
+
|
|
85
|
+
@cython.locals(entries=dict)
|
|
86
|
+
cpdef list entries_with_server(self, str server)
|
|
87
|
+
|
|
88
|
+
@cython.locals(record=DNSRecord, now=double)
|
|
89
|
+
cpdef current_entry_with_name_and_alias(self, str name, str alias)
|
|
90
|
+
|
|
91
|
+
cpdef void _async_set_created_ttl(
|
|
92
|
+
self,
|
|
93
|
+
DNSRecord record,
|
|
94
|
+
double now,
|
|
95
|
+
unsigned int ttl
|
|
96
|
+
)
|
zeroconf/_cache.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""A pure python implementation of multicast DNS service discovery.
|
|
2
|
+
|
|
3
|
+
Licensed under LGPL-2.1-or-later; see COPYING for details. This file is
|
|
4
|
+
part of a continuously modified work; modification dates are recorded
|
|
5
|
+
in the project's git history.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from heapq import heapify, heappop, heappush
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
from ._dns import (
|
|
15
|
+
DNSAddress,
|
|
16
|
+
DNSEntry,
|
|
17
|
+
DNSHinfo,
|
|
18
|
+
DNSNsec,
|
|
19
|
+
DNSPointer,
|
|
20
|
+
DNSRecord,
|
|
21
|
+
DNSService,
|
|
22
|
+
DNSText,
|
|
23
|
+
)
|
|
24
|
+
from ._utils.time import current_time_millis
|
|
25
|
+
from .const import _MAX_CACHE_RECORDS, _ONE_SECOND, _TYPE_PTR
|
|
26
|
+
|
|
27
|
+
_UNIQUE_RECORD_TYPES = (DNSAddress, DNSHinfo, DNSPointer, DNSText, DNSService)
|
|
28
|
+
_UniqueRecordsType = DNSAddress | DNSHinfo | DNSPointer | DNSText | DNSService
|
|
29
|
+
_DNSRecordCacheType = dict[str, dict[DNSRecord, DNSRecord]]
|
|
30
|
+
_UniqueType = tuple[str, int, int]
|
|
31
|
+
_DNSRecord = DNSRecord
|
|
32
|
+
_str = str
|
|
33
|
+
_float = float
|
|
34
|
+
_int = int
|
|
35
|
+
|
|
36
|
+
# The minimum number of scheduled record expirations before we start cleaning up
|
|
37
|
+
# the expiration heap. This is a performance optimization to avoid cleaning up the
|
|
38
|
+
# heap too often when there are only a few scheduled expirations.
|
|
39
|
+
_MIN_SCHEDULED_RECORD_EXPIRATION = 100
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _remove_key(cache: _DNSRecordCacheType, key: _str, record: _DNSRecord) -> None:
|
|
43
|
+
"""Remove a key from a DNSRecord cache
|
|
44
|
+
|
|
45
|
+
This function must be run in from event loop.
|
|
46
|
+
"""
|
|
47
|
+
record_cache = cache[key]
|
|
48
|
+
del record_cache[record]
|
|
49
|
+
if not record_cache:
|
|
50
|
+
del cache[key]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DNSCache:
|
|
54
|
+
"""Store of received DNS records, indexed for fast lookup."""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self.cache: _DNSRecordCacheType = {}
|
|
58
|
+
self._expire_heap: list[tuple[float, DNSRecord]] = []
|
|
59
|
+
self._expirations: dict[DNSRecord, float] = {}
|
|
60
|
+
self.service_cache: _DNSRecordCacheType = {}
|
|
61
|
+
self._total_records: int = 0
|
|
62
|
+
|
|
63
|
+
# Functions prefixed with async_ are NOT threadsafe and must
|
|
64
|
+
# be run in the event loop.
|
|
65
|
+
|
|
66
|
+
def async_add_records(self, entries: Iterable[DNSRecord]) -> bool:
|
|
67
|
+
"""Add multiple records.
|
|
68
|
+
|
|
69
|
+
Returns true if any of the records were not in the cache.
|
|
70
|
+
|
|
71
|
+
This function must be run in from event loop.
|
|
72
|
+
"""
|
|
73
|
+
new = False
|
|
74
|
+
for entry in entries:
|
|
75
|
+
if self._async_add(entry):
|
|
76
|
+
new = True
|
|
77
|
+
return new
|
|
78
|
+
|
|
79
|
+
def async_all_by_details(self, name: _str, type_: _int, class_: _int) -> list[DNSRecord]:
|
|
80
|
+
"""Return every cached record carrying this name, type and class.
|
|
81
|
+
|
|
82
|
+
This function is not thread-safe and must be called from
|
|
83
|
+
the event loop.
|
|
84
|
+
"""
|
|
85
|
+
key = name.lower()
|
|
86
|
+
records = self.cache.get(key)
|
|
87
|
+
matches: list[DNSRecord] = []
|
|
88
|
+
if records is None:
|
|
89
|
+
return matches
|
|
90
|
+
for record in records.values():
|
|
91
|
+
if type_ == record.type and class_ == record.class_:
|
|
92
|
+
matches.append(record)
|
|
93
|
+
return matches
|
|
94
|
+
|
|
95
|
+
def async_entries_with_name(self, name: str) -> list[DNSRecord]:
|
|
96
|
+
"""All cached records for a name; event loop only, not threadsafe."""
|
|
97
|
+
return self.entries_with_name(name)
|
|
98
|
+
|
|
99
|
+
def async_entries_with_server(self, name: str) -> list[DNSRecord]:
|
|
100
|
+
"""All cached records for a server name; event loop only, not threadsafe."""
|
|
101
|
+
return self.entries_with_server(name)
|
|
102
|
+
|
|
103
|
+
# The below functions are threadsafe and do not need to be run in the
|
|
104
|
+
# event loop, however they all make copies so they significantly
|
|
105
|
+
# inefficient.
|
|
106
|
+
|
|
107
|
+
def async_expire(self, now: _float) -> list[DNSRecord]:
|
|
108
|
+
"""Purge expired entries from the cache.
|
|
109
|
+
|
|
110
|
+
This function must be run in from event loop.
|
|
111
|
+
|
|
112
|
+
:param now: The current time in milliseconds.
|
|
113
|
+
"""
|
|
114
|
+
if not self._expire_heap:
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
expired: list[DNSRecord] = []
|
|
118
|
+
while self._expire_heap:
|
|
119
|
+
when_record = self._expire_heap[0]
|
|
120
|
+
when = when_record[0]
|
|
121
|
+
if when > now:
|
|
122
|
+
break
|
|
123
|
+
heappop(self._expire_heap)
|
|
124
|
+
# Skip entries left behind by a TTL re-add; the live tuple is
|
|
125
|
+
# later in the heap and will be removed when it reaches the top.
|
|
126
|
+
record = when_record[1]
|
|
127
|
+
if self._expirations.get(record) == when:
|
|
128
|
+
expired.append(record)
|
|
129
|
+
|
|
130
|
+
self._maybe_rebuild_heap()
|
|
131
|
+
self.async_remove_records(expired)
|
|
132
|
+
return expired
|
|
133
|
+
|
|
134
|
+
def async_get_unique(self, entry: _UniqueRecordsType) -> DNSRecord | None:
|
|
135
|
+
"""Look up the cached copy of a unique record, or None.
|
|
136
|
+
|
|
137
|
+
Event loop only; not threadsafe.
|
|
138
|
+
"""
|
|
139
|
+
store = self.cache.get(entry.key)
|
|
140
|
+
if store is None:
|
|
141
|
+
return None
|
|
142
|
+
return store.get(entry)
|
|
143
|
+
|
|
144
|
+
def async_mark_unique_records_older_than_1s_to_expire(
|
|
145
|
+
self,
|
|
146
|
+
unique_types: set[_UniqueType],
|
|
147
|
+
answers: Iterable[DNSRecord],
|
|
148
|
+
now: _float,
|
|
149
|
+
) -> None:
|
|
150
|
+
# rfc6762#section-10.2 para 2
|
|
151
|
+
# Since unique is set, all old records with that name, rrtype,
|
|
152
|
+
# and rrclass that were received more than one second ago are declared
|
|
153
|
+
# invalid, and marked to expire from the cache in one second.
|
|
154
|
+
answers_rrset = set(answers)
|
|
155
|
+
for name, type_, class_ in unique_types:
|
|
156
|
+
for record in self.async_all_by_details(name, type_, class_):
|
|
157
|
+
created_double = record.created
|
|
158
|
+
if (now - created_double > _ONE_SECOND) and record not in answers_rrset:
|
|
159
|
+
# Expire in 1s
|
|
160
|
+
self._async_set_created_ttl(record, now, 1)
|
|
161
|
+
|
|
162
|
+
def async_remove_records(self, entries: Iterable[DNSRecord]) -> None:
|
|
163
|
+
"""Remove multiple records.
|
|
164
|
+
|
|
165
|
+
This function must be run in from event loop.
|
|
166
|
+
"""
|
|
167
|
+
for entry in entries:
|
|
168
|
+
self._async_remove(entry)
|
|
169
|
+
|
|
170
|
+
def current_entry_with_name_and_alias(self, name: str, alias: str) -> DNSRecord | None:
|
|
171
|
+
now = current_time_millis()
|
|
172
|
+
for record in reversed(self.entries_with_name(name)):
|
|
173
|
+
if (
|
|
174
|
+
record.type == _TYPE_PTR
|
|
175
|
+
and not record.is_expired(now)
|
|
176
|
+
and cast(DNSPointer, record).alias == alias
|
|
177
|
+
):
|
|
178
|
+
return record
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def entries_with_name(self, name: str) -> list[DNSRecord]:
|
|
182
|
+
if entries := self.cache.get(name.lower()):
|
|
183
|
+
return list(entries.values())
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
def entries_with_server(self, server: str) -> list[DNSRecord]:
|
|
187
|
+
"""All cached records whose server field matches."""
|
|
188
|
+
if entries := self.service_cache.get(server.lower()):
|
|
189
|
+
return list(entries.values())
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
def get(self, entry: DNSEntry) -> DNSRecord | None:
|
|
193
|
+
if isinstance(entry, _UNIQUE_RECORD_TYPES):
|
|
194
|
+
return self.cache.get(entry.key, {}).get(entry)
|
|
195
|
+
for cached_entry in reversed(list(self.cache.get(entry.key, {}).values())):
|
|
196
|
+
if entry.__eq__(cached_entry):
|
|
197
|
+
return cached_entry
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def get_all_by_details(self, name: str, type_: _int, class_: _int) -> list[DNSRecord]:
|
|
201
|
+
"""Return every cached record carrying this name, type and class."""
|
|
202
|
+
key = name.lower()
|
|
203
|
+
records = self.cache.get(key)
|
|
204
|
+
if records is None:
|
|
205
|
+
return []
|
|
206
|
+
return [entry for entry in list(records.values()) if type_ == entry.type and class_ == entry.class_]
|
|
207
|
+
|
|
208
|
+
def get_by_details(self, name: str, type_: _int, class_: _int) -> DNSRecord | None:
|
|
209
|
+
"""Return the first cached record carrying this name, type and class, or None.
|
|
210
|
+
|
|
211
|
+
Calling this function is not recommended as it will only
|
|
212
|
+
return one record even if there are multiple entries.
|
|
213
|
+
|
|
214
|
+
For example if there are multiple A or AAAA addresses this
|
|
215
|
+
function will return the last one that was added to the cache
|
|
216
|
+
which may not be the one you expect.
|
|
217
|
+
|
|
218
|
+
Use get_all_by_details instead.
|
|
219
|
+
"""
|
|
220
|
+
key = name.lower()
|
|
221
|
+
records = self.cache.get(key)
|
|
222
|
+
if records is None:
|
|
223
|
+
return None
|
|
224
|
+
for cached_entry in reversed(list(records.values())):
|
|
225
|
+
if type_ == cached_entry.type and class_ == cached_entry.class_:
|
|
226
|
+
return cached_entry
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def names(self) -> list[str]:
|
|
230
|
+
"""Return a copy of the list of current cache names."""
|
|
231
|
+
return list(self.cache)
|
|
232
|
+
|
|
233
|
+
def _async_add(self, record: _DNSRecord) -> bool:
|
|
234
|
+
"""Store a record, returning True when it was not cached before.
|
|
235
|
+
|
|
236
|
+
Event loop only; not threadsafe.
|
|
237
|
+
"""
|
|
238
|
+
# Previously storage of records was implemented as a list
|
|
239
|
+
# instead a dict. Since DNSRecords are now hashable, the implementation
|
|
240
|
+
# uses a dict to ensure that adding a new record to the cache
|
|
241
|
+
# replaces any existing records that are __eq__ to each other which
|
|
242
|
+
# removes the risk that accessing the cache from the wrong
|
|
243
|
+
# direction would return the old incorrect entry.
|
|
244
|
+
store = self.cache.get(record.key)
|
|
245
|
+
is_new = store is None or record not in store
|
|
246
|
+
# Bound total cache size; evict closest-to-expiration entry to
|
|
247
|
+
# make room before inserting a new record. Prevents a LAN-local
|
|
248
|
+
# flood of unique-name records from growing the cache without
|
|
249
|
+
# bound (RFC 6762 §10 advisory caching, defense-in-depth).
|
|
250
|
+
if is_new and self._total_records >= _MAX_CACHE_RECORDS:
|
|
251
|
+
self._async_evict_oldest()
|
|
252
|
+
# The victim may have been the last record under
|
|
253
|
+
# ``record.key``, in which case ``_remove_key`` deleted
|
|
254
|
+
# the bucket. Re-fetch before creating below.
|
|
255
|
+
store = self.cache.get(record.key)
|
|
256
|
+
if store is None:
|
|
257
|
+
store = self.cache[record.key] = {}
|
|
258
|
+
new = is_new and not isinstance(record, DNSNsec)
|
|
259
|
+
if is_new:
|
|
260
|
+
self._total_records += 1
|
|
261
|
+
store[record] = record
|
|
262
|
+
when = record.created + (record.ttl * 1000)
|
|
263
|
+
if self._expirations.get(record) != when:
|
|
264
|
+
heappush(self._expire_heap, (when, record))
|
|
265
|
+
self._expirations[record] = when
|
|
266
|
+
# Re-adds of an existing record with a new TTL push a fresh
|
|
267
|
+
# entry but leave the prior tuple behind as stale, so a peer
|
|
268
|
+
# that just replays cached records can grow ``_expire_heap``
|
|
269
|
+
# without ever tripping the cap. Rebuild when stale entries
|
|
270
|
+
# dominate.
|
|
271
|
+
self._maybe_rebuild_heap()
|
|
272
|
+
|
|
273
|
+
if isinstance(record, DNSService):
|
|
274
|
+
service_record = record
|
|
275
|
+
if (service_store := self.service_cache.get(service_record.server_key)) is None:
|
|
276
|
+
service_store = self.service_cache[service_record.server_key] = {}
|
|
277
|
+
service_store[service_record] = service_record
|
|
278
|
+
return new
|
|
279
|
+
|
|
280
|
+
def _async_evict_oldest(self) -> None:
|
|
281
|
+
"""Drop the closest-to-expiration record to make room for a new one."""
|
|
282
|
+
while self._expire_heap:
|
|
283
|
+
when_record = heappop(self._expire_heap)
|
|
284
|
+
record = when_record[1]
|
|
285
|
+
if self._expirations.get(record) != when_record[0]:
|
|
286
|
+
continue
|
|
287
|
+
self._async_remove(record)
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
def _async_remove(self, record: _DNSRecord) -> None:
|
|
291
|
+
"""Drop a record from the cache. Event loop only; not threadsafe."""
|
|
292
|
+
if isinstance(record, DNSService):
|
|
293
|
+
service_record = record
|
|
294
|
+
_remove_key(self.service_cache, service_record.server_key, service_record)
|
|
295
|
+
_remove_key(self.cache, record.key, record)
|
|
296
|
+
self._expirations.pop(record, None)
|
|
297
|
+
self._total_records -= 1
|
|
298
|
+
|
|
299
|
+
def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _int) -> None:
|
|
300
|
+
"""Stamp a record with a new ttl and creation moment, then cache it."""
|
|
301
|
+
# It would be better if we made a copy instead of mutating the record
|
|
302
|
+
# in place, but records currently don't have a copy method.
|
|
303
|
+
record._set_created_ttl(now, ttl)
|
|
304
|
+
self._async_add(record)
|
|
305
|
+
|
|
306
|
+
def _maybe_rebuild_heap(self) -> None:
|
|
307
|
+
"""Rebuild ``_expire_heap`` when stale entries dominate live ones."""
|
|
308
|
+
expire_heap_len = len(self._expire_heap)
|
|
309
|
+
if (
|
|
310
|
+
expire_heap_len > _MIN_SCHEDULED_RECORD_EXPIRATION
|
|
311
|
+
and expire_heap_len > len(self._expirations) * 2
|
|
312
|
+
):
|
|
313
|
+
self._expire_heap = [
|
|
314
|
+
entry for entry in self._expire_heap if self._expirations.get(entry[1]) == entry[0]
|
|
315
|
+
]
|
|
316
|
+
heapify(self._expire_heap)
|