aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/dns
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/dns')
-rw-r--r--.venv/lib/python3.12/site-packages/dns/__init__.py70
-rw-r--r--.venv/lib/python3.12/site-packages/dns/_asyncbackend.py100
-rw-r--r--.venv/lib/python3.12/site-packages/dns/_asyncio_backend.py275
-rw-r--r--.venv/lib/python3.12/site-packages/dns/_ddr.py154
-rw-r--r--.venv/lib/python3.12/site-packages/dns/_features.py95
-rw-r--r--.venv/lib/python3.12/site-packages/dns/_immutable_ctx.py76
-rw-r--r--.venv/lib/python3.12/site-packages/dns/_trio_backend.py253
-rw-r--r--.venv/lib/python3.12/site-packages/dns/asyncbackend.py101
-rw-r--r--.venv/lib/python3.12/site-packages/dns/asyncquery.py913
-rw-r--r--.venv/lib/python3.12/site-packages/dns/asyncresolver.py475
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssec.py1247
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssecalgs/__init__.py121
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssecalgs/base.py89
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssecalgs/cryptography.py68
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssecalgs/dsa.py106
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssecalgs/ecdsa.py97
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssecalgs/eddsa.py70
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssecalgs/rsa.py124
-rw-r--r--.venv/lib/python3.12/site-packages/dns/dnssectypes.py71
-rw-r--r--.venv/lib/python3.12/site-packages/dns/e164.py116
-rw-r--r--.venv/lib/python3.12/site-packages/dns/edns.py572
-rw-r--r--.venv/lib/python3.12/site-packages/dns/entropy.py130
-rw-r--r--.venv/lib/python3.12/site-packages/dns/enum.py116
-rw-r--r--.venv/lib/python3.12/site-packages/dns/exception.py169
-rw-r--r--.venv/lib/python3.12/site-packages/dns/flags.py123
-rw-r--r--.venv/lib/python3.12/site-packages/dns/grange.py72
-rw-r--r--.venv/lib/python3.12/site-packages/dns/immutable.py68
-rw-r--r--.venv/lib/python3.12/site-packages/dns/inet.py197
-rw-r--r--.venv/lib/python3.12/site-packages/dns/ipv4.py77
-rw-r--r--.venv/lib/python3.12/site-packages/dns/ipv6.py217
-rw-r--r--.venv/lib/python3.12/site-packages/dns/message.py1933
-rw-r--r--.venv/lib/python3.12/site-packages/dns/name.py1284
-rw-r--r--.venv/lib/python3.12/site-packages/dns/namedict.py109
-rw-r--r--.venv/lib/python3.12/site-packages/dns/nameserver.py363
-rw-r--r--.venv/lib/python3.12/site-packages/dns/node.py359
-rw-r--r--.venv/lib/python3.12/site-packages/dns/opcode.py117
-rw-r--r--.venv/lib/python3.12/site-packages/dns/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/dns/query.py1665
-rw-r--r--.venv/lib/python3.12/site-packages/dns/quic/__init__.py80
-rw-r--r--.venv/lib/python3.12/site-packages/dns/quic/_asyncio.py267
-rw-r--r--.venv/lib/python3.12/site-packages/dns/quic/_common.py339
-rw-r--r--.venv/lib/python3.12/site-packages/dns/quic/_sync.py295
-rw-r--r--.venv/lib/python3.12/site-packages/dns/quic/_trio.py246
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rcode.py168
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdata.py911
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdataclass.py118
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdataset.py512
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdatatype.py336
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AFSDB.py45
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AMTRELAY.py91
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AVC.py26
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CAA.py71
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CDNSKEY.py33
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CDS.py29
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CERT.py116
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CNAME.py28
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CSYNC.py68
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DLV.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DNAME.py27
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DNSKEY.py33
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DS.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/EUI48.py30
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/EUI64.py30
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/GPOS.py126
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/HINFO.py64
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/HIP.py85
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/ISDN.py78
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/L32.py41
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/L64.py47
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/LOC.py353
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/LP.py42
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/MX.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NID.py47
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NINFO.py26
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NS.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC.py67
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC3.py126
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py69
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py53
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/OPT.py77
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/PTR.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RESINFO.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RP.py58
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RRSIG.py157
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RT.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SMIMEA.py9
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SOA.py86
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SPF.py26
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SSHFP.py68
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TKEY.py142
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TLSA.py9
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TSIG.py160
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TXT.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/URI.py79
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/WALLET.py9
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/X25.py57
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/ZONEMD.py66
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/__init__.py70
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/CH/A.py59
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/CH/__init__.py22
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/A.py51
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/AAAA.py51
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/APL.py150
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/DHCID.py54
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/HTTPS.py9
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/IPSECKEY.py91
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/KX.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NAPTR.py110
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NSAP.py60
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NSAP_PTR.py24
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/PX.py73
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/SRV.py75
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/SVCB.py9
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/WKS.py100
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/IN/__init__.py35
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/__init__.py33
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/dnskeybase.py87
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/dsbase.py85
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/euibase.py70
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/mxbase.py87
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/nsbase.py63
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/svcbbase.py585
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/tlsabase.py71
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/txtbase.py106
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rdtypes/util.py257
-rw-r--r--.venv/lib/python3.12/site-packages/dns/renderer.py346
-rw-r--r--.venv/lib/python3.12/site-packages/dns/resolver.py2053
-rw-r--r--.venv/lib/python3.12/site-packages/dns/reversename.py105
-rw-r--r--.venv/lib/python3.12/site-packages/dns/rrset.py285
-rw-r--r--.venv/lib/python3.12/site-packages/dns/serial.py118
-rw-r--r--.venv/lib/python3.12/site-packages/dns/set.py308
-rw-r--r--.venv/lib/python3.12/site-packages/dns/tokenizer.py708
-rw-r--r--.venv/lib/python3.12/site-packages/dns/transaction.py649
-rw-r--r--.venv/lib/python3.12/site-packages/dns/tsig.py352
-rw-r--r--.venv/lib/python3.12/site-packages/dns/tsigkeyring.py68
-rw-r--r--.venv/lib/python3.12/site-packages/dns/ttl.py92
-rw-r--r--.venv/lib/python3.12/site-packages/dns/update.py386
-rw-r--r--.venv/lib/python3.12/site-packages/dns/version.py58
-rw-r--r--.venv/lib/python3.12/site-packages/dns/versioned.py318
-rw-r--r--.venv/lib/python3.12/site-packages/dns/win32util.py242
-rw-r--r--.venv/lib/python3.12/site-packages/dns/wire.py89
-rw-r--r--.venv/lib/python3.12/site-packages/dns/xfr.py343
-rw-r--r--.venv/lib/python3.12/site-packages/dns/zone.py1434
-rw-r--r--.venv/lib/python3.12/site-packages/dns/zonefile.py744
-rw-r--r--.venv/lib/python3.12/site-packages/dns/zonetypes.py37
145 files changed, 29756 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/dns/__init__.py b/.venv/lib/python3.12/site-packages/dns/__init__.py
new file mode 100644
index 00000000..a4249b9e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/__init__.py
@@ -0,0 +1,70 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009, 2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""dnspython DNS toolkit"""
+
+__all__ = [
+ "asyncbackend",
+ "asyncquery",
+ "asyncresolver",
+ "dnssec",
+ "dnssecalgs",
+ "dnssectypes",
+ "e164",
+ "edns",
+ "entropy",
+ "exception",
+ "flags",
+ "immutable",
+ "inet",
+ "ipv4",
+ "ipv6",
+ "message",
+ "name",
+ "namedict",
+ "node",
+ "opcode",
+ "query",
+ "quic",
+ "rcode",
+ "rdata",
+ "rdataclass",
+ "rdataset",
+ "rdatatype",
+ "renderer",
+ "resolver",
+ "reversename",
+ "rrset",
+ "serial",
+ "set",
+ "tokenizer",
+ "transaction",
+ "tsig",
+ "tsigkeyring",
+ "ttl",
+ "rdtypes",
+ "update",
+ "version",
+ "versioned",
+ "wire",
+ "xfr",
+ "zone",
+ "zonetypes",
+ "zonefile",
+]
+
+from dns.version import version as __version__ # noqa
diff --git a/.venv/lib/python3.12/site-packages/dns/_asyncbackend.py b/.venv/lib/python3.12/site-packages/dns/_asyncbackend.py
new file mode 100644
index 00000000..f6760fd0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/_asyncbackend.py
@@ -0,0 +1,100 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# This is a nullcontext for both sync and async. 3.7 has a nullcontext,
+# but it is only for sync use.
+
+
+class NullContext:
+ def __init__(self, enter_result=None):
+ self.enter_result = enter_result
+
+ def __enter__(self):
+ return self.enter_result
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ pass
+
+ async def __aenter__(self):
+ return self.enter_result
+
+ async def __aexit__(self, exc_type, exc_value, traceback):
+ pass
+
+
+# These are declared here so backends can import them without creating
+# circular dependencies with dns.asyncbackend.
+
+
+class Socket: # pragma: no cover
+ def __init__(self, family: int, type: int):
+ self.family = family
+ self.type = type
+
+ async def close(self):
+ pass
+
+ async def getpeername(self):
+ raise NotImplementedError
+
+ async def getsockname(self):
+ raise NotImplementedError
+
+ async def getpeercert(self, timeout):
+ raise NotImplementedError
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_value, traceback):
+ await self.close()
+
+
+class DatagramSocket(Socket): # pragma: no cover
+ async def sendto(self, what, destination, timeout):
+ raise NotImplementedError
+
+ async def recvfrom(self, size, timeout):
+ raise NotImplementedError
+
+
+class StreamSocket(Socket): # pragma: no cover
+ async def sendall(self, what, timeout):
+ raise NotImplementedError
+
+ async def recv(self, size, timeout):
+ raise NotImplementedError
+
+
+class NullTransport:
+ async def connect_tcp(self, host, port, timeout, local_address):
+ raise NotImplementedError
+
+
+class Backend: # pragma: no cover
+ def name(self):
+ return "unknown"
+
+ async def make_socket(
+ self,
+ af,
+ socktype,
+ proto=0,
+ source=None,
+ destination=None,
+ timeout=None,
+ ssl_context=None,
+ server_hostname=None,
+ ):
+ raise NotImplementedError
+
+ def datagram_connection_required(self):
+ return False
+
+ async def sleep(self, interval):
+ raise NotImplementedError
+
+ def get_transport_class(self):
+ raise NotImplementedError
+
+ async def wait_for(self, awaitable, timeout):
+ raise NotImplementedError
diff --git a/.venv/lib/python3.12/site-packages/dns/_asyncio_backend.py b/.venv/lib/python3.12/site-packages/dns/_asyncio_backend.py
new file mode 100644
index 00000000..6ab168de
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/_asyncio_backend.py
@@ -0,0 +1,275 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+"""asyncio library query support"""
+
+import asyncio
+import socket
+import sys
+
+import dns._asyncbackend
+import dns._features
+import dns.exception
+import dns.inet
+
+_is_win32 = sys.platform == "win32"
+
+
+def _get_running_loop():
+ try:
+ return asyncio.get_running_loop()
+ except AttributeError: # pragma: no cover
+ return asyncio.get_event_loop()
+
+
+class _DatagramProtocol:
+ def __init__(self):
+ self.transport = None
+ self.recvfrom = None
+
+ def connection_made(self, transport):
+ self.transport = transport
+
+ def datagram_received(self, data, addr):
+ if self.recvfrom and not self.recvfrom.done():
+ self.recvfrom.set_result((data, addr))
+
+ def error_received(self, exc): # pragma: no cover
+ if self.recvfrom and not self.recvfrom.done():
+ self.recvfrom.set_exception(exc)
+
+ def connection_lost(self, exc):
+ if self.recvfrom and not self.recvfrom.done():
+ if exc is None:
+ # EOF we triggered. Is there a better way to do this?
+ try:
+ raise EOFError("EOF")
+ except EOFError as e:
+ self.recvfrom.set_exception(e)
+ else:
+ self.recvfrom.set_exception(exc)
+
+ def close(self):
+ self.transport.close()
+
+
+async def _maybe_wait_for(awaitable, timeout):
+ if timeout is not None:
+ try:
+ return await asyncio.wait_for(awaitable, timeout)
+ except asyncio.TimeoutError:
+ raise dns.exception.Timeout(timeout=timeout)
+ else:
+ return await awaitable
+
+
+class DatagramSocket(dns._asyncbackend.DatagramSocket):
+ def __init__(self, family, transport, protocol):
+ super().__init__(family, socket.SOCK_DGRAM)
+ self.transport = transport
+ self.protocol = protocol
+
+ async def sendto(self, what, destination, timeout): # pragma: no cover
+ # no timeout for asyncio sendto
+ self.transport.sendto(what, destination)
+ return len(what)
+
+ async def recvfrom(self, size, timeout):
+ # ignore size as there's no way I know to tell protocol about it
+ done = _get_running_loop().create_future()
+ try:
+ assert self.protocol.recvfrom is None
+ self.protocol.recvfrom = done
+ await _maybe_wait_for(done, timeout)
+ return done.result()
+ finally:
+ self.protocol.recvfrom = None
+
+ async def close(self):
+ self.protocol.close()
+
+ async def getpeername(self):
+ return self.transport.get_extra_info("peername")
+
+ async def getsockname(self):
+ return self.transport.get_extra_info("sockname")
+
+ async def getpeercert(self, timeout):
+ raise NotImplementedError
+
+
+class StreamSocket(dns._asyncbackend.StreamSocket):
+ def __init__(self, af, reader, writer):
+ super().__init__(af, socket.SOCK_STREAM)
+ self.reader = reader
+ self.writer = writer
+
+ async def sendall(self, what, timeout):
+ self.writer.write(what)
+ return await _maybe_wait_for(self.writer.drain(), timeout)
+
+ async def recv(self, size, timeout):
+ return await _maybe_wait_for(self.reader.read(size), timeout)
+
+ async def close(self):
+ self.writer.close()
+
+ async def getpeername(self):
+ return self.writer.get_extra_info("peername")
+
+ async def getsockname(self):
+ return self.writer.get_extra_info("sockname")
+
+ async def getpeercert(self, timeout):
+ return self.writer.get_extra_info("peercert")
+
+
+if dns._features.have("doh"):
+ import anyio
+ import httpcore
+ import httpcore._backends.anyio
+ import httpx
+
+ _CoreAsyncNetworkBackend = httpcore.AsyncNetworkBackend
+ _CoreAnyIOStream = httpcore._backends.anyio.AnyIOStream
+
+ from dns.query import _compute_times, _expiration_for_this_attempt, _remaining
+
+ class _NetworkBackend(_CoreAsyncNetworkBackend):
+ def __init__(self, resolver, local_port, bootstrap_address, family):
+ super().__init__()
+ self._local_port = local_port
+ self._resolver = resolver
+ self._bootstrap_address = bootstrap_address
+ self._family = family
+ if local_port != 0:
+ raise NotImplementedError(
+ "the asyncio transport for HTTPX cannot set the local port"
+ )
+
+ async def connect_tcp(
+ self, host, port, timeout, local_address, socket_options=None
+ ): # pylint: disable=signature-differs
+ addresses = []
+ _, expiration = _compute_times(timeout)
+ if dns.inet.is_address(host):
+ addresses.append(host)
+ elif self._bootstrap_address is not None:
+ addresses.append(self._bootstrap_address)
+ else:
+ timeout = _remaining(expiration)
+ family = self._family
+ if local_address:
+ family = dns.inet.af_for_address(local_address)
+ answers = await self._resolver.resolve_name(
+ host, family=family, lifetime=timeout
+ )
+ addresses = answers.addresses()
+ for address in addresses:
+ try:
+ attempt_expiration = _expiration_for_this_attempt(2.0, expiration)
+ timeout = _remaining(attempt_expiration)
+ with anyio.fail_after(timeout):
+ stream = await anyio.connect_tcp(
+ remote_host=address,
+ remote_port=port,
+ local_host=local_address,
+ )
+ return _CoreAnyIOStream(stream)
+ except Exception:
+ pass
+ raise httpcore.ConnectError
+
+ async def connect_unix_socket(
+ self, path, timeout, socket_options=None
+ ): # pylint: disable=signature-differs
+ raise NotImplementedError
+
+ async def sleep(self, seconds): # pylint: disable=signature-differs
+ await anyio.sleep(seconds)
+
+ class _HTTPTransport(httpx.AsyncHTTPTransport):
+ def __init__(
+ self,
+ *args,
+ local_port=0,
+ bootstrap_address=None,
+ resolver=None,
+ family=socket.AF_UNSPEC,
+ **kwargs,
+ ):
+ if resolver is None and bootstrap_address is None:
+ # pylint: disable=import-outside-toplevel,redefined-outer-name
+ import dns.asyncresolver
+
+ resolver = dns.asyncresolver.Resolver()
+ super().__init__(*args, **kwargs)
+ self._pool._network_backend = _NetworkBackend(
+ resolver, local_port, bootstrap_address, family
+ )
+
+else:
+ _HTTPTransport = dns._asyncbackend.NullTransport # type: ignore
+
+
+class Backend(dns._asyncbackend.Backend):
+ def name(self):
+ return "asyncio"
+
+ async def make_socket(
+ self,
+ af,
+ socktype,
+ proto=0,
+ source=None,
+ destination=None,
+ timeout=None,
+ ssl_context=None,
+ server_hostname=None,
+ ):
+ loop = _get_running_loop()
+ if socktype == socket.SOCK_DGRAM:
+ if _is_win32 and source is None:
+ # Win32 wants explicit binding before recvfrom(). This is the
+ # proper fix for [#637].
+ source = (dns.inet.any_for_af(af), 0)
+ transport, protocol = await loop.create_datagram_endpoint(
+ _DatagramProtocol,
+ source,
+ family=af,
+ proto=proto,
+ remote_addr=destination,
+ )
+ return DatagramSocket(af, transport, protocol)
+ elif socktype == socket.SOCK_STREAM:
+ if destination is None:
+ # This shouldn't happen, but we check to make code analysis software
+ # happier.
+ raise ValueError("destination required for stream sockets")
+ (r, w) = await _maybe_wait_for(
+ asyncio.open_connection(
+ destination[0],
+ destination[1],
+ ssl=ssl_context,
+ family=af,
+ proto=proto,
+ local_addr=source,
+ server_hostname=server_hostname,
+ ),
+ timeout,
+ )
+ return StreamSocket(af, r, w)
+ raise NotImplementedError(
+ "unsupported socket " + f"type {socktype}"
+ ) # pragma: no cover
+
+ async def sleep(self, interval):
+ await asyncio.sleep(interval)
+
+ def datagram_connection_required(self):
+ return False
+
+ def get_transport_class(self):
+ return _HTTPTransport
+
+ async def wait_for(self, awaitable, timeout):
+ return await _maybe_wait_for(awaitable, timeout)
diff --git a/.venv/lib/python3.12/site-packages/dns/_ddr.py b/.venv/lib/python3.12/site-packages/dns/_ddr.py
new file mode 100644
index 00000000..bf5c11eb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/_ddr.py
@@ -0,0 +1,154 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+#
+# Support for Discovery of Designated Resolvers
+
+import socket
+import time
+from urllib.parse import urlparse
+
+import dns.asyncbackend
+import dns.inet
+import dns.name
+import dns.nameserver
+import dns.query
+import dns.rdtypes.svcbbase
+
+# The special name of the local resolver when using DDR
+_local_resolver_name = dns.name.from_text("_dns.resolver.arpa")
+
+
+#
+# Processing is split up into I/O independent and I/O dependent parts to
+# make supporting sync and async versions easy.
+#
+
+
+class _SVCBInfo:
+ def __init__(self, bootstrap_address, port, hostname, nameservers):
+ self.bootstrap_address = bootstrap_address
+ self.port = port
+ self.hostname = hostname
+ self.nameservers = nameservers
+
+ def ddr_check_certificate(self, cert):
+ """Verify that the _SVCBInfo's address is in the cert's subjectAltName (SAN)"""
+ for name, value in cert["subjectAltName"]:
+ if name == "IP Address" and value == self.bootstrap_address:
+ return True
+ return False
+
+ def make_tls_context(self):
+ ssl = dns.query.ssl
+ ctx = ssl.create_default_context()
+ ctx.minimum_version = ssl.TLSVersion.TLSv1_2
+ return ctx
+
+ def ddr_tls_check_sync(self, lifetime):
+ ctx = self.make_tls_context()
+ expiration = time.time() + lifetime
+ with socket.create_connection(
+ (self.bootstrap_address, self.port), lifetime
+ ) as s:
+ with ctx.wrap_socket(s, server_hostname=self.hostname) as ts:
+ ts.settimeout(dns.query._remaining(expiration))
+ ts.do_handshake()
+ cert = ts.getpeercert()
+ return self.ddr_check_certificate(cert)
+
+ async def ddr_tls_check_async(self, lifetime, backend=None):
+ if backend is None:
+ backend = dns.asyncbackend.get_default_backend()
+ ctx = self.make_tls_context()
+ expiration = time.time() + lifetime
+ async with await backend.make_socket(
+ dns.inet.af_for_address(self.bootstrap_address),
+ socket.SOCK_STREAM,
+ 0,
+ None,
+ (self.bootstrap_address, self.port),
+ lifetime,
+ ctx,
+ self.hostname,
+ ) as ts:
+ cert = await ts.getpeercert(dns.query._remaining(expiration))
+ return self.ddr_check_certificate(cert)
+
+
+def _extract_nameservers_from_svcb(answer):
+ bootstrap_address = answer.nameserver
+ if not dns.inet.is_address(bootstrap_address):
+ return []
+ infos = []
+ for rr in answer.rrset.processing_order():
+ nameservers = []
+ param = rr.params.get(dns.rdtypes.svcbbase.ParamKey.ALPN)
+ if param is None:
+ continue
+ alpns = set(param.ids)
+ host = rr.target.to_text(omit_final_dot=True)
+ port = None
+ param = rr.params.get(dns.rdtypes.svcbbase.ParamKey.PORT)
+ if param is not None:
+ port = param.port
+ # For now we ignore address hints and address resolution and always use the
+ # bootstrap address
+ if b"h2" in alpns:
+ param = rr.params.get(dns.rdtypes.svcbbase.ParamKey.DOHPATH)
+ if param is None or not param.value.endswith(b"{?dns}"):
+ continue
+ path = param.value[:-6].decode()
+ if not path.startswith("/"):
+ path = "/" + path
+ if port is None:
+ port = 443
+ url = f"https://{host}:{port}{path}"
+ # check the URL
+ try:
+ urlparse(url)
+ nameservers.append(dns.nameserver.DoHNameserver(url, bootstrap_address))
+ except Exception:
+ # continue processing other ALPN types
+ pass
+ if b"dot" in alpns:
+ if port is None:
+ port = 853
+ nameservers.append(
+ dns.nameserver.DoTNameserver(bootstrap_address, port, host)
+ )
+ if b"doq" in alpns:
+ if port is None:
+ port = 853
+ nameservers.append(
+ dns.nameserver.DoQNameserver(bootstrap_address, port, True, host)
+ )
+ if len(nameservers) > 0:
+ infos.append(_SVCBInfo(bootstrap_address, port, host, nameservers))
+ return infos
+
+
+def _get_nameservers_sync(answer, lifetime):
+ """Return a list of TLS-validated resolver nameservers extracted from an SVCB
+ answer."""
+ nameservers = []
+ infos = _extract_nameservers_from_svcb(answer)
+ for info in infos:
+ try:
+ if info.ddr_tls_check_sync(lifetime):
+ nameservers.extend(info.nameservers)
+ except Exception:
+ pass
+ return nameservers
+
+
+async def _get_nameservers_async(answer, lifetime):
+ """Return a list of TLS-validated resolver nameservers extracted from an SVCB
+ answer."""
+ nameservers = []
+ infos = _extract_nameservers_from_svcb(answer)
+ for info in infos:
+ try:
+ if await info.ddr_tls_check_async(lifetime):
+ nameservers.extend(info.nameservers)
+ except Exception:
+ pass
+ return nameservers
diff --git a/.venv/lib/python3.12/site-packages/dns/_features.py b/.venv/lib/python3.12/site-packages/dns/_features.py
new file mode 100644
index 00000000..fa6d4955
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/_features.py
@@ -0,0 +1,95 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import importlib.metadata
+import itertools
+import string
+from typing import Dict, List, Tuple
+
+
+def _tuple_from_text(version: str) -> Tuple:
+ text_parts = version.split(".")
+ int_parts = []
+ for text_part in text_parts:
+ digit_prefix = "".join(
+ itertools.takewhile(lambda x: x in string.digits, text_part)
+ )
+ try:
+ int_parts.append(int(digit_prefix))
+ except Exception:
+ break
+ return tuple(int_parts)
+
+
+def _version_check(
+ requirement: str,
+) -> bool:
+ """Is the requirement fulfilled?
+
+ The requirement must be of the form
+
+ package>=version
+ """
+ package, minimum = requirement.split(">=")
+ try:
+ version = importlib.metadata.version(package)
+ # This shouldn't happen, but it apparently can.
+ if version is None:
+ return False
+ except Exception:
+ return False
+ t_version = _tuple_from_text(version)
+ t_minimum = _tuple_from_text(minimum)
+ if t_version < t_minimum:
+ return False
+ return True
+
+
+_cache: Dict[str, bool] = {}
+
+
+def have(feature: str) -> bool:
+ """Is *feature* available?
+
+ This tests if all optional packages needed for the
+ feature are available and recent enough.
+
+ Returns ``True`` if the feature is available,
+ and ``False`` if it is not or if metadata is
+ missing.
+ """
+ value = _cache.get(feature)
+ if value is not None:
+ return value
+ requirements = _requirements.get(feature)
+ if requirements is None:
+ # we make a cache entry here for consistency not performance
+ _cache[feature] = False
+ return False
+ ok = True
+ for requirement in requirements:
+ if not _version_check(requirement):
+ ok = False
+ break
+ _cache[feature] = ok
+ return ok
+
+
+def force(feature: str, enabled: bool) -> None:
+ """Force the status of *feature* to be *enabled*.
+
+ This method is provided as a workaround for any cases
+ where importlib.metadata is ineffective, or for testing.
+ """
+ _cache[feature] = enabled
+
+
+_requirements: Dict[str, List[str]] = {
+ ### BEGIN generated requirements
+ "dnssec": ["cryptography>=43"],
+ "doh": ["httpcore>=1.0.0", "httpx>=0.26.0", "h2>=4.1.0"],
+ "doq": ["aioquic>=1.0.0"],
+ "idna": ["idna>=3.7"],
+ "trio": ["trio>=0.23"],
+ "wmi": ["wmi>=1.5.1"],
+ ### END generated requirements
+}
diff --git a/.venv/lib/python3.12/site-packages/dns/_immutable_ctx.py b/.venv/lib/python3.12/site-packages/dns/_immutable_ctx.py
new file mode 100644
index 00000000..ae7a33bf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/_immutable_ctx.py
@@ -0,0 +1,76 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# This implementation of the immutable decorator requires python >=
+# 3.7, and is significantly more storage efficient when making classes
+# with slots immutable. It's also faster.
+
+import contextvars
+import inspect
+
+_in__init__ = contextvars.ContextVar("_immutable_in__init__", default=False)
+
+
+class _Immutable:
+ """Immutable mixin class"""
+
+ # We set slots to the empty list to say "we don't have any attributes".
+ # We do this so that if we're mixed in with a class with __slots__, we
+ # don't cause a __dict__ to be added which would waste space.
+
+ __slots__ = ()
+
+ def __setattr__(self, name, value):
+ if _in__init__.get() is not self:
+ raise TypeError("object doesn't support attribute assignment")
+ else:
+ super().__setattr__(name, value)
+
+ def __delattr__(self, name):
+ if _in__init__.get() is not self:
+ raise TypeError("object doesn't support attribute assignment")
+ else:
+ super().__delattr__(name)
+
+
+def _immutable_init(f):
+ def nf(*args, **kwargs):
+ previous = _in__init__.set(args[0])
+ try:
+ # call the actual __init__
+ f(*args, **kwargs)
+ finally:
+ _in__init__.reset(previous)
+
+ nf.__signature__ = inspect.signature(f)
+ return nf
+
+
+def immutable(cls):
+ if _Immutable in cls.__mro__:
+ # Some ancestor already has the mixin, so just make sure we keep
+ # following the __init__ protocol.
+ cls.__init__ = _immutable_init(cls.__init__)
+ if hasattr(cls, "__setstate__"):
+ cls.__setstate__ = _immutable_init(cls.__setstate__)
+ ncls = cls
+ else:
+ # Mixin the Immutable class and follow the __init__ protocol.
+ class ncls(_Immutable, cls):
+ # We have to do the __slots__ declaration here too!
+ __slots__ = ()
+
+ @_immutable_init
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ if hasattr(cls, "__setstate__"):
+
+ @_immutable_init
+ def __setstate__(self, *args, **kwargs):
+ super().__setstate__(*args, **kwargs)
+
+ # make ncls have the same name and module as cls
+ ncls.__name__ = cls.__name__
+ ncls.__qualname__ = cls.__qualname__
+ ncls.__module__ = cls.__module__
+ return ncls
diff --git a/.venv/lib/python3.12/site-packages/dns/_trio_backend.py b/.venv/lib/python3.12/site-packages/dns/_trio_backend.py
new file mode 100644
index 00000000..0ed904dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/_trio_backend.py
@@ -0,0 +1,253 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+"""trio async I/O library query support"""
+
+import socket
+
+import trio
+import trio.socket # type: ignore
+
+import dns._asyncbackend
+import dns._features
+import dns.exception
+import dns.inet
+
+if not dns._features.have("trio"):
+ raise ImportError("trio not found or too old")
+
+
+def _maybe_timeout(timeout):
+ if timeout is not None:
+ return trio.move_on_after(timeout)
+ else:
+ return dns._asyncbackend.NullContext()
+
+
+# for brevity
+_lltuple = dns.inet.low_level_address_tuple
+
+# pylint: disable=redefined-outer-name
+
+
+class DatagramSocket(dns._asyncbackend.DatagramSocket):
+ def __init__(self, sock):
+ super().__init__(sock.family, socket.SOCK_DGRAM)
+ self.socket = sock
+
+ async def sendto(self, what, destination, timeout):
+ with _maybe_timeout(timeout):
+ if destination is None:
+ return await self.socket.send(what)
+ else:
+ return await self.socket.sendto(what, destination)
+ raise dns.exception.Timeout(
+ timeout=timeout
+ ) # pragma: no cover lgtm[py/unreachable-statement]
+
+ async def recvfrom(self, size, timeout):
+ with _maybe_timeout(timeout):
+ return await self.socket.recvfrom(size)
+ raise dns.exception.Timeout(timeout=timeout) # lgtm[py/unreachable-statement]
+
+ async def close(self):
+ self.socket.close()
+
+ async def getpeername(self):
+ return self.socket.getpeername()
+
+ async def getsockname(self):
+ return self.socket.getsockname()
+
+ async def getpeercert(self, timeout):
+ raise NotImplementedError
+
+
+class StreamSocket(dns._asyncbackend.StreamSocket):
+ def __init__(self, family, stream, tls=False):
+ super().__init__(family, socket.SOCK_STREAM)
+ self.stream = stream
+ self.tls = tls
+
+ async def sendall(self, what, timeout):
+ with _maybe_timeout(timeout):
+ return await self.stream.send_all(what)
+ raise dns.exception.Timeout(timeout=timeout) # lgtm[py/unreachable-statement]
+
+ async def recv(self, size, timeout):
+ with _maybe_timeout(timeout):
+ return await self.stream.receive_some(size)
+ raise dns.exception.Timeout(timeout=timeout) # lgtm[py/unreachable-statement]
+
+ async def close(self):
+ await self.stream.aclose()
+
+ async def getpeername(self):
+ if self.tls:
+ return self.stream.transport_stream.socket.getpeername()
+ else:
+ return self.stream.socket.getpeername()
+
+ async def getsockname(self):
+ if self.tls:
+ return self.stream.transport_stream.socket.getsockname()
+ else:
+ return self.stream.socket.getsockname()
+
+ async def getpeercert(self, timeout):
+ if self.tls:
+ with _maybe_timeout(timeout):
+ await self.stream.do_handshake()
+ return self.stream.getpeercert()
+ else:
+ raise NotImplementedError
+
+
+if dns._features.have("doh"):
+ import httpcore
+ import httpcore._backends.trio
+ import httpx
+
+ _CoreAsyncNetworkBackend = httpcore.AsyncNetworkBackend
+ _CoreTrioStream = httpcore._backends.trio.TrioStream
+
+ from dns.query import _compute_times, _expiration_for_this_attempt, _remaining
+
+ class _NetworkBackend(_CoreAsyncNetworkBackend):
+ def __init__(self, resolver, local_port, bootstrap_address, family):
+ super().__init__()
+ self._local_port = local_port
+ self._resolver = resolver
+ self._bootstrap_address = bootstrap_address
+ self._family = family
+
+ async def connect_tcp(
+ self, host, port, timeout, local_address, socket_options=None
+ ): # pylint: disable=signature-differs
+ addresses = []
+ _, expiration = _compute_times(timeout)
+ if dns.inet.is_address(host):
+ addresses.append(host)
+ elif self._bootstrap_address is not None:
+ addresses.append(self._bootstrap_address)
+ else:
+ timeout = _remaining(expiration)
+ family = self._family
+ if local_address:
+ family = dns.inet.af_for_address(local_address)
+ answers = await self._resolver.resolve_name(
+ host, family=family, lifetime=timeout
+ )
+ addresses = answers.addresses()
+ for address in addresses:
+ try:
+ af = dns.inet.af_for_address(address)
+ if local_address is not None or self._local_port != 0:
+ source = (local_address, self._local_port)
+ else:
+ source = None
+ destination = (address, port)
+ attempt_expiration = _expiration_for_this_attempt(2.0, expiration)
+ timeout = _remaining(attempt_expiration)
+ sock = await Backend().make_socket(
+ af, socket.SOCK_STREAM, 0, source, destination, timeout
+ )
+ return _CoreTrioStream(sock.stream)
+ except Exception:
+ continue
+ raise httpcore.ConnectError
+
+ async def connect_unix_socket(
+ self, path, timeout, socket_options=None
+ ): # pylint: disable=signature-differs
+ raise NotImplementedError
+
+ async def sleep(self, seconds): # pylint: disable=signature-differs
+ await trio.sleep(seconds)
+
+ class _HTTPTransport(httpx.AsyncHTTPTransport):
+ def __init__(
+ self,
+ *args,
+ local_port=0,
+ bootstrap_address=None,
+ resolver=None,
+ family=socket.AF_UNSPEC,
+ **kwargs,
+ ):
+ if resolver is None and bootstrap_address is None:
+ # pylint: disable=import-outside-toplevel,redefined-outer-name
+ import dns.asyncresolver
+
+ resolver = dns.asyncresolver.Resolver()
+ super().__init__(*args, **kwargs)
+ self._pool._network_backend = _NetworkBackend(
+ resolver, local_port, bootstrap_address, family
+ )
+
+else:
+ _HTTPTransport = dns._asyncbackend.NullTransport # type: ignore
+
+
+class Backend(dns._asyncbackend.Backend):
+ def name(self):
+ return "trio"
+
+ async def make_socket(
+ self,
+ af,
+ socktype,
+ proto=0,
+ source=None,
+ destination=None,
+ timeout=None,
+ ssl_context=None,
+ server_hostname=None,
+ ):
+ s = trio.socket.socket(af, socktype, proto)
+ stream = None
+ try:
+ if source:
+ await s.bind(_lltuple(source, af))
+ if socktype == socket.SOCK_STREAM or destination is not None:
+ connected = False
+ with _maybe_timeout(timeout):
+ await s.connect(_lltuple(destination, af))
+ connected = True
+ if not connected:
+ raise dns.exception.Timeout(
+ timeout=timeout
+ ) # lgtm[py/unreachable-statement]
+ except Exception: # pragma: no cover
+ s.close()
+ raise
+ if socktype == socket.SOCK_DGRAM:
+ return DatagramSocket(s)
+ elif socktype == socket.SOCK_STREAM:
+ stream = trio.SocketStream(s)
+ tls = False
+ if ssl_context:
+ tls = True
+ try:
+ stream = trio.SSLStream(
+ stream, ssl_context, server_hostname=server_hostname
+ )
+ except Exception: # pragma: no cover
+ await stream.aclose()
+ raise
+ return StreamSocket(af, stream, tls)
+ raise NotImplementedError(
+ "unsupported socket " + f"type {socktype}"
+ ) # pragma: no cover
+
+ async def sleep(self, interval):
+ await trio.sleep(interval)
+
+ def get_transport_class(self):
+ return _HTTPTransport
+
+ async def wait_for(self, awaitable, timeout):
+ with _maybe_timeout(timeout):
+ return await awaitable
+ raise dns.exception.Timeout(
+ timeout=timeout
+ ) # pragma: no cover lgtm[py/unreachable-statement]
diff --git a/.venv/lib/python3.12/site-packages/dns/asyncbackend.py b/.venv/lib/python3.12/site-packages/dns/asyncbackend.py
new file mode 100644
index 00000000..0ec58b06
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/asyncbackend.py
@@ -0,0 +1,101 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+from typing import Dict
+
+import dns.exception
+
+# pylint: disable=unused-import
+from dns._asyncbackend import ( # noqa: F401 lgtm[py/unused-import]
+ Backend,
+ DatagramSocket,
+ Socket,
+ StreamSocket,
+)
+
+# pylint: enable=unused-import
+
+_default_backend = None
+
+_backends: Dict[str, Backend] = {}
+
+# Allow sniffio import to be disabled for testing purposes
+_no_sniffio = False
+
+
+class AsyncLibraryNotFoundError(dns.exception.DNSException):
+ pass
+
+
+def get_backend(name: str) -> Backend:
+ """Get the specified asynchronous backend.
+
+ *name*, a ``str``, the name of the backend. Currently the "trio"
+ and "asyncio" backends are available.
+
+ Raises NotImplementedError if an unknown backend name is specified.
+ """
+ # pylint: disable=import-outside-toplevel,redefined-outer-name
+ backend = _backends.get(name)
+ if backend:
+ return backend
+ if name == "trio":
+ import dns._trio_backend
+
+ backend = dns._trio_backend.Backend()
+ elif name == "asyncio":
+ import dns._asyncio_backend
+
+ backend = dns._asyncio_backend.Backend()
+ else:
+ raise NotImplementedError(f"unimplemented async backend {name}")
+ _backends[name] = backend
+ return backend
+
+
+def sniff() -> str:
+ """Attempt to determine the in-use asynchronous I/O library by using
+ the ``sniffio`` module if it is available.
+
+ Returns the name of the library, or raises AsyncLibraryNotFoundError
+ if the library cannot be determined.
+ """
+ # pylint: disable=import-outside-toplevel
+ try:
+ if _no_sniffio:
+ raise ImportError
+ import sniffio
+
+ try:
+ return sniffio.current_async_library()
+ except sniffio.AsyncLibraryNotFoundError:
+ raise AsyncLibraryNotFoundError("sniffio cannot determine async library")
+ except ImportError:
+ import asyncio
+
+ try:
+ asyncio.get_running_loop()
+ return "asyncio"
+ except RuntimeError:
+ raise AsyncLibraryNotFoundError("no async library detected")
+
+
+def get_default_backend() -> Backend:
+ """Get the default backend, initializing it if necessary."""
+ if _default_backend:
+ return _default_backend
+
+ return set_default_backend(sniff())
+
+
+def set_default_backend(name: str) -> Backend:
+ """Set the default backend.
+
+ It's not normally necessary to call this method, as
+ ``get_default_backend()`` will initialize the backend
+ appropriately in many cases. If ``sniffio`` is not installed, or
+ in testing situations, this function allows the backend to be set
+ explicitly.
+ """
+ global _default_backend
+ _default_backend = get_backend(name)
+ return _default_backend
diff --git a/.venv/lib/python3.12/site-packages/dns/asyncquery.py b/.venv/lib/python3.12/site-packages/dns/asyncquery.py
new file mode 100644
index 00000000..efad0fd7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/asyncquery.py
@@ -0,0 +1,913 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Talk to a DNS server."""
+
+import base64
+import contextlib
+import random
+import socket
+import struct
+import time
+import urllib.parse
+from typing import Any, Dict, Optional, Tuple, Union, cast
+
+import dns.asyncbackend
+import dns.exception
+import dns.inet
+import dns.message
+import dns.name
+import dns.quic
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+import dns.transaction
+from dns._asyncbackend import NullContext
+from dns.query import (
+ BadResponse,
+ HTTPVersion,
+ NoDOH,
+ NoDOQ,
+ UDPMode,
+ _check_status,
+ _compute_times,
+ _make_dot_ssl_context,
+ _matches_destination,
+ _remaining,
+ have_doh,
+ ssl,
+)
+
+if have_doh:
+ import httpx
+
+# for brevity
+_lltuple = dns.inet.low_level_address_tuple
+
+
+def _source_tuple(af, address, port):
+ # Make a high level source tuple, or return None if address and port
+ # are both None
+ if address or port:
+ if address is None:
+ if af == socket.AF_INET:
+ address = "0.0.0.0"
+ elif af == socket.AF_INET6:
+ address = "::"
+ else:
+ raise NotImplementedError(f"unknown address family {af}")
+ return (address, port)
+ else:
+ return None
+
+
+def _timeout(expiration, now=None):
+ if expiration is not None:
+ if not now:
+ now = time.time()
+ return max(expiration - now, 0)
+ else:
+ return None
+
+
+async def send_udp(
+ sock: dns.asyncbackend.DatagramSocket,
+ what: Union[dns.message.Message, bytes],
+ destination: Any,
+ expiration: Optional[float] = None,
+) -> Tuple[int, float]:
+ """Send a DNS message to the specified UDP socket.
+
+ *sock*, a ``dns.asyncbackend.DatagramSocket``.
+
+ *what*, a ``bytes`` or ``dns.message.Message``, the message to send.
+
+ *destination*, a destination tuple appropriate for the address family
+ of the socket, specifying where to send the query.
+
+ *expiration*, a ``float`` or ``None``, the absolute time at which
+ a timeout exception should be raised. If ``None``, no timeout will
+ occur. The expiration value is meaningless for the asyncio backend, as
+ asyncio's transport sendto() never blocks.
+
+ Returns an ``(int, float)`` tuple of bytes sent and the sent time.
+ """
+
+ if isinstance(what, dns.message.Message):
+ what = what.to_wire()
+ sent_time = time.time()
+ n = await sock.sendto(what, destination, _timeout(expiration, sent_time))
+ return (n, sent_time)
+
+
+async def receive_udp(
+ sock: dns.asyncbackend.DatagramSocket,
+ destination: Optional[Any] = None,
+ expiration: Optional[float] = None,
+ ignore_unexpected: bool = False,
+ one_rr_per_rrset: bool = False,
+ keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None,
+ request_mac: Optional[bytes] = b"",
+ ignore_trailing: bool = False,
+ raise_on_truncation: bool = False,
+ ignore_errors: bool = False,
+ query: Optional[dns.message.Message] = None,
+) -> Any:
+ """Read a DNS message from a UDP socket.
+
+ *sock*, a ``dns.asyncbackend.DatagramSocket``.
+
+ See :py:func:`dns.query.receive_udp()` for the documentation of the other
+ parameters, and exceptions.
+
+ Returns a ``(dns.message.Message, float, tuple)`` tuple of the received message, the
+ received time, and the address where the message arrived from.
+ """
+
+ wire = b""
+ while True:
+ (wire, from_address) = await sock.recvfrom(65535, _timeout(expiration))
+ if not _matches_destination(
+ sock.family, from_address, destination, ignore_unexpected
+ ):
+ continue
+ received_time = time.time()
+ try:
+ r = dns.message.from_wire(
+ wire,
+ keyring=keyring,
+ request_mac=request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ raise_on_truncation=raise_on_truncation,
+ )
+ except dns.message.Truncated as e:
+ # See the comment in query.py for details.
+ if (
+ ignore_errors
+ and query is not None
+ and not query.is_response(e.message())
+ ):
+ continue
+ else:
+ raise
+ except Exception:
+ if ignore_errors:
+ continue
+ else:
+ raise
+ if ignore_errors and query is not None and not query.is_response(r):
+ continue
+ return (r, received_time, from_address)
+
+
+async def udp(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 53,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ ignore_unexpected: bool = False,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ raise_on_truncation: bool = False,
+ sock: Optional[dns.asyncbackend.DatagramSocket] = None,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+ ignore_errors: bool = False,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via UDP.
+
+ *sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``,
+ the socket to use for the query. If ``None``, the default, a
+ socket is created. Note that if a socket is provided, the
+ *source*, *source_port*, and *backend* are ignored.
+
+ *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
+ the default, then dnspython will use the default backend.
+
+ See :py:func:`dns.query.udp()` for the documentation of the other
+ parameters, exceptions, and return type of this method.
+ """
+ wire = q.to_wire()
+ (begin_time, expiration) = _compute_times(timeout)
+ af = dns.inet.af_for_address(where)
+ destination = _lltuple((where, port), af)
+ if sock:
+ cm: contextlib.AbstractAsyncContextManager = NullContext(sock)
+ else:
+ if not backend:
+ backend = dns.asyncbackend.get_default_backend()
+ stuple = _source_tuple(af, source, source_port)
+ if backend.datagram_connection_required():
+ dtuple = (where, port)
+ else:
+ dtuple = None
+ cm = await backend.make_socket(af, socket.SOCK_DGRAM, 0, stuple, dtuple)
+ async with cm as s:
+ await send_udp(s, wire, destination, expiration)
+ (r, received_time, _) = await receive_udp(
+ s,
+ destination,
+ expiration,
+ ignore_unexpected,
+ one_rr_per_rrset,
+ q.keyring,
+ q.mac,
+ ignore_trailing,
+ raise_on_truncation,
+ ignore_errors,
+ q,
+ )
+ r.time = received_time - begin_time
+ # We don't need to check q.is_response() if we are in ignore_errors mode
+ # as receive_udp() will have checked it.
+ if not (ignore_errors or q.is_response(r)):
+ raise BadResponse
+ return r
+
+
+async def udp_with_fallback(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 53,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ ignore_unexpected: bool = False,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ udp_sock: Optional[dns.asyncbackend.DatagramSocket] = None,
+ tcp_sock: Optional[dns.asyncbackend.StreamSocket] = None,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+ ignore_errors: bool = False,
+) -> Tuple[dns.message.Message, bool]:
+ """Return the response to the query, trying UDP first and falling back
+ to TCP if UDP results in a truncated response.
+
+ *udp_sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``,
+ the socket to use for the UDP query. If ``None``, the default, a
+ socket is created. Note that if a socket is provided the *source*,
+ *source_port*, and *backend* are ignored for the UDP query.
+
+ *tcp_sock*, a ``dns.asyncbackend.StreamSocket``, or ``None``, the
+ socket to use for the TCP query. If ``None``, the default, a
+ socket is created. Note that if a socket is provided *where*,
+ *source*, *source_port*, and *backend* are ignored for the TCP query.
+
+ *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
+ the default, then dnspython will use the default backend.
+
+ See :py:func:`dns.query.udp_with_fallback()` for the documentation
+ of the other parameters, exceptions, and return type of this
+ method.
+ """
+ try:
+ response = await udp(
+ q,
+ where,
+ timeout,
+ port,
+ source,
+ source_port,
+ ignore_unexpected,
+ one_rr_per_rrset,
+ ignore_trailing,
+ True,
+ udp_sock,
+ backend,
+ ignore_errors,
+ )
+ return (response, False)
+ except dns.message.Truncated:
+ response = await tcp(
+ q,
+ where,
+ timeout,
+ port,
+ source,
+ source_port,
+ one_rr_per_rrset,
+ ignore_trailing,
+ tcp_sock,
+ backend,
+ )
+ return (response, True)
+
+
+async def send_tcp(
+ sock: dns.asyncbackend.StreamSocket,
+ what: Union[dns.message.Message, bytes],
+ expiration: Optional[float] = None,
+) -> Tuple[int, float]:
+ """Send a DNS message to the specified TCP socket.
+
+ *sock*, a ``dns.asyncbackend.StreamSocket``.
+
+ See :py:func:`dns.query.send_tcp()` for the documentation of the other
+ parameters, exceptions, and return type of this method.
+ """
+
+ if isinstance(what, dns.message.Message):
+ tcpmsg = what.to_wire(prepend_length=True)
+ else:
+ # copying the wire into tcpmsg is inefficient, but lets us
+ # avoid writev() or doing a short write that would get pushed
+ # onto the net
+ tcpmsg = len(what).to_bytes(2, "big") + what
+ sent_time = time.time()
+ await sock.sendall(tcpmsg, _timeout(expiration, sent_time))
+ return (len(tcpmsg), sent_time)
+
+
+async def _read_exactly(sock, count, expiration):
+ """Read the specified number of bytes from stream. Keep trying until we
+ either get the desired amount, or we hit EOF.
+ """
+ s = b""
+ while count > 0:
+ n = await sock.recv(count, _timeout(expiration))
+ if n == b"":
+ raise EOFError("EOF")
+ count = count - len(n)
+ s = s + n
+ return s
+
+
+async def receive_tcp(
+ sock: dns.asyncbackend.StreamSocket,
+ expiration: Optional[float] = None,
+ one_rr_per_rrset: bool = False,
+ keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None,
+ request_mac: Optional[bytes] = b"",
+ ignore_trailing: bool = False,
+) -> Tuple[dns.message.Message, float]:
+ """Read a DNS message from a TCP socket.
+
+ *sock*, a ``dns.asyncbackend.StreamSocket``.
+
+ See :py:func:`dns.query.receive_tcp()` for the documentation of the other
+ parameters, exceptions, and return type of this method.
+ """
+
+ ldata = await _read_exactly(sock, 2, expiration)
+ (l,) = struct.unpack("!H", ldata)
+ wire = await _read_exactly(sock, l, expiration)
+ received_time = time.time()
+ r = dns.message.from_wire(
+ wire,
+ keyring=keyring,
+ request_mac=request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ return (r, received_time)
+
+
+async def tcp(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 53,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ sock: Optional[dns.asyncbackend.StreamSocket] = None,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via TCP.
+
+ *sock*, a ``dns.asyncbacket.StreamSocket``, or ``None``, the
+ socket to use for the query. If ``None``, the default, a socket
+ is created. Note that if a socket is provided
+ *where*, *port*, *source*, *source_port*, and *backend* are ignored.
+
+ *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
+ the default, then dnspython will use the default backend.
+
+ See :py:func:`dns.query.tcp()` for the documentation of the other
+ parameters, exceptions, and return type of this method.
+ """
+
+ wire = q.to_wire()
+ (begin_time, expiration) = _compute_times(timeout)
+ if sock:
+ # Verify that the socket is connected, as if it's not connected,
+ # it's not writable, and the polling in send_tcp() will time out or
+ # hang forever.
+ await sock.getpeername()
+ cm: contextlib.AbstractAsyncContextManager = NullContext(sock)
+ else:
+ # These are simple (address, port) pairs, not family-dependent tuples
+ # you pass to low-level socket code.
+ af = dns.inet.af_for_address(where)
+ stuple = _source_tuple(af, source, source_port)
+ dtuple = (where, port)
+ if not backend:
+ backend = dns.asyncbackend.get_default_backend()
+ cm = await backend.make_socket(
+ af, socket.SOCK_STREAM, 0, stuple, dtuple, timeout
+ )
+ async with cm as s:
+ await send_tcp(s, wire, expiration)
+ (r, received_time) = await receive_tcp(
+ s, expiration, one_rr_per_rrset, q.keyring, q.mac, ignore_trailing
+ )
+ r.time = received_time - begin_time
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+
+
+async def tls(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 853,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ sock: Optional[dns.asyncbackend.StreamSocket] = None,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+ ssl_context: Optional[ssl.SSLContext] = None,
+ server_hostname: Optional[str] = None,
+ verify: Union[bool, str] = True,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via TLS.
+
+ *sock*, an ``asyncbackend.StreamSocket``, or ``None``, the socket
+ to use for the query. If ``None``, the default, a socket is
+ created. Note that if a socket is provided, it must be a
+ connected SSL stream socket, and *where*, *port*,
+ *source*, *source_port*, *backend*, *ssl_context*, and *server_hostname*
+ are ignored.
+
+ *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
+ the default, then dnspython will use the default backend.
+
+ See :py:func:`dns.query.tls()` for the documentation of the other
+ parameters, exceptions, and return type of this method.
+ """
+ (begin_time, expiration) = _compute_times(timeout)
+ if sock:
+ cm: contextlib.AbstractAsyncContextManager = NullContext(sock)
+ else:
+ if ssl_context is None:
+ ssl_context = _make_dot_ssl_context(server_hostname, verify)
+ af = dns.inet.af_for_address(where)
+ stuple = _source_tuple(af, source, source_port)
+ dtuple = (where, port)
+ if not backend:
+ backend = dns.asyncbackend.get_default_backend()
+ cm = await backend.make_socket(
+ af,
+ socket.SOCK_STREAM,
+ 0,
+ stuple,
+ dtuple,
+ timeout,
+ ssl_context,
+ server_hostname,
+ )
+ async with cm as s:
+ timeout = _timeout(expiration)
+ response = await tcp(
+ q,
+ where,
+ timeout,
+ port,
+ source,
+ source_port,
+ one_rr_per_rrset,
+ ignore_trailing,
+ s,
+ backend,
+ )
+ end_time = time.time()
+ response.time = end_time - begin_time
+ return response
+
+
+def _maybe_get_resolver(
+ resolver: Optional["dns.asyncresolver.Resolver"],
+) -> "dns.asyncresolver.Resolver":
+ # We need a separate method for this to avoid overriding the global
+ # variable "dns" with the as-yet undefined local variable "dns"
+ # in https().
+ if resolver is None:
+ # pylint: disable=import-outside-toplevel,redefined-outer-name
+ import dns.asyncresolver
+
+ resolver = dns.asyncresolver.Resolver()
+ return resolver
+
+
+async def https(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 443,
+ source: Optional[str] = None,
+ source_port: int = 0, # pylint: disable=W0613
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ client: Optional["httpx.AsyncClient"] = None,
+ path: str = "/dns-query",
+ post: bool = True,
+ verify: Union[bool, str] = True,
+ bootstrap_address: Optional[str] = None,
+ resolver: Optional["dns.asyncresolver.Resolver"] = None,
+ family: int = socket.AF_UNSPEC,
+ http_version: HTTPVersion = HTTPVersion.DEFAULT,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via DNS-over-HTTPS.
+
+ *client*, a ``httpx.AsyncClient``. If provided, the client to use for
+ the query.
+
+ Unlike the other dnspython async functions, a backend cannot be provided
+ in this function because httpx always auto-detects the async backend.
+
+ See :py:func:`dns.query.https()` for the documentation of the other
+ parameters, exceptions, and return type of this method.
+ """
+
+ try:
+ af = dns.inet.af_for_address(where)
+ except ValueError:
+ af = None
+ if af is not None and dns.inet.is_address(where):
+ if af == socket.AF_INET:
+ url = f"https://{where}:{port}{path}"
+ elif af == socket.AF_INET6:
+ url = f"https://[{where}]:{port}{path}"
+ else:
+ url = where
+
+ extensions = {}
+ if bootstrap_address is None:
+ # pylint: disable=possibly-used-before-assignment
+ parsed = urllib.parse.urlparse(url)
+ if parsed.hostname is None:
+ raise ValueError("no hostname in URL")
+ if dns.inet.is_address(parsed.hostname):
+ bootstrap_address = parsed.hostname
+ extensions["sni_hostname"] = parsed.hostname
+ if parsed.port is not None:
+ port = parsed.port
+
+ if http_version == HTTPVersion.H3 or (
+ http_version == HTTPVersion.DEFAULT and not have_doh
+ ):
+ if bootstrap_address is None:
+ resolver = _maybe_get_resolver(resolver)
+ assert parsed.hostname is not None # for mypy
+ answers = await resolver.resolve_name(parsed.hostname, family)
+ bootstrap_address = random.choice(list(answers.addresses()))
+ return await _http3(
+ q,
+ bootstrap_address,
+ url,
+ timeout,
+ port,
+ source,
+ source_port,
+ one_rr_per_rrset,
+ ignore_trailing,
+ verify=verify,
+ post=post,
+ )
+
+ if not have_doh:
+ raise NoDOH # pragma: no cover
+ # pylint: disable=possibly-used-before-assignment
+ if client and not isinstance(client, httpx.AsyncClient):
+ raise ValueError("session parameter must be an httpx.AsyncClient")
+ # pylint: enable=possibly-used-before-assignment
+
+ wire = q.to_wire()
+ headers = {"accept": "application/dns-message"}
+
+ h1 = http_version in (HTTPVersion.H1, HTTPVersion.DEFAULT)
+ h2 = http_version in (HTTPVersion.H2, HTTPVersion.DEFAULT)
+
+ backend = dns.asyncbackend.get_default_backend()
+
+ if source is None:
+ local_address = None
+ local_port = 0
+ else:
+ local_address = source
+ local_port = source_port
+
+ if client:
+ cm: contextlib.AbstractAsyncContextManager = NullContext(client)
+ else:
+ transport = backend.get_transport_class()(
+ local_address=local_address,
+ http1=h1,
+ http2=h2,
+ verify=verify,
+ local_port=local_port,
+ bootstrap_address=bootstrap_address,
+ resolver=resolver,
+ family=family,
+ )
+
+ cm = httpx.AsyncClient(http1=h1, http2=h2, verify=verify, transport=transport)
+
+ async with cm as the_client:
+ # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH
+ # GET and POST examples
+ if post:
+ headers.update(
+ {
+ "content-type": "application/dns-message",
+ "content-length": str(len(wire)),
+ }
+ )
+ response = await backend.wait_for(
+ the_client.post(
+ url,
+ headers=headers,
+ content=wire,
+ extensions=extensions,
+ ),
+ timeout,
+ )
+ else:
+ wire = base64.urlsafe_b64encode(wire).rstrip(b"=")
+ twire = wire.decode() # httpx does a repr() if we give it bytes
+ response = await backend.wait_for(
+ the_client.get(
+ url,
+ headers=headers,
+ params={"dns": twire},
+ extensions=extensions,
+ ),
+ timeout,
+ )
+
+ # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH
+ # status codes
+ if response.status_code < 200 or response.status_code > 299:
+ raise ValueError(
+ f"{where} responded with status code {response.status_code}"
+ f"\nResponse body: {response.content!r}"
+ )
+ r = dns.message.from_wire(
+ response.content,
+ keyring=q.keyring,
+ request_mac=q.request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ r.time = response.elapsed.total_seconds()
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+
+
+async def _http3(
+ q: dns.message.Message,
+ where: str,
+ url: str,
+ timeout: Optional[float] = None,
+ port: int = 853,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ verify: Union[bool, str] = True,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+ hostname: Optional[str] = None,
+ post: bool = True,
+) -> dns.message.Message:
+ if not dns.quic.have_quic:
+ raise NoDOH("DNS-over-HTTP3 is not available.") # pragma: no cover
+
+ url_parts = urllib.parse.urlparse(url)
+ hostname = url_parts.hostname
+ if url_parts.port is not None:
+ port = url_parts.port
+
+ q.id = 0
+ wire = q.to_wire()
+ (cfactory, mfactory) = dns.quic.factories_for_backend(backend)
+
+ async with cfactory() as context:
+ async with mfactory(
+ context, verify_mode=verify, server_name=hostname, h3=True
+ ) as the_manager:
+ the_connection = the_manager.connect(where, port, source, source_port)
+ (start, expiration) = _compute_times(timeout)
+ stream = await the_connection.make_stream(timeout)
+ async with stream:
+ # note that send_h3() does not need await
+ stream.send_h3(url, wire, post)
+ wire = await stream.receive(_remaining(expiration))
+ _check_status(stream.headers(), where, wire)
+ finish = time.time()
+ r = dns.message.from_wire(
+ wire,
+ keyring=q.keyring,
+ request_mac=q.request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ r.time = max(finish - start, 0.0)
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+
+
+async def quic(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 853,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ connection: Optional[dns.quic.AsyncQuicConnection] = None,
+ verify: Union[bool, str] = True,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+ hostname: Optional[str] = None,
+ server_hostname: Optional[str] = None,
+) -> dns.message.Message:
+ """Return the response obtained after sending an asynchronous query via
+ DNS-over-QUIC.
+
+ *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
+ the default, then dnspython will use the default backend.
+
+ See :py:func:`dns.query.quic()` for the documentation of the other
+ parameters, exceptions, and return type of this method.
+ """
+
+ if not dns.quic.have_quic:
+ raise NoDOQ("DNS-over-QUIC is not available.") # pragma: no cover
+
+ if server_hostname is not None and hostname is None:
+ hostname = server_hostname
+
+ q.id = 0
+ wire = q.to_wire()
+ the_connection: dns.quic.AsyncQuicConnection
+ if connection:
+ cfactory = dns.quic.null_factory
+ mfactory = dns.quic.null_factory
+ the_connection = connection
+ else:
+ (cfactory, mfactory) = dns.quic.factories_for_backend(backend)
+
+ async with cfactory() as context:
+ async with mfactory(
+ context,
+ verify_mode=verify,
+ server_name=server_hostname,
+ ) as the_manager:
+ if not connection:
+ the_connection = the_manager.connect(where, port, source, source_port)
+ (start, expiration) = _compute_times(timeout)
+ stream = await the_connection.make_stream(timeout)
+ async with stream:
+ await stream.send(wire, True)
+ wire = await stream.receive(_remaining(expiration))
+ finish = time.time()
+ r = dns.message.from_wire(
+ wire,
+ keyring=q.keyring,
+ request_mac=q.request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ r.time = max(finish - start, 0.0)
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+
+
+async def _inbound_xfr(
+ txn_manager: dns.transaction.TransactionManager,
+ s: dns.asyncbackend.Socket,
+ query: dns.message.Message,
+ serial: Optional[int],
+ timeout: Optional[float],
+ expiration: float,
+) -> Any:
+ """Given a socket, does the zone transfer."""
+ rdtype = query.question[0].rdtype
+ is_ixfr = rdtype == dns.rdatatype.IXFR
+ origin = txn_manager.from_wire_origin()
+ wire = query.to_wire()
+ is_udp = s.type == socket.SOCK_DGRAM
+ if is_udp:
+ udp_sock = cast(dns.asyncbackend.DatagramSocket, s)
+ await udp_sock.sendto(wire, None, _timeout(expiration))
+ else:
+ tcp_sock = cast(dns.asyncbackend.StreamSocket, s)
+ tcpmsg = struct.pack("!H", len(wire)) + wire
+ await tcp_sock.sendall(tcpmsg, expiration)
+ with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound:
+ done = False
+ tsig_ctx = None
+ while not done:
+ (_, mexpiration) = _compute_times(timeout)
+ if mexpiration is None or (
+ expiration is not None and mexpiration > expiration
+ ):
+ mexpiration = expiration
+ if is_udp:
+ timeout = _timeout(mexpiration)
+ (rwire, _) = await udp_sock.recvfrom(65535, timeout)
+ else:
+ ldata = await _read_exactly(tcp_sock, 2, mexpiration)
+ (l,) = struct.unpack("!H", ldata)
+ rwire = await _read_exactly(tcp_sock, l, mexpiration)
+ r = dns.message.from_wire(
+ rwire,
+ keyring=query.keyring,
+ request_mac=query.mac,
+ xfr=True,
+ origin=origin,
+ tsig_ctx=tsig_ctx,
+ multi=(not is_udp),
+ one_rr_per_rrset=is_ixfr,
+ )
+ done = inbound.process_message(r)
+ yield r
+ tsig_ctx = r.tsig_ctx
+ if query.keyring and not r.had_tsig:
+ raise dns.exception.FormError("missing TSIG")
+
+
+async def inbound_xfr(
+ where: str,
+ txn_manager: dns.transaction.TransactionManager,
+ query: Optional[dns.message.Message] = None,
+ port: int = 53,
+ timeout: Optional[float] = None,
+ lifetime: Optional[float] = None,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ udp_mode: UDPMode = UDPMode.NEVER,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+) -> None:
+ """Conduct an inbound transfer and apply it via a transaction from the
+ txn_manager.
+
+ *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
+ the default, then dnspython will use the default backend.
+
+ See :py:func:`dns.query.inbound_xfr()` for the documentation of
+ the other parameters, exceptions, and return type of this method.
+ """
+ if query is None:
+ (query, serial) = dns.xfr.make_query(txn_manager)
+ else:
+ serial = dns.xfr.extract_serial_from_query(query)
+ af = dns.inet.af_for_address(where)
+ stuple = _source_tuple(af, source, source_port)
+ dtuple = (where, port)
+ if not backend:
+ backend = dns.asyncbackend.get_default_backend()
+ (_, expiration) = _compute_times(lifetime)
+ if query.question[0].rdtype == dns.rdatatype.IXFR and udp_mode != UDPMode.NEVER:
+ s = await backend.make_socket(
+ af, socket.SOCK_DGRAM, 0, stuple, dtuple, _timeout(expiration)
+ )
+ async with s:
+ try:
+ async for _ in _inbound_xfr(
+ txn_manager, s, query, serial, timeout, expiration
+ ):
+ pass
+ return
+ except dns.xfr.UseTCP:
+ if udp_mode == UDPMode.ONLY:
+ raise
+
+ s = await backend.make_socket(
+ af, socket.SOCK_STREAM, 0, stuple, dtuple, _timeout(expiration)
+ )
+ async with s:
+ async for _ in _inbound_xfr(txn_manager, s, query, serial, timeout, expiration):
+ pass
diff --git a/.venv/lib/python3.12/site-packages/dns/asyncresolver.py b/.venv/lib/python3.12/site-packages/dns/asyncresolver.py
new file mode 100644
index 00000000..8f5e062a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/asyncresolver.py
@@ -0,0 +1,475 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Asynchronous DNS stub resolver."""
+
+import socket
+import time
+from typing import Any, Dict, List, Optional, Union
+
+import dns._ddr
+import dns.asyncbackend
+import dns.asyncquery
+import dns.exception
+import dns.name
+import dns.query
+import dns.rdataclass
+import dns.rdatatype
+import dns.resolver # lgtm[py/import-and-import-from]
+
+# import some resolver symbols for brevity
+from dns.resolver import NXDOMAIN, NoAnswer, NoRootSOA, NotAbsolute
+
+# for indentation purposes below
+_udp = dns.asyncquery.udp
+_tcp = dns.asyncquery.tcp
+
+
+class Resolver(dns.resolver.BaseResolver):
+ """Asynchronous DNS stub resolver."""
+
+ async def resolve(
+ self,
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ tcp: bool = False,
+ source: Optional[str] = None,
+ raise_on_no_answer: bool = True,
+ source_port: int = 0,
+ lifetime: Optional[float] = None,
+ search: Optional[bool] = None,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+ ) -> dns.resolver.Answer:
+ """Query nameservers asynchronously to find the answer to the question.
+
+ *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
+ the default, then dnspython will use the default backend.
+
+ See :py:func:`dns.resolver.Resolver.resolve()` for the
+ documentation of the other parameters, exceptions, and return
+ type of this method.
+ """
+
+ resolution = dns.resolver._Resolution(
+ self, qname, rdtype, rdclass, tcp, raise_on_no_answer, search
+ )
+ if not backend:
+ backend = dns.asyncbackend.get_default_backend()
+ start = time.time()
+ while True:
+ (request, answer) = resolution.next_request()
+ # Note we need to say "if answer is not None" and not just
+ # "if answer" because answer implements __len__, and python
+ # will call that. We want to return if we have an answer
+ # object, including in cases where its length is 0.
+ if answer is not None:
+ # cache hit!
+ return answer
+ assert request is not None # needed for type checking
+ done = False
+ while not done:
+ (nameserver, tcp, backoff) = resolution.next_nameserver()
+ if backoff:
+ await backend.sleep(backoff)
+ timeout = self._compute_timeout(start, lifetime, resolution.errors)
+ try:
+ response = await nameserver.async_query(
+ request,
+ timeout=timeout,
+ source=source,
+ source_port=source_port,
+ max_size=tcp,
+ backend=backend,
+ )
+ except Exception as ex:
+ (_, done) = resolution.query_result(None, ex)
+ continue
+ (answer, done) = resolution.query_result(response, None)
+ # Note we need to say "if answer is not None" and not just
+ # "if answer" because answer implements __len__, and python
+ # will call that. We want to return if we have an answer
+ # object, including in cases where its length is 0.
+ if answer is not None:
+ return answer
+
+ async def resolve_address(
+ self, ipaddr: str, *args: Any, **kwargs: Any
+ ) -> dns.resolver.Answer:
+ """Use an asynchronous resolver to run a reverse query for PTR
+ records.
+
+ This utilizes the resolve() method to perform a PTR lookup on the
+ specified IP address.
+
+ *ipaddr*, a ``str``, the IPv4 or IPv6 address you want to get
+ the PTR record for.
+
+ All other arguments that can be passed to the resolve() function
+ except for rdtype and rdclass are also supported by this
+ function.
+
+ """
+ # We make a modified kwargs for type checking happiness, as otherwise
+ # we get a legit warning about possibly having rdtype and rdclass
+ # in the kwargs more than once.
+ modified_kwargs: Dict[str, Any] = {}
+ modified_kwargs.update(kwargs)
+ modified_kwargs["rdtype"] = dns.rdatatype.PTR
+ modified_kwargs["rdclass"] = dns.rdataclass.IN
+ return await self.resolve(
+ dns.reversename.from_address(ipaddr), *args, **modified_kwargs
+ )
+
+ async def resolve_name(
+ self,
+ name: Union[dns.name.Name, str],
+ family: int = socket.AF_UNSPEC,
+ **kwargs: Any,
+ ) -> dns.resolver.HostAnswers:
+ """Use an asynchronous resolver to query for address records.
+
+ This utilizes the resolve() method to perform A and/or AAAA lookups on
+ the specified name.
+
+ *qname*, a ``dns.name.Name`` or ``str``, the name to resolve.
+
+ *family*, an ``int``, the address family. If socket.AF_UNSPEC
+ (the default), both A and AAAA records will be retrieved.
+
+ All other arguments that can be passed to the resolve() function
+ except for rdtype and rdclass are also supported by this
+ function.
+ """
+ # We make a modified kwargs for type checking happiness, as otherwise
+ # we get a legit warning about possibly having rdtype and rdclass
+ # in the kwargs more than once.
+ modified_kwargs: Dict[str, Any] = {}
+ modified_kwargs.update(kwargs)
+ modified_kwargs.pop("rdtype", None)
+ modified_kwargs["rdclass"] = dns.rdataclass.IN
+
+ if family == socket.AF_INET:
+ v4 = await self.resolve(name, dns.rdatatype.A, **modified_kwargs)
+ return dns.resolver.HostAnswers.make(v4=v4)
+ elif family == socket.AF_INET6:
+ v6 = await self.resolve(name, dns.rdatatype.AAAA, **modified_kwargs)
+ return dns.resolver.HostAnswers.make(v6=v6)
+ elif family != socket.AF_UNSPEC:
+ raise NotImplementedError(f"unknown address family {family}")
+
+ raise_on_no_answer = modified_kwargs.pop("raise_on_no_answer", True)
+ lifetime = modified_kwargs.pop("lifetime", None)
+ start = time.time()
+ v6 = await self.resolve(
+ name,
+ dns.rdatatype.AAAA,
+ raise_on_no_answer=False,
+ lifetime=self._compute_timeout(start, lifetime),
+ **modified_kwargs,
+ )
+ # Note that setting name ensures we query the same name
+ # for A as we did for AAAA. (This is just in case search lists
+ # are active by default in the resolver configuration and
+ # we might be talking to a server that says NXDOMAIN when it
+ # wants to say NOERROR no data.
+ name = v6.qname
+ v4 = await self.resolve(
+ name,
+ dns.rdatatype.A,
+ raise_on_no_answer=False,
+ lifetime=self._compute_timeout(start, lifetime),
+ **modified_kwargs,
+ )
+ answers = dns.resolver.HostAnswers.make(
+ v6=v6, v4=v4, add_empty=not raise_on_no_answer
+ )
+ if not answers:
+ raise NoAnswer(response=v6.response)
+ return answers
+
+ # pylint: disable=redefined-outer-name
+
+ async def canonical_name(self, name: Union[dns.name.Name, str]) -> dns.name.Name:
+ """Determine the canonical name of *name*.
+
+ The canonical name is the name the resolver uses for queries
+ after all CNAME and DNAME renamings have been applied.
+
+ *name*, a ``dns.name.Name`` or ``str``, the query name.
+
+ This method can raise any exception that ``resolve()`` can
+ raise, other than ``dns.resolver.NoAnswer`` and
+ ``dns.resolver.NXDOMAIN``.
+
+ Returns a ``dns.name.Name``.
+ """
+ try:
+ answer = await self.resolve(name, raise_on_no_answer=False)
+ canonical_name = answer.canonical_name
+ except dns.resolver.NXDOMAIN as e:
+ canonical_name = e.canonical_name
+ return canonical_name
+
+ async def try_ddr(self, lifetime: float = 5.0) -> None:
+ """Try to update the resolver's nameservers using Discovery of Designated
+ Resolvers (DDR). If successful, the resolver will subsequently use
+ DNS-over-HTTPS or DNS-over-TLS for future queries.
+
+ *lifetime*, a float, is the maximum time to spend attempting DDR. The default
+ is 5 seconds.
+
+ If the SVCB query is successful and results in a non-empty list of nameservers,
+ then the resolver's nameservers are set to the returned servers in priority
+ order.
+
+ The current implementation does not use any address hints from the SVCB record,
+ nor does it resolve addresses for the SCVB target name, rather it assumes that
+ the bootstrap nameserver will always be one of the addresses and uses it.
+ A future revision to the code may offer fuller support. The code verifies that
+ the bootstrap nameserver is in the Subject Alternative Name field of the
+ TLS certficate.
+ """
+ try:
+ expiration = time.time() + lifetime
+ answer = await self.resolve(
+ dns._ddr._local_resolver_name, "svcb", lifetime=lifetime
+ )
+ timeout = dns.query._remaining(expiration)
+ nameservers = await dns._ddr._get_nameservers_async(answer, timeout)
+ if len(nameservers) > 0:
+ self.nameservers = nameservers
+ except Exception:
+ pass
+
+
+default_resolver = None
+
+
+def get_default_resolver() -> Resolver:
+ """Get the default asynchronous resolver, initializing it if necessary."""
+ if default_resolver is None:
+ reset_default_resolver()
+ assert default_resolver is not None
+ return default_resolver
+
+
+def reset_default_resolver() -> None:
+ """Re-initialize default asynchronous resolver.
+
+ Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX
+ systems) will be re-read immediately.
+ """
+
+ global default_resolver
+ default_resolver = Resolver()
+
+
+async def resolve(
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ tcp: bool = False,
+ source: Optional[str] = None,
+ raise_on_no_answer: bool = True,
+ source_port: int = 0,
+ lifetime: Optional[float] = None,
+ search: Optional[bool] = None,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+) -> dns.resolver.Answer:
+ """Query nameservers asynchronously to find the answer to the question.
+
+ This is a convenience function that uses the default resolver
+ object to make the query.
+
+ See :py:func:`dns.asyncresolver.Resolver.resolve` for more
+ information on the parameters.
+ """
+
+ return await get_default_resolver().resolve(
+ qname,
+ rdtype,
+ rdclass,
+ tcp,
+ source,
+ raise_on_no_answer,
+ source_port,
+ lifetime,
+ search,
+ backend,
+ )
+
+
+async def resolve_address(
+ ipaddr: str, *args: Any, **kwargs: Any
+) -> dns.resolver.Answer:
+ """Use a resolver to run a reverse query for PTR records.
+
+ See :py:func:`dns.asyncresolver.Resolver.resolve_address` for more
+ information on the parameters.
+ """
+
+ return await get_default_resolver().resolve_address(ipaddr, *args, **kwargs)
+
+
+async def resolve_name(
+ name: Union[dns.name.Name, str], family: int = socket.AF_UNSPEC, **kwargs: Any
+) -> dns.resolver.HostAnswers:
+ """Use a resolver to asynchronously query for address records.
+
+ See :py:func:`dns.asyncresolver.Resolver.resolve_name` for more
+ information on the parameters.
+ """
+
+ return await get_default_resolver().resolve_name(name, family, **kwargs)
+
+
+async def canonical_name(name: Union[dns.name.Name, str]) -> dns.name.Name:
+ """Determine the canonical name of *name*.
+
+ See :py:func:`dns.resolver.Resolver.canonical_name` for more
+ information on the parameters and possible exceptions.
+ """
+
+ return await get_default_resolver().canonical_name(name)
+
+
+async def try_ddr(timeout: float = 5.0) -> None:
+ """Try to update the default resolver's nameservers using Discovery of Designated
+ Resolvers (DDR). If successful, the resolver will subsequently use
+ DNS-over-HTTPS or DNS-over-TLS for future queries.
+
+ See :py:func:`dns.resolver.Resolver.try_ddr` for more information.
+ """
+ return await get_default_resolver().try_ddr(timeout)
+
+
+async def zone_for_name(
+ name: Union[dns.name.Name, str],
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+ tcp: bool = False,
+ resolver: Optional[Resolver] = None,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+) -> dns.name.Name:
+ """Find the name of the zone which contains the specified name.
+
+ See :py:func:`dns.resolver.Resolver.zone_for_name` for more
+ information on the parameters and possible exceptions.
+ """
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, dns.name.root)
+ if resolver is None:
+ resolver = get_default_resolver()
+ if not name.is_absolute():
+ raise NotAbsolute(name)
+ while True:
+ try:
+ answer = await resolver.resolve(
+ name, dns.rdatatype.SOA, rdclass, tcp, backend=backend
+ )
+ assert answer.rrset is not None
+ if answer.rrset.name == name:
+ return name
+ # otherwise we were CNAMEd or DNAMEd and need to look higher
+ except (NXDOMAIN, NoAnswer):
+ pass
+ try:
+ name = name.parent()
+ except dns.name.NoParent: # pragma: no cover
+ raise NoRootSOA
+
+
+async def make_resolver_at(
+ where: Union[dns.name.Name, str],
+ port: int = 53,
+ family: int = socket.AF_UNSPEC,
+ resolver: Optional[Resolver] = None,
+) -> Resolver:
+ """Make a stub resolver using the specified destination as the full resolver.
+
+ *where*, a ``dns.name.Name`` or ``str`` the domain name or IP address of the
+ full resolver.
+
+ *port*, an ``int``, the port to use. If not specified, the default is 53.
+
+ *family*, an ``int``, the address family to use. This parameter is used if
+ *where* is not an address. The default is ``socket.AF_UNSPEC`` in which case
+ the first address returned by ``resolve_name()`` will be used, otherwise the
+ first address of the specified family will be used.
+
+ *resolver*, a ``dns.asyncresolver.Resolver`` or ``None``, the resolver to use for
+ resolution of hostnames. If not specified, the default resolver will be used.
+
+ Returns a ``dns.resolver.Resolver`` or raises an exception.
+ """
+ if resolver is None:
+ resolver = get_default_resolver()
+ nameservers: List[Union[str, dns.nameserver.Nameserver]] = []
+ if isinstance(where, str) and dns.inet.is_address(where):
+ nameservers.append(dns.nameserver.Do53Nameserver(where, port))
+ else:
+ answers = await resolver.resolve_name(where, family)
+ for address in answers.addresses():
+ nameservers.append(dns.nameserver.Do53Nameserver(address, port))
+ res = dns.asyncresolver.Resolver(configure=False)
+ res.nameservers = nameservers
+ return res
+
+
+async def resolve_at(
+ where: Union[dns.name.Name, str],
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ tcp: bool = False,
+ source: Optional[str] = None,
+ raise_on_no_answer: bool = True,
+ source_port: int = 0,
+ lifetime: Optional[float] = None,
+ search: Optional[bool] = None,
+ backend: Optional[dns.asyncbackend.Backend] = None,
+ port: int = 53,
+ family: int = socket.AF_UNSPEC,
+ resolver: Optional[Resolver] = None,
+) -> dns.resolver.Answer:
+ """Query nameservers to find the answer to the question.
+
+ This is a convenience function that calls ``dns.asyncresolver.make_resolver_at()``
+ to make a resolver, and then uses it to resolve the query.
+
+ See ``dns.asyncresolver.Resolver.resolve`` for more information on the resolution
+ parameters, and ``dns.asyncresolver.make_resolver_at`` for information about the
+ resolver parameters *where*, *port*, *family*, and *resolver*.
+
+ If making more than one query, it is more efficient to call
+ ``dns.asyncresolver.make_resolver_at()`` and then use that resolver for the queries
+ instead of calling ``resolve_at()`` multiple times.
+ """
+ res = await make_resolver_at(where, port, family, resolver)
+ return await res.resolve(
+ qname,
+ rdtype,
+ rdclass,
+ tcp,
+ source,
+ raise_on_no_answer,
+ source_port,
+ lifetime,
+ search,
+ backend,
+ )
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssec.py b/.venv/lib/python3.12/site-packages/dns/dnssec.py
new file mode 100644
index 00000000..b69d0a12
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssec.py
@@ -0,0 +1,1247 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Common DNSSEC-related functions and constants."""
+
+
+import base64
+import contextlib
+import functools
+import hashlib
+import struct
+import time
+from datetime import datetime
+from typing import Callable, Dict, List, Optional, Set, Tuple, Union, cast
+
+import dns._features
+import dns.exception
+import dns.name
+import dns.node
+import dns.rdata
+import dns.rdataclass
+import dns.rdataset
+import dns.rdatatype
+import dns.rrset
+import dns.transaction
+import dns.zone
+from dns.dnssectypes import Algorithm, DSDigest, NSEC3Hash
+from dns.exception import ( # pylint: disable=W0611
+ AlgorithmKeyMismatch,
+ DeniedByPolicy,
+ UnsupportedAlgorithm,
+ ValidationFailure,
+)
+from dns.rdtypes.ANY.CDNSKEY import CDNSKEY
+from dns.rdtypes.ANY.CDS import CDS
+from dns.rdtypes.ANY.DNSKEY import DNSKEY
+from dns.rdtypes.ANY.DS import DS
+from dns.rdtypes.ANY.NSEC import NSEC, Bitmap
+from dns.rdtypes.ANY.NSEC3PARAM import NSEC3PARAM
+from dns.rdtypes.ANY.RRSIG import RRSIG, sigtime_to_posixtime
+from dns.rdtypes.dnskeybase import Flag
+
+PublicKey = Union[
+ "GenericPublicKey",
+ "rsa.RSAPublicKey",
+ "ec.EllipticCurvePublicKey",
+ "ed25519.Ed25519PublicKey",
+ "ed448.Ed448PublicKey",
+]
+
+PrivateKey = Union[
+ "GenericPrivateKey",
+ "rsa.RSAPrivateKey",
+ "ec.EllipticCurvePrivateKey",
+ "ed25519.Ed25519PrivateKey",
+ "ed448.Ed448PrivateKey",
+]
+
+RRsetSigner = Callable[[dns.transaction.Transaction, dns.rrset.RRset], None]
+
+
+def algorithm_from_text(text: str) -> Algorithm:
+ """Convert text into a DNSSEC algorithm value.
+
+ *text*, a ``str``, the text to convert to into an algorithm value.
+
+ Returns an ``int``.
+ """
+
+ return Algorithm.from_text(text)
+
+
+def algorithm_to_text(value: Union[Algorithm, int]) -> str:
+ """Convert a DNSSEC algorithm value to text
+
+ *value*, a ``dns.dnssec.Algorithm``.
+
+ Returns a ``str``, the name of a DNSSEC algorithm.
+ """
+
+ return Algorithm.to_text(value)
+
+
+def to_timestamp(value: Union[datetime, str, float, int]) -> int:
+ """Convert various format to a timestamp"""
+ if isinstance(value, datetime):
+ return int(value.timestamp())
+ elif isinstance(value, str):
+ return sigtime_to_posixtime(value)
+ elif isinstance(value, float):
+ return int(value)
+ elif isinstance(value, int):
+ return value
+ else:
+ raise TypeError("Unsupported timestamp type")
+
+
+def key_id(key: Union[DNSKEY, CDNSKEY]) -> int:
+ """Return the key id (a 16-bit number) for the specified key.
+
+ *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY``
+
+ Returns an ``int`` between 0 and 65535
+ """
+
+ rdata = key.to_wire()
+ assert rdata is not None # for mypy
+ if key.algorithm == Algorithm.RSAMD5:
+ return (rdata[-3] << 8) + rdata[-2]
+ else:
+ total = 0
+ for i in range(len(rdata) // 2):
+ total += (rdata[2 * i] << 8) + rdata[2 * i + 1]
+ if len(rdata) % 2 != 0:
+ total += rdata[len(rdata) - 1] << 8
+ total += (total >> 16) & 0xFFFF
+ return total & 0xFFFF
+
+
+class Policy:
+ def __init__(self):
+ pass
+
+ def ok_to_sign(self, _: DNSKEY) -> bool: # pragma: no cover
+ return False
+
+ def ok_to_validate(self, _: DNSKEY) -> bool: # pragma: no cover
+ return False
+
+ def ok_to_create_ds(self, _: DSDigest) -> bool: # pragma: no cover
+ return False
+
+ def ok_to_validate_ds(self, _: DSDigest) -> bool: # pragma: no cover
+ return False
+
+
+class SimpleDeny(Policy):
+ def __init__(self, deny_sign, deny_validate, deny_create_ds, deny_validate_ds):
+ super().__init__()
+ self._deny_sign = deny_sign
+ self._deny_validate = deny_validate
+ self._deny_create_ds = deny_create_ds
+ self._deny_validate_ds = deny_validate_ds
+
+ def ok_to_sign(self, key: DNSKEY) -> bool:
+ return key.algorithm not in self._deny_sign
+
+ def ok_to_validate(self, key: DNSKEY) -> bool:
+ return key.algorithm not in self._deny_validate
+
+ def ok_to_create_ds(self, algorithm: DSDigest) -> bool:
+ return algorithm not in self._deny_create_ds
+
+ def ok_to_validate_ds(self, algorithm: DSDigest) -> bool:
+ return algorithm not in self._deny_validate_ds
+
+
+rfc_8624_policy = SimpleDeny(
+ {Algorithm.RSAMD5, Algorithm.DSA, Algorithm.DSANSEC3SHA1, Algorithm.ECCGOST},
+ {Algorithm.RSAMD5, Algorithm.DSA, Algorithm.DSANSEC3SHA1},
+ {DSDigest.NULL, DSDigest.SHA1, DSDigest.GOST},
+ {DSDigest.NULL},
+)
+
+allow_all_policy = SimpleDeny(set(), set(), set(), set())
+
+
+default_policy = rfc_8624_policy
+
+
+def make_ds(
+ name: Union[dns.name.Name, str],
+ key: dns.rdata.Rdata,
+ algorithm: Union[DSDigest, str],
+ origin: Optional[dns.name.Name] = None,
+ policy: Optional[Policy] = None,
+ validating: bool = False,
+) -> DS:
+ """Create a DS record for a DNSSEC key.
+
+ *name*, a ``dns.name.Name`` or ``str``, the owner name of the DS record.
+
+ *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY`` or ``dns.rdtypes.ANY.DNSKEY.CDNSKEY``,
+ the key the DS is about.
+
+ *algorithm*, a ``str`` or ``int`` specifying the hash algorithm.
+ The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case
+ does not matter for these strings.
+
+ *origin*, a ``dns.name.Name`` or ``None``. If *key* is a relative name,
+ then it will be made absolute using the specified origin.
+
+ *policy*, a ``dns.dnssec.Policy`` or ``None``. If ``None``, the default policy,
+ ``dns.dnssec.default_policy`` is used; this policy defaults to that of RFC 8624.
+
+ *validating*, a ``bool``. If ``True``, then policy is checked in
+ validating mode, i.e. "Is it ok to validate using this digest algorithm?".
+ Otherwise the policy is checked in creating mode, i.e. "Is it ok to create a DS with
+ this digest algorithm?".
+
+ Raises ``UnsupportedAlgorithm`` if the algorithm is unknown.
+
+ Raises ``DeniedByPolicy`` if the algorithm is denied by policy.
+
+ Returns a ``dns.rdtypes.ANY.DS.DS``
+ """
+
+ if policy is None:
+ policy = default_policy
+ try:
+ if isinstance(algorithm, str):
+ algorithm = DSDigest[algorithm.upper()]
+ except Exception:
+ raise UnsupportedAlgorithm(f'unsupported algorithm "{algorithm}"')
+ if validating:
+ check = policy.ok_to_validate_ds
+ else:
+ check = policy.ok_to_create_ds
+ if not check(algorithm):
+ raise DeniedByPolicy
+ if not isinstance(key, (DNSKEY, CDNSKEY)):
+ raise ValueError("key is not a DNSKEY/CDNSKEY")
+ if algorithm == DSDigest.SHA1:
+ dshash = hashlib.sha1()
+ elif algorithm == DSDigest.SHA256:
+ dshash = hashlib.sha256()
+ elif algorithm == DSDigest.SHA384:
+ dshash = hashlib.sha384()
+ else:
+ raise UnsupportedAlgorithm(f'unsupported algorithm "{algorithm}"')
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, origin)
+ wire = name.canonicalize().to_wire()
+ kwire = key.to_wire(origin=origin)
+ assert wire is not None and kwire is not None # for mypy
+ dshash.update(wire)
+ dshash.update(kwire)
+ digest = dshash.digest()
+
+ dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, algorithm) + digest
+ ds = dns.rdata.from_wire(
+ dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0, len(dsrdata)
+ )
+ return cast(DS, ds)
+
+
+def make_cds(
+ name: Union[dns.name.Name, str],
+ key: dns.rdata.Rdata,
+ algorithm: Union[DSDigest, str],
+ origin: Optional[dns.name.Name] = None,
+) -> CDS:
+ """Create a CDS record for a DNSSEC key.
+
+ *name*, a ``dns.name.Name`` or ``str``, the owner name of the DS record.
+
+ *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY`` or ``dns.rdtypes.ANY.DNSKEY.CDNSKEY``,
+ the key the DS is about.
+
+ *algorithm*, a ``str`` or ``int`` specifying the hash algorithm.
+ The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case
+ does not matter for these strings.
+
+ *origin*, a ``dns.name.Name`` or ``None``. If *key* is a relative name,
+ then it will be made absolute using the specified origin.
+
+ Raises ``UnsupportedAlgorithm`` if the algorithm is unknown.
+
+ Returns a ``dns.rdtypes.ANY.DS.CDS``
+ """
+
+ ds = make_ds(name, key, algorithm, origin)
+ return CDS(
+ rdclass=ds.rdclass,
+ rdtype=dns.rdatatype.CDS,
+ key_tag=ds.key_tag,
+ algorithm=ds.algorithm,
+ digest_type=ds.digest_type,
+ digest=ds.digest,
+ )
+
+
+def _find_candidate_keys(
+ keys: Dict[dns.name.Name, Union[dns.rdataset.Rdataset, dns.node.Node]], rrsig: RRSIG
+) -> Optional[List[DNSKEY]]:
+ value = keys.get(rrsig.signer)
+ if isinstance(value, dns.node.Node):
+ rdataset = value.get_rdataset(dns.rdataclass.IN, dns.rdatatype.DNSKEY)
+ else:
+ rdataset = value
+ if rdataset is None:
+ return None
+ return [
+ cast(DNSKEY, rd)
+ for rd in rdataset
+ if rd.algorithm == rrsig.algorithm
+ and key_id(rd) == rrsig.key_tag
+ and (rd.flags & Flag.ZONE) == Flag.ZONE # RFC 4034 2.1.1
+ and rd.protocol == 3 # RFC 4034 2.1.2
+ ]
+
+
+def _get_rrname_rdataset(
+ rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+) -> Tuple[dns.name.Name, dns.rdataset.Rdataset]:
+ if isinstance(rrset, tuple):
+ return rrset[0], rrset[1]
+ else:
+ return rrset.name, rrset
+
+
+def _validate_signature(sig: bytes, data: bytes, key: DNSKEY) -> None:
+ # pylint: disable=possibly-used-before-assignment
+ public_cls = get_algorithm_cls_from_dnskey(key).public_cls
+ try:
+ public_key = public_cls.from_dnskey(key)
+ except ValueError:
+ raise ValidationFailure("invalid public key")
+ public_key.verify(sig, data)
+
+
+def _validate_rrsig(
+ rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+ rrsig: RRSIG,
+ keys: Dict[dns.name.Name, Union[dns.node.Node, dns.rdataset.Rdataset]],
+ origin: Optional[dns.name.Name] = None,
+ now: Optional[float] = None,
+ policy: Optional[Policy] = None,
+) -> None:
+ """Validate an RRset against a single signature rdata, throwing an
+ exception if validation is not successful.
+
+ *rrset*, the RRset to validate. This can be a
+ ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+ tuple.
+
+ *rrsig*, a ``dns.rdata.Rdata``, the signature to validate.
+
+ *keys*, the key dictionary, used to find the DNSKEY associated
+ with a given name. The dictionary is keyed by a
+ ``dns.name.Name``, and has ``dns.node.Node`` or
+ ``dns.rdataset.Rdataset`` values.
+
+ *origin*, a ``dns.name.Name`` or ``None``, the origin to use for relative
+ names.
+
+ *now*, a ``float`` or ``None``, the time, in seconds since the epoch, to
+ use as the current time when validating. If ``None``, the actual current
+ time is used.
+
+ *policy*, a ``dns.dnssec.Policy`` or ``None``. If ``None``, the default policy,
+ ``dns.dnssec.default_policy`` is used; this policy defaults to that of RFC 8624.
+
+ Raises ``ValidationFailure`` if the signature is expired, not yet valid,
+ the public key is invalid, the algorithm is unknown, the verification
+ fails, etc.
+
+ Raises ``UnsupportedAlgorithm`` if the algorithm is recognized by
+ dnspython but not implemented.
+ """
+
+ if policy is None:
+ policy = default_policy
+
+ candidate_keys = _find_candidate_keys(keys, rrsig)
+ if candidate_keys is None:
+ raise ValidationFailure("unknown key")
+
+ if now is None:
+ now = time.time()
+ if rrsig.expiration < now:
+ raise ValidationFailure("expired")
+ if rrsig.inception > now:
+ raise ValidationFailure("not yet valid")
+
+ data = _make_rrsig_signature_data(rrset, rrsig, origin)
+
+ # pylint: disable=possibly-used-before-assignment
+ for candidate_key in candidate_keys:
+ if not policy.ok_to_validate(candidate_key):
+ continue
+ try:
+ _validate_signature(rrsig.signature, data, candidate_key)
+ return
+ except (InvalidSignature, ValidationFailure):
+ # this happens on an individual validation failure
+ continue
+ # nothing verified -- raise failure:
+ raise ValidationFailure("verify failure")
+
+
+def _validate(
+ rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+ rrsigset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+ keys: Dict[dns.name.Name, Union[dns.node.Node, dns.rdataset.Rdataset]],
+ origin: Optional[dns.name.Name] = None,
+ now: Optional[float] = None,
+ policy: Optional[Policy] = None,
+) -> None:
+ """Validate an RRset against a signature RRset, throwing an exception
+ if none of the signatures validate.
+
+ *rrset*, the RRset to validate. This can be a
+ ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+ tuple.
+
+ *rrsigset*, the signature RRset. This can be a
+ ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+ tuple.
+
+ *keys*, the key dictionary, used to find the DNSKEY associated
+ with a given name. The dictionary is keyed by a
+ ``dns.name.Name``, and has ``dns.node.Node`` or
+ ``dns.rdataset.Rdataset`` values.
+
+ *origin*, a ``dns.name.Name``, the origin to use for relative names;
+ defaults to None.
+
+ *now*, an ``int`` or ``None``, the time, in seconds since the epoch, to
+ use as the current time when validating. If ``None``, the actual current
+ time is used.
+
+ *policy*, a ``dns.dnssec.Policy`` or ``None``. If ``None``, the default policy,
+ ``dns.dnssec.default_policy`` is used; this policy defaults to that of RFC 8624.
+
+ Raises ``ValidationFailure`` if the signature is expired, not yet valid,
+ the public key is invalid, the algorithm is unknown, the verification
+ fails, etc.
+ """
+
+ if policy is None:
+ policy = default_policy
+
+ if isinstance(origin, str):
+ origin = dns.name.from_text(origin, dns.name.root)
+
+ if isinstance(rrset, tuple):
+ rrname = rrset[0]
+ else:
+ rrname = rrset.name
+
+ if isinstance(rrsigset, tuple):
+ rrsigname = rrsigset[0]
+ rrsigrdataset = rrsigset[1]
+ else:
+ rrsigname = rrsigset.name
+ rrsigrdataset = rrsigset
+
+ rrname = rrname.choose_relativity(origin)
+ rrsigname = rrsigname.choose_relativity(origin)
+ if rrname != rrsigname:
+ raise ValidationFailure("owner names do not match")
+
+ for rrsig in rrsigrdataset:
+ if not isinstance(rrsig, RRSIG):
+ raise ValidationFailure("expected an RRSIG")
+ try:
+ _validate_rrsig(rrset, rrsig, keys, origin, now, policy)
+ return
+ except (ValidationFailure, UnsupportedAlgorithm):
+ pass
+ raise ValidationFailure("no RRSIGs validated")
+
+
+def _sign(
+ rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+ private_key: PrivateKey,
+ signer: dns.name.Name,
+ dnskey: DNSKEY,
+ inception: Optional[Union[datetime, str, int, float]] = None,
+ expiration: Optional[Union[datetime, str, int, float]] = None,
+ lifetime: Optional[int] = None,
+ verify: bool = False,
+ policy: Optional[Policy] = None,
+ origin: Optional[dns.name.Name] = None,
+ deterministic: bool = True,
+) -> RRSIG:
+ """Sign RRset using private key.
+
+ *rrset*, the RRset to validate. This can be a
+ ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+ tuple.
+
+ *private_key*, the private key to use for signing, a
+ ``cryptography.hazmat.primitives.asymmetric`` private key class applicable
+ for DNSSEC.
+
+ *signer*, a ``dns.name.Name``, the Signer's name.
+
+ *dnskey*, a ``DNSKEY`` matching ``private_key``.
+
+ *inception*, a ``datetime``, ``str``, ``int``, ``float`` or ``None``, the
+ signature inception time. If ``None``, the current time is used. If a ``str``, the
+ format is "YYYYMMDDHHMMSS" or alternatively the number of seconds since the UNIX
+ epoch in text form; this is the same the RRSIG rdata's text form.
+ Values of type `int` or `float` are interpreted as seconds since the UNIX epoch.
+
+ *expiration*, a ``datetime``, ``str``, ``int``, ``float`` or ``None``, the signature
+ expiration time. If ``None``, the expiration time will be the inception time plus
+ the value of the *lifetime* parameter. See the description of *inception* above
+ for how the various parameter types are interpreted.
+
+ *lifetime*, an ``int`` or ``None``, the signature lifetime in seconds. This
+ parameter is only meaningful if *expiration* is ``None``.
+
+ *verify*, a ``bool``. If set to ``True``, the signer will verify signatures
+ after they are created; the default is ``False``.
+
+ *policy*, a ``dns.dnssec.Policy`` or ``None``. If ``None``, the default policy,
+ ``dns.dnssec.default_policy`` is used; this policy defaults to that of RFC 8624.
+
+ *origin*, a ``dns.name.Name`` or ``None``. If ``None``, the default, then all
+ names in the rrset (including its owner name) must be absolute; otherwise the
+ specified origin will be used to make names absolute when signing.
+
+ *deterministic*, a ``bool``. If ``True``, the default, use deterministic
+ (reproducible) signatures when supported by the algorithm used for signing.
+ Currently, this only affects ECDSA.
+
+ Raises ``DeniedByPolicy`` if the signature is denied by policy.
+ """
+
+ if policy is None:
+ policy = default_policy
+ if not policy.ok_to_sign(dnskey):
+ raise DeniedByPolicy
+
+ if isinstance(rrset, tuple):
+ rdclass = rrset[1].rdclass
+ rdtype = rrset[1].rdtype
+ rrname = rrset[0]
+ original_ttl = rrset[1].ttl
+ else:
+ rdclass = rrset.rdclass
+ rdtype = rrset.rdtype
+ rrname = rrset.name
+ original_ttl = rrset.ttl
+
+ if inception is not None:
+ rrsig_inception = to_timestamp(inception)
+ else:
+ rrsig_inception = int(time.time())
+
+ if expiration is not None:
+ rrsig_expiration = to_timestamp(expiration)
+ elif lifetime is not None:
+ rrsig_expiration = rrsig_inception + lifetime
+ else:
+ raise ValueError("expiration or lifetime must be specified")
+
+ # Derelativize now because we need a correct labels length for the
+ # rrsig_template.
+ if origin is not None:
+ rrname = rrname.derelativize(origin)
+ labels = len(rrname) - 1
+
+ # Adjust labels appropriately for wildcards.
+ if rrname.is_wild():
+ labels -= 1
+
+ rrsig_template = RRSIG(
+ rdclass=rdclass,
+ rdtype=dns.rdatatype.RRSIG,
+ type_covered=rdtype,
+ algorithm=dnskey.algorithm,
+ labels=labels,
+ original_ttl=original_ttl,
+ expiration=rrsig_expiration,
+ inception=rrsig_inception,
+ key_tag=key_id(dnskey),
+ signer=signer,
+ signature=b"",
+ )
+
+ data = dns.dnssec._make_rrsig_signature_data(rrset, rrsig_template, origin)
+
+ # pylint: disable=possibly-used-before-assignment
+ if isinstance(private_key, GenericPrivateKey):
+ signing_key = private_key
+ else:
+ try:
+ private_cls = get_algorithm_cls_from_dnskey(dnskey)
+ signing_key = private_cls(key=private_key)
+ except UnsupportedAlgorithm:
+ raise TypeError("Unsupported key algorithm")
+
+ signature = signing_key.sign(data, verify, deterministic)
+
+ return cast(RRSIG, rrsig_template.replace(signature=signature))
+
+
+def _make_rrsig_signature_data(
+ rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+ rrsig: RRSIG,
+ origin: Optional[dns.name.Name] = None,
+) -> bytes:
+ """Create signature rdata.
+
+ *rrset*, the RRset to sign/validate. This can be a
+ ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+ tuple.
+
+ *rrsig*, a ``dns.rdata.Rdata``, the signature to validate, or the
+ signature template used when signing.
+
+ *origin*, a ``dns.name.Name`` or ``None``, the origin to use for relative
+ names.
+
+ Raises ``UnsupportedAlgorithm`` if the algorithm is recognized by
+ dnspython but not implemented.
+ """
+
+ if isinstance(origin, str):
+ origin = dns.name.from_text(origin, dns.name.root)
+
+ signer = rrsig.signer
+ if not signer.is_absolute():
+ if origin is None:
+ raise ValidationFailure("relative RR name without an origin specified")
+ signer = signer.derelativize(origin)
+
+ # For convenience, allow the rrset to be specified as a (name,
+ # rdataset) tuple as well as a proper rrset
+ rrname, rdataset = _get_rrname_rdataset(rrset)
+
+ data = b""
+ wire = rrsig.to_wire(origin=signer)
+ assert wire is not None # for mypy
+ data += wire[:18]
+ data += rrsig.signer.to_digestable(signer)
+
+ # Derelativize the name before considering labels.
+ if not rrname.is_absolute():
+ if origin is None:
+ raise ValidationFailure("relative RR name without an origin specified")
+ rrname = rrname.derelativize(origin)
+
+ name_len = len(rrname)
+ if rrname.is_wild() and rrsig.labels != name_len - 2:
+ raise ValidationFailure("wild owner name has wrong label length")
+ if name_len - 1 < rrsig.labels:
+ raise ValidationFailure("owner name longer than RRSIG labels")
+ elif rrsig.labels < name_len - 1:
+ suffix = rrname.split(rrsig.labels + 1)[1]
+ rrname = dns.name.from_text("*", suffix)
+ rrnamebuf = rrname.to_digestable()
+ rrfixed = struct.pack("!HHI", rdataset.rdtype, rdataset.rdclass, rrsig.original_ttl)
+ rdatas = [rdata.to_digestable(origin) for rdata in rdataset]
+ for rdata in sorted(rdatas):
+ data += rrnamebuf
+ data += rrfixed
+ rrlen = struct.pack("!H", len(rdata))
+ data += rrlen
+ data += rdata
+
+ return data
+
+
+def _make_dnskey(
+ public_key: PublicKey,
+ algorithm: Union[int, str],
+ flags: int = Flag.ZONE,
+ protocol: int = 3,
+) -> DNSKEY:
+ """Convert a public key to DNSKEY Rdata
+
+ *public_key*, a ``PublicKey`` (``GenericPublicKey`` or
+ ``cryptography.hazmat.primitives.asymmetric``) to convert.
+
+ *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm.
+
+ *flags*: DNSKEY flags field as an integer.
+
+ *protocol*: DNSKEY protocol field as an integer.
+
+ Raises ``ValueError`` if the specified key algorithm parameters are not
+ unsupported, ``TypeError`` if the key type is unsupported,
+ `UnsupportedAlgorithm` if the algorithm is unknown and
+ `AlgorithmKeyMismatch` if the algorithm does not match the key type.
+
+ Return DNSKEY ``Rdata``.
+ """
+
+ algorithm = Algorithm.make(algorithm)
+
+ # pylint: disable=possibly-used-before-assignment
+ if isinstance(public_key, GenericPublicKey):
+ return public_key.to_dnskey(flags=flags, protocol=protocol)
+ else:
+ public_cls = get_algorithm_cls(algorithm).public_cls
+ return public_cls(key=public_key).to_dnskey(flags=flags, protocol=protocol)
+
+
+def _make_cdnskey(
+ public_key: PublicKey,
+ algorithm: Union[int, str],
+ flags: int = Flag.ZONE,
+ protocol: int = 3,
+) -> CDNSKEY:
+ """Convert a public key to CDNSKEY Rdata
+
+ *public_key*, the public key to convert, a
+ ``cryptography.hazmat.primitives.asymmetric`` public key class applicable
+ for DNSSEC.
+
+ *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm.
+
+ *flags*: DNSKEY flags field as an integer.
+
+ *protocol*: DNSKEY protocol field as an integer.
+
+ Raises ``ValueError`` if the specified key algorithm parameters are not
+ unsupported, ``TypeError`` if the key type is unsupported,
+ `UnsupportedAlgorithm` if the algorithm is unknown and
+ `AlgorithmKeyMismatch` if the algorithm does not match the key type.
+
+ Return CDNSKEY ``Rdata``.
+ """
+
+ dnskey = _make_dnskey(public_key, algorithm, flags, protocol)
+
+ return CDNSKEY(
+ rdclass=dnskey.rdclass,
+ rdtype=dns.rdatatype.CDNSKEY,
+ flags=dnskey.flags,
+ protocol=dnskey.protocol,
+ algorithm=dnskey.algorithm,
+ key=dnskey.key,
+ )
+
+
+def nsec3_hash(
+ domain: Union[dns.name.Name, str],
+ salt: Optional[Union[str, bytes]],
+ iterations: int,
+ algorithm: Union[int, str],
+) -> str:
+ """
+ Calculate the NSEC3 hash, according to
+ https://tools.ietf.org/html/rfc5155#section-5
+
+ *domain*, a ``dns.name.Name`` or ``str``, the name to hash.
+
+ *salt*, a ``str``, ``bytes``, or ``None``, the hash salt. If a
+ string, it is decoded as a hex string.
+
+ *iterations*, an ``int``, the number of iterations.
+
+ *algorithm*, a ``str`` or ``int``, the hash algorithm.
+ The only defined algorithm is SHA1.
+
+ Returns a ``str``, the encoded NSEC3 hash.
+ """
+
+ b32_conversion = str.maketrans(
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", "0123456789ABCDEFGHIJKLMNOPQRSTUV"
+ )
+
+ try:
+ if isinstance(algorithm, str):
+ algorithm = NSEC3Hash[algorithm.upper()]
+ except Exception:
+ raise ValueError("Wrong hash algorithm (only SHA1 is supported)")
+
+ if algorithm != NSEC3Hash.SHA1:
+ raise ValueError("Wrong hash algorithm (only SHA1 is supported)")
+
+ if salt is None:
+ salt_encoded = b""
+ elif isinstance(salt, str):
+ if len(salt) % 2 == 0:
+ salt_encoded = bytes.fromhex(salt)
+ else:
+ raise ValueError("Invalid salt length")
+ else:
+ salt_encoded = salt
+
+ if not isinstance(domain, dns.name.Name):
+ domain = dns.name.from_text(domain)
+ domain_encoded = domain.canonicalize().to_wire()
+ assert domain_encoded is not None
+
+ digest = hashlib.sha1(domain_encoded + salt_encoded).digest()
+ for _ in range(iterations):
+ digest = hashlib.sha1(digest + salt_encoded).digest()
+
+ output = base64.b32encode(digest).decode("utf-8")
+ output = output.translate(b32_conversion)
+
+ return output
+
+
+def make_ds_rdataset(
+ rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+ algorithms: Set[Union[DSDigest, str]],
+ origin: Optional[dns.name.Name] = None,
+) -> dns.rdataset.Rdataset:
+ """Create a DS record from DNSKEY/CDNSKEY/CDS.
+
+ *rrset*, the RRset to create DS Rdataset for. This can be a
+ ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+ tuple.
+
+ *algorithms*, a set of ``str`` or ``int`` specifying the hash algorithms.
+ The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case
+ does not matter for these strings. If the RRset is a CDS, only digest
+ algorithms matching algorithms are accepted.
+
+ *origin*, a ``dns.name.Name`` or ``None``. If `key` is a relative name,
+ then it will be made absolute using the specified origin.
+
+ Raises ``UnsupportedAlgorithm`` if any of the algorithms are unknown and
+ ``ValueError`` if the given RRset is not usable.
+
+ Returns a ``dns.rdataset.Rdataset``
+ """
+
+ rrname, rdataset = _get_rrname_rdataset(rrset)
+
+ if rdataset.rdtype not in (
+ dns.rdatatype.DNSKEY,
+ dns.rdatatype.CDNSKEY,
+ dns.rdatatype.CDS,
+ ):
+ raise ValueError("rrset not a DNSKEY/CDNSKEY/CDS")
+
+ _algorithms = set()
+ for algorithm in algorithms:
+ try:
+ if isinstance(algorithm, str):
+ algorithm = DSDigest[algorithm.upper()]
+ except Exception:
+ raise UnsupportedAlgorithm(f'unsupported algorithm "{algorithm}"')
+ _algorithms.add(algorithm)
+
+ if rdataset.rdtype == dns.rdatatype.CDS:
+ res = []
+ for rdata in cds_rdataset_to_ds_rdataset(rdataset):
+ if rdata.digest_type in _algorithms:
+ res.append(rdata)
+ if len(res) == 0:
+ raise ValueError("no acceptable CDS rdata found")
+ return dns.rdataset.from_rdata_list(rdataset.ttl, res)
+
+ res = []
+ for algorithm in _algorithms:
+ res.extend(dnskey_rdataset_to_cds_rdataset(rrname, rdataset, algorithm, origin))
+ return dns.rdataset.from_rdata_list(rdataset.ttl, res)
+
+
+def cds_rdataset_to_ds_rdataset(
+ rdataset: dns.rdataset.Rdataset,
+) -> dns.rdataset.Rdataset:
+ """Create a CDS record from DS.
+
+ *rdataset*, a ``dns.rdataset.Rdataset``, to create DS Rdataset for.
+
+ Raises ``ValueError`` if the rdataset is not CDS.
+
+ Returns a ``dns.rdataset.Rdataset``
+ """
+
+ if rdataset.rdtype != dns.rdatatype.CDS:
+ raise ValueError("rdataset not a CDS")
+ res = []
+ for rdata in rdataset:
+ res.append(
+ CDS(
+ rdclass=rdata.rdclass,
+ rdtype=dns.rdatatype.DS,
+ key_tag=rdata.key_tag,
+ algorithm=rdata.algorithm,
+ digest_type=rdata.digest_type,
+ digest=rdata.digest,
+ )
+ )
+ return dns.rdataset.from_rdata_list(rdataset.ttl, res)
+
+
+def dnskey_rdataset_to_cds_rdataset(
+ name: Union[dns.name.Name, str],
+ rdataset: dns.rdataset.Rdataset,
+ algorithm: Union[DSDigest, str],
+ origin: Optional[dns.name.Name] = None,
+) -> dns.rdataset.Rdataset:
+ """Create a CDS record from DNSKEY/CDNSKEY.
+
+ *name*, a ``dns.name.Name`` or ``str``, the owner name of the CDS record.
+
+ *rdataset*, a ``dns.rdataset.Rdataset``, to create DS Rdataset for.
+
+ *algorithm*, a ``str`` or ``int`` specifying the hash algorithm.
+ The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case
+ does not matter for these strings.
+
+ *origin*, a ``dns.name.Name`` or ``None``. If `key` is a relative name,
+ then it will be made absolute using the specified origin.
+
+ Raises ``UnsupportedAlgorithm`` if the algorithm is unknown or
+ ``ValueError`` if the rdataset is not DNSKEY/CDNSKEY.
+
+ Returns a ``dns.rdataset.Rdataset``
+ """
+
+ if rdataset.rdtype not in (dns.rdatatype.DNSKEY, dns.rdatatype.CDNSKEY):
+ raise ValueError("rdataset not a DNSKEY/CDNSKEY")
+ res = []
+ for rdata in rdataset:
+ res.append(make_cds(name, rdata, algorithm, origin))
+ return dns.rdataset.from_rdata_list(rdataset.ttl, res)
+
+
+def dnskey_rdataset_to_cdnskey_rdataset(
+ rdataset: dns.rdataset.Rdataset,
+) -> dns.rdataset.Rdataset:
+ """Create a CDNSKEY record from DNSKEY.
+
+ *rdataset*, a ``dns.rdataset.Rdataset``, to create CDNSKEY Rdataset for.
+
+ Returns a ``dns.rdataset.Rdataset``
+ """
+
+ if rdataset.rdtype != dns.rdatatype.DNSKEY:
+ raise ValueError("rdataset not a DNSKEY")
+ res = []
+ for rdata in rdataset:
+ res.append(
+ CDNSKEY(
+ rdclass=rdataset.rdclass,
+ rdtype=rdataset.rdtype,
+ flags=rdata.flags,
+ protocol=rdata.protocol,
+ algorithm=rdata.algorithm,
+ key=rdata.key,
+ )
+ )
+ return dns.rdataset.from_rdata_list(rdataset.ttl, res)
+
+
+def default_rrset_signer(
+ txn: dns.transaction.Transaction,
+ rrset: dns.rrset.RRset,
+ signer: dns.name.Name,
+ ksks: List[Tuple[PrivateKey, DNSKEY]],
+ zsks: List[Tuple[PrivateKey, DNSKEY]],
+ inception: Optional[Union[datetime, str, int, float]] = None,
+ expiration: Optional[Union[datetime, str, int, float]] = None,
+ lifetime: Optional[int] = None,
+ policy: Optional[Policy] = None,
+ origin: Optional[dns.name.Name] = None,
+ deterministic: bool = True,
+) -> None:
+ """Default RRset signer"""
+
+ if rrset.rdtype in set(
+ [
+ dns.rdatatype.RdataType.DNSKEY,
+ dns.rdatatype.RdataType.CDS,
+ dns.rdatatype.RdataType.CDNSKEY,
+ ]
+ ):
+ keys = ksks
+ else:
+ keys = zsks
+
+ for private_key, dnskey in keys:
+ rrsig = dns.dnssec.sign(
+ rrset=rrset,
+ private_key=private_key,
+ dnskey=dnskey,
+ inception=inception,
+ expiration=expiration,
+ lifetime=lifetime,
+ signer=signer,
+ policy=policy,
+ origin=origin,
+ deterministic=deterministic,
+ )
+ txn.add(rrset.name, rrset.ttl, rrsig)
+
+
+def sign_zone(
+ zone: dns.zone.Zone,
+ txn: Optional[dns.transaction.Transaction] = None,
+ keys: Optional[List[Tuple[PrivateKey, DNSKEY]]] = None,
+ add_dnskey: bool = True,
+ dnskey_ttl: Optional[int] = None,
+ inception: Optional[Union[datetime, str, int, float]] = None,
+ expiration: Optional[Union[datetime, str, int, float]] = None,
+ lifetime: Optional[int] = None,
+ nsec3: Optional[NSEC3PARAM] = None,
+ rrset_signer: Optional[RRsetSigner] = None,
+ policy: Optional[Policy] = None,
+ deterministic: bool = True,
+) -> None:
+ """Sign zone.
+
+ *zone*, a ``dns.zone.Zone``, the zone to sign.
+
+ *txn*, a ``dns.transaction.Transaction``, an optional transaction to use for
+ signing.
+
+ *keys*, a list of (``PrivateKey``, ``DNSKEY``) tuples, to use for signing. KSK/ZSK
+ roles are assigned automatically if the SEP flag is used, otherwise all RRsets are
+ signed by all keys.
+
+ *add_dnskey*, a ``bool``. If ``True``, the default, all specified DNSKEYs are
+ automatically added to the zone on signing.
+
+ *dnskey_ttl*, a``int``, specifies the TTL for DNSKEY RRs. If not specified the TTL
+ of the existing DNSKEY RRset used or the TTL of the SOA RRset.
+
+ *inception*, a ``datetime``, ``str``, ``int``, ``float`` or ``None``, the signature
+ inception time. If ``None``, the current time is used. If a ``str``, the format is
+ "YYYYMMDDHHMMSS" or alternatively the number of seconds since the UNIX epoch in text
+ form; this is the same the RRSIG rdata's text form. Values of type `int` or `float`
+ are interpreted as seconds since the UNIX epoch.
+
+ *expiration*, a ``datetime``, ``str``, ``int``, ``float`` or ``None``, the signature
+ expiration time. If ``None``, the expiration time will be the inception time plus
+ the value of the *lifetime* parameter. See the description of *inception* above for
+ how the various parameter types are interpreted.
+
+ *lifetime*, an ``int`` or ``None``, the signature lifetime in seconds. This
+ parameter is only meaningful if *expiration* is ``None``.
+
+ *nsec3*, a ``NSEC3PARAM`` Rdata, configures signing using NSEC3. Not yet
+ implemented.
+
+ *rrset_signer*, a ``Callable``, an optional function for signing RRsets. The
+ function requires two arguments: transaction and RRset. If the not specified,
+ ``dns.dnssec.default_rrset_signer`` will be used.
+
+ *deterministic*, a ``bool``. If ``True``, the default, use deterministic
+ (reproducible) signatures when supported by the algorithm used for signing.
+ Currently, this only affects ECDSA.
+
+ Returns ``None``.
+ """
+
+ ksks = []
+ zsks = []
+
+ # if we have both KSKs and ZSKs, split by SEP flag. if not, sign all
+ # records with all keys
+ if keys:
+ for key in keys:
+ if key[1].flags & Flag.SEP:
+ ksks.append(key)
+ else:
+ zsks.append(key)
+ if not ksks:
+ ksks = keys
+ if not zsks:
+ zsks = keys
+ else:
+ keys = []
+
+ if txn:
+ cm: contextlib.AbstractContextManager = contextlib.nullcontext(txn)
+ else:
+ cm = zone.writer()
+
+ if zone.origin is None:
+ raise ValueError("no zone origin")
+
+ with cm as _txn:
+ if add_dnskey:
+ if dnskey_ttl is None:
+ dnskey = _txn.get(zone.origin, dns.rdatatype.DNSKEY)
+ if dnskey:
+ dnskey_ttl = dnskey.ttl
+ else:
+ soa = _txn.get(zone.origin, dns.rdatatype.SOA)
+ dnskey_ttl = soa.ttl
+ for _, dnskey in keys:
+ _txn.add(zone.origin, dnskey_ttl, dnskey)
+
+ if nsec3:
+ raise NotImplementedError("Signing with NSEC3 not yet implemented")
+ else:
+ _rrset_signer = rrset_signer or functools.partial(
+ default_rrset_signer,
+ signer=zone.origin,
+ ksks=ksks,
+ zsks=zsks,
+ inception=inception,
+ expiration=expiration,
+ lifetime=lifetime,
+ policy=policy,
+ origin=zone.origin,
+ deterministic=deterministic,
+ )
+ return _sign_zone_nsec(zone, _txn, _rrset_signer)
+
+
+def _sign_zone_nsec(
+ zone: dns.zone.Zone,
+ txn: dns.transaction.Transaction,
+ rrset_signer: Optional[RRsetSigner] = None,
+) -> None:
+ """NSEC zone signer"""
+
+ def _txn_add_nsec(
+ txn: dns.transaction.Transaction,
+ name: dns.name.Name,
+ next_secure: Optional[dns.name.Name],
+ rdclass: dns.rdataclass.RdataClass,
+ ttl: int,
+ rrset_signer: Optional[RRsetSigner] = None,
+ ) -> None:
+ """NSEC zone signer helper"""
+ mandatory_types = set(
+ [dns.rdatatype.RdataType.RRSIG, dns.rdatatype.RdataType.NSEC]
+ )
+ node = txn.get_node(name)
+ if node and next_secure:
+ types = (
+ set([rdataset.rdtype for rdataset in node.rdatasets]) | mandatory_types
+ )
+ windows = Bitmap.from_rdtypes(list(types))
+ rrset = dns.rrset.from_rdata(
+ name,
+ ttl,
+ NSEC(
+ rdclass=rdclass,
+ rdtype=dns.rdatatype.RdataType.NSEC,
+ next=next_secure,
+ windows=windows,
+ ),
+ )
+ txn.add(rrset)
+ if rrset_signer:
+ rrset_signer(txn, rrset)
+
+ rrsig_ttl = zone.get_soa().minimum
+ delegation = None
+ last_secure = None
+
+ for name in sorted(txn.iterate_names()):
+ if delegation and name.is_subdomain(delegation):
+ # names below delegations are not secure
+ continue
+ elif txn.get(name, dns.rdatatype.NS) and name != zone.origin:
+ # inside delegation
+ delegation = name
+ else:
+ # outside delegation
+ delegation = None
+
+ if rrset_signer:
+ node = txn.get_node(name)
+ if node:
+ for rdataset in node.rdatasets:
+ if rdataset.rdtype == dns.rdatatype.RRSIG:
+ # do not sign RRSIGs
+ continue
+ elif delegation and rdataset.rdtype != dns.rdatatype.DS:
+ # do not sign delegations except DS records
+ continue
+ else:
+ rrset = dns.rrset.from_rdata(name, rdataset.ttl, *rdataset)
+ rrset_signer(txn, rrset)
+
+ # We need "is not None" as the empty name is False because its length is 0.
+ if last_secure is not None:
+ _txn_add_nsec(txn, last_secure, name, zone.rdclass, rrsig_ttl, rrset_signer)
+ last_secure = name
+
+ if last_secure:
+ _txn_add_nsec(
+ txn, last_secure, zone.origin, zone.rdclass, rrsig_ttl, rrset_signer
+ )
+
+
+def _need_pyca(*args, **kwargs):
+ raise ImportError(
+ "DNSSEC validation requires python cryptography"
+ ) # pragma: no cover
+
+
+if dns._features.have("dnssec"):
+ from cryptography.exceptions import InvalidSignature
+ from cryptography.hazmat.primitives.asymmetric import dsa # pylint: disable=W0611
+ from cryptography.hazmat.primitives.asymmetric import ec # pylint: disable=W0611
+ from cryptography.hazmat.primitives.asymmetric import ed448 # pylint: disable=W0611
+ from cryptography.hazmat.primitives.asymmetric import rsa # pylint: disable=W0611
+ from cryptography.hazmat.primitives.asymmetric import ( # pylint: disable=W0611
+ ed25519,
+ )
+
+ from dns.dnssecalgs import ( # pylint: disable=C0412
+ get_algorithm_cls,
+ get_algorithm_cls_from_dnskey,
+ )
+ from dns.dnssecalgs.base import GenericPrivateKey, GenericPublicKey
+
+ validate = _validate # type: ignore
+ validate_rrsig = _validate_rrsig # type: ignore
+ sign = _sign
+ make_dnskey = _make_dnskey
+ make_cdnskey = _make_cdnskey
+ _have_pyca = True
+else: # pragma: no cover
+ validate = _need_pyca
+ validate_rrsig = _need_pyca
+ sign = _need_pyca
+ make_dnskey = _need_pyca
+ make_cdnskey = _need_pyca
+ _have_pyca = False
+
+### BEGIN generated Algorithm constants
+
+RSAMD5 = Algorithm.RSAMD5
+DH = Algorithm.DH
+DSA = Algorithm.DSA
+ECC = Algorithm.ECC
+RSASHA1 = Algorithm.RSASHA1
+DSANSEC3SHA1 = Algorithm.DSANSEC3SHA1
+RSASHA1NSEC3SHA1 = Algorithm.RSASHA1NSEC3SHA1
+RSASHA256 = Algorithm.RSASHA256
+RSASHA512 = Algorithm.RSASHA512
+ECCGOST = Algorithm.ECCGOST
+ECDSAP256SHA256 = Algorithm.ECDSAP256SHA256
+ECDSAP384SHA384 = Algorithm.ECDSAP384SHA384
+ED25519 = Algorithm.ED25519
+ED448 = Algorithm.ED448
+INDIRECT = Algorithm.INDIRECT
+PRIVATEDNS = Algorithm.PRIVATEDNS
+PRIVATEOID = Algorithm.PRIVATEOID
+
+### END generated Algorithm constants
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssecalgs/__init__.py b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/__init__.py
new file mode 100644
index 00000000..602367e3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/__init__.py
@@ -0,0 +1,121 @@
+from typing import Dict, Optional, Tuple, Type, Union
+
+import dns.name
+from dns.dnssecalgs.base import GenericPrivateKey
+from dns.dnssectypes import Algorithm
+from dns.exception import UnsupportedAlgorithm
+from dns.rdtypes.ANY.DNSKEY import DNSKEY
+
+if dns._features.have("dnssec"):
+ from dns.dnssecalgs.dsa import PrivateDSA, PrivateDSANSEC3SHA1
+ from dns.dnssecalgs.ecdsa import PrivateECDSAP256SHA256, PrivateECDSAP384SHA384
+ from dns.dnssecalgs.eddsa import PrivateED448, PrivateED25519
+ from dns.dnssecalgs.rsa import (
+ PrivateRSAMD5,
+ PrivateRSASHA1,
+ PrivateRSASHA1NSEC3SHA1,
+ PrivateRSASHA256,
+ PrivateRSASHA512,
+ )
+
+ _have_cryptography = True
+else:
+ _have_cryptography = False
+
+AlgorithmPrefix = Optional[Union[bytes, dns.name.Name]]
+
+algorithms: Dict[Tuple[Algorithm, AlgorithmPrefix], Type[GenericPrivateKey]] = {}
+if _have_cryptography:
+ # pylint: disable=possibly-used-before-assignment
+ algorithms.update(
+ {
+ (Algorithm.RSAMD5, None): PrivateRSAMD5,
+ (Algorithm.DSA, None): PrivateDSA,
+ (Algorithm.RSASHA1, None): PrivateRSASHA1,
+ (Algorithm.DSANSEC3SHA1, None): PrivateDSANSEC3SHA1,
+ (Algorithm.RSASHA1NSEC3SHA1, None): PrivateRSASHA1NSEC3SHA1,
+ (Algorithm.RSASHA256, None): PrivateRSASHA256,
+ (Algorithm.RSASHA512, None): PrivateRSASHA512,
+ (Algorithm.ECDSAP256SHA256, None): PrivateECDSAP256SHA256,
+ (Algorithm.ECDSAP384SHA384, None): PrivateECDSAP384SHA384,
+ (Algorithm.ED25519, None): PrivateED25519,
+ (Algorithm.ED448, None): PrivateED448,
+ }
+ )
+
+
+def get_algorithm_cls(
+ algorithm: Union[int, str], prefix: AlgorithmPrefix = None
+) -> Type[GenericPrivateKey]:
+ """Get Private Key class from Algorithm.
+
+ *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm.
+
+ Raises ``UnsupportedAlgorithm`` if the algorithm is unknown.
+
+ Returns a ``dns.dnssecalgs.GenericPrivateKey``
+ """
+ algorithm = Algorithm.make(algorithm)
+ cls = algorithms.get((algorithm, prefix))
+ if cls:
+ return cls
+ raise UnsupportedAlgorithm(
+ f'algorithm "{Algorithm.to_text(algorithm)}" not supported by dnspython'
+ )
+
+
+def get_algorithm_cls_from_dnskey(dnskey: DNSKEY) -> Type[GenericPrivateKey]:
+ """Get Private Key class from DNSKEY.
+
+ *dnskey*, a ``DNSKEY`` to get Algorithm class for.
+
+ Raises ``UnsupportedAlgorithm`` if the algorithm is unknown.
+
+ Returns a ``dns.dnssecalgs.GenericPrivateKey``
+ """
+ prefix: AlgorithmPrefix = None
+ if dnskey.algorithm == Algorithm.PRIVATEDNS:
+ prefix, _ = dns.name.from_wire(dnskey.key, 0)
+ elif dnskey.algorithm == Algorithm.PRIVATEOID:
+ length = int(dnskey.key[0])
+ prefix = dnskey.key[0 : length + 1]
+ return get_algorithm_cls(dnskey.algorithm, prefix)
+
+
+def register_algorithm_cls(
+ algorithm: Union[int, str],
+ algorithm_cls: Type[GenericPrivateKey],
+ name: Optional[Union[dns.name.Name, str]] = None,
+ oid: Optional[bytes] = None,
+) -> None:
+ """Register Algorithm Private Key class.
+
+ *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm.
+
+ *algorithm_cls*: A `GenericPrivateKey` class.
+
+ *name*, an optional ``dns.name.Name`` or ``str``, for for PRIVATEDNS algorithms.
+
+ *oid*: an optional BER-encoded `bytes` for PRIVATEOID algorithms.
+
+ Raises ``ValueError`` if a name or oid is specified incorrectly.
+ """
+ if not issubclass(algorithm_cls, GenericPrivateKey):
+ raise TypeError("Invalid algorithm class")
+ algorithm = Algorithm.make(algorithm)
+ prefix: AlgorithmPrefix = None
+ if algorithm == Algorithm.PRIVATEDNS:
+ if name is None:
+ raise ValueError("Name required for PRIVATEDNS algorithms")
+ if isinstance(name, str):
+ name = dns.name.from_text(name)
+ prefix = name
+ elif algorithm == Algorithm.PRIVATEOID:
+ if oid is None:
+ raise ValueError("OID required for PRIVATEOID algorithms")
+ prefix = bytes([len(oid)]) + oid
+ elif name:
+ raise ValueError("Name only supported for PRIVATEDNS algorithm")
+ elif oid:
+ raise ValueError("OID only supported for PRIVATEOID algorithm")
+ algorithms[(algorithm, prefix)] = algorithm_cls
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssecalgs/base.py b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/base.py
new file mode 100644
index 00000000..752ee480
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/base.py
@@ -0,0 +1,89 @@
+from abc import ABC, abstractmethod # pylint: disable=no-name-in-module
+from typing import Any, Optional, Type
+
+import dns.rdataclass
+import dns.rdatatype
+from dns.dnssectypes import Algorithm
+from dns.exception import AlgorithmKeyMismatch
+from dns.rdtypes.ANY.DNSKEY import DNSKEY
+from dns.rdtypes.dnskeybase import Flag
+
+
+class GenericPublicKey(ABC):
+ algorithm: Algorithm
+
+ @abstractmethod
+ def __init__(self, key: Any) -> None:
+ pass
+
+ @abstractmethod
+ def verify(self, signature: bytes, data: bytes) -> None:
+ """Verify signed DNSSEC data"""
+
+ @abstractmethod
+ def encode_key_bytes(self) -> bytes:
+ """Encode key as bytes for DNSKEY"""
+
+ @classmethod
+ def _ensure_algorithm_key_combination(cls, key: DNSKEY) -> None:
+ if key.algorithm != cls.algorithm:
+ raise AlgorithmKeyMismatch
+
+ def to_dnskey(self, flags: int = Flag.ZONE, protocol: int = 3) -> DNSKEY:
+ """Return public key as DNSKEY"""
+ return DNSKEY(
+ rdclass=dns.rdataclass.IN,
+ rdtype=dns.rdatatype.DNSKEY,
+ flags=flags,
+ protocol=protocol,
+ algorithm=self.algorithm,
+ key=self.encode_key_bytes(),
+ )
+
+ @classmethod
+ @abstractmethod
+ def from_dnskey(cls, key: DNSKEY) -> "GenericPublicKey":
+ """Create public key from DNSKEY"""
+
+ @classmethod
+ @abstractmethod
+ def from_pem(cls, public_pem: bytes) -> "GenericPublicKey":
+ """Create public key from PEM-encoded SubjectPublicKeyInfo as specified
+ in RFC 5280"""
+
+ @abstractmethod
+ def to_pem(self) -> bytes:
+ """Return public-key as PEM-encoded SubjectPublicKeyInfo as specified
+ in RFC 5280"""
+
+
+class GenericPrivateKey(ABC):
+ public_cls: Type[GenericPublicKey]
+
+ @abstractmethod
+ def __init__(self, key: Any) -> None:
+ pass
+
+ @abstractmethod
+ def sign(
+ self,
+ data: bytes,
+ verify: bool = False,
+ deterministic: bool = True,
+ ) -> bytes:
+ """Sign DNSSEC data"""
+
+ @abstractmethod
+ def public_key(self) -> "GenericPublicKey":
+ """Return public key instance"""
+
+ @classmethod
+ @abstractmethod
+ def from_pem(
+ cls, private_pem: bytes, password: Optional[bytes] = None
+ ) -> "GenericPrivateKey":
+ """Create private key from PEM-encoded PKCS#8"""
+
+ @abstractmethod
+ def to_pem(self, password: Optional[bytes] = None) -> bytes:
+ """Return private key as PEM-encoded PKCS#8"""
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssecalgs/cryptography.py b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/cryptography.py
new file mode 100644
index 00000000..5a31a812
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/cryptography.py
@@ -0,0 +1,68 @@
+from typing import Any, Optional, Type
+
+from cryptography.hazmat.primitives import serialization
+
+from dns.dnssecalgs.base import GenericPrivateKey, GenericPublicKey
+from dns.exception import AlgorithmKeyMismatch
+
+
+class CryptographyPublicKey(GenericPublicKey):
+ key: Any = None
+ key_cls: Any = None
+
+ def __init__(self, key: Any) -> None: # pylint: disable=super-init-not-called
+ if self.key_cls is None:
+ raise TypeError("Undefined private key class")
+ if not isinstance( # pylint: disable=isinstance-second-argument-not-valid-type
+ key, self.key_cls
+ ):
+ raise AlgorithmKeyMismatch
+ self.key = key
+
+ @classmethod
+ def from_pem(cls, public_pem: bytes) -> "GenericPublicKey":
+ key = serialization.load_pem_public_key(public_pem)
+ return cls(key=key)
+
+ def to_pem(self) -> bytes:
+ return self.key.public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+
+
+class CryptographyPrivateKey(GenericPrivateKey):
+ key: Any = None
+ key_cls: Any = None
+ public_cls: Type[CryptographyPublicKey]
+
+ def __init__(self, key: Any) -> None: # pylint: disable=super-init-not-called
+ if self.key_cls is None:
+ raise TypeError("Undefined private key class")
+ if not isinstance( # pylint: disable=isinstance-second-argument-not-valid-type
+ key, self.key_cls
+ ):
+ raise AlgorithmKeyMismatch
+ self.key = key
+
+ def public_key(self) -> "CryptographyPublicKey":
+ return self.public_cls(key=self.key.public_key())
+
+ @classmethod
+ def from_pem(
+ cls, private_pem: bytes, password: Optional[bytes] = None
+ ) -> "GenericPrivateKey":
+ key = serialization.load_pem_private_key(private_pem, password=password)
+ return cls(key=key)
+
+ def to_pem(self, password: Optional[bytes] = None) -> bytes:
+ encryption_algorithm: serialization.KeySerializationEncryption
+ if password:
+ encryption_algorithm = serialization.BestAvailableEncryption(password)
+ else:
+ encryption_algorithm = serialization.NoEncryption()
+ return self.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=encryption_algorithm,
+ )
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssecalgs/dsa.py b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/dsa.py
new file mode 100644
index 00000000..adca3def
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/dsa.py
@@ -0,0 +1,106 @@
+import struct
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import dsa, utils
+
+from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey
+from dns.dnssectypes import Algorithm
+from dns.rdtypes.ANY.DNSKEY import DNSKEY
+
+
+class PublicDSA(CryptographyPublicKey):
+ key: dsa.DSAPublicKey
+ key_cls = dsa.DSAPublicKey
+ algorithm = Algorithm.DSA
+ chosen_hash = hashes.SHA1()
+
+ def verify(self, signature: bytes, data: bytes) -> None:
+ sig_r = signature[1:21]
+ sig_s = signature[21:]
+ sig = utils.encode_dss_signature(
+ int.from_bytes(sig_r, "big"), int.from_bytes(sig_s, "big")
+ )
+ self.key.verify(sig, data, self.chosen_hash)
+
+ def encode_key_bytes(self) -> bytes:
+ """Encode a public key per RFC 2536, section 2."""
+ pn = self.key.public_numbers()
+ dsa_t = (self.key.key_size // 8 - 64) // 8
+ if dsa_t > 8:
+ raise ValueError("unsupported DSA key size")
+ octets = 64 + dsa_t * 8
+ res = struct.pack("!B", dsa_t)
+ res += pn.parameter_numbers.q.to_bytes(20, "big")
+ res += pn.parameter_numbers.p.to_bytes(octets, "big")
+ res += pn.parameter_numbers.g.to_bytes(octets, "big")
+ res += pn.y.to_bytes(octets, "big")
+ return res
+
+ @classmethod
+ def from_dnskey(cls, key: DNSKEY) -> "PublicDSA":
+ cls._ensure_algorithm_key_combination(key)
+ keyptr = key.key
+ (t,) = struct.unpack("!B", keyptr[0:1])
+ keyptr = keyptr[1:]
+ octets = 64 + t * 8
+ dsa_q = keyptr[0:20]
+ keyptr = keyptr[20:]
+ dsa_p = keyptr[0:octets]
+ keyptr = keyptr[octets:]
+ dsa_g = keyptr[0:octets]
+ keyptr = keyptr[octets:]
+ dsa_y = keyptr[0:octets]
+ return cls(
+ key=dsa.DSAPublicNumbers( # type: ignore
+ int.from_bytes(dsa_y, "big"),
+ dsa.DSAParameterNumbers(
+ int.from_bytes(dsa_p, "big"),
+ int.from_bytes(dsa_q, "big"),
+ int.from_bytes(dsa_g, "big"),
+ ),
+ ).public_key(default_backend()),
+ )
+
+
+class PrivateDSA(CryptographyPrivateKey):
+ key: dsa.DSAPrivateKey
+ key_cls = dsa.DSAPrivateKey
+ public_cls = PublicDSA
+
+ def sign(
+ self,
+ data: bytes,
+ verify: bool = False,
+ deterministic: bool = True,
+ ) -> bytes:
+ """Sign using a private key per RFC 2536, section 3."""
+ public_dsa_key = self.key.public_key()
+ if public_dsa_key.key_size > 1024:
+ raise ValueError("DSA key size overflow")
+ der_signature = self.key.sign(data, self.public_cls.chosen_hash)
+ dsa_r, dsa_s = utils.decode_dss_signature(der_signature)
+ dsa_t = (public_dsa_key.key_size // 8 - 64) // 8
+ octets = 20
+ signature = (
+ struct.pack("!B", dsa_t)
+ + int.to_bytes(dsa_r, length=octets, byteorder="big")
+ + int.to_bytes(dsa_s, length=octets, byteorder="big")
+ )
+ if verify:
+ self.public_key().verify(signature, data)
+ return signature
+
+ @classmethod
+ def generate(cls, key_size: int) -> "PrivateDSA":
+ return cls(
+ key=dsa.generate_private_key(key_size=key_size),
+ )
+
+
+class PublicDSANSEC3SHA1(PublicDSA):
+ algorithm = Algorithm.DSANSEC3SHA1
+
+
+class PrivateDSANSEC3SHA1(PrivateDSA):
+ public_cls = PublicDSANSEC3SHA1
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssecalgs/ecdsa.py b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/ecdsa.py
new file mode 100644
index 00000000..86d5764c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/ecdsa.py
@@ -0,0 +1,97 @@
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import ec, utils
+
+from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey
+from dns.dnssectypes import Algorithm
+from dns.rdtypes.ANY.DNSKEY import DNSKEY
+
+
+class PublicECDSA(CryptographyPublicKey):
+ key: ec.EllipticCurvePublicKey
+ key_cls = ec.EllipticCurvePublicKey
+ algorithm: Algorithm
+ chosen_hash: hashes.HashAlgorithm
+ curve: ec.EllipticCurve
+ octets: int
+
+ def verify(self, signature: bytes, data: bytes) -> None:
+ sig_r = signature[0 : self.octets]
+ sig_s = signature[self.octets :]
+ sig = utils.encode_dss_signature(
+ int.from_bytes(sig_r, "big"), int.from_bytes(sig_s, "big")
+ )
+ self.key.verify(sig, data, ec.ECDSA(self.chosen_hash))
+
+ def encode_key_bytes(self) -> bytes:
+ """Encode a public key per RFC 6605, section 4."""
+ pn = self.key.public_numbers()
+ return pn.x.to_bytes(self.octets, "big") + pn.y.to_bytes(self.octets, "big")
+
+ @classmethod
+ def from_dnskey(cls, key: DNSKEY) -> "PublicECDSA":
+ cls._ensure_algorithm_key_combination(key)
+ ecdsa_x = key.key[0 : cls.octets]
+ ecdsa_y = key.key[cls.octets : cls.octets * 2]
+ return cls(
+ key=ec.EllipticCurvePublicNumbers(
+ curve=cls.curve,
+ x=int.from_bytes(ecdsa_x, "big"),
+ y=int.from_bytes(ecdsa_y, "big"),
+ ).public_key(default_backend()),
+ )
+
+
+class PrivateECDSA(CryptographyPrivateKey):
+ key: ec.EllipticCurvePrivateKey
+ key_cls = ec.EllipticCurvePrivateKey
+ public_cls = PublicECDSA
+
+ def sign(
+ self,
+ data: bytes,
+ verify: bool = False,
+ deterministic: bool = True,
+ ) -> bytes:
+ """Sign using a private key per RFC 6605, section 4."""
+ algorithm = ec.ECDSA(
+ self.public_cls.chosen_hash, deterministic_signing=deterministic
+ )
+ der_signature = self.key.sign(data, algorithm)
+ dsa_r, dsa_s = utils.decode_dss_signature(der_signature)
+ signature = int.to_bytes(
+ dsa_r, length=self.public_cls.octets, byteorder="big"
+ ) + int.to_bytes(dsa_s, length=self.public_cls.octets, byteorder="big")
+ if verify:
+ self.public_key().verify(signature, data)
+ return signature
+
+ @classmethod
+ def generate(cls) -> "PrivateECDSA":
+ return cls(
+ key=ec.generate_private_key(
+ curve=cls.public_cls.curve, backend=default_backend()
+ ),
+ )
+
+
+class PublicECDSAP256SHA256(PublicECDSA):
+ algorithm = Algorithm.ECDSAP256SHA256
+ chosen_hash = hashes.SHA256()
+ curve = ec.SECP256R1()
+ octets = 32
+
+
+class PrivateECDSAP256SHA256(PrivateECDSA):
+ public_cls = PublicECDSAP256SHA256
+
+
+class PublicECDSAP384SHA384(PublicECDSA):
+ algorithm = Algorithm.ECDSAP384SHA384
+ chosen_hash = hashes.SHA384()
+ curve = ec.SECP384R1()
+ octets = 48
+
+
+class PrivateECDSAP384SHA384(PrivateECDSA):
+ public_cls = PublicECDSAP384SHA384
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssecalgs/eddsa.py b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/eddsa.py
new file mode 100644
index 00000000..604bcbfe
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/eddsa.py
@@ -0,0 +1,70 @@
+from typing import Type
+
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import ed448, ed25519
+
+from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey
+from dns.dnssectypes import Algorithm
+from dns.rdtypes.ANY.DNSKEY import DNSKEY
+
+
+class PublicEDDSA(CryptographyPublicKey):
+ def verify(self, signature: bytes, data: bytes) -> None:
+ self.key.verify(signature, data)
+
+ def encode_key_bytes(self) -> bytes:
+ """Encode a public key per RFC 8080, section 3."""
+ return self.key.public_bytes(
+ encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
+ )
+
+ @classmethod
+ def from_dnskey(cls, key: DNSKEY) -> "PublicEDDSA":
+ cls._ensure_algorithm_key_combination(key)
+ return cls(
+ key=cls.key_cls.from_public_bytes(key.key),
+ )
+
+
+class PrivateEDDSA(CryptographyPrivateKey):
+ public_cls: Type[PublicEDDSA]
+
+ def sign(
+ self,
+ data: bytes,
+ verify: bool = False,
+ deterministic: bool = True,
+ ) -> bytes:
+ """Sign using a private key per RFC 8080, section 4."""
+ signature = self.key.sign(data)
+ if verify:
+ self.public_key().verify(signature, data)
+ return signature
+
+ @classmethod
+ def generate(cls) -> "PrivateEDDSA":
+ return cls(key=cls.key_cls.generate())
+
+
+class PublicED25519(PublicEDDSA):
+ key: ed25519.Ed25519PublicKey
+ key_cls = ed25519.Ed25519PublicKey
+ algorithm = Algorithm.ED25519
+
+
+class PrivateED25519(PrivateEDDSA):
+ key: ed25519.Ed25519PrivateKey
+ key_cls = ed25519.Ed25519PrivateKey
+ public_cls = PublicED25519
+
+
+class PublicED448(PublicEDDSA):
+ key: ed448.Ed448PublicKey
+ key_cls = ed448.Ed448PublicKey
+ algorithm = Algorithm.ED448
+
+
+class PrivateED448(PrivateEDDSA):
+ key: ed448.Ed448PrivateKey
+ key_cls = ed448.Ed448PrivateKey
+ public_cls = PublicED448
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssecalgs/rsa.py b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/rsa.py
new file mode 100644
index 00000000..27537aad
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssecalgs/rsa.py
@@ -0,0 +1,124 @@
+import math
+import struct
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import padding, rsa
+
+from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey
+from dns.dnssectypes import Algorithm
+from dns.rdtypes.ANY.DNSKEY import DNSKEY
+
+
+class PublicRSA(CryptographyPublicKey):
+ key: rsa.RSAPublicKey
+ key_cls = rsa.RSAPublicKey
+ algorithm: Algorithm
+ chosen_hash: hashes.HashAlgorithm
+
+ def verify(self, signature: bytes, data: bytes) -> None:
+ self.key.verify(signature, data, padding.PKCS1v15(), self.chosen_hash)
+
+ def encode_key_bytes(self) -> bytes:
+ """Encode a public key per RFC 3110, section 2."""
+ pn = self.key.public_numbers()
+ _exp_len = math.ceil(int.bit_length(pn.e) / 8)
+ exp = int.to_bytes(pn.e, length=_exp_len, byteorder="big")
+ if _exp_len > 255:
+ exp_header = b"\0" + struct.pack("!H", _exp_len)
+ else:
+ exp_header = struct.pack("!B", _exp_len)
+ if pn.n.bit_length() < 512 or pn.n.bit_length() > 4096:
+ raise ValueError("unsupported RSA key length")
+ return exp_header + exp + pn.n.to_bytes((pn.n.bit_length() + 7) // 8, "big")
+
+ @classmethod
+ def from_dnskey(cls, key: DNSKEY) -> "PublicRSA":
+ cls._ensure_algorithm_key_combination(key)
+ keyptr = key.key
+ (bytes_,) = struct.unpack("!B", keyptr[0:1])
+ keyptr = keyptr[1:]
+ if bytes_ == 0:
+ (bytes_,) = struct.unpack("!H", keyptr[0:2])
+ keyptr = keyptr[2:]
+ rsa_e = keyptr[0:bytes_]
+ rsa_n = keyptr[bytes_:]
+ return cls(
+ key=rsa.RSAPublicNumbers(
+ int.from_bytes(rsa_e, "big"), int.from_bytes(rsa_n, "big")
+ ).public_key(default_backend())
+ )
+
+
+class PrivateRSA(CryptographyPrivateKey):
+ key: rsa.RSAPrivateKey
+ key_cls = rsa.RSAPrivateKey
+ public_cls = PublicRSA
+ default_public_exponent = 65537
+
+ def sign(
+ self,
+ data: bytes,
+ verify: bool = False,
+ deterministic: bool = True,
+ ) -> bytes:
+ """Sign using a private key per RFC 3110, section 3."""
+ signature = self.key.sign(data, padding.PKCS1v15(), self.public_cls.chosen_hash)
+ if verify:
+ self.public_key().verify(signature, data)
+ return signature
+
+ @classmethod
+ def generate(cls, key_size: int) -> "PrivateRSA":
+ return cls(
+ key=rsa.generate_private_key(
+ public_exponent=cls.default_public_exponent,
+ key_size=key_size,
+ backend=default_backend(),
+ )
+ )
+
+
+class PublicRSAMD5(PublicRSA):
+ algorithm = Algorithm.RSAMD5
+ chosen_hash = hashes.MD5()
+
+
+class PrivateRSAMD5(PrivateRSA):
+ public_cls = PublicRSAMD5
+
+
+class PublicRSASHA1(PublicRSA):
+ algorithm = Algorithm.RSASHA1
+ chosen_hash = hashes.SHA1()
+
+
+class PrivateRSASHA1(PrivateRSA):
+ public_cls = PublicRSASHA1
+
+
+class PublicRSASHA1NSEC3SHA1(PublicRSA):
+ algorithm = Algorithm.RSASHA1NSEC3SHA1
+ chosen_hash = hashes.SHA1()
+
+
+class PrivateRSASHA1NSEC3SHA1(PrivateRSA):
+ public_cls = PublicRSASHA1NSEC3SHA1
+
+
+class PublicRSASHA256(PublicRSA):
+ algorithm = Algorithm.RSASHA256
+ chosen_hash = hashes.SHA256()
+
+
+class PrivateRSASHA256(PrivateRSA):
+ public_cls = PublicRSASHA256
+
+
+class PublicRSASHA512(PublicRSA):
+ algorithm = Algorithm.RSASHA512
+ chosen_hash = hashes.SHA512()
+
+
+class PrivateRSASHA512(PrivateRSA):
+ public_cls = PublicRSASHA512
diff --git a/.venv/lib/python3.12/site-packages/dns/dnssectypes.py b/.venv/lib/python3.12/site-packages/dns/dnssectypes.py
new file mode 100644
index 00000000..02131e0a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/dnssectypes.py
@@ -0,0 +1,71 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Common DNSSEC-related types."""
+
+# This is a separate file to avoid import circularity between dns.dnssec and
+# the implementations of the DS and DNSKEY types.
+
+import dns.enum
+
+
+class Algorithm(dns.enum.IntEnum):
+ RSAMD5 = 1
+ DH = 2
+ DSA = 3
+ ECC = 4
+ RSASHA1 = 5
+ DSANSEC3SHA1 = 6
+ RSASHA1NSEC3SHA1 = 7
+ RSASHA256 = 8
+ RSASHA512 = 10
+ ECCGOST = 12
+ ECDSAP256SHA256 = 13
+ ECDSAP384SHA384 = 14
+ ED25519 = 15
+ ED448 = 16
+ INDIRECT = 252
+ PRIVATEDNS = 253
+ PRIVATEOID = 254
+
+ @classmethod
+ def _maximum(cls):
+ return 255
+
+
+class DSDigest(dns.enum.IntEnum):
+ """DNSSEC Delegation Signer Digest Algorithm"""
+
+ NULL = 0
+ SHA1 = 1
+ SHA256 = 2
+ GOST = 3
+ SHA384 = 4
+
+ @classmethod
+ def _maximum(cls):
+ return 255
+
+
+class NSEC3Hash(dns.enum.IntEnum):
+ """NSEC3 hash algorithm"""
+
+ SHA1 = 1
+
+ @classmethod
+ def _maximum(cls):
+ return 255
diff --git a/.venv/lib/python3.12/site-packages/dns/e164.py b/.venv/lib/python3.12/site-packages/dns/e164.py
new file mode 100644
index 00000000..453736d4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/e164.py
@@ -0,0 +1,116 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS E.164 helpers."""
+
+from typing import Iterable, Optional, Union
+
+import dns.exception
+import dns.name
+import dns.resolver
+
+#: The public E.164 domain.
+public_enum_domain = dns.name.from_text("e164.arpa.")
+
+
+def from_e164(
+ text: str, origin: Optional[dns.name.Name] = public_enum_domain
+) -> dns.name.Name:
+ """Convert an E.164 number in textual form into a Name object whose
+ value is the ENUM domain name for that number.
+
+ Non-digits in the text are ignored, i.e. "16505551212",
+ "+1.650.555.1212" and "1 (650) 555-1212" are all the same.
+
+ *text*, a ``str``, is an E.164 number in textual form.
+
+ *origin*, a ``dns.name.Name``, the domain in which the number
+ should be constructed. The default is ``e164.arpa.``.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ parts = [d for d in text if d.isdigit()]
+ parts.reverse()
+ return dns.name.from_text(".".join(parts), origin=origin)
+
+
+def to_e164(
+ name: dns.name.Name,
+ origin: Optional[dns.name.Name] = public_enum_domain,
+ want_plus_prefix: bool = True,
+) -> str:
+ """Convert an ENUM domain name into an E.164 number.
+
+ Note that dnspython does not have any information about preferred
+ number formats within national numbering plans, so all numbers are
+ emitted as a simple string of digits, prefixed by a '+' (unless
+ *want_plus_prefix* is ``False``).
+
+ *name* is a ``dns.name.Name``, the ENUM domain name.
+
+ *origin* is a ``dns.name.Name``, a domain containing the ENUM
+ domain name. The name is relativized to this domain before being
+ converted to text. If ``None``, no relativization is done.
+
+ *want_plus_prefix* is a ``bool``. If True, add a '+' to the beginning of
+ the returned number.
+
+ Returns a ``str``.
+
+ """
+ if origin is not None:
+ name = name.relativize(origin)
+ dlabels = [d for d in name.labels if d.isdigit() and len(d) == 1]
+ if len(dlabels) != len(name.labels):
+ raise dns.exception.SyntaxError("non-digit labels in ENUM domain name")
+ dlabels.reverse()
+ text = b"".join(dlabels)
+ if want_plus_prefix:
+ text = b"+" + text
+ return text.decode()
+
+
+def query(
+ number: str,
+ domains: Iterable[Union[dns.name.Name, str]],
+ resolver: Optional[dns.resolver.Resolver] = None,
+) -> dns.resolver.Answer:
+ """Look for NAPTR RRs for the specified number in the specified domains.
+
+ e.g. lookup('16505551212', ['e164.dnspython.org.', 'e164.arpa.'])
+
+ *number*, a ``str`` is the number to look for.
+
+ *domains* is an iterable containing ``dns.name.Name`` values.
+
+ *resolver*, a ``dns.resolver.Resolver``, is the resolver to use. If
+ ``None``, the default resolver is used.
+ """
+
+ if resolver is None:
+ resolver = dns.resolver.get_default_resolver()
+ e_nx = dns.resolver.NXDOMAIN()
+ for domain in domains:
+ if isinstance(domain, str):
+ domain = dns.name.from_text(domain)
+ qname = dns.e164.from_e164(number, domain)
+ try:
+ return resolver.resolve(qname, "NAPTR")
+ except dns.resolver.NXDOMAIN as e:
+ e_nx += e
+ raise e_nx
diff --git a/.venv/lib/python3.12/site-packages/dns/edns.py b/.venv/lib/python3.12/site-packages/dns/edns.py
new file mode 100644
index 00000000..f7d9ff99
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/edns.py
@@ -0,0 +1,572 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2009-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""EDNS Options"""
+
+import binascii
+import math
+import socket
+import struct
+from typing import Any, Dict, Optional, Union
+
+import dns.enum
+import dns.inet
+import dns.rdata
+import dns.wire
+
+
+class OptionType(dns.enum.IntEnum):
+ #: NSID
+ NSID = 3
+ #: DAU
+ DAU = 5
+ #: DHU
+ DHU = 6
+ #: N3U
+ N3U = 7
+ #: ECS (client-subnet)
+ ECS = 8
+ #: EXPIRE
+ EXPIRE = 9
+ #: COOKIE
+ COOKIE = 10
+ #: KEEPALIVE
+ KEEPALIVE = 11
+ #: PADDING
+ PADDING = 12
+ #: CHAIN
+ CHAIN = 13
+ #: EDE (extended-dns-error)
+ EDE = 15
+ #: REPORTCHANNEL
+ REPORTCHANNEL = 18
+
+ @classmethod
+ def _maximum(cls):
+ return 65535
+
+
+class Option:
+ """Base class for all EDNS option types."""
+
+ def __init__(self, otype: Union[OptionType, str]):
+ """Initialize an option.
+
+ *otype*, a ``dns.edns.OptionType``, is the option type.
+ """
+ self.otype = OptionType.make(otype)
+
+ def to_wire(self, file: Optional[Any] = None) -> Optional[bytes]:
+ """Convert an option to wire format.
+
+ Returns a ``bytes`` or ``None``.
+
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def to_text(self) -> str:
+ raise NotImplementedError # pragma: no cover
+
+ @classmethod
+ def from_wire_parser(cls, otype: OptionType, parser: "dns.wire.Parser") -> "Option":
+ """Build an EDNS option object from wire format.
+
+ *otype*, a ``dns.edns.OptionType``, is the option type.
+
+ *parser*, a ``dns.wire.Parser``, the parser, which should be
+ restructed to the option length.
+
+ Returns a ``dns.edns.Option``.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def _cmp(self, other):
+ """Compare an EDNS option with another option of the same type.
+
+ Returns < 0 if < *other*, 0 if == *other*, and > 0 if > *other*.
+ """
+ wire = self.to_wire()
+ owire = other.to_wire()
+ if wire == owire:
+ return 0
+ if wire > owire:
+ return 1
+ return -1
+
+ def __eq__(self, other):
+ if not isinstance(other, Option):
+ return False
+ if self.otype != other.otype:
+ return False
+ return self._cmp(other) == 0
+
+ def __ne__(self, other):
+ if not isinstance(other, Option):
+ return True
+ if self.otype != other.otype:
+ return True
+ return self._cmp(other) != 0
+
+ def __lt__(self, other):
+ if not isinstance(other, Option) or self.otype != other.otype:
+ return NotImplemented
+ return self._cmp(other) < 0
+
+ def __le__(self, other):
+ if not isinstance(other, Option) or self.otype != other.otype:
+ return NotImplemented
+ return self._cmp(other) <= 0
+
+ def __ge__(self, other):
+ if not isinstance(other, Option) or self.otype != other.otype:
+ return NotImplemented
+ return self._cmp(other) >= 0
+
+ def __gt__(self, other):
+ if not isinstance(other, Option) or self.otype != other.otype:
+ return NotImplemented
+ return self._cmp(other) > 0
+
+ def __str__(self):
+ return self.to_text()
+
+
+class GenericOption(Option): # lgtm[py/missing-equals]
+ """Generic Option Class
+
+ This class is used for EDNS option types for which we have no better
+ implementation.
+ """
+
+ def __init__(self, otype: Union[OptionType, str], data: Union[bytes, str]):
+ super().__init__(otype)
+ self.data = dns.rdata.Rdata._as_bytes(data, True)
+
+ def to_wire(self, file: Optional[Any] = None) -> Optional[bytes]:
+ if file:
+ file.write(self.data)
+ return None
+ else:
+ return self.data
+
+ def to_text(self) -> str:
+ return "Generic %d" % self.otype
+
+ @classmethod
+ def from_wire_parser(
+ cls, otype: Union[OptionType, str], parser: "dns.wire.Parser"
+ ) -> Option:
+ return cls(otype, parser.get_remaining())
+
+
+class ECSOption(Option): # lgtm[py/missing-equals]
+ """EDNS Client Subnet (ECS, RFC7871)"""
+
+ def __init__(self, address: str, srclen: Optional[int] = None, scopelen: int = 0):
+ """*address*, a ``str``, is the client address information.
+
+ *srclen*, an ``int``, the source prefix length, which is the
+ leftmost number of bits of the address to be used for the
+ lookup. The default is 24 for IPv4 and 56 for IPv6.
+
+ *scopelen*, an ``int``, the scope prefix length. This value
+ must be 0 in queries, and should be set in responses.
+ """
+
+ super().__init__(OptionType.ECS)
+ af = dns.inet.af_for_address(address)
+
+ if af == socket.AF_INET6:
+ self.family = 2
+ if srclen is None:
+ srclen = 56
+ address = dns.rdata.Rdata._as_ipv6_address(address)
+ srclen = dns.rdata.Rdata._as_int(srclen, 0, 128)
+ scopelen = dns.rdata.Rdata._as_int(scopelen, 0, 128)
+ elif af == socket.AF_INET:
+ self.family = 1
+ if srclen is None:
+ srclen = 24
+ address = dns.rdata.Rdata._as_ipv4_address(address)
+ srclen = dns.rdata.Rdata._as_int(srclen, 0, 32)
+ scopelen = dns.rdata.Rdata._as_int(scopelen, 0, 32)
+ else: # pragma: no cover (this will never happen)
+ raise ValueError("Bad address family")
+
+ assert srclen is not None
+ self.address = address
+ self.srclen = srclen
+ self.scopelen = scopelen
+
+ addrdata = dns.inet.inet_pton(af, address)
+ nbytes = int(math.ceil(srclen / 8.0))
+
+ # Truncate to srclen and pad to the end of the last octet needed
+ # See RFC section 6
+ self.addrdata = addrdata[:nbytes]
+ nbits = srclen % 8
+ if nbits != 0:
+ last = struct.pack("B", ord(self.addrdata[-1:]) & (0xFF << (8 - nbits)))
+ self.addrdata = self.addrdata[:-1] + last
+
+ def to_text(self) -> str:
+ return f"ECS {self.address}/{self.srclen} scope/{self.scopelen}"
+
+ @staticmethod
+ def from_text(text: str) -> Option:
+ """Convert a string into a `dns.edns.ECSOption`
+
+ *text*, a `str`, the text form of the option.
+
+ Returns a `dns.edns.ECSOption`.
+
+ Examples:
+
+ >>> import dns.edns
+ >>>
+ >>> # basic example
+ >>> dns.edns.ECSOption.from_text('1.2.3.4/24')
+ >>>
+ >>> # also understands scope
+ >>> dns.edns.ECSOption.from_text('1.2.3.4/24/32')
+ >>>
+ >>> # IPv6
+ >>> dns.edns.ECSOption.from_text('2001:4b98::1/64/64')
+ >>>
+ >>> # it understands results from `dns.edns.ECSOption.to_text()`
+ >>> dns.edns.ECSOption.from_text('ECS 1.2.3.4/24/32')
+ """
+ optional_prefix = "ECS"
+ tokens = text.split()
+ ecs_text = None
+ if len(tokens) == 1:
+ ecs_text = tokens[0]
+ elif len(tokens) == 2:
+ if tokens[0] != optional_prefix:
+ raise ValueError(f'could not parse ECS from "{text}"')
+ ecs_text = tokens[1]
+ else:
+ raise ValueError(f'could not parse ECS from "{text}"')
+ n_slashes = ecs_text.count("/")
+ if n_slashes == 1:
+ address, tsrclen = ecs_text.split("/")
+ tscope = "0"
+ elif n_slashes == 2:
+ address, tsrclen, tscope = ecs_text.split("/")
+ else:
+ raise ValueError(f'could not parse ECS from "{text}"')
+ try:
+ scope = int(tscope)
+ except ValueError:
+ raise ValueError("invalid scope " + f'"{tscope}": scope must be an integer')
+ try:
+ srclen = int(tsrclen)
+ except ValueError:
+ raise ValueError(
+ "invalid srclen " + f'"{tsrclen}": srclen must be an integer'
+ )
+ return ECSOption(address, srclen, scope)
+
+ def to_wire(self, file: Optional[Any] = None) -> Optional[bytes]:
+ value = (
+ struct.pack("!HBB", self.family, self.srclen, self.scopelen) + self.addrdata
+ )
+ if file:
+ file.write(value)
+ return None
+ else:
+ return value
+
+ @classmethod
+ def from_wire_parser(
+ cls, otype: Union[OptionType, str], parser: "dns.wire.Parser"
+ ) -> Option:
+ family, src, scope = parser.get_struct("!HBB")
+ addrlen = int(math.ceil(src / 8.0))
+ prefix = parser.get_bytes(addrlen)
+ if family == 1:
+ pad = 4 - addrlen
+ addr = dns.ipv4.inet_ntoa(prefix + b"\x00" * pad)
+ elif family == 2:
+ pad = 16 - addrlen
+ addr = dns.ipv6.inet_ntoa(prefix + b"\x00" * pad)
+ else:
+ raise ValueError("unsupported family")
+
+ return cls(addr, src, scope)
+
+
+class EDECode(dns.enum.IntEnum):
+ OTHER = 0
+ UNSUPPORTED_DNSKEY_ALGORITHM = 1
+ UNSUPPORTED_DS_DIGEST_TYPE = 2
+ STALE_ANSWER = 3
+ FORGED_ANSWER = 4
+ DNSSEC_INDETERMINATE = 5
+ DNSSEC_BOGUS = 6
+ SIGNATURE_EXPIRED = 7
+ SIGNATURE_NOT_YET_VALID = 8
+ DNSKEY_MISSING = 9
+ RRSIGS_MISSING = 10
+ NO_ZONE_KEY_BIT_SET = 11
+ NSEC_MISSING = 12
+ CACHED_ERROR = 13
+ NOT_READY = 14
+ BLOCKED = 15
+ CENSORED = 16
+ FILTERED = 17
+ PROHIBITED = 18
+ STALE_NXDOMAIN_ANSWER = 19
+ NOT_AUTHORITATIVE = 20
+ NOT_SUPPORTED = 21
+ NO_REACHABLE_AUTHORITY = 22
+ NETWORK_ERROR = 23
+ INVALID_DATA = 24
+
+ @classmethod
+ def _maximum(cls):
+ return 65535
+
+
+class EDEOption(Option): # lgtm[py/missing-equals]
+ """Extended DNS Error (EDE, RFC8914)"""
+
+ _preserve_case = {"DNSKEY", "DS", "DNSSEC", "RRSIGs", "NSEC", "NXDOMAIN"}
+
+ def __init__(self, code: Union[EDECode, str], text: Optional[str] = None):
+ """*code*, a ``dns.edns.EDECode`` or ``str``, the info code of the
+ extended error.
+
+ *text*, a ``str`` or ``None``, specifying additional information about
+ the error.
+ """
+
+ super().__init__(OptionType.EDE)
+
+ self.code = EDECode.make(code)
+ if text is not None and not isinstance(text, str):
+ raise ValueError("text must be string or None")
+ self.text = text
+
+ def to_text(self) -> str:
+ output = f"EDE {self.code}"
+ if self.code in EDECode:
+ desc = EDECode.to_text(self.code)
+ desc = " ".join(
+ word if word in self._preserve_case else word.title()
+ for word in desc.split("_")
+ )
+ output += f" ({desc})"
+ if self.text is not None:
+ output += f": {self.text}"
+ return output
+
+ def to_wire(self, file: Optional[Any] = None) -> Optional[bytes]:
+ value = struct.pack("!H", self.code)
+ if self.text is not None:
+ value += self.text.encode("utf8")
+
+ if file:
+ file.write(value)
+ return None
+ else:
+ return value
+
+ @classmethod
+ def from_wire_parser(
+ cls, otype: Union[OptionType, str], parser: "dns.wire.Parser"
+ ) -> Option:
+ code = EDECode.make(parser.get_uint16())
+ text = parser.get_remaining()
+
+ if text:
+ if text[-1] == 0: # text MAY be null-terminated
+ text = text[:-1]
+ btext = text.decode("utf8")
+ else:
+ btext = None
+
+ return cls(code, btext)
+
+
+class NSIDOption(Option):
+ def __init__(self, nsid: bytes):
+ super().__init__(OptionType.NSID)
+ self.nsid = nsid
+
+ def to_wire(self, file: Any = None) -> Optional[bytes]:
+ if file:
+ file.write(self.nsid)
+ return None
+ else:
+ return self.nsid
+
+ def to_text(self) -> str:
+ if all(c >= 0x20 and c <= 0x7E for c in self.nsid):
+ # All ASCII printable, so it's probably a string.
+ value = self.nsid.decode()
+ else:
+ value = binascii.hexlify(self.nsid).decode()
+ return f"NSID {value}"
+
+ @classmethod
+ def from_wire_parser(
+ cls, otype: Union[OptionType, str], parser: dns.wire.Parser
+ ) -> Option:
+ return cls(parser.get_remaining())
+
+
+class CookieOption(Option):
+ def __init__(self, client: bytes, server: bytes):
+ super().__init__(dns.edns.OptionType.COOKIE)
+ self.client = client
+ self.server = server
+ if len(client) != 8:
+ raise ValueError("client cookie must be 8 bytes")
+ if len(server) != 0 and (len(server) < 8 or len(server) > 32):
+ raise ValueError("server cookie must be empty or between 8 and 32 bytes")
+
+ def to_wire(self, file: Any = None) -> Optional[bytes]:
+ if file:
+ file.write(self.client)
+ if len(self.server) > 0:
+ file.write(self.server)
+ return None
+ else:
+ return self.client + self.server
+
+ def to_text(self) -> str:
+ client = binascii.hexlify(self.client).decode()
+ if len(self.server) > 0:
+ server = binascii.hexlify(self.server).decode()
+ else:
+ server = ""
+ return f"COOKIE {client}{server}"
+
+ @classmethod
+ def from_wire_parser(
+ cls, otype: Union[OptionType, str], parser: dns.wire.Parser
+ ) -> Option:
+ return cls(parser.get_bytes(8), parser.get_remaining())
+
+
+class ReportChannelOption(Option):
+ # RFC 9567
+ def __init__(self, agent_domain: dns.name.Name):
+ super().__init__(OptionType.REPORTCHANNEL)
+ self.agent_domain = agent_domain
+
+ def to_wire(self, file: Any = None) -> Optional[bytes]:
+ return self.agent_domain.to_wire(file)
+
+ def to_text(self) -> str:
+ return "REPORTCHANNEL " + self.agent_domain.to_text()
+
+ @classmethod
+ def from_wire_parser(
+ cls, otype: Union[OptionType, str], parser: dns.wire.Parser
+ ) -> Option:
+ return cls(parser.get_name())
+
+
+_type_to_class: Dict[OptionType, Any] = {
+ OptionType.ECS: ECSOption,
+ OptionType.EDE: EDEOption,
+ OptionType.NSID: NSIDOption,
+ OptionType.COOKIE: CookieOption,
+ OptionType.REPORTCHANNEL: ReportChannelOption,
+}
+
+
+def get_option_class(otype: OptionType) -> Any:
+ """Return the class for the specified option type.
+
+ The GenericOption class is used if a more specific class is not
+ known.
+ """
+
+ cls = _type_to_class.get(otype)
+ if cls is None:
+ cls = GenericOption
+ return cls
+
+
+def option_from_wire_parser(
+ otype: Union[OptionType, str], parser: "dns.wire.Parser"
+) -> Option:
+ """Build an EDNS option object from wire format.
+
+ *otype*, an ``int``, is the option type.
+
+ *parser*, a ``dns.wire.Parser``, the parser, which should be
+ restricted to the option length.
+
+ Returns an instance of a subclass of ``dns.edns.Option``.
+ """
+ otype = OptionType.make(otype)
+ cls = get_option_class(otype)
+ return cls.from_wire_parser(otype, parser)
+
+
+def option_from_wire(
+ otype: Union[OptionType, str], wire: bytes, current: int, olen: int
+) -> Option:
+ """Build an EDNS option object from wire format.
+
+ *otype*, an ``int``, is the option type.
+
+ *wire*, a ``bytes``, is the wire-format message.
+
+ *current*, an ``int``, is the offset in *wire* of the beginning
+ of the rdata.
+
+ *olen*, an ``int``, is the length of the wire-format option data
+
+ Returns an instance of a subclass of ``dns.edns.Option``.
+ """
+ parser = dns.wire.Parser(wire, current)
+ with parser.restrict_to(olen):
+ return option_from_wire_parser(otype, parser)
+
+
+def register_type(implementation: Any, otype: OptionType) -> None:
+ """Register the implementation of an option type.
+
+ *implementation*, a ``class``, is a subclass of ``dns.edns.Option``.
+
+ *otype*, an ``int``, is the option type.
+ """
+
+ _type_to_class[otype] = implementation
+
+
+### BEGIN generated OptionType constants
+
+NSID = OptionType.NSID
+DAU = OptionType.DAU
+DHU = OptionType.DHU
+N3U = OptionType.N3U
+ECS = OptionType.ECS
+EXPIRE = OptionType.EXPIRE
+COOKIE = OptionType.COOKIE
+KEEPALIVE = OptionType.KEEPALIVE
+PADDING = OptionType.PADDING
+CHAIN = OptionType.CHAIN
+EDE = OptionType.EDE
+REPORTCHANNEL = OptionType.REPORTCHANNEL
+
+### END generated OptionType constants
diff --git a/.venv/lib/python3.12/site-packages/dns/entropy.py b/.venv/lib/python3.12/site-packages/dns/entropy.py
new file mode 100644
index 00000000..4dcdc627
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/entropy.py
@@ -0,0 +1,130 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2009-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import hashlib
+import os
+import random
+import threading
+import time
+from typing import Any, Optional
+
+
+class EntropyPool:
+ # This is an entropy pool for Python implementations that do not
+ # have a working SystemRandom. I'm not sure there are any, but
+ # leaving this code doesn't hurt anything as the library code
+ # is used if present.
+
+ def __init__(self, seed: Optional[bytes] = None):
+ self.pool_index = 0
+ self.digest: Optional[bytearray] = None
+ self.next_byte = 0
+ self.lock = threading.Lock()
+ self.hash = hashlib.sha1()
+ self.hash_len = 20
+ self.pool = bytearray(b"\0" * self.hash_len)
+ if seed is not None:
+ self._stir(seed)
+ self.seeded = True
+ self.seed_pid = os.getpid()
+ else:
+ self.seeded = False
+ self.seed_pid = 0
+
+ def _stir(self, entropy: bytes) -> None:
+ for c in entropy:
+ if self.pool_index == self.hash_len:
+ self.pool_index = 0
+ b = c & 0xFF
+ self.pool[self.pool_index] ^= b
+ self.pool_index += 1
+
+ def stir(self, entropy: bytes) -> None:
+ with self.lock:
+ self._stir(entropy)
+
+ def _maybe_seed(self) -> None:
+ if not self.seeded or self.seed_pid != os.getpid():
+ try:
+ seed = os.urandom(16)
+ except Exception: # pragma: no cover
+ try:
+ with open("/dev/urandom", "rb", 0) as r:
+ seed = r.read(16)
+ except Exception:
+ seed = str(time.time()).encode()
+ self.seeded = True
+ self.seed_pid = os.getpid()
+ self.digest = None
+ seed = bytearray(seed)
+ self._stir(seed)
+
+ def random_8(self) -> int:
+ with self.lock:
+ self._maybe_seed()
+ if self.digest is None or self.next_byte == self.hash_len:
+ self.hash.update(bytes(self.pool))
+ self.digest = bytearray(self.hash.digest())
+ self._stir(self.digest)
+ self.next_byte = 0
+ value = self.digest[self.next_byte]
+ self.next_byte += 1
+ return value
+
+ def random_16(self) -> int:
+ return self.random_8() * 256 + self.random_8()
+
+ def random_32(self) -> int:
+ return self.random_16() * 65536 + self.random_16()
+
+ def random_between(self, first: int, last: int) -> int:
+ size = last - first + 1
+ if size > 4294967296:
+ raise ValueError("too big")
+ if size > 65536:
+ rand = self.random_32
+ max = 4294967295
+ elif size > 256:
+ rand = self.random_16
+ max = 65535
+ else:
+ rand = self.random_8
+ max = 255
+ return first + size * rand() // (max + 1)
+
+
+pool = EntropyPool()
+
+system_random: Optional[Any]
+try:
+ system_random = random.SystemRandom()
+except Exception: # pragma: no cover
+ system_random = None
+
+
+def random_16() -> int:
+ if system_random is not None:
+ return system_random.randrange(0, 65536)
+ else:
+ return pool.random_16()
+
+
+def between(first: int, last: int) -> int:
+ if system_random is not None:
+ return system_random.randrange(first, last + 1)
+ else:
+ return pool.random_between(first, last)
diff --git a/.venv/lib/python3.12/site-packages/dns/enum.py b/.venv/lib/python3.12/site-packages/dns/enum.py
new file mode 100644
index 00000000..71461f17
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/enum.py
@@ -0,0 +1,116 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import enum
+from typing import Type, TypeVar, Union
+
+TIntEnum = TypeVar("TIntEnum", bound="IntEnum")
+
+
+class IntEnum(enum.IntEnum):
+ @classmethod
+ def _missing_(cls, value):
+ cls._check_value(value)
+ val = int.__new__(cls, value)
+ val._name_ = cls._extra_to_text(value, None) or f"{cls._prefix()}{value}"
+ val._value_ = value
+ return val
+
+ @classmethod
+ def _check_value(cls, value):
+ max = cls._maximum()
+ if not isinstance(value, int):
+ raise TypeError
+ if value < 0 or value > max:
+ name = cls._short_name()
+ raise ValueError(f"{name} must be an int between >= 0 and <= {max}")
+
+ @classmethod
+ def from_text(cls: Type[TIntEnum], text: str) -> TIntEnum:
+ text = text.upper()
+ try:
+ return cls[text]
+ except KeyError:
+ pass
+ value = cls._extra_from_text(text)
+ if value:
+ return value
+ prefix = cls._prefix()
+ if text.startswith(prefix) and text[len(prefix) :].isdigit():
+ value = int(text[len(prefix) :])
+ cls._check_value(value)
+ try:
+ return cls(value)
+ except ValueError:
+ return value
+ raise cls._unknown_exception_class()
+
+ @classmethod
+ def to_text(cls: Type[TIntEnum], value: int) -> str:
+ cls._check_value(value)
+ try:
+ text = cls(value).name
+ except ValueError:
+ text = None
+ text = cls._extra_to_text(value, text)
+ if text is None:
+ text = f"{cls._prefix()}{value}"
+ return text
+
+ @classmethod
+ def make(cls: Type[TIntEnum], value: Union[int, str]) -> TIntEnum:
+ """Convert text or a value into an enumerated type, if possible.
+
+ *value*, the ``int`` or ``str`` to convert.
+
+ Raises a class-specific exception if a ``str`` is provided that
+ cannot be converted.
+
+ Raises ``ValueError`` if the value is out of range.
+
+ Returns an enumeration from the calling class corresponding to the
+ value, if one is defined, or an ``int`` otherwise.
+ """
+
+ if isinstance(value, str):
+ return cls.from_text(value)
+ cls._check_value(value)
+ return cls(value)
+
+ @classmethod
+ def _maximum(cls):
+ raise NotImplementedError # pragma: no cover
+
+ @classmethod
+ def _short_name(cls):
+ return cls.__name__.lower()
+
+ @classmethod
+ def _prefix(cls):
+ return ""
+
+ @classmethod
+ def _extra_from_text(cls, text): # pylint: disable=W0613
+ return None
+
+ @classmethod
+ def _extra_to_text(cls, value, current_text): # pylint: disable=W0613
+ return current_text
+
+ @classmethod
+ def _unknown_exception_class(cls):
+ return ValueError
diff --git a/.venv/lib/python3.12/site-packages/dns/exception.py b/.venv/lib/python3.12/site-packages/dns/exception.py
new file mode 100644
index 00000000..223f2d68
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/exception.py
@@ -0,0 +1,169 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Common DNS Exceptions.
+
+Dnspython modules may also define their own exceptions, which will
+always be subclasses of ``DNSException``.
+"""
+
+
+from typing import Optional, Set
+
+
+class DNSException(Exception):
+ """Abstract base class shared by all dnspython exceptions.
+
+ It supports two basic modes of operation:
+
+ a) Old/compatible mode is used if ``__init__`` was called with
+ empty *kwargs*. In compatible mode all *args* are passed
+ to the standard Python Exception class as before and all *args* are
+ printed by the standard ``__str__`` implementation. Class variable
+ ``msg`` (or doc string if ``msg`` is ``None``) is returned from ``str()``
+ if *args* is empty.
+
+ b) New/parametrized mode is used if ``__init__`` was called with
+ non-empty *kwargs*.
+ In the new mode *args* must be empty and all kwargs must match
+ those set in class variable ``supp_kwargs``. All kwargs are stored inside
+ ``self.kwargs`` and used in a new ``__str__`` implementation to construct
+ a formatted message based on the ``fmt`` class variable, a ``string``.
+
+ In the simplest case it is enough to override the ``supp_kwargs``
+ and ``fmt`` class variables to get nice parametrized messages.
+ """
+
+ msg: Optional[str] = None # non-parametrized message
+ supp_kwargs: Set[str] = set() # accepted parameters for _fmt_kwargs (sanity check)
+ fmt: Optional[str] = None # message parametrized with results from _fmt_kwargs
+
+ def __init__(self, *args, **kwargs):
+ self._check_params(*args, **kwargs)
+ if kwargs:
+ # This call to a virtual method from __init__ is ok in our usage
+ self.kwargs = self._check_kwargs(**kwargs) # lgtm[py/init-calls-subclass]
+ self.msg = str(self)
+ else:
+ self.kwargs = dict() # defined but empty for old mode exceptions
+ if self.msg is None:
+ # doc string is better implicit message than empty string
+ self.msg = self.__doc__
+ if args:
+ super().__init__(*args)
+ else:
+ super().__init__(self.msg)
+
+ def _check_params(self, *args, **kwargs):
+ """Old exceptions supported only args and not kwargs.
+
+ For sanity we do not allow to mix old and new behavior."""
+ if args or kwargs:
+ assert bool(args) != bool(
+ kwargs
+ ), "keyword arguments are mutually exclusive with positional args"
+
+ def _check_kwargs(self, **kwargs):
+ if kwargs:
+ assert (
+ set(kwargs.keys()) == self.supp_kwargs
+ ), f"following set of keyword args is required: {self.supp_kwargs}"
+ return kwargs
+
+ def _fmt_kwargs(self, **kwargs):
+ """Format kwargs before printing them.
+
+ Resulting dictionary has to have keys necessary for str.format call
+ on fmt class variable.
+ """
+ fmtargs = {}
+ for kw, data in kwargs.items():
+ if isinstance(data, (list, set)):
+ # convert list of <someobj> to list of str(<someobj>)
+ fmtargs[kw] = list(map(str, data))
+ if len(fmtargs[kw]) == 1:
+ # remove list brackets [] from single-item lists
+ fmtargs[kw] = fmtargs[kw].pop()
+ else:
+ fmtargs[kw] = data
+ return fmtargs
+
+ def __str__(self):
+ if self.kwargs and self.fmt:
+ # provide custom message constructed from keyword arguments
+ fmtargs = self._fmt_kwargs(**self.kwargs)
+ return self.fmt.format(**fmtargs)
+ else:
+ # print *args directly in the same way as old DNSException
+ return super().__str__()
+
+
+class FormError(DNSException):
+ """DNS message is malformed."""
+
+
+class SyntaxError(DNSException):
+ """Text input is malformed."""
+
+
+class UnexpectedEnd(SyntaxError):
+ """Text input ended unexpectedly."""
+
+
+class TooBig(DNSException):
+ """The DNS message is too big."""
+
+
+class Timeout(DNSException):
+ """The DNS operation timed out."""
+
+ supp_kwargs = {"timeout"}
+ fmt = "The DNS operation timed out after {timeout:.3f} seconds"
+
+ # We do this as otherwise mypy complains about unexpected keyword argument
+ # idna_exception
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+
+class UnsupportedAlgorithm(DNSException):
+ """The DNSSEC algorithm is not supported."""
+
+
+class AlgorithmKeyMismatch(UnsupportedAlgorithm):
+ """The DNSSEC algorithm is not supported for the given key type."""
+
+
+class ValidationFailure(DNSException):
+ """The DNSSEC signature is invalid."""
+
+
+class DeniedByPolicy(DNSException):
+ """Denied by DNSSEC policy."""
+
+
+class ExceptionWrapper:
+ def __init__(self, exception_class):
+ self.exception_class = exception_class
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if exc_type is not None and not isinstance(exc_val, self.exception_class):
+ raise self.exception_class(str(exc_val)) from exc_val
+ return False
diff --git a/.venv/lib/python3.12/site-packages/dns/flags.py b/.venv/lib/python3.12/site-packages/dns/flags.py
new file mode 100644
index 00000000..4c60be13
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/flags.py
@@ -0,0 +1,123 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Message Flags."""
+
+import enum
+from typing import Any
+
+# Standard DNS flags
+
+
+class Flag(enum.IntFlag):
+ #: Query Response
+ QR = 0x8000
+ #: Authoritative Answer
+ AA = 0x0400
+ #: Truncated Response
+ TC = 0x0200
+ #: Recursion Desired
+ RD = 0x0100
+ #: Recursion Available
+ RA = 0x0080
+ #: Authentic Data
+ AD = 0x0020
+ #: Checking Disabled
+ CD = 0x0010
+
+
+# EDNS flags
+
+
+class EDNSFlag(enum.IntFlag):
+ #: DNSSEC answer OK
+ DO = 0x8000
+
+
+def _from_text(text: str, enum_class: Any) -> int:
+ flags = 0
+ tokens = text.split()
+ for t in tokens:
+ flags |= enum_class[t.upper()]
+ return flags
+
+
+def _to_text(flags: int, enum_class: Any) -> str:
+ text_flags = []
+ for k, v in enum_class.__members__.items():
+ if flags & v != 0:
+ text_flags.append(k)
+ return " ".join(text_flags)
+
+
+def from_text(text: str) -> int:
+ """Convert a space-separated list of flag text values into a flags
+ value.
+
+ Returns an ``int``
+ """
+
+ return _from_text(text, Flag)
+
+
+def to_text(flags: int) -> str:
+ """Convert a flags value into a space-separated list of flag text
+ values.
+
+ Returns a ``str``.
+ """
+
+ return _to_text(flags, Flag)
+
+
+def edns_from_text(text: str) -> int:
+ """Convert a space-separated list of EDNS flag text values into a EDNS
+ flags value.
+
+ Returns an ``int``
+ """
+
+ return _from_text(text, EDNSFlag)
+
+
+def edns_to_text(flags: int) -> str:
+ """Convert an EDNS flags value into a space-separated list of EDNS flag
+ text values.
+
+ Returns a ``str``.
+ """
+
+ return _to_text(flags, EDNSFlag)
+
+
+### BEGIN generated Flag constants
+
+QR = Flag.QR
+AA = Flag.AA
+TC = Flag.TC
+RD = Flag.RD
+RA = Flag.RA
+AD = Flag.AD
+CD = Flag.CD
+
+### END generated Flag constants
+
+### BEGIN generated EDNSFlag constants
+
+DO = EDNSFlag.DO
+
+### END generated EDNSFlag constants
diff --git a/.venv/lib/python3.12/site-packages/dns/grange.py b/.venv/lib/python3.12/site-packages/dns/grange.py
new file mode 100644
index 00000000..a967ca41
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/grange.py
@@ -0,0 +1,72 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2012-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS GENERATE range conversion."""
+
+from typing import Tuple
+
+import dns
+
+
+def from_text(text: str) -> Tuple[int, int, int]:
+ """Convert the text form of a range in a ``$GENERATE`` statement to an
+ integer.
+
+ *text*, a ``str``, the textual range in ``$GENERATE`` form.
+
+ Returns a tuple of three ``int`` values ``(start, stop, step)``.
+ """
+
+ start = -1
+ stop = -1
+ step = 1
+ cur = ""
+ state = 0
+ # state 0 1 2
+ # x - y / z
+
+ if text and text[0] == "-":
+ raise dns.exception.SyntaxError("Start cannot be a negative number")
+
+ for c in text:
+ if c == "-" and state == 0:
+ start = int(cur)
+ cur = ""
+ state = 1
+ elif c == "/":
+ stop = int(cur)
+ cur = ""
+ state = 2
+ elif c.isdigit():
+ cur += c
+ else:
+ raise dns.exception.SyntaxError(f"Could not parse {c}")
+
+ if state == 0:
+ raise dns.exception.SyntaxError("no stop value specified")
+ elif state == 1:
+ stop = int(cur)
+ else:
+ assert state == 2
+ step = int(cur)
+
+ assert step >= 1
+ assert start >= 0
+ if start > stop:
+ raise dns.exception.SyntaxError("start must be <= stop")
+
+ return (start, stop, step)
diff --git a/.venv/lib/python3.12/site-packages/dns/immutable.py b/.venv/lib/python3.12/site-packages/dns/immutable.py
new file mode 100644
index 00000000..36b0362c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/immutable.py
@@ -0,0 +1,68 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import collections.abc
+from typing import Any, Callable
+
+from dns._immutable_ctx import immutable
+
+
+@immutable
+class Dict(collections.abc.Mapping): # lgtm[py/missing-equals]
+ def __init__(
+ self,
+ dictionary: Any,
+ no_copy: bool = False,
+ map_factory: Callable[[], collections.abc.MutableMapping] = dict,
+ ):
+ """Make an immutable dictionary from the specified dictionary.
+
+ If *no_copy* is `True`, then *dictionary* will be wrapped instead
+ of copied. Only set this if you are sure there will be no external
+ references to the dictionary.
+ """
+ if no_copy and isinstance(dictionary, collections.abc.MutableMapping):
+ self._odict = dictionary
+ else:
+ self._odict = map_factory()
+ self._odict.update(dictionary)
+ self._hash = None
+
+ def __getitem__(self, key):
+ return self._odict.__getitem__(key)
+
+ def __hash__(self): # pylint: disable=invalid-hash-returned
+ if self._hash is None:
+ h = 0
+ for key in sorted(self._odict.keys()):
+ h ^= hash(key)
+ object.__setattr__(self, "_hash", h)
+ # this does return an int, but pylint doesn't figure that out
+ return self._hash
+
+ def __len__(self):
+ return len(self._odict)
+
+ def __iter__(self):
+ return iter(self._odict)
+
+
+def constify(o: Any) -> Any:
+ """
+ Convert mutable types to immutable types.
+ """
+ if isinstance(o, bytearray):
+ return bytes(o)
+ if isinstance(o, tuple):
+ try:
+ hash(o)
+ return o
+ except Exception:
+ return tuple(constify(elt) for elt in o)
+ if isinstance(o, list):
+ return tuple(constify(elt) for elt in o)
+ if isinstance(o, dict):
+ cdict = dict()
+ for k, v in o.items():
+ cdict[k] = constify(v)
+ return Dict(cdict, True)
+ return o
diff --git a/.venv/lib/python3.12/site-packages/dns/inet.py b/.venv/lib/python3.12/site-packages/dns/inet.py
new file mode 100644
index 00000000..4a03f996
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/inet.py
@@ -0,0 +1,197 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Generic Internet address helper functions."""
+
+import socket
+from typing import Any, Optional, Tuple
+
+import dns.ipv4
+import dns.ipv6
+
+# We assume that AF_INET and AF_INET6 are always defined. We keep
+# these here for the benefit of any old code (unlikely though that
+# is!).
+AF_INET = socket.AF_INET
+AF_INET6 = socket.AF_INET6
+
+
+def inet_pton(family: int, text: str) -> bytes:
+ """Convert the textual form of a network address into its binary form.
+
+ *family* is an ``int``, the address family.
+
+ *text* is a ``str``, the textual address.
+
+ Raises ``NotImplementedError`` if the address family specified is not
+ implemented.
+
+ Returns a ``bytes``.
+ """
+
+ if family == AF_INET:
+ return dns.ipv4.inet_aton(text)
+ elif family == AF_INET6:
+ return dns.ipv6.inet_aton(text, True)
+ else:
+ raise NotImplementedError
+
+
+def inet_ntop(family: int, address: bytes) -> str:
+ """Convert the binary form of a network address into its textual form.
+
+ *family* is an ``int``, the address family.
+
+ *address* is a ``bytes``, the network address in binary form.
+
+ Raises ``NotImplementedError`` if the address family specified is not
+ implemented.
+
+ Returns a ``str``.
+ """
+
+ if family == AF_INET:
+ return dns.ipv4.inet_ntoa(address)
+ elif family == AF_INET6:
+ return dns.ipv6.inet_ntoa(address)
+ else:
+ raise NotImplementedError
+
+
+def af_for_address(text: str) -> int:
+ """Determine the address family of a textual-form network address.
+
+ *text*, a ``str``, the textual address.
+
+ Raises ``ValueError`` if the address family cannot be determined
+ from the input.
+
+ Returns an ``int``.
+ """
+
+ try:
+ dns.ipv4.inet_aton(text)
+ return AF_INET
+ except Exception:
+ try:
+ dns.ipv6.inet_aton(text, True)
+ return AF_INET6
+ except Exception:
+ raise ValueError
+
+
+def is_multicast(text: str) -> bool:
+ """Is the textual-form network address a multicast address?
+
+ *text*, a ``str``, the textual address.
+
+ Raises ``ValueError`` if the address family cannot be determined
+ from the input.
+
+ Returns a ``bool``.
+ """
+
+ try:
+ first = dns.ipv4.inet_aton(text)[0]
+ return first >= 224 and first <= 239
+ except Exception:
+ try:
+ first = dns.ipv6.inet_aton(text, True)[0]
+ return first == 255
+ except Exception:
+ raise ValueError
+
+
+def is_address(text: str) -> bool:
+ """Is the specified string an IPv4 or IPv6 address?
+
+ *text*, a ``str``, the textual address.
+
+ Returns a ``bool``.
+ """
+
+ try:
+ dns.ipv4.inet_aton(text)
+ return True
+ except Exception:
+ try:
+ dns.ipv6.inet_aton(text, True)
+ return True
+ except Exception:
+ return False
+
+
+def low_level_address_tuple(
+ high_tuple: Tuple[str, int], af: Optional[int] = None
+) -> Any:
+ """Given a "high-level" address tuple, i.e.
+ an (address, port) return the appropriate "low-level" address tuple
+ suitable for use in socket calls.
+
+ If an *af* other than ``None`` is provided, it is assumed the
+ address in the high-level tuple is valid and has that af. If af
+ is ``None``, then af_for_address will be called.
+ """
+ address, port = high_tuple
+ if af is None:
+ af = af_for_address(address)
+ if af == AF_INET:
+ return (address, port)
+ elif af == AF_INET6:
+ i = address.find("%")
+ if i < 0:
+ # no scope, shortcut!
+ return (address, port, 0, 0)
+ # try to avoid getaddrinfo()
+ addrpart = address[:i]
+ scope = address[i + 1 :]
+ if scope.isdigit():
+ return (addrpart, port, 0, int(scope))
+ try:
+ return (addrpart, port, 0, socket.if_nametoindex(scope))
+ except AttributeError: # pragma: no cover (we can't really test this)
+ ai_flags = socket.AI_NUMERICHOST
+ ((*_, tup), *_) = socket.getaddrinfo(address, port, flags=ai_flags)
+ return tup
+ else:
+ raise NotImplementedError(f"unknown address family {af}")
+
+
+def any_for_af(af):
+ """Return the 'any' address for the specified address family."""
+ if af == socket.AF_INET:
+ return "0.0.0.0"
+ elif af == socket.AF_INET6:
+ return "::"
+ raise NotImplementedError(f"unknown address family {af}")
+
+
+def canonicalize(text: str) -> str:
+ """Verify that *address* is a valid text form IPv4 or IPv6 address and return its
+ canonical text form. IPv6 addresses with scopes are rejected.
+
+ *text*, a ``str``, the address in textual form.
+
+ Raises ``ValueError`` if the text is not valid.
+ """
+ try:
+ return dns.ipv6.canonicalize(text)
+ except Exception:
+ try:
+ return dns.ipv4.canonicalize(text)
+ except Exception:
+ raise ValueError
diff --git a/.venv/lib/python3.12/site-packages/dns/ipv4.py b/.venv/lib/python3.12/site-packages/dns/ipv4.py
new file mode 100644
index 00000000..65ee69c0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/ipv4.py
@@ -0,0 +1,77 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""IPv4 helper functions."""
+
+import struct
+from typing import Union
+
+import dns.exception
+
+
+def inet_ntoa(address: bytes) -> str:
+ """Convert an IPv4 address in binary form to text form.
+
+ *address*, a ``bytes``, the IPv4 address in binary form.
+
+ Returns a ``str``.
+ """
+
+ if len(address) != 4:
+ raise dns.exception.SyntaxError
+ return "%u.%u.%u.%u" % (address[0], address[1], address[2], address[3])
+
+
+def inet_aton(text: Union[str, bytes]) -> bytes:
+ """Convert an IPv4 address in text form to binary form.
+
+ *text*, a ``str`` or ``bytes``, the IPv4 address in textual form.
+
+ Returns a ``bytes``.
+ """
+
+ if not isinstance(text, bytes):
+ btext = text.encode()
+ else:
+ btext = text
+ parts = btext.split(b".")
+ if len(parts) != 4:
+ raise dns.exception.SyntaxError
+ for part in parts:
+ if not part.isdigit():
+ raise dns.exception.SyntaxError
+ if len(part) > 1 and part[0] == ord("0"):
+ # No leading zeros
+ raise dns.exception.SyntaxError
+ try:
+ b = [int(part) for part in parts]
+ return struct.pack("BBBB", *b)
+ except Exception:
+ raise dns.exception.SyntaxError
+
+
+def canonicalize(text: Union[str, bytes]) -> str:
+ """Verify that *address* is a valid text form IPv4 address and return its
+ canonical text form.
+
+ *text*, a ``str`` or ``bytes``, the IPv4 address in textual form.
+
+ Raises ``dns.exception.SyntaxError`` if the text is not valid.
+ """
+ # Note that inet_aton() only accepts canonial form, but we still run through
+ # inet_ntoa() to ensure the output is a str.
+ return dns.ipv4.inet_ntoa(dns.ipv4.inet_aton(text))
diff --git a/.venv/lib/python3.12/site-packages/dns/ipv6.py b/.venv/lib/python3.12/site-packages/dns/ipv6.py
new file mode 100644
index 00000000..4dd1d1ca
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/ipv6.py
@@ -0,0 +1,217 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""IPv6 helper functions."""
+
+import binascii
+import re
+from typing import List, Union
+
+import dns.exception
+import dns.ipv4
+
+_leading_zero = re.compile(r"0+([0-9a-f]+)")
+
+
+def inet_ntoa(address: bytes) -> str:
+ """Convert an IPv6 address in binary form to text form.
+
+ *address*, a ``bytes``, the IPv6 address in binary form.
+
+ Raises ``ValueError`` if the address isn't 16 bytes long.
+ Returns a ``str``.
+ """
+
+ if len(address) != 16:
+ raise ValueError("IPv6 addresses are 16 bytes long")
+ hex = binascii.hexlify(address)
+ chunks = []
+ i = 0
+ l = len(hex)
+ while i < l:
+ chunk = hex[i : i + 4].decode()
+ # strip leading zeros. we do this with an re instead of
+ # with lstrip() because lstrip() didn't support chars until
+ # python 2.2.2
+ m = _leading_zero.match(chunk)
+ if m is not None:
+ chunk = m.group(1)
+ chunks.append(chunk)
+ i += 4
+ #
+ # Compress the longest subsequence of 0-value chunks to ::
+ #
+ best_start = 0
+ best_len = 0
+ start = -1
+ last_was_zero = False
+ for i in range(8):
+ if chunks[i] != "0":
+ if last_was_zero:
+ end = i
+ current_len = end - start
+ if current_len > best_len:
+ best_start = start
+ best_len = current_len
+ last_was_zero = False
+ elif not last_was_zero:
+ start = i
+ last_was_zero = True
+ if last_was_zero:
+ end = 8
+ current_len = end - start
+ if current_len > best_len:
+ best_start = start
+ best_len = current_len
+ if best_len > 1:
+ if best_start == 0 and (best_len == 6 or best_len == 5 and chunks[5] == "ffff"):
+ # We have an embedded IPv4 address
+ if best_len == 6:
+ prefix = "::"
+ else:
+ prefix = "::ffff:"
+ thex = prefix + dns.ipv4.inet_ntoa(address[12:])
+ else:
+ thex = (
+ ":".join(chunks[:best_start])
+ + "::"
+ + ":".join(chunks[best_start + best_len :])
+ )
+ else:
+ thex = ":".join(chunks)
+ return thex
+
+
+_v4_ending = re.compile(rb"(.*):(\d+\.\d+\.\d+\.\d+)$")
+_colon_colon_start = re.compile(rb"::.*")
+_colon_colon_end = re.compile(rb".*::$")
+
+
+def inet_aton(text: Union[str, bytes], ignore_scope: bool = False) -> bytes:
+ """Convert an IPv6 address in text form to binary form.
+
+ *text*, a ``str`` or ``bytes``, the IPv6 address in textual form.
+
+ *ignore_scope*, a ``bool``. If ``True``, a scope will be ignored.
+ If ``False``, the default, it is an error for a scope to be present.
+
+ Returns a ``bytes``.
+ """
+
+ #
+ # Our aim here is not something fast; we just want something that works.
+ #
+ if not isinstance(text, bytes):
+ btext = text.encode()
+ else:
+ btext = text
+
+ if ignore_scope:
+ parts = btext.split(b"%")
+ l = len(parts)
+ if l == 2:
+ btext = parts[0]
+ elif l > 2:
+ raise dns.exception.SyntaxError
+
+ if btext == b"":
+ raise dns.exception.SyntaxError
+ elif btext.endswith(b":") and not btext.endswith(b"::"):
+ raise dns.exception.SyntaxError
+ elif btext.startswith(b":") and not btext.startswith(b"::"):
+ raise dns.exception.SyntaxError
+ elif btext == b"::":
+ btext = b"0::"
+ #
+ # Get rid of the icky dot-quad syntax if we have it.
+ #
+ m = _v4_ending.match(btext)
+ if m is not None:
+ b = dns.ipv4.inet_aton(m.group(2))
+ btext = (
+ f"{m.group(1).decode()}:{b[0]:02x}{b[1]:02x}:{b[2]:02x}{b[3]:02x}"
+ ).encode()
+ #
+ # Try to turn '::<whatever>' into ':<whatever>'; if no match try to
+ # turn '<whatever>::' into '<whatever>:'
+ #
+ m = _colon_colon_start.match(btext)
+ if m is not None:
+ btext = btext[1:]
+ else:
+ m = _colon_colon_end.match(btext)
+ if m is not None:
+ btext = btext[:-1]
+ #
+ # Now canonicalize into 8 chunks of 4 hex digits each
+ #
+ chunks = btext.split(b":")
+ l = len(chunks)
+ if l > 8:
+ raise dns.exception.SyntaxError
+ seen_empty = False
+ canonical: List[bytes] = []
+ for c in chunks:
+ if c == b"":
+ if seen_empty:
+ raise dns.exception.SyntaxError
+ seen_empty = True
+ for _ in range(0, 8 - l + 1):
+ canonical.append(b"0000")
+ else:
+ lc = len(c)
+ if lc > 4:
+ raise dns.exception.SyntaxError
+ if lc != 4:
+ c = (b"0" * (4 - lc)) + c
+ canonical.append(c)
+ if l < 8 and not seen_empty:
+ raise dns.exception.SyntaxError
+ btext = b"".join(canonical)
+
+ #
+ # Finally we can go to binary.
+ #
+ try:
+ return binascii.unhexlify(btext)
+ except (binascii.Error, TypeError):
+ raise dns.exception.SyntaxError
+
+
+_mapped_prefix = b"\x00" * 10 + b"\xff\xff"
+
+
+def is_mapped(address: bytes) -> bool:
+ """Is the specified address a mapped IPv4 address?
+
+ *address*, a ``bytes`` is an IPv6 address in binary form.
+
+ Returns a ``bool``.
+ """
+
+ return address.startswith(_mapped_prefix)
+
+
+def canonicalize(text: Union[str, bytes]) -> str:
+ """Verify that *address* is a valid text form IPv6 address and return its
+ canonical text form. Addresses with scopes are rejected.
+
+ *text*, a ``str`` or ``bytes``, the IPv6 address in textual form.
+
+ Raises ``dns.exception.SyntaxError`` if the text is not valid.
+ """
+ return dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(text))
diff --git a/.venv/lib/python3.12/site-packages/dns/message.py b/.venv/lib/python3.12/site-packages/dns/message.py
new file mode 100644
index 00000000..e978a0a2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/message.py
@@ -0,0 +1,1933 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Messages"""
+
+import contextlib
+import enum
+import io
+import time
+from typing import Any, Dict, List, Optional, Tuple, Union, cast
+
+import dns.edns
+import dns.entropy
+import dns.enum
+import dns.exception
+import dns.flags
+import dns.name
+import dns.opcode
+import dns.rcode
+import dns.rdata
+import dns.rdataclass
+import dns.rdatatype
+import dns.rdtypes.ANY.OPT
+import dns.rdtypes.ANY.TSIG
+import dns.renderer
+import dns.rrset
+import dns.tsig
+import dns.ttl
+import dns.wire
+
+
+class ShortHeader(dns.exception.FormError):
+ """The DNS packet passed to from_wire() is too short."""
+
+
+class TrailingJunk(dns.exception.FormError):
+ """The DNS packet passed to from_wire() has extra junk at the end of it."""
+
+
+class UnknownHeaderField(dns.exception.DNSException):
+ """The header field name was not recognized when converting from text
+ into a message."""
+
+
+class BadEDNS(dns.exception.FormError):
+ """An OPT record occurred somewhere other than
+ the additional data section."""
+
+
+class BadTSIG(dns.exception.FormError):
+ """A TSIG record occurred somewhere other than the end of
+ the additional data section."""
+
+
+class UnknownTSIGKey(dns.exception.DNSException):
+ """A TSIG with an unknown key was received."""
+
+
+class Truncated(dns.exception.DNSException):
+ """The truncated flag is set."""
+
+ supp_kwargs = {"message"}
+
+ # We do this as otherwise mypy complains about unexpected keyword argument
+ # idna_exception
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def message(self):
+ """As much of the message as could be processed.
+
+ Returns a ``dns.message.Message``.
+ """
+ return self.kwargs["message"]
+
+
+class NotQueryResponse(dns.exception.DNSException):
+ """Message is not a response to a query."""
+
+
+class ChainTooLong(dns.exception.DNSException):
+ """The CNAME chain is too long."""
+
+
+class AnswerForNXDOMAIN(dns.exception.DNSException):
+ """The rcode is NXDOMAIN but an answer was found."""
+
+
+class NoPreviousName(dns.exception.SyntaxError):
+ """No previous name was known."""
+
+
+class MessageSection(dns.enum.IntEnum):
+ """Message sections"""
+
+ QUESTION = 0
+ ANSWER = 1
+ AUTHORITY = 2
+ ADDITIONAL = 3
+
+ @classmethod
+ def _maximum(cls):
+ return 3
+
+
+class MessageError:
+ def __init__(self, exception: Exception, offset: int):
+ self.exception = exception
+ self.offset = offset
+
+
+DEFAULT_EDNS_PAYLOAD = 1232
+MAX_CHAIN = 16
+
+IndexKeyType = Tuple[
+ int,
+ dns.name.Name,
+ dns.rdataclass.RdataClass,
+ dns.rdatatype.RdataType,
+ Optional[dns.rdatatype.RdataType],
+ Optional[dns.rdataclass.RdataClass],
+]
+IndexType = Dict[IndexKeyType, dns.rrset.RRset]
+SectionType = Union[int, str, List[dns.rrset.RRset]]
+
+
+class Message:
+ """A DNS message."""
+
+ _section_enum = MessageSection
+
+ def __init__(self, id: Optional[int] = None):
+ if id is None:
+ self.id = dns.entropy.random_16()
+ else:
+ self.id = id
+ self.flags = 0
+ self.sections: List[List[dns.rrset.RRset]] = [[], [], [], []]
+ self.opt: Optional[dns.rrset.RRset] = None
+ self.request_payload = 0
+ self.pad = 0
+ self.keyring: Any = None
+ self.tsig: Optional[dns.rrset.RRset] = None
+ self.request_mac = b""
+ self.xfr = False
+ self.origin: Optional[dns.name.Name] = None
+ self.tsig_ctx: Optional[Any] = None
+ self.index: IndexType = {}
+ self.errors: List[MessageError] = []
+ self.time = 0.0
+ self.wire: Optional[bytes] = None
+
+ @property
+ def question(self) -> List[dns.rrset.RRset]:
+ """The question section."""
+ return self.sections[0]
+
+ @question.setter
+ def question(self, v):
+ self.sections[0] = v
+
+ @property
+ def answer(self) -> List[dns.rrset.RRset]:
+ """The answer section."""
+ return self.sections[1]
+
+ @answer.setter
+ def answer(self, v):
+ self.sections[1] = v
+
+ @property
+ def authority(self) -> List[dns.rrset.RRset]:
+ """The authority section."""
+ return self.sections[2]
+
+ @authority.setter
+ def authority(self, v):
+ self.sections[2] = v
+
+ @property
+ def additional(self) -> List[dns.rrset.RRset]:
+ """The additional data section."""
+ return self.sections[3]
+
+ @additional.setter
+ def additional(self, v):
+ self.sections[3] = v
+
+ def __repr__(self):
+ return "<DNS message, ID " + repr(self.id) + ">"
+
+ def __str__(self):
+ return self.to_text()
+
+ def to_text(
+ self,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ **kw: Dict[str, Any],
+ ) -> str:
+ """Convert the message to text.
+
+ The *origin*, *relativize*, and any other keyword
+ arguments are passed to the RRset ``to_wire()`` method.
+
+ Returns a ``str``.
+ """
+
+ s = io.StringIO()
+ s.write("id %d\n" % self.id)
+ s.write(f"opcode {dns.opcode.to_text(self.opcode())}\n")
+ s.write(f"rcode {dns.rcode.to_text(self.rcode())}\n")
+ s.write(f"flags {dns.flags.to_text(self.flags)}\n")
+ if self.edns >= 0:
+ s.write(f"edns {self.edns}\n")
+ if self.ednsflags != 0:
+ s.write(f"eflags {dns.flags.edns_to_text(self.ednsflags)}\n")
+ s.write("payload %d\n" % self.payload)
+ for opt in self.options:
+ s.write(f"option {opt.to_text()}\n")
+ for name, which in self._section_enum.__members__.items():
+ s.write(f";{name}\n")
+ for rrset in self.section_from_number(which):
+ s.write(rrset.to_text(origin, relativize, **kw))
+ s.write("\n")
+ #
+ # We strip off the final \n so the caller can print the result without
+ # doing weird things to get around eccentricities in Python print
+ # formatting
+ #
+ return s.getvalue()[:-1]
+
+ def __eq__(self, other):
+ """Two messages are equal if they have the same content in the
+ header, question, answer, and authority sections.
+
+ Returns a ``bool``.
+ """
+
+ if not isinstance(other, Message):
+ return False
+ if self.id != other.id:
+ return False
+ if self.flags != other.flags:
+ return False
+ for i, section in enumerate(self.sections):
+ other_section = other.sections[i]
+ for n in section:
+ if n not in other_section:
+ return False
+ for n in other_section:
+ if n not in section:
+ return False
+ return True
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def is_response(self, other: "Message") -> bool:
+ """Is *other*, also a ``dns.message.Message``, a response to this
+ message?
+
+ Returns a ``bool``.
+ """
+
+ if (
+ other.flags & dns.flags.QR == 0
+ or self.id != other.id
+ or dns.opcode.from_flags(self.flags) != dns.opcode.from_flags(other.flags)
+ ):
+ return False
+ if other.rcode() in {
+ dns.rcode.FORMERR,
+ dns.rcode.SERVFAIL,
+ dns.rcode.NOTIMP,
+ dns.rcode.REFUSED,
+ }:
+ # We don't check the question section in these cases if
+ # the other question section is empty, even though they
+ # still really ought to have a question section.
+ if len(other.question) == 0:
+ return True
+ if dns.opcode.is_update(self.flags):
+ # This is assuming the "sender doesn't include anything
+ # from the update", but we don't care to check the other
+ # case, which is that all the sections are returned and
+ # identical.
+ return True
+ for n in self.question:
+ if n not in other.question:
+ return False
+ for n in other.question:
+ if n not in self.question:
+ return False
+ return True
+
+ def section_number(self, section: List[dns.rrset.RRset]) -> int:
+ """Return the "section number" of the specified section for use
+ in indexing.
+
+ *section* is one of the section attributes of this message.
+
+ Raises ``ValueError`` if the section isn't known.
+
+ Returns an ``int``.
+ """
+
+ for i, our_section in enumerate(self.sections):
+ if section is our_section:
+ return self._section_enum(i)
+ raise ValueError("unknown section")
+
+ def section_from_number(self, number: int) -> List[dns.rrset.RRset]:
+ """Return the section list associated with the specified section
+ number.
+
+ *number* is a section number `int` or the text form of a section
+ name.
+
+ Raises ``ValueError`` if the section isn't known.
+
+ Returns a ``list``.
+ """
+
+ section = self._section_enum.make(number)
+ return self.sections[section]
+
+ def find_rrset(
+ self,
+ section: SectionType,
+ name: dns.name.Name,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ deleting: Optional[dns.rdataclass.RdataClass] = None,
+ create: bool = False,
+ force_unique: bool = False,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ ) -> dns.rrset.RRset:
+ """Find the RRset with the given attributes in the specified section.
+
+ *section*, an ``int`` section number, a ``str`` section name, or one of
+ the section attributes of this message. This specifies the
+ the section of the message to search. For example::
+
+ my_message.find_rrset(my_message.answer, name, rdclass, rdtype)
+ my_message.find_rrset(dns.message.ANSWER, name, rdclass, rdtype)
+ my_message.find_rrset("ANSWER", name, rdclass, rdtype)
+
+ *name*, a ``dns.name.Name`` or ``str``, the name of the RRset.
+
+ *rdclass*, an ``int`` or ``str``, the class of the RRset.
+
+ *rdtype*, an ``int`` or ``str``, the type of the RRset.
+
+ *covers*, an ``int`` or ``str``, the covers value of the RRset.
+ The default is ``dns.rdatatype.NONE``.
+
+ *deleting*, an ``int``, ``str``, or ``None``, the deleting value of the
+ RRset. The default is ``None``.
+
+ *create*, a ``bool``. If ``True``, create the RRset if it is not found.
+ The created RRset is appended to *section*.
+
+ *force_unique*, a ``bool``. If ``True`` and *create* is also ``True``,
+ create a new RRset regardless of whether a matching RRset exists
+ already. The default is ``False``. This is useful when creating
+ DDNS Update messages, as order matters for them.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ Raises ``KeyError`` if the RRset was not found and create was
+ ``False``.
+
+ Returns a ``dns.rrset.RRset object``.
+ """
+
+ if isinstance(section, int):
+ section_number = section
+ section = self.section_from_number(section_number)
+ elif isinstance(section, str):
+ section_number = self._section_enum.from_text(section)
+ section = self.section_from_number(section_number)
+ else:
+ section_number = self.section_number(section)
+ if isinstance(name, str):
+ name = dns.name.from_text(name, idna_codec=idna_codec)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ covers = dns.rdatatype.RdataType.make(covers)
+ if deleting is not None:
+ deleting = dns.rdataclass.RdataClass.make(deleting)
+ key = (section_number, name, rdclass, rdtype, covers, deleting)
+ if not force_unique:
+ if self.index is not None:
+ rrset = self.index.get(key)
+ if rrset is not None:
+ return rrset
+ else:
+ for rrset in section:
+ if rrset.full_match(name, rdclass, rdtype, covers, deleting):
+ return rrset
+ if not create:
+ raise KeyError
+ rrset = dns.rrset.RRset(name, rdclass, rdtype, covers, deleting)
+ section.append(rrset)
+ if self.index is not None:
+ self.index[key] = rrset
+ return rrset
+
+ def get_rrset(
+ self,
+ section: SectionType,
+ name: dns.name.Name,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ deleting: Optional[dns.rdataclass.RdataClass] = None,
+ create: bool = False,
+ force_unique: bool = False,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ ) -> Optional[dns.rrset.RRset]:
+ """Get the RRset with the given attributes in the specified section.
+
+ If the RRset is not found, None is returned.
+
+ *section*, an ``int`` section number, a ``str`` section name, or one of
+ the section attributes of this message. This specifies the
+ the section of the message to search. For example::
+
+ my_message.get_rrset(my_message.answer, name, rdclass, rdtype)
+ my_message.get_rrset(dns.message.ANSWER, name, rdclass, rdtype)
+ my_message.get_rrset("ANSWER", name, rdclass, rdtype)
+
+ *name*, a ``dns.name.Name`` or ``str``, the name of the RRset.
+
+ *rdclass*, an ``int`` or ``str``, the class of the RRset.
+
+ *rdtype*, an ``int`` or ``str``, the type of the RRset.
+
+ *covers*, an ``int`` or ``str``, the covers value of the RRset.
+ The default is ``dns.rdatatype.NONE``.
+
+ *deleting*, an ``int``, ``str``, or ``None``, the deleting value of the
+ RRset. The default is ``None``.
+
+ *create*, a ``bool``. If ``True``, create the RRset if it is not found.
+ The created RRset is appended to *section*.
+
+ *force_unique*, a ``bool``. If ``True`` and *create* is also ``True``,
+ create a new RRset regardless of whether a matching RRset exists
+ already. The default is ``False``. This is useful when creating
+ DDNS Update messages, as order matters for them.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ Returns a ``dns.rrset.RRset object`` or ``None``.
+ """
+
+ try:
+ rrset = self.find_rrset(
+ section,
+ name,
+ rdclass,
+ rdtype,
+ covers,
+ deleting,
+ create,
+ force_unique,
+ idna_codec,
+ )
+ except KeyError:
+ rrset = None
+ return rrset
+
+ def section_count(self, section: SectionType) -> int:
+ """Returns the number of records in the specified section.
+
+ *section*, an ``int`` section number, a ``str`` section name, or one of
+ the section attributes of this message. This specifies the
+ the section of the message to count. For example::
+
+ my_message.section_count(my_message.answer)
+ my_message.section_count(dns.message.ANSWER)
+ my_message.section_count("ANSWER")
+ """
+
+ if isinstance(section, int):
+ section_number = section
+ section = self.section_from_number(section_number)
+ elif isinstance(section, str):
+ section_number = self._section_enum.from_text(section)
+ section = self.section_from_number(section_number)
+ else:
+ section_number = self.section_number(section)
+ count = sum(max(1, len(rrs)) for rrs in section)
+ if section_number == MessageSection.ADDITIONAL:
+ if self.opt is not None:
+ count += 1
+ if self.tsig is not None:
+ count += 1
+ return count
+
+ def _compute_opt_reserve(self) -> int:
+ """Compute the size required for the OPT RR, padding excluded"""
+ if not self.opt:
+ return 0
+ # 1 byte for the root name, 10 for the standard RR fields
+ size = 11
+ # This would be more efficient if options had a size() method, but we won't
+ # worry about that for now. We also don't worry if there is an existing padding
+ # option, as it is unlikely and probably harmless, as the worst case is that we
+ # may add another, and this seems to be legal.
+ for option in self.opt[0].options:
+ wire = option.to_wire()
+ # We add 4 here to account for the option type and length
+ size += len(wire) + 4
+ if self.pad:
+ # Padding will be added, so again add the option type and length.
+ size += 4
+ return size
+
+ def _compute_tsig_reserve(self) -> int:
+ """Compute the size required for the TSIG RR"""
+ # This would be more efficient if TSIGs had a size method, but we won't
+ # worry about for now. Also, we can't really cope with the potential
+ # compressibility of the TSIG owner name, so we estimate with the uncompressed
+ # size. We will disable compression when TSIG and padding are both is active
+ # so that the padding comes out right.
+ if not self.tsig:
+ return 0
+ f = io.BytesIO()
+ self.tsig.to_wire(f)
+ return len(f.getvalue())
+
+ def to_wire(
+ self,
+ origin: Optional[dns.name.Name] = None,
+ max_size: int = 0,
+ multi: bool = False,
+ tsig_ctx: Optional[Any] = None,
+ prepend_length: bool = False,
+ prefer_truncation: bool = False,
+ **kw: Dict[str, Any],
+ ) -> bytes:
+ """Return a string containing the message in DNS compressed wire
+ format.
+
+ Additional keyword arguments are passed to the RRset ``to_wire()``
+ method.
+
+ *origin*, a ``dns.name.Name`` or ``None``, the origin to be appended
+ to any relative names. If ``None``, and the message has an origin
+ attribute that is not ``None``, then it will be used.
+
+ *max_size*, an ``int``, the maximum size of the wire format
+ output; default is 0, which means "the message's request
+ payload, if nonzero, or 65535".
+
+ *multi*, a ``bool``, should be set to ``True`` if this message is
+ part of a multiple message sequence.
+
+ *tsig_ctx*, a ``dns.tsig.HMACTSig`` or ``dns.tsig.GSSTSig`` object, the
+ ongoing TSIG context, used when signing zone transfers.
+
+ *prepend_length*, a ``bool``, should be set to ``True`` if the caller
+ wants the message length prepended to the message itself. This is
+ useful for messages sent over TCP, TLS (DoT), or QUIC (DoQ).
+
+ *prefer_truncation*, a ``bool``, should be set to ``True`` if the caller
+ wants the message to be truncated if it would otherwise exceed the
+ maximum length. If the truncation occurs before the additional section,
+ the TC bit will be set.
+
+ Raises ``dns.exception.TooBig`` if *max_size* was exceeded.
+
+ Returns a ``bytes``.
+ """
+
+ if origin is None and self.origin is not None:
+ origin = self.origin
+ if max_size == 0:
+ if self.request_payload != 0:
+ max_size = self.request_payload
+ else:
+ max_size = 65535
+ if max_size < 512:
+ max_size = 512
+ elif max_size > 65535:
+ max_size = 65535
+ r = dns.renderer.Renderer(self.id, self.flags, max_size, origin)
+ opt_reserve = self._compute_opt_reserve()
+ r.reserve(opt_reserve)
+ tsig_reserve = self._compute_tsig_reserve()
+ r.reserve(tsig_reserve)
+ try:
+ for rrset in self.question:
+ r.add_question(rrset.name, rrset.rdtype, rrset.rdclass)
+ for rrset in self.answer:
+ r.add_rrset(dns.renderer.ANSWER, rrset, **kw)
+ for rrset in self.authority:
+ r.add_rrset(dns.renderer.AUTHORITY, rrset, **kw)
+ for rrset in self.additional:
+ r.add_rrset(dns.renderer.ADDITIONAL, rrset, **kw)
+ except dns.exception.TooBig:
+ if prefer_truncation:
+ if r.section < dns.renderer.ADDITIONAL:
+ r.flags |= dns.flags.TC
+ else:
+ raise
+ r.release_reserved()
+ if self.opt is not None:
+ r.add_opt(self.opt, self.pad, opt_reserve, tsig_reserve)
+ r.write_header()
+ if self.tsig is not None:
+ (new_tsig, ctx) = dns.tsig.sign(
+ r.get_wire(),
+ self.keyring,
+ self.tsig[0],
+ int(time.time()),
+ self.request_mac,
+ tsig_ctx,
+ multi,
+ )
+ self.tsig.clear()
+ self.tsig.add(new_tsig)
+ r.add_rrset(dns.renderer.ADDITIONAL, self.tsig)
+ r.write_header()
+ if multi:
+ self.tsig_ctx = ctx
+ wire = r.get_wire()
+ self.wire = wire
+ if prepend_length:
+ wire = len(wire).to_bytes(2, "big") + wire
+ return wire
+
+ @staticmethod
+ def _make_tsig(
+ keyname, algorithm, time_signed, fudge, mac, original_id, error, other
+ ):
+ tsig = dns.rdtypes.ANY.TSIG.TSIG(
+ dns.rdataclass.ANY,
+ dns.rdatatype.TSIG,
+ algorithm,
+ time_signed,
+ fudge,
+ mac,
+ original_id,
+ error,
+ other,
+ )
+ return dns.rrset.from_rdata(keyname, 0, tsig)
+
+ def use_tsig(
+ self,
+ keyring: Any,
+ keyname: Optional[Union[dns.name.Name, str]] = None,
+ fudge: int = 300,
+ original_id: Optional[int] = None,
+ tsig_error: int = 0,
+ other_data: bytes = b"",
+ algorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm,
+ ) -> None:
+ """When sending, a TSIG signature using the specified key
+ should be added.
+
+ *key*, a ``dns.tsig.Key`` is the key to use. If a key is specified,
+ the *keyring* and *algorithm* fields are not used.
+
+ *keyring*, a ``dict``, ``callable`` or ``dns.tsig.Key``, is either
+ the TSIG keyring or key to use.
+
+ The format of a keyring dict is a mapping from TSIG key name, as
+ ``dns.name.Name`` to ``dns.tsig.Key`` or a TSIG secret, a ``bytes``.
+ If a ``dict`` *keyring* is specified but a *keyname* is not, the key
+ used will be the first key in the *keyring*. Note that the order of
+ keys in a dictionary is not defined, so applications should supply a
+ keyname when a ``dict`` keyring is used, unless they know the keyring
+ contains only one key. If a ``callable`` keyring is specified, the
+ callable will be called with the message and the keyname, and is
+ expected to return a key.
+
+ *keyname*, a ``dns.name.Name``, ``str`` or ``None``, the name of
+ this TSIG key to use; defaults to ``None``. If *keyring* is a
+ ``dict``, the key must be defined in it. If *keyring* is a
+ ``dns.tsig.Key``, this is ignored.
+
+ *fudge*, an ``int``, the TSIG time fudge.
+
+ *original_id*, an ``int``, the TSIG original id. If ``None``,
+ the message's id is used.
+
+ *tsig_error*, an ``int``, the TSIG error code.
+
+ *other_data*, a ``bytes``, the TSIG other data.
+
+ *algorithm*, a ``dns.name.Name`` or ``str``, the TSIG algorithm to use. This is
+ only used if *keyring* is a ``dict``, and the key entry is a ``bytes``.
+ """
+
+ if isinstance(keyring, dns.tsig.Key):
+ key = keyring
+ keyname = key.name
+ elif callable(keyring):
+ key = keyring(self, keyname)
+ else:
+ if isinstance(keyname, str):
+ keyname = dns.name.from_text(keyname)
+ if keyname is None:
+ keyname = next(iter(keyring))
+ key = keyring[keyname]
+ if isinstance(key, bytes):
+ key = dns.tsig.Key(keyname, key, algorithm)
+ self.keyring = key
+ if original_id is None:
+ original_id = self.id
+ self.tsig = self._make_tsig(
+ keyname,
+ self.keyring.algorithm,
+ 0,
+ fudge,
+ b"\x00" * dns.tsig.mac_sizes[self.keyring.algorithm],
+ original_id,
+ tsig_error,
+ other_data,
+ )
+
+ @property
+ def keyname(self) -> Optional[dns.name.Name]:
+ if self.tsig:
+ return self.tsig.name
+ else:
+ return None
+
+ @property
+ def keyalgorithm(self) -> Optional[dns.name.Name]:
+ if self.tsig:
+ return self.tsig[0].algorithm
+ else:
+ return None
+
+ @property
+ def mac(self) -> Optional[bytes]:
+ if self.tsig:
+ return self.tsig[0].mac
+ else:
+ return None
+
+ @property
+ def tsig_error(self) -> Optional[int]:
+ if self.tsig:
+ return self.tsig[0].error
+ else:
+ return None
+
+ @property
+ def had_tsig(self) -> bool:
+ return bool(self.tsig)
+
+ @staticmethod
+ def _make_opt(flags=0, payload=DEFAULT_EDNS_PAYLOAD, options=None):
+ opt = dns.rdtypes.ANY.OPT.OPT(payload, dns.rdatatype.OPT, options or ())
+ return dns.rrset.from_rdata(dns.name.root, int(flags), opt)
+
+ def use_edns(
+ self,
+ edns: Optional[Union[int, bool]] = 0,
+ ednsflags: int = 0,
+ payload: int = DEFAULT_EDNS_PAYLOAD,
+ request_payload: Optional[int] = None,
+ options: Optional[List[dns.edns.Option]] = None,
+ pad: int = 0,
+ ) -> None:
+ """Configure EDNS behavior.
+
+ *edns*, an ``int``, is the EDNS level to use. Specifying ``None``, ``False``,
+ or ``-1`` means "do not use EDNS", and in this case the other parameters are
+ ignored. Specifying ``True`` is equivalent to specifying 0, i.e. "use EDNS0".
+
+ *ednsflags*, an ``int``, the EDNS flag values.
+
+ *payload*, an ``int``, is the EDNS sender's payload field, which is the maximum
+ size of UDP datagram the sender can handle. I.e. how big a response to this
+ message can be.
+
+ *request_payload*, an ``int``, is the EDNS payload size to use when sending this
+ message. If not specified, defaults to the value of *payload*.
+
+ *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS options.
+
+ *pad*, a non-negative ``int``. If 0, the default, do not pad; otherwise add
+ padding bytes to make the message size a multiple of *pad*. Note that if
+ padding is non-zero, an EDNS PADDING option will always be added to the
+ message.
+ """
+
+ if edns is None or edns is False:
+ edns = -1
+ elif edns is True:
+ edns = 0
+ if edns < 0:
+ self.opt = None
+ self.request_payload = 0
+ else:
+ # make sure the EDNS version in ednsflags agrees with edns
+ ednsflags &= 0xFF00FFFF
+ ednsflags |= edns << 16
+ if options is None:
+ options = []
+ self.opt = self._make_opt(ednsflags, payload, options)
+ if request_payload is None:
+ request_payload = payload
+ self.request_payload = request_payload
+ if pad < 0:
+ raise ValueError("pad must be non-negative")
+ self.pad = pad
+
+ @property
+ def edns(self) -> int:
+ if self.opt:
+ return (self.ednsflags & 0xFF0000) >> 16
+ else:
+ return -1
+
+ @property
+ def ednsflags(self) -> int:
+ if self.opt:
+ return self.opt.ttl
+ else:
+ return 0
+
+ @ednsflags.setter
+ def ednsflags(self, v):
+ if self.opt:
+ self.opt.ttl = v
+ elif v:
+ self.opt = self._make_opt(v)
+
+ @property
+ def payload(self) -> int:
+ if self.opt:
+ return self.opt[0].payload
+ else:
+ return 0
+
+ @property
+ def options(self) -> Tuple:
+ if self.opt:
+ return self.opt[0].options
+ else:
+ return ()
+
+ def want_dnssec(self, wanted: bool = True) -> None:
+ """Enable or disable 'DNSSEC desired' flag in requests.
+
+ *wanted*, a ``bool``. If ``True``, then DNSSEC data is
+ desired in the response, EDNS is enabled if required, and then
+ the DO bit is set. If ``False``, the DO bit is cleared if
+ EDNS is enabled.
+ """
+
+ if wanted:
+ self.ednsflags |= dns.flags.DO
+ elif self.opt:
+ self.ednsflags &= ~int(dns.flags.DO)
+
+ def rcode(self) -> dns.rcode.Rcode:
+ """Return the rcode.
+
+ Returns a ``dns.rcode.Rcode``.
+ """
+ return dns.rcode.from_flags(int(self.flags), int(self.ednsflags))
+
+ def set_rcode(self, rcode: dns.rcode.Rcode) -> None:
+ """Set the rcode.
+
+ *rcode*, a ``dns.rcode.Rcode``, is the rcode to set.
+ """
+ (value, evalue) = dns.rcode.to_flags(rcode)
+ self.flags &= 0xFFF0
+ self.flags |= value
+ self.ednsflags &= 0x00FFFFFF
+ self.ednsflags |= evalue
+
+ def opcode(self) -> dns.opcode.Opcode:
+ """Return the opcode.
+
+ Returns a ``dns.opcode.Opcode``.
+ """
+ return dns.opcode.from_flags(int(self.flags))
+
+ def set_opcode(self, opcode: dns.opcode.Opcode) -> None:
+ """Set the opcode.
+
+ *opcode*, a ``dns.opcode.Opcode``, is the opcode to set.
+ """
+ self.flags &= 0x87FF
+ self.flags |= dns.opcode.to_flags(opcode)
+
+ def get_options(self, otype: dns.edns.OptionType) -> List[dns.edns.Option]:
+ """Return the list of options of the specified type."""
+ return [option for option in self.options if option.otype == otype]
+
+ def extended_errors(self) -> List[dns.edns.EDEOption]:
+ """Return the list of Extended DNS Error (EDE) options in the message"""
+ return cast(List[dns.edns.EDEOption], self.get_options(dns.edns.OptionType.EDE))
+
+ def _get_one_rr_per_rrset(self, value):
+ # What the caller picked is fine.
+ return value
+
+ # pylint: disable=unused-argument
+
+ def _parse_rr_header(self, section, name, rdclass, rdtype):
+ return (rdclass, rdtype, None, False)
+
+ # pylint: enable=unused-argument
+
+ def _parse_special_rr_header(self, section, count, position, name, rdclass, rdtype):
+ if rdtype == dns.rdatatype.OPT:
+ if (
+ section != MessageSection.ADDITIONAL
+ or self.opt
+ or name != dns.name.root
+ ):
+ raise BadEDNS
+ elif rdtype == dns.rdatatype.TSIG:
+ if (
+ section != MessageSection.ADDITIONAL
+ or rdclass != dns.rdatatype.ANY
+ or position != count - 1
+ ):
+ raise BadTSIG
+ return (rdclass, rdtype, None, False)
+
+
+class ChainingResult:
+ """The result of a call to dns.message.QueryMessage.resolve_chaining().
+
+ The ``answer`` attribute is the answer RRSet, or ``None`` if it doesn't
+ exist.
+
+ The ``canonical_name`` attribute is the canonical name after all
+ chaining has been applied (this is the same name as ``rrset.name`` in cases
+ where rrset is not ``None``).
+
+ The ``minimum_ttl`` attribute is the minimum TTL, i.e. the TTL to
+ use if caching the data. It is the smallest of all the CNAME TTLs
+ and either the answer TTL if it exists or the SOA TTL and SOA
+ minimum values for negative answers.
+
+ The ``cnames`` attribute is a list of all the CNAME RRSets followed to
+ get to the canonical name.
+ """
+
+ def __init__(
+ self,
+ canonical_name: dns.name.Name,
+ answer: Optional[dns.rrset.RRset],
+ minimum_ttl: int,
+ cnames: List[dns.rrset.RRset],
+ ):
+ self.canonical_name = canonical_name
+ self.answer = answer
+ self.minimum_ttl = minimum_ttl
+ self.cnames = cnames
+
+
+class QueryMessage(Message):
+ def resolve_chaining(self) -> ChainingResult:
+ """Follow the CNAME chain in the response to determine the answer
+ RRset.
+
+ Raises ``dns.message.NotQueryResponse`` if the message is not
+ a response.
+
+ Raises ``dns.message.ChainTooLong`` if the CNAME chain is too long.
+
+ Raises ``dns.message.AnswerForNXDOMAIN`` if the rcode is NXDOMAIN
+ but an answer was found.
+
+ Raises ``dns.exception.FormError`` if the question count is not 1.
+
+ Returns a ChainingResult object.
+ """
+ if self.flags & dns.flags.QR == 0:
+ raise NotQueryResponse
+ if len(self.question) != 1:
+ raise dns.exception.FormError
+ question = self.question[0]
+ qname = question.name
+ min_ttl = dns.ttl.MAX_TTL
+ answer = None
+ count = 0
+ cnames = []
+ while count < MAX_CHAIN:
+ try:
+ answer = self.find_rrset(
+ self.answer, qname, question.rdclass, question.rdtype
+ )
+ min_ttl = min(min_ttl, answer.ttl)
+ break
+ except KeyError:
+ if question.rdtype != dns.rdatatype.CNAME:
+ try:
+ crrset = self.find_rrset(
+ self.answer, qname, question.rdclass, dns.rdatatype.CNAME
+ )
+ cnames.append(crrset)
+ min_ttl = min(min_ttl, crrset.ttl)
+ for rd in crrset:
+ qname = rd.target
+ break
+ count += 1
+ continue
+ except KeyError:
+ # Exit the chaining loop
+ break
+ else:
+ # Exit the chaining loop
+ break
+ if count >= MAX_CHAIN:
+ raise ChainTooLong
+ if self.rcode() == dns.rcode.NXDOMAIN and answer is not None:
+ raise AnswerForNXDOMAIN
+ if answer is None:
+ # Further minimize the TTL with NCACHE.
+ auname = qname
+ while True:
+ # Look for an SOA RR whose owner name is a superdomain
+ # of qname.
+ try:
+ srrset = self.find_rrset(
+ self.authority, auname, question.rdclass, dns.rdatatype.SOA
+ )
+ min_ttl = min(min_ttl, srrset.ttl, srrset[0].minimum)
+ break
+ except KeyError:
+ try:
+ auname = auname.parent()
+ except dns.name.NoParent:
+ break
+ return ChainingResult(qname, answer, min_ttl, cnames)
+
+ def canonical_name(self) -> dns.name.Name:
+ """Return the canonical name of the first name in the question
+ section.
+
+ Raises ``dns.message.NotQueryResponse`` if the message is not
+ a response.
+
+ Raises ``dns.message.ChainTooLong`` if the CNAME chain is too long.
+
+ Raises ``dns.message.AnswerForNXDOMAIN`` if the rcode is NXDOMAIN
+ but an answer was found.
+
+ Raises ``dns.exception.FormError`` if the question count is not 1.
+ """
+ return self.resolve_chaining().canonical_name
+
+
+def _maybe_import_update():
+ # We avoid circular imports by doing this here. We do it in another
+ # function as doing it in _message_factory_from_opcode() makes "dns"
+ # a local symbol, and the first line fails :)
+
+ # pylint: disable=redefined-outer-name,import-outside-toplevel,unused-import
+ import dns.update # noqa: F401
+
+
+def _message_factory_from_opcode(opcode):
+ if opcode == dns.opcode.QUERY:
+ return QueryMessage
+ elif opcode == dns.opcode.UPDATE:
+ _maybe_import_update()
+ return dns.update.UpdateMessage
+ else:
+ return Message
+
+
+class _WireReader:
+ """Wire format reader.
+
+ parser: the binary parser
+ message: The message object being built
+ initialize_message: Callback to set message parsing options
+ question_only: Are we only reading the question?
+ one_rr_per_rrset: Put each RR into its own RRset?
+ keyring: TSIG keyring
+ ignore_trailing: Ignore trailing junk at end of request?
+ multi: Is this message part of a multi-message sequence?
+ DNS dynamic updates.
+ continue_on_error: try to extract as much information as possible from
+ the message, accumulating MessageErrors in the *errors* attribute instead of
+ raising them.
+ """
+
+ def __init__(
+ self,
+ wire,
+ initialize_message,
+ question_only=False,
+ one_rr_per_rrset=False,
+ ignore_trailing=False,
+ keyring=None,
+ multi=False,
+ continue_on_error=False,
+ ):
+ self.parser = dns.wire.Parser(wire)
+ self.message = None
+ self.initialize_message = initialize_message
+ self.question_only = question_only
+ self.one_rr_per_rrset = one_rr_per_rrset
+ self.ignore_trailing = ignore_trailing
+ self.keyring = keyring
+ self.multi = multi
+ self.continue_on_error = continue_on_error
+ self.errors = []
+
+ def _get_question(self, section_number, qcount):
+ """Read the next *qcount* records from the wire data and add them to
+ the question section.
+ """
+ assert self.message is not None
+ section = self.message.sections[section_number]
+ for _ in range(qcount):
+ qname = self.parser.get_name(self.message.origin)
+ (rdtype, rdclass) = self.parser.get_struct("!HH")
+ (rdclass, rdtype, _, _) = self.message._parse_rr_header(
+ section_number, qname, rdclass, rdtype
+ )
+ self.message.find_rrset(
+ section, qname, rdclass, rdtype, create=True, force_unique=True
+ )
+
+ def _add_error(self, e):
+ self.errors.append(MessageError(e, self.parser.current))
+
+ def _get_section(self, section_number, count):
+ """Read the next I{count} records from the wire data and add them to
+ the specified section.
+
+ section_number: the section of the message to which to add records
+ count: the number of records to read
+ """
+ assert self.message is not None
+ section = self.message.sections[section_number]
+ force_unique = self.one_rr_per_rrset
+ for i in range(count):
+ rr_start = self.parser.current
+ absolute_name = self.parser.get_name()
+ if self.message.origin is not None:
+ name = absolute_name.relativize(self.message.origin)
+ else:
+ name = absolute_name
+ (rdtype, rdclass, ttl, rdlen) = self.parser.get_struct("!HHIH")
+ if rdtype in (dns.rdatatype.OPT, dns.rdatatype.TSIG):
+ (
+ rdclass,
+ rdtype,
+ deleting,
+ empty,
+ ) = self.message._parse_special_rr_header(
+ section_number, count, i, name, rdclass, rdtype
+ )
+ else:
+ (rdclass, rdtype, deleting, empty) = self.message._parse_rr_header(
+ section_number, name, rdclass, rdtype
+ )
+ rdata_start = self.parser.current
+ try:
+ if empty:
+ if rdlen > 0:
+ raise dns.exception.FormError
+ rd = None
+ covers = dns.rdatatype.NONE
+ else:
+ with self.parser.restrict_to(rdlen):
+ rd = dns.rdata.from_wire_parser(
+ rdclass, rdtype, self.parser, self.message.origin
+ )
+ covers = rd.covers()
+ if self.message.xfr and rdtype == dns.rdatatype.SOA:
+ force_unique = True
+ if rdtype == dns.rdatatype.OPT:
+ self.message.opt = dns.rrset.from_rdata(name, ttl, rd)
+ elif rdtype == dns.rdatatype.TSIG:
+ if self.keyring is None or self.keyring is True:
+ raise UnknownTSIGKey("got signed message without keyring")
+ elif isinstance(self.keyring, dict):
+ key = self.keyring.get(absolute_name)
+ if isinstance(key, bytes):
+ key = dns.tsig.Key(absolute_name, key, rd.algorithm)
+ elif callable(self.keyring):
+ key = self.keyring(self.message, absolute_name)
+ else:
+ key = self.keyring
+ if key is None:
+ raise UnknownTSIGKey(f"key '{name}' unknown")
+ if key:
+ self.message.keyring = key
+ self.message.tsig_ctx = dns.tsig.validate(
+ self.parser.wire,
+ key,
+ absolute_name,
+ rd,
+ int(time.time()),
+ self.message.request_mac,
+ rr_start,
+ self.message.tsig_ctx,
+ self.multi,
+ )
+ self.message.tsig = dns.rrset.from_rdata(absolute_name, 0, rd)
+ else:
+ rrset = self.message.find_rrset(
+ section,
+ name,
+ rdclass,
+ rdtype,
+ covers,
+ deleting,
+ True,
+ force_unique,
+ )
+ if rd is not None:
+ if ttl > 0x7FFFFFFF:
+ ttl = 0
+ rrset.add(rd, ttl)
+ except Exception as e:
+ if self.continue_on_error:
+ self._add_error(e)
+ self.parser.seek(rdata_start + rdlen)
+ else:
+ raise
+
+ def read(self):
+ """Read a wire format DNS message and build a dns.message.Message
+ object."""
+
+ if self.parser.remaining() < 12:
+ raise ShortHeader
+ (id, flags, qcount, ancount, aucount, adcount) = self.parser.get_struct(
+ "!HHHHHH"
+ )
+ factory = _message_factory_from_opcode(dns.opcode.from_flags(flags))
+ self.message = factory(id=id)
+ self.message.flags = dns.flags.Flag(flags)
+ self.message.wire = self.parser.wire
+ self.initialize_message(self.message)
+ self.one_rr_per_rrset = self.message._get_one_rr_per_rrset(
+ self.one_rr_per_rrset
+ )
+ try:
+ self._get_question(MessageSection.QUESTION, qcount)
+ if self.question_only:
+ return self.message
+ self._get_section(MessageSection.ANSWER, ancount)
+ self._get_section(MessageSection.AUTHORITY, aucount)
+ self._get_section(MessageSection.ADDITIONAL, adcount)
+ if not self.ignore_trailing and self.parser.remaining() != 0:
+ raise TrailingJunk
+ if self.multi and self.message.tsig_ctx and not self.message.had_tsig:
+ self.message.tsig_ctx.update(self.parser.wire)
+ except Exception as e:
+ if self.continue_on_error:
+ self._add_error(e)
+ else:
+ raise
+ return self.message
+
+
+def from_wire(
+ wire: bytes,
+ keyring: Optional[Any] = None,
+ request_mac: Optional[bytes] = b"",
+ xfr: bool = False,
+ origin: Optional[dns.name.Name] = None,
+ tsig_ctx: Optional[Union[dns.tsig.HMACTSig, dns.tsig.GSSTSig]] = None,
+ multi: bool = False,
+ question_only: bool = False,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ raise_on_truncation: bool = False,
+ continue_on_error: bool = False,
+) -> Message:
+ """Convert a DNS wire format message into a message object.
+
+ *keyring*, a ``dns.tsig.Key``, ``dict``, ``bool``, or ``None``, the key or keyring
+ to use if the message is signed. If ``None`` or ``True``, then trying to decode
+ a message with a TSIG will fail as it cannot be validated. If ``False``, then
+ TSIG validation is disabled.
+
+ *request_mac*, a ``bytes`` or ``None``. If the message is a response to a
+ TSIG-signed request, *request_mac* should be set to the MAC of that request.
+
+ *xfr*, a ``bool``, should be set to ``True`` if this message is part of a zone
+ transfer.
+
+ *origin*, a ``dns.name.Name`` or ``None``. If the message is part of a zone
+ transfer, *origin* should be the origin name of the zone. If not ``None``, names
+ will be relativized to the origin.
+
+ *tsig_ctx*, a ``dns.tsig.HMACTSig`` or ``dns.tsig.GSSTSig`` object, the ongoing TSIG
+ context, used when validating zone transfers.
+
+ *multi*, a ``bool``, should be set to ``True`` if this message is part of a multiple
+ message sequence.
+
+ *question_only*, a ``bool``. If ``True``, read only up to the end of the question
+ section.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset.
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the
+ message.
+
+ *raise_on_truncation*, a ``bool``. If ``True``, raise an exception if the TC bit is
+ set.
+
+ *continue_on_error*, a ``bool``. If ``True``, try to continue parsing even if
+ errors occur. Erroneous rdata will be ignored. Errors will be accumulated as a
+ list of MessageError objects in the message's ``errors`` attribute. This option is
+ recommended only for DNS analysis tools, or for use in a server as part of an error
+ handling path. The default is ``False``.
+
+ Raises ``dns.message.ShortHeader`` if the message is less than 12 octets long.
+
+ Raises ``dns.message.TrailingJunk`` if there were octets in the message past the end
+ of the proper DNS message, and *ignore_trailing* is ``False``.
+
+ Raises ``dns.message.BadEDNS`` if an OPT record was in the wrong section, or
+ occurred more than once.
+
+ Raises ``dns.message.BadTSIG`` if a TSIG record was not the last record of the
+ additional data section.
+
+ Raises ``dns.message.Truncated`` if the TC flag is set and *raise_on_truncation* is
+ ``True``.
+
+ Returns a ``dns.message.Message``.
+ """
+
+ # We permit None for request_mac solely for backwards compatibility
+ if request_mac is None:
+ request_mac = b""
+
+ def initialize_message(message):
+ message.request_mac = request_mac
+ message.xfr = xfr
+ message.origin = origin
+ message.tsig_ctx = tsig_ctx
+
+ reader = _WireReader(
+ wire,
+ initialize_message,
+ question_only,
+ one_rr_per_rrset,
+ ignore_trailing,
+ keyring,
+ multi,
+ continue_on_error,
+ )
+ try:
+ m = reader.read()
+ except dns.exception.FormError:
+ if (
+ reader.message
+ and (reader.message.flags & dns.flags.TC)
+ and raise_on_truncation
+ ):
+ raise Truncated(message=reader.message)
+ else:
+ raise
+ # Reading a truncated message might not have any errors, so we
+ # have to do this check here too.
+ if m.flags & dns.flags.TC and raise_on_truncation:
+ raise Truncated(message=m)
+ if continue_on_error:
+ m.errors = reader.errors
+
+ return m
+
+
+class _TextReader:
+ """Text format reader.
+
+ tok: the tokenizer.
+ message: The message object being built.
+ DNS dynamic updates.
+ last_name: The most recently read name when building a message object.
+ one_rr_per_rrset: Put each RR into its own RRset?
+ origin: The origin for relative names
+ relativize: relativize names?
+ relativize_to: the origin to relativize to.
+ """
+
+ def __init__(
+ self,
+ text,
+ idna_codec,
+ one_rr_per_rrset=False,
+ origin=None,
+ relativize=True,
+ relativize_to=None,
+ ):
+ self.message = None
+ self.tok = dns.tokenizer.Tokenizer(text, idna_codec=idna_codec)
+ self.last_name = None
+ self.one_rr_per_rrset = one_rr_per_rrset
+ self.origin = origin
+ self.relativize = relativize
+ self.relativize_to = relativize_to
+ self.id = None
+ self.edns = -1
+ self.ednsflags = 0
+ self.payload = DEFAULT_EDNS_PAYLOAD
+ self.rcode = None
+ self.opcode = dns.opcode.QUERY
+ self.flags = 0
+
+ def _header_line(self, _):
+ """Process one line from the text format header section."""
+
+ token = self.tok.get()
+ what = token.value
+ if what == "id":
+ self.id = self.tok.get_int()
+ elif what == "flags":
+ while True:
+ token = self.tok.get()
+ if not token.is_identifier():
+ self.tok.unget(token)
+ break
+ self.flags = self.flags | dns.flags.from_text(token.value)
+ elif what == "edns":
+ self.edns = self.tok.get_int()
+ self.ednsflags = self.ednsflags | (self.edns << 16)
+ elif what == "eflags":
+ if self.edns < 0:
+ self.edns = 0
+ while True:
+ token = self.tok.get()
+ if not token.is_identifier():
+ self.tok.unget(token)
+ break
+ self.ednsflags = self.ednsflags | dns.flags.edns_from_text(token.value)
+ elif what == "payload":
+ self.payload = self.tok.get_int()
+ if self.edns < 0:
+ self.edns = 0
+ elif what == "opcode":
+ text = self.tok.get_string()
+ self.opcode = dns.opcode.from_text(text)
+ self.flags = self.flags | dns.opcode.to_flags(self.opcode)
+ elif what == "rcode":
+ text = self.tok.get_string()
+ self.rcode = dns.rcode.from_text(text)
+ else:
+ raise UnknownHeaderField
+ self.tok.get_eol()
+
+ def _question_line(self, section_number):
+ """Process one line from the text format question section."""
+
+ section = self.message.sections[section_number]
+ token = self.tok.get(want_leading=True)
+ if not token.is_whitespace():
+ self.last_name = self.tok.as_name(
+ token, self.message.origin, self.relativize, self.relativize_to
+ )
+ name = self.last_name
+ if name is None:
+ raise NoPreviousName
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ # Class
+ try:
+ rdclass = dns.rdataclass.from_text(token.value)
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ except dns.exception.SyntaxError:
+ raise dns.exception.SyntaxError
+ except Exception:
+ rdclass = dns.rdataclass.IN
+ # Type
+ rdtype = dns.rdatatype.from_text(token.value)
+ (rdclass, rdtype, _, _) = self.message._parse_rr_header(
+ section_number, name, rdclass, rdtype
+ )
+ self.message.find_rrset(
+ section, name, rdclass, rdtype, create=True, force_unique=True
+ )
+ self.tok.get_eol()
+
+ def _rr_line(self, section_number):
+ """Process one line from the text format answer, authority, or
+ additional data sections.
+ """
+
+ section = self.message.sections[section_number]
+ # Name
+ token = self.tok.get(want_leading=True)
+ if not token.is_whitespace():
+ self.last_name = self.tok.as_name(
+ token, self.message.origin, self.relativize, self.relativize_to
+ )
+ name = self.last_name
+ if name is None:
+ raise NoPreviousName
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ # TTL
+ try:
+ ttl = int(token.value, 0)
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ except dns.exception.SyntaxError:
+ raise dns.exception.SyntaxError
+ except Exception:
+ ttl = 0
+ # Class
+ try:
+ rdclass = dns.rdataclass.from_text(token.value)
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ except dns.exception.SyntaxError:
+ raise dns.exception.SyntaxError
+ except Exception:
+ rdclass = dns.rdataclass.IN
+ # Type
+ rdtype = dns.rdatatype.from_text(token.value)
+ (rdclass, rdtype, deleting, empty) = self.message._parse_rr_header(
+ section_number, name, rdclass, rdtype
+ )
+ token = self.tok.get()
+ if empty and not token.is_eol_or_eof():
+ raise dns.exception.SyntaxError
+ if not empty and token.is_eol_or_eof():
+ raise dns.exception.UnexpectedEnd
+ if not token.is_eol_or_eof():
+ self.tok.unget(token)
+ rd = dns.rdata.from_text(
+ rdclass,
+ rdtype,
+ self.tok,
+ self.message.origin,
+ self.relativize,
+ self.relativize_to,
+ )
+ covers = rd.covers()
+ else:
+ rd = None
+ covers = dns.rdatatype.NONE
+ rrset = self.message.find_rrset(
+ section,
+ name,
+ rdclass,
+ rdtype,
+ covers,
+ deleting,
+ True,
+ self.one_rr_per_rrset,
+ )
+ if rd is not None:
+ rrset.add(rd, ttl)
+
+ def _make_message(self):
+ factory = _message_factory_from_opcode(self.opcode)
+ message = factory(id=self.id)
+ message.flags = self.flags
+ if self.edns >= 0:
+ message.use_edns(self.edns, self.ednsflags, self.payload)
+ if self.rcode:
+ message.set_rcode(self.rcode)
+ if self.origin:
+ message.origin = self.origin
+ return message
+
+ def read(self):
+ """Read a text format DNS message and build a dns.message.Message
+ object."""
+
+ line_method = self._header_line
+ section_number = None
+ while 1:
+ token = self.tok.get(True, True)
+ if token.is_eol_or_eof():
+ break
+ if token.is_comment():
+ u = token.value.upper()
+ if u == "HEADER":
+ line_method = self._header_line
+
+ if self.message:
+ message = self.message
+ else:
+ # If we don't have a message, create one with the current
+ # opcode, so that we know which section names to parse.
+ message = self._make_message()
+ try:
+ section_number = message._section_enum.from_text(u)
+ # We found a section name. If we don't have a message,
+ # use the one we just created.
+ if not self.message:
+ self.message = message
+ self.one_rr_per_rrset = message._get_one_rr_per_rrset(
+ self.one_rr_per_rrset
+ )
+ if section_number == MessageSection.QUESTION:
+ line_method = self._question_line
+ else:
+ line_method = self._rr_line
+ except Exception:
+ # It's just a comment.
+ pass
+ self.tok.get_eol()
+ continue
+ self.tok.unget(token)
+ line_method(section_number)
+ if not self.message:
+ self.message = self._make_message()
+ return self.message
+
+
+def from_text(
+ text: str,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ one_rr_per_rrset: bool = False,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ relativize_to: Optional[dns.name.Name] = None,
+) -> Message:
+ """Convert the text format message into a message object.
+
+ The reader stops after reading the first blank line in the input to
+ facilitate reading multiple messages from a single file with
+ ``dns.message.from_file()``.
+
+ *text*, a ``str``, the text format message.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, then each RR is put
+ into its own rrset. The default is ``False``.
+
+ *origin*, a ``dns.name.Name`` (or ``None``), the
+ origin to use for relative names.
+
+ *relativize*, a ``bool``. If true, name will be relativized.
+
+ *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use
+ when relativizing names. If not set, the *origin* value will be used.
+
+ Raises ``dns.message.UnknownHeaderField`` if a header is unknown.
+
+ Raises ``dns.exception.SyntaxError`` if the text is badly formed.
+
+ Returns a ``dns.message.Message object``
+ """
+
+ # 'text' can also be a file, but we don't publish that fact
+ # since it's an implementation detail. The official file
+ # interface is from_file().
+
+ reader = _TextReader(
+ text, idna_codec, one_rr_per_rrset, origin, relativize, relativize_to
+ )
+ return reader.read()
+
+
+def from_file(
+ f: Any,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ one_rr_per_rrset: bool = False,
+) -> Message:
+ """Read the next text format message from the specified file.
+
+ Message blocks are separated by a single blank line.
+
+ *f*, a ``file`` or ``str``. If *f* is text, it is treated as the
+ pathname of a file to open.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, then each RR is put
+ into its own rrset. The default is ``False``.
+
+ Raises ``dns.message.UnknownHeaderField`` if a header is unknown.
+
+ Raises ``dns.exception.SyntaxError`` if the text is badly formed.
+
+ Returns a ``dns.message.Message object``
+ """
+
+ if isinstance(f, str):
+ cm: contextlib.AbstractContextManager = open(f)
+ else:
+ cm = contextlib.nullcontext(f)
+ with cm as f:
+ return from_text(f, idna_codec, one_rr_per_rrset)
+ assert False # for mypy lgtm[py/unreachable-statement]
+
+
+def make_query(
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ use_edns: Optional[Union[int, bool]] = None,
+ want_dnssec: bool = False,
+ ednsflags: Optional[int] = None,
+ payload: Optional[int] = None,
+ request_payload: Optional[int] = None,
+ options: Optional[List[dns.edns.Option]] = None,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ id: Optional[int] = None,
+ flags: int = dns.flags.RD,
+ pad: int = 0,
+) -> QueryMessage:
+ """Make a query message.
+
+ The query name, type, and class may all be specified either
+ as objects of the appropriate type, or as strings.
+
+ The query will have a randomly chosen query id, and its DNS flags
+ will be set to dns.flags.RD.
+
+ qname, a ``dns.name.Name`` or ``str``, the query name.
+
+ *rdtype*, an ``int`` or ``str``, the desired rdata type.
+
+ *rdclass*, an ``int`` or ``str``, the desired rdata class; the default
+ is class IN.
+
+ *use_edns*, an ``int``, ``bool`` or ``None``. The EDNS level to use; the
+ default is ``None``. If ``None``, EDNS will be enabled only if other
+ parameters (*ednsflags*, *payload*, *request_payload*, or *options*) are
+ set.
+ See the description of dns.message.Message.use_edns() for the possible
+ values for use_edns and their meanings.
+
+ *want_dnssec*, a ``bool``. If ``True``, DNSSEC data is desired.
+
+ *ednsflags*, an ``int``, the EDNS flag values.
+
+ *payload*, an ``int``, is the EDNS sender's payload field, which is the
+ maximum size of UDP datagram the sender can handle. I.e. how big
+ a response to this message can be.
+
+ *request_payload*, an ``int``, is the EDNS payload size to use when
+ sending this message. If not specified, defaults to the value of
+ *payload*.
+
+ *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS
+ options.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ *id*, an ``int`` or ``None``, the desired query id. The default is
+ ``None``, which generates a random query id.
+
+ *flags*, an ``int``, the desired query flags. The default is
+ ``dns.flags.RD``.
+
+ *pad*, a non-negative ``int``. If 0, the default, do not pad; otherwise add
+ padding bytes to make the message size a multiple of *pad*. Note that if
+ padding is non-zero, an EDNS PADDING option will always be added to the
+ message.
+
+ Returns a ``dns.message.QueryMessage``
+ """
+
+ if isinstance(qname, str):
+ qname = dns.name.from_text(qname, idna_codec=idna_codec)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ m = QueryMessage(id=id)
+ m.flags = dns.flags.Flag(flags)
+ m.find_rrset(m.question, qname, rdclass, rdtype, create=True, force_unique=True)
+ # only pass keywords on to use_edns if they have been set to a
+ # non-None value. Setting a field will turn EDNS on if it hasn't
+ # been configured.
+ kwargs: Dict[str, Any] = {}
+ if ednsflags is not None:
+ kwargs["ednsflags"] = ednsflags
+ if payload is not None:
+ kwargs["payload"] = payload
+ if request_payload is not None:
+ kwargs["request_payload"] = request_payload
+ if options is not None:
+ kwargs["options"] = options
+ if kwargs and use_edns is None:
+ use_edns = 0
+ kwargs["edns"] = use_edns
+ kwargs["pad"] = pad
+ m.use_edns(**kwargs)
+ m.want_dnssec(want_dnssec)
+ return m
+
+
+class CopyMode(enum.Enum):
+ """
+ How should sections be copied when making an update response?
+ """
+
+ NOTHING = 0
+ QUESTION = 1
+ EVERYTHING = 2
+
+
+def make_response(
+ query: Message,
+ recursion_available: bool = False,
+ our_payload: int = 8192,
+ fudge: int = 300,
+ tsig_error: int = 0,
+ pad: Optional[int] = None,
+ copy_mode: Optional[CopyMode] = None,
+) -> Message:
+ """Make a message which is a response for the specified query.
+ The message returned is really a response skeleton; it has all of the infrastructure
+ required of a response, but none of the content.
+
+ Response section(s) which are copied are shallow copies of the matching section(s)
+ in the query, so the query's RRsets should not be changed.
+
+ *query*, a ``dns.message.Message``, the query to respond to.
+
+ *recursion_available*, a ``bool``, should RA be set in the response?
+
+ *our_payload*, an ``int``, the payload size to advertise in EDNS responses.
+
+ *fudge*, an ``int``, the TSIG time fudge.
+
+ *tsig_error*, an ``int``, the TSIG error.
+
+ *pad*, a non-negative ``int`` or ``None``. If 0, the default, do not pad; otherwise
+ if not ``None`` add padding bytes to make the message size a multiple of *pad*. Note
+ that if padding is non-zero, an EDNS PADDING option will always be added to the
+ message. If ``None``, add padding following RFC 8467, namely if the request is
+ padded, pad the response to 468 otherwise do not pad.
+
+ *copy_mode*, a ``dns.message.CopyMode`` or ``None``, determines how sections are
+ copied. The default, ``None`` copies sections according to the default for the
+ message's opcode, which is currently ``dns.message.CopyMode.QUESTION`` for all
+ opcodes. ``dns.message.CopyMode.QUESTION`` copies only the question section.
+ ``dns.message.CopyMode.EVERYTHING`` copies all sections other than OPT or TSIG
+ records, which are created appropriately if needed. ``dns.message.CopyMode.NOTHING``
+ copies no sections; note that this mode is for server testing purposes and is
+ otherwise not recommended for use. In particular, ``dns.message.is_response()``
+ will be ``False`` if you create a response this way and the rcode is not
+ ``FORMERR``, ``SERVFAIL``, ``NOTIMP``, or ``REFUSED``.
+
+ Returns a ``dns.message.Message`` object whose specific class is appropriate for the
+ query. For example, if query is a ``dns.update.UpdateMessage``, the response will
+ be one too.
+ """
+
+ if query.flags & dns.flags.QR:
+ raise dns.exception.FormError("specified query message is not a query")
+ opcode = query.opcode()
+ factory = _message_factory_from_opcode(opcode)
+ response = factory(id=query.id)
+ response.flags = dns.flags.QR | (query.flags & dns.flags.RD)
+ if recursion_available:
+ response.flags |= dns.flags.RA
+ response.set_opcode(opcode)
+ if copy_mode is None:
+ copy_mode = CopyMode.QUESTION
+ if copy_mode != CopyMode.NOTHING:
+ response.question = list(query.question)
+ if copy_mode == CopyMode.EVERYTHING:
+ response.answer = list(query.answer)
+ response.authority = list(query.authority)
+ response.additional = list(query.additional)
+ if query.edns >= 0:
+ if pad is None:
+ # Set response padding per RFC 8467
+ pad = 0
+ for option in query.options:
+ if option.otype == dns.edns.OptionType.PADDING:
+ pad = 468
+ response.use_edns(0, 0, our_payload, query.payload, pad=pad)
+ if query.had_tsig:
+ response.use_tsig(
+ query.keyring,
+ query.keyname,
+ fudge,
+ None,
+ tsig_error,
+ b"",
+ query.keyalgorithm,
+ )
+ response.request_mac = query.mac
+ return response
+
+
+### BEGIN generated MessageSection constants
+
+QUESTION = MessageSection.QUESTION
+ANSWER = MessageSection.ANSWER
+AUTHORITY = MessageSection.AUTHORITY
+ADDITIONAL = MessageSection.ADDITIONAL
+
+### END generated MessageSection constants
diff --git a/.venv/lib/python3.12/site-packages/dns/name.py b/.venv/lib/python3.12/site-packages/dns/name.py
new file mode 100644
index 00000000..f79f0d0f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/name.py
@@ -0,0 +1,1284 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Names.
+"""
+
+import copy
+import encodings.idna # type: ignore
+import functools
+import struct
+from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Union
+
+import dns._features
+import dns.enum
+import dns.exception
+import dns.immutable
+import dns.wire
+
+if dns._features.have("idna"):
+ import idna # type: ignore
+
+ have_idna_2008 = True
+else: # pragma: no cover
+ have_idna_2008 = False
+
+CompressType = Dict["Name", int]
+
+
+class NameRelation(dns.enum.IntEnum):
+ """Name relation result from fullcompare()."""
+
+ # This is an IntEnum for backwards compatibility in case anyone
+ # has hardwired the constants.
+
+ #: The compared names have no relationship to each other.
+ NONE = 0
+ #: the first name is a superdomain of the second.
+ SUPERDOMAIN = 1
+ #: The first name is a subdomain of the second.
+ SUBDOMAIN = 2
+ #: The compared names are equal.
+ EQUAL = 3
+ #: The compared names have a common ancestor.
+ COMMONANCESTOR = 4
+
+ @classmethod
+ def _maximum(cls):
+ return cls.COMMONANCESTOR # pragma: no cover
+
+ @classmethod
+ def _short_name(cls):
+ return cls.__name__ # pragma: no cover
+
+
+# Backwards compatibility
+NAMERELN_NONE = NameRelation.NONE
+NAMERELN_SUPERDOMAIN = NameRelation.SUPERDOMAIN
+NAMERELN_SUBDOMAIN = NameRelation.SUBDOMAIN
+NAMERELN_EQUAL = NameRelation.EQUAL
+NAMERELN_COMMONANCESTOR = NameRelation.COMMONANCESTOR
+
+
+class EmptyLabel(dns.exception.SyntaxError):
+ """A DNS label is empty."""
+
+
+class BadEscape(dns.exception.SyntaxError):
+ """An escaped code in a text format of DNS name is invalid."""
+
+
+class BadPointer(dns.exception.FormError):
+ """A DNS compression pointer points forward instead of backward."""
+
+
+class BadLabelType(dns.exception.FormError):
+ """The label type in DNS name wire format is unknown."""
+
+
+class NeedAbsoluteNameOrOrigin(dns.exception.DNSException):
+ """An attempt was made to convert a non-absolute name to
+ wire when there was also a non-absolute (or missing) origin."""
+
+
+class NameTooLong(dns.exception.FormError):
+ """A DNS name is > 255 octets long."""
+
+
+class LabelTooLong(dns.exception.SyntaxError):
+ """A DNS label is > 63 octets long."""
+
+
+class AbsoluteConcatenation(dns.exception.DNSException):
+ """An attempt was made to append anything other than the
+ empty name to an absolute DNS name."""
+
+
+class NoParent(dns.exception.DNSException):
+ """An attempt was made to get the parent of the root name
+ or the empty name."""
+
+
+class NoIDNA2008(dns.exception.DNSException):
+ """IDNA 2008 processing was requested but the idna module is not
+ available."""
+
+
+class IDNAException(dns.exception.DNSException):
+ """IDNA processing raised an exception."""
+
+ supp_kwargs = {"idna_exception"}
+ fmt = "IDNA processing exception: {idna_exception}"
+
+ # We do this as otherwise mypy complains about unexpected keyword argument
+ # idna_exception
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+
+class NeedSubdomainOfOrigin(dns.exception.DNSException):
+ """An absolute name was provided that is not a subdomain of the specified origin."""
+
+
+_escaped = b'"().;\\@$'
+_escaped_text = '"().;\\@$'
+
+
+def _escapify(label: Union[bytes, str]) -> str:
+ """Escape the characters in label which need it.
+ @returns: the escaped string
+ @rtype: string"""
+ if isinstance(label, bytes):
+ # Ordinary DNS label mode. Escape special characters and values
+ # < 0x20 or > 0x7f.
+ text = ""
+ for c in label:
+ if c in _escaped:
+ text += "\\" + chr(c)
+ elif c > 0x20 and c < 0x7F:
+ text += chr(c)
+ else:
+ text += "\\%03d" % c
+ return text
+
+ # Unicode label mode. Escape only special characters and values < 0x20
+ text = ""
+ for uc in label:
+ if uc in _escaped_text:
+ text += "\\" + uc
+ elif uc <= "\x20":
+ text += "\\%03d" % ord(uc)
+ else:
+ text += uc
+ return text
+
+
+class IDNACodec:
+ """Abstract base class for IDNA encoder/decoders."""
+
+ def __init__(self):
+ pass
+
+ def is_idna(self, label: bytes) -> bool:
+ return label.lower().startswith(b"xn--")
+
+ def encode(self, label: str) -> bytes:
+ raise NotImplementedError # pragma: no cover
+
+ def decode(self, label: bytes) -> str:
+ # We do not apply any IDNA policy on decode.
+ if self.is_idna(label):
+ try:
+ slabel = label[4:].decode("punycode")
+ return _escapify(slabel)
+ except Exception as e:
+ raise IDNAException(idna_exception=e)
+ else:
+ return _escapify(label)
+
+
+class IDNA2003Codec(IDNACodec):
+ """IDNA 2003 encoder/decoder."""
+
+ def __init__(self, strict_decode: bool = False):
+ """Initialize the IDNA 2003 encoder/decoder.
+
+ *strict_decode* is a ``bool``. If `True`, then IDNA2003 checking
+ is done when decoding. This can cause failures if the name
+ was encoded with IDNA2008. The default is `False`.
+ """
+
+ super().__init__()
+ self.strict_decode = strict_decode
+
+ def encode(self, label: str) -> bytes:
+ """Encode *label*."""
+
+ if label == "":
+ return b""
+ try:
+ return encodings.idna.ToASCII(label)
+ except UnicodeError:
+ raise LabelTooLong
+
+ def decode(self, label: bytes) -> str:
+ """Decode *label*."""
+ if not self.strict_decode:
+ return super().decode(label)
+ if label == b"":
+ return ""
+ try:
+ return _escapify(encodings.idna.ToUnicode(label))
+ except Exception as e:
+ raise IDNAException(idna_exception=e)
+
+
+class IDNA2008Codec(IDNACodec):
+ """IDNA 2008 encoder/decoder."""
+
+ def __init__(
+ self,
+ uts_46: bool = False,
+ transitional: bool = False,
+ allow_pure_ascii: bool = False,
+ strict_decode: bool = False,
+ ):
+ """Initialize the IDNA 2008 encoder/decoder.
+
+ *uts_46* is a ``bool``. If True, apply Unicode IDNA
+ compatibility processing as described in Unicode Technical
+ Standard #46 (https://unicode.org/reports/tr46/).
+ If False, do not apply the mapping. The default is False.
+
+ *transitional* is a ``bool``: If True, use the
+ "transitional" mode described in Unicode Technical Standard
+ #46. The default is False.
+
+ *allow_pure_ascii* is a ``bool``. If True, then a label which
+ consists of only ASCII characters is allowed. This is less
+ strict than regular IDNA 2008, but is also necessary for mixed
+ names, e.g. a name with starting with "_sip._tcp." and ending
+ in an IDN suffix which would otherwise be disallowed. The
+ default is False.
+
+ *strict_decode* is a ``bool``: If True, then IDNA2008 checking
+ is done when decoding. This can cause failures if the name
+ was encoded with IDNA2003. The default is False.
+ """
+ super().__init__()
+ self.uts_46 = uts_46
+ self.transitional = transitional
+ self.allow_pure_ascii = allow_pure_ascii
+ self.strict_decode = strict_decode
+
+ def encode(self, label: str) -> bytes:
+ if label == "":
+ return b""
+ if self.allow_pure_ascii and is_all_ascii(label):
+ encoded = label.encode("ascii")
+ if len(encoded) > 63:
+ raise LabelTooLong
+ return encoded
+ if not have_idna_2008:
+ raise NoIDNA2008
+ try:
+ if self.uts_46:
+ # pylint: disable=possibly-used-before-assignment
+ label = idna.uts46_remap(label, False, self.transitional)
+ return idna.alabel(label)
+ except idna.IDNAError as e:
+ if e.args[0] == "Label too long":
+ raise LabelTooLong
+ else:
+ raise IDNAException(idna_exception=e)
+
+ def decode(self, label: bytes) -> str:
+ if not self.strict_decode:
+ return super().decode(label)
+ if label == b"":
+ return ""
+ if not have_idna_2008:
+ raise NoIDNA2008
+ try:
+ ulabel = idna.ulabel(label)
+ if self.uts_46:
+ ulabel = idna.uts46_remap(ulabel, False, self.transitional)
+ return _escapify(ulabel)
+ except (idna.IDNAError, UnicodeError) as e:
+ raise IDNAException(idna_exception=e)
+
+
+IDNA_2003_Practical = IDNA2003Codec(False)
+IDNA_2003_Strict = IDNA2003Codec(True)
+IDNA_2003 = IDNA_2003_Practical
+IDNA_2008_Practical = IDNA2008Codec(True, False, True, False)
+IDNA_2008_UTS_46 = IDNA2008Codec(True, False, False, False)
+IDNA_2008_Strict = IDNA2008Codec(False, False, False, True)
+IDNA_2008_Transitional = IDNA2008Codec(True, True, False, False)
+IDNA_2008 = IDNA_2008_Practical
+
+
+def _validate_labels(labels: Tuple[bytes, ...]) -> None:
+ """Check for empty labels in the middle of a label sequence,
+ labels that are too long, and for too many labels.
+
+ Raises ``dns.name.NameTooLong`` if the name as a whole is too long.
+
+ Raises ``dns.name.EmptyLabel`` if a label is empty (i.e. the root
+ label) and appears in a position other than the end of the label
+ sequence
+
+ """
+
+ l = len(labels)
+ total = 0
+ i = -1
+ j = 0
+ for label in labels:
+ ll = len(label)
+ total += ll + 1
+ if ll > 63:
+ raise LabelTooLong
+ if i < 0 and label == b"":
+ i = j
+ j += 1
+ if total > 255:
+ raise NameTooLong
+ if i >= 0 and i != l - 1:
+ raise EmptyLabel
+
+
+def _maybe_convert_to_binary(label: Union[bytes, str]) -> bytes:
+ """If label is ``str``, convert it to ``bytes``. If it is already
+ ``bytes`` just return it.
+
+ """
+
+ if isinstance(label, bytes):
+ return label
+ if isinstance(label, str):
+ return label.encode()
+ raise ValueError # pragma: no cover
+
+
+@dns.immutable.immutable
+class Name:
+ """A DNS name.
+
+ The dns.name.Name class represents a DNS name as a tuple of
+ labels. Each label is a ``bytes`` in DNS wire format. Instances
+ of the class are immutable.
+ """
+
+ __slots__ = ["labels"]
+
+ def __init__(self, labels: Iterable[Union[bytes, str]]):
+ """*labels* is any iterable whose values are ``str`` or ``bytes``."""
+
+ blabels = [_maybe_convert_to_binary(x) for x in labels]
+ self.labels = tuple(blabels)
+ _validate_labels(self.labels)
+
+ def __copy__(self):
+ return Name(self.labels)
+
+ def __deepcopy__(self, memo):
+ return Name(copy.deepcopy(self.labels, memo))
+
+ def __getstate__(self):
+ # Names can be pickled
+ return {"labels": self.labels}
+
+ def __setstate__(self, state):
+ super().__setattr__("labels", state["labels"])
+ _validate_labels(self.labels)
+
+ def is_absolute(self) -> bool:
+ """Is the most significant label of this name the root label?
+
+ Returns a ``bool``.
+ """
+
+ return len(self.labels) > 0 and self.labels[-1] == b""
+
+ def is_wild(self) -> bool:
+ """Is this name wild? (I.e. Is the least significant label '*'?)
+
+ Returns a ``bool``.
+ """
+
+ return len(self.labels) > 0 and self.labels[0] == b"*"
+
+ def __hash__(self) -> int:
+ """Return a case-insensitive hash of the name.
+
+ Returns an ``int``.
+ """
+
+ h = 0
+ for label in self.labels:
+ for c in label.lower():
+ h += (h << 3) + c
+ return h
+
+ def fullcompare(self, other: "Name") -> Tuple[NameRelation, int, int]:
+ """Compare two names, returning a 3-tuple
+ ``(relation, order, nlabels)``.
+
+ *relation* describes the relation ship between the names,
+ and is one of: ``dns.name.NameRelation.NONE``,
+ ``dns.name.NameRelation.SUPERDOMAIN``, ``dns.name.NameRelation.SUBDOMAIN``,
+ ``dns.name.NameRelation.EQUAL``, or ``dns.name.NameRelation.COMMONANCESTOR``.
+
+ *order* is < 0 if *self* < *other*, > 0 if *self* > *other*, and ==
+ 0 if *self* == *other*. A relative name is always less than an
+ absolute name. If both names have the same relativity, then
+ the DNSSEC order relation is used to order them.
+
+ *nlabels* is the number of significant labels that the two names
+ have in common.
+
+ Here are some examples. Names ending in "." are absolute names,
+ those not ending in "." are relative names.
+
+ ============= ============= =========== ===== =======
+ self other relation order nlabels
+ ============= ============= =========== ===== =======
+ www.example. www.example. equal 0 3
+ www.example. example. subdomain > 0 2
+ example. www.example. superdomain < 0 2
+ example1.com. example2.com. common anc. < 0 2
+ example1 example2. none < 0 0
+ example1. example2 none > 0 0
+ ============= ============= =========== ===== =======
+ """
+
+ sabs = self.is_absolute()
+ oabs = other.is_absolute()
+ if sabs != oabs:
+ if sabs:
+ return (NameRelation.NONE, 1, 0)
+ else:
+ return (NameRelation.NONE, -1, 0)
+ l1 = len(self.labels)
+ l2 = len(other.labels)
+ ldiff = l1 - l2
+ if ldiff < 0:
+ l = l1
+ else:
+ l = l2
+
+ order = 0
+ nlabels = 0
+ namereln = NameRelation.NONE
+ while l > 0:
+ l -= 1
+ l1 -= 1
+ l2 -= 1
+ label1 = self.labels[l1].lower()
+ label2 = other.labels[l2].lower()
+ if label1 < label2:
+ order = -1
+ if nlabels > 0:
+ namereln = NameRelation.COMMONANCESTOR
+ return (namereln, order, nlabels)
+ elif label1 > label2:
+ order = 1
+ if nlabels > 0:
+ namereln = NameRelation.COMMONANCESTOR
+ return (namereln, order, nlabels)
+ nlabels += 1
+ order = ldiff
+ if ldiff < 0:
+ namereln = NameRelation.SUPERDOMAIN
+ elif ldiff > 0:
+ namereln = NameRelation.SUBDOMAIN
+ else:
+ namereln = NameRelation.EQUAL
+ return (namereln, order, nlabels)
+
+ def is_subdomain(self, other: "Name") -> bool:
+ """Is self a subdomain of other?
+
+ Note that the notion of subdomain includes equality, e.g.
+ "dnspython.org" is a subdomain of itself.
+
+ Returns a ``bool``.
+ """
+
+ (nr, _, _) = self.fullcompare(other)
+ if nr == NameRelation.SUBDOMAIN or nr == NameRelation.EQUAL:
+ return True
+ return False
+
+ def is_superdomain(self, other: "Name") -> bool:
+ """Is self a superdomain of other?
+
+ Note that the notion of superdomain includes equality, e.g.
+ "dnspython.org" is a superdomain of itself.
+
+ Returns a ``bool``.
+ """
+
+ (nr, _, _) = self.fullcompare(other)
+ if nr == NameRelation.SUPERDOMAIN or nr == NameRelation.EQUAL:
+ return True
+ return False
+
+ def canonicalize(self) -> "Name":
+ """Return a name which is equal to the current name, but is in
+ DNSSEC canonical form.
+ """
+
+ return Name([x.lower() for x in self.labels])
+
+ def __eq__(self, other):
+ if isinstance(other, Name):
+ return self.fullcompare(other)[1] == 0
+ else:
+ return False
+
+ def __ne__(self, other):
+ if isinstance(other, Name):
+ return self.fullcompare(other)[1] != 0
+ else:
+ return True
+
+ def __lt__(self, other):
+ if isinstance(other, Name):
+ return self.fullcompare(other)[1] < 0
+ else:
+ return NotImplemented
+
+ def __le__(self, other):
+ if isinstance(other, Name):
+ return self.fullcompare(other)[1] <= 0
+ else:
+ return NotImplemented
+
+ def __ge__(self, other):
+ if isinstance(other, Name):
+ return self.fullcompare(other)[1] >= 0
+ else:
+ return NotImplemented
+
+ def __gt__(self, other):
+ if isinstance(other, Name):
+ return self.fullcompare(other)[1] > 0
+ else:
+ return NotImplemented
+
+ def __repr__(self):
+ return "<DNS name " + self.__str__() + ">"
+
+ def __str__(self):
+ return self.to_text(False)
+
+ def to_text(self, omit_final_dot: bool = False) -> str:
+ """Convert name to DNS text format.
+
+ *omit_final_dot* is a ``bool``. If True, don't emit the final
+ dot (denoting the root label) for absolute names. The default
+ is False.
+
+ Returns a ``str``.
+ """
+
+ if len(self.labels) == 0:
+ return "@"
+ if len(self.labels) == 1 and self.labels[0] == b"":
+ return "."
+ if omit_final_dot and self.is_absolute():
+ l = self.labels[:-1]
+ else:
+ l = self.labels
+ s = ".".join(map(_escapify, l))
+ return s
+
+ def to_unicode(
+ self, omit_final_dot: bool = False, idna_codec: Optional[IDNACodec] = None
+ ) -> str:
+ """Convert name to Unicode text format.
+
+ IDN ACE labels are converted to Unicode.
+
+ *omit_final_dot* is a ``bool``. If True, don't emit the final
+ dot (denoting the root label) for absolute names. The default
+ is False.
+ *idna_codec* specifies the IDNA encoder/decoder. If None, the
+ dns.name.IDNA_2003_Practical encoder/decoder is used.
+ The IDNA_2003_Practical decoder does
+ not impose any policy, it just decodes punycode, so if you
+ don't want checking for compliance, you can use this decoder
+ for IDNA2008 as well.
+
+ Returns a ``str``.
+ """
+
+ if len(self.labels) == 0:
+ return "@"
+ if len(self.labels) == 1 and self.labels[0] == b"":
+ return "."
+ if omit_final_dot and self.is_absolute():
+ l = self.labels[:-1]
+ else:
+ l = self.labels
+ if idna_codec is None:
+ idna_codec = IDNA_2003_Practical
+ return ".".join([idna_codec.decode(x) for x in l])
+
+ def to_digestable(self, origin: Optional["Name"] = None) -> bytes:
+ """Convert name to a format suitable for digesting in hashes.
+
+ The name is canonicalized and converted to uncompressed wire
+ format. All names in wire format are absolute. If the name
+ is a relative name, then an origin must be supplied.
+
+ *origin* is a ``dns.name.Name`` or ``None``. If the name is
+ relative and origin is not ``None``, then origin will be appended
+ to the name.
+
+ Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is
+ relative and no origin was provided.
+
+ Returns a ``bytes``.
+ """
+
+ digest = self.to_wire(origin=origin, canonicalize=True)
+ assert digest is not None
+ return digest
+
+ def to_wire(
+ self,
+ file: Optional[Any] = None,
+ compress: Optional[CompressType] = None,
+ origin: Optional["Name"] = None,
+ canonicalize: bool = False,
+ ) -> Optional[bytes]:
+ """Convert name to wire format, possibly compressing it.
+
+ *file* is the file where the name is emitted (typically an
+ io.BytesIO file). If ``None`` (the default), a ``bytes``
+ containing the wire name will be returned.
+
+ *compress*, a ``dict``, is the compression table to use. If
+ ``None`` (the default), names will not be compressed. Note that
+ the compression code assumes that compression offset 0 is the
+ start of *file*, and thus compression will not be correct
+ if this is not the case.
+
+ *origin* is a ``dns.name.Name`` or ``None``. If the name is
+ relative and origin is not ``None``, then *origin* will be appended
+ to it.
+
+ *canonicalize*, a ``bool``, indicates whether the name should
+ be canonicalized; that is, converted to a format suitable for
+ digesting in hashes.
+
+ Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is
+ relative and no origin was provided.
+
+ Returns a ``bytes`` or ``None``.
+ """
+
+ if file is None:
+ out = bytearray()
+ for label in self.labels:
+ out.append(len(label))
+ if canonicalize:
+ out += label.lower()
+ else:
+ out += label
+ if not self.is_absolute():
+ if origin is None or not origin.is_absolute():
+ raise NeedAbsoluteNameOrOrigin
+ for label in origin.labels:
+ out.append(len(label))
+ if canonicalize:
+ out += label.lower()
+ else:
+ out += label
+ return bytes(out)
+
+ labels: Iterable[bytes]
+ if not self.is_absolute():
+ if origin is None or not origin.is_absolute():
+ raise NeedAbsoluteNameOrOrigin
+ labels = list(self.labels)
+ labels.extend(list(origin.labels))
+ else:
+ labels = self.labels
+ i = 0
+ for label in labels:
+ n = Name(labels[i:])
+ i += 1
+ if compress is not None:
+ pos = compress.get(n)
+ else:
+ pos = None
+ if pos is not None:
+ value = 0xC000 + pos
+ s = struct.pack("!H", value)
+ file.write(s)
+ break
+ else:
+ if compress is not None and len(n) > 1:
+ pos = file.tell()
+ if pos <= 0x3FFF:
+ compress[n] = pos
+ l = len(label)
+ file.write(struct.pack("!B", l))
+ if l > 0:
+ if canonicalize:
+ file.write(label.lower())
+ else:
+ file.write(label)
+ return None
+
+ def __len__(self) -> int:
+ """The length of the name (in labels).
+
+ Returns an ``int``.
+ """
+
+ return len(self.labels)
+
+ def __getitem__(self, index):
+ return self.labels[index]
+
+ def __add__(self, other):
+ return self.concatenate(other)
+
+ def __sub__(self, other):
+ return self.relativize(other)
+
+ def split(self, depth: int) -> Tuple["Name", "Name"]:
+ """Split a name into a prefix and suffix names at the specified depth.
+
+ *depth* is an ``int`` specifying the number of labels in the suffix
+
+ Raises ``ValueError`` if *depth* was not >= 0 and <= the length of the
+ name.
+
+ Returns the tuple ``(prefix, suffix)``.
+ """
+
+ l = len(self.labels)
+ if depth == 0:
+ return (self, dns.name.empty)
+ elif depth == l:
+ return (dns.name.empty, self)
+ elif depth < 0 or depth > l:
+ raise ValueError("depth must be >= 0 and <= the length of the name")
+ return (Name(self[:-depth]), Name(self[-depth:]))
+
+ def concatenate(self, other: "Name") -> "Name":
+ """Return a new name which is the concatenation of self and other.
+
+ Raises ``dns.name.AbsoluteConcatenation`` if the name is
+ absolute and *other* is not the empty name.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ if self.is_absolute() and len(other) > 0:
+ raise AbsoluteConcatenation
+ labels = list(self.labels)
+ labels.extend(list(other.labels))
+ return Name(labels)
+
+ def relativize(self, origin: "Name") -> "Name":
+ """If the name is a subdomain of *origin*, return a new name which is
+ the name relative to origin. Otherwise return the name.
+
+ For example, relativizing ``www.dnspython.org.`` to origin
+ ``dnspython.org.`` returns the name ``www``. Relativizing ``example.``
+ to origin ``dnspython.org.`` returns ``example.``.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ if origin is not None and self.is_subdomain(origin):
+ return Name(self[: -len(origin)])
+ else:
+ return self
+
+ def derelativize(self, origin: "Name") -> "Name":
+ """If the name is a relative name, return a new name which is the
+ concatenation of the name and origin. Otherwise return the name.
+
+ For example, derelativizing ``www`` to origin ``dnspython.org.``
+ returns the name ``www.dnspython.org.``. Derelativizing ``example.``
+ to origin ``dnspython.org.`` returns ``example.``.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ if not self.is_absolute():
+ return self.concatenate(origin)
+ else:
+ return self
+
+ def choose_relativity(
+ self, origin: Optional["Name"] = None, relativize: bool = True
+ ) -> "Name":
+ """Return a name with the relativity desired by the caller.
+
+ If *origin* is ``None``, then the name is returned.
+ Otherwise, if *relativize* is ``True`` the name is
+ relativized, and if *relativize* is ``False`` the name is
+ derelativized.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ if origin:
+ if relativize:
+ return self.relativize(origin)
+ else:
+ return self.derelativize(origin)
+ else:
+ return self
+
+ def parent(self) -> "Name":
+ """Return the parent of the name.
+
+ For example, the parent of ``www.dnspython.org.`` is ``dnspython.org``.
+
+ Raises ``dns.name.NoParent`` if the name is either the root name or the
+ empty name, and thus has no parent.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ if self == root or self == empty:
+ raise NoParent
+ return Name(self.labels[1:])
+
+ def predecessor(self, origin: "Name", prefix_ok: bool = True) -> "Name":
+ """Return the maximal predecessor of *name* in the DNSSEC ordering in the zone
+ whose origin is *origin*, or return the longest name under *origin* if the
+ name is origin (i.e. wrap around to the longest name, which may still be
+ *origin* due to length considerations.
+
+ The relativity of the name is preserved, so if this name is relative
+ then the method will return a relative name, and likewise if this name
+ is absolute then the predecessor will be absolute.
+
+ *prefix_ok* indicates if prefixing labels is allowed, and
+ defaults to ``True``. Normally it is good to allow this, but if computing
+ a maximal predecessor at a zone cut point then ``False`` must be specified.
+ """
+ return _handle_relativity_and_call(
+ _absolute_predecessor, self, origin, prefix_ok
+ )
+
+ def successor(self, origin: "Name", prefix_ok: bool = True) -> "Name":
+ """Return the minimal successor of *name* in the DNSSEC ordering in the zone
+ whose origin is *origin*, or return *origin* if the successor cannot be
+ computed due to name length limitations.
+
+ Note that *origin* is returned in the "too long" cases because wrapping
+ around to the origin is how NSEC records express "end of the zone".
+
+ The relativity of the name is preserved, so if this name is relative
+ then the method will return a relative name, and likewise if this name
+ is absolute then the successor will be absolute.
+
+ *prefix_ok* indicates if prefixing a new minimal label is allowed, and
+ defaults to ``True``. Normally it is good to allow this, but if computing
+ a minimal successor at a zone cut point then ``False`` must be specified.
+ """
+ return _handle_relativity_and_call(_absolute_successor, self, origin, prefix_ok)
+
+
+#: The root name, '.'
+root = Name([b""])
+
+#: The empty name.
+empty = Name([])
+
+
+def from_unicode(
+ text: str, origin: Optional[Name] = root, idna_codec: Optional[IDNACodec] = None
+) -> Name:
+ """Convert unicode text into a Name object.
+
+ Labels are encoded in IDN ACE form according to rules specified by
+ the IDNA codec.
+
+ *text*, a ``str``, is the text to convert into a name.
+
+ *origin*, a ``dns.name.Name``, specifies the origin to
+ append to non-absolute names. The default is the root name.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ if not isinstance(text, str):
+ raise ValueError("input to from_unicode() must be a unicode string")
+ if not (origin is None or isinstance(origin, Name)):
+ raise ValueError("origin must be a Name or None")
+ labels = []
+ label = ""
+ escaping = False
+ edigits = 0
+ total = 0
+ if idna_codec is None:
+ idna_codec = IDNA_2003
+ if text == "@":
+ text = ""
+ if text:
+ if text in [".", "\u3002", "\uff0e", "\uff61"]:
+ return Name([b""]) # no Unicode "u" on this constant!
+ for c in text:
+ if escaping:
+ if edigits == 0:
+ if c.isdigit():
+ total = int(c)
+ edigits += 1
+ else:
+ label += c
+ escaping = False
+ else:
+ if not c.isdigit():
+ raise BadEscape
+ total *= 10
+ total += int(c)
+ edigits += 1
+ if edigits == 3:
+ escaping = False
+ label += chr(total)
+ elif c in [".", "\u3002", "\uff0e", "\uff61"]:
+ if len(label) == 0:
+ raise EmptyLabel
+ labels.append(idna_codec.encode(label))
+ label = ""
+ elif c == "\\":
+ escaping = True
+ edigits = 0
+ total = 0
+ else:
+ label += c
+ if escaping:
+ raise BadEscape
+ if len(label) > 0:
+ labels.append(idna_codec.encode(label))
+ else:
+ labels.append(b"")
+
+ if (len(labels) == 0 or labels[-1] != b"") and origin is not None:
+ labels.extend(list(origin.labels))
+ return Name(labels)
+
+
+def is_all_ascii(text: str) -> bool:
+ for c in text:
+ if ord(c) > 0x7F:
+ return False
+ return True
+
+
+def from_text(
+ text: Union[bytes, str],
+ origin: Optional[Name] = root,
+ idna_codec: Optional[IDNACodec] = None,
+) -> Name:
+ """Convert text into a Name object.
+
+ *text*, a ``bytes`` or ``str``, is the text to convert into a name.
+
+ *origin*, a ``dns.name.Name``, specifies the origin to
+ append to non-absolute names. The default is the root name.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ if isinstance(text, str):
+ if not is_all_ascii(text):
+ # Some codepoint in the input text is > 127, so IDNA applies.
+ return from_unicode(text, origin, idna_codec)
+ # The input is all ASCII, so treat this like an ordinary non-IDNA
+ # domain name. Note that "all ASCII" is about the input text,
+ # not the codepoints in the domain name. E.g. if text has value
+ #
+ # r'\150\151\152\153\154\155\156\157\158\159'
+ #
+ # then it's still "all ASCII" even though the domain name has
+ # codepoints > 127.
+ text = text.encode("ascii")
+ if not isinstance(text, bytes):
+ raise ValueError("input to from_text() must be a string")
+ if not (origin is None or isinstance(origin, Name)):
+ raise ValueError("origin must be a Name or None")
+ labels = []
+ label = b""
+ escaping = False
+ edigits = 0
+ total = 0
+ if text == b"@":
+ text = b""
+ if text:
+ if text == b".":
+ return Name([b""])
+ for c in text:
+ byte_ = struct.pack("!B", c)
+ if escaping:
+ if edigits == 0:
+ if byte_.isdigit():
+ total = int(byte_)
+ edigits += 1
+ else:
+ label += byte_
+ escaping = False
+ else:
+ if not byte_.isdigit():
+ raise BadEscape
+ total *= 10
+ total += int(byte_)
+ edigits += 1
+ if edigits == 3:
+ escaping = False
+ label += struct.pack("!B", total)
+ elif byte_ == b".":
+ if len(label) == 0:
+ raise EmptyLabel
+ labels.append(label)
+ label = b""
+ elif byte_ == b"\\":
+ escaping = True
+ edigits = 0
+ total = 0
+ else:
+ label += byte_
+ if escaping:
+ raise BadEscape
+ if len(label) > 0:
+ labels.append(label)
+ else:
+ labels.append(b"")
+ if (len(labels) == 0 or labels[-1] != b"") and origin is not None:
+ labels.extend(list(origin.labels))
+ return Name(labels)
+
+
+# we need 'dns.wire.Parser' quoted as dns.name and dns.wire depend on each other.
+
+
+def from_wire_parser(parser: "dns.wire.Parser") -> Name:
+ """Convert possibly compressed wire format into a Name.
+
+ *parser* is a dns.wire.Parser.
+
+ Raises ``dns.name.BadPointer`` if a compression pointer did not
+ point backwards in the message.
+
+ Raises ``dns.name.BadLabelType`` if an invalid label type was encountered.
+
+ Returns a ``dns.name.Name``
+ """
+
+ labels = []
+ biggest_pointer = parser.current
+ with parser.restore_furthest():
+ count = parser.get_uint8()
+ while count != 0:
+ if count < 64:
+ labels.append(parser.get_bytes(count))
+ elif count >= 192:
+ current = (count & 0x3F) * 256 + parser.get_uint8()
+ if current >= biggest_pointer:
+ raise BadPointer
+ biggest_pointer = current
+ parser.seek(current)
+ else:
+ raise BadLabelType
+ count = parser.get_uint8()
+ labels.append(b"")
+ return Name(labels)
+
+
+def from_wire(message: bytes, current: int) -> Tuple[Name, int]:
+ """Convert possibly compressed wire format into a Name.
+
+ *message* is a ``bytes`` containing an entire DNS message in DNS
+ wire form.
+
+ *current*, an ``int``, is the offset of the beginning of the name
+ from the start of the message
+
+ Raises ``dns.name.BadPointer`` if a compression pointer did not
+ point backwards in the message.
+
+ Raises ``dns.name.BadLabelType`` if an invalid label type was encountered.
+
+ Returns a ``(dns.name.Name, int)`` tuple consisting of the name
+ that was read and the number of bytes of the wire format message
+ which were consumed reading it.
+ """
+
+ if not isinstance(message, bytes):
+ raise ValueError("input to from_wire() must be a byte string")
+ parser = dns.wire.Parser(message, current)
+ name = from_wire_parser(parser)
+ return (name, parser.current - current)
+
+
+# RFC 4471 Support
+
+_MINIMAL_OCTET = b"\x00"
+_MINIMAL_OCTET_VALUE = ord(_MINIMAL_OCTET)
+_SUCCESSOR_PREFIX = Name([_MINIMAL_OCTET])
+_MAXIMAL_OCTET = b"\xff"
+_MAXIMAL_OCTET_VALUE = ord(_MAXIMAL_OCTET)
+_AT_SIGN_VALUE = ord("@")
+_LEFT_SQUARE_BRACKET_VALUE = ord("[")
+
+
+def _wire_length(labels):
+ return functools.reduce(lambda v, x: v + len(x) + 1, labels, 0)
+
+
+def _pad_to_max_name(name):
+ needed = 255 - _wire_length(name.labels)
+ new_labels = []
+ while needed > 64:
+ new_labels.append(_MAXIMAL_OCTET * 63)
+ needed -= 64
+ if needed >= 2:
+ new_labels.append(_MAXIMAL_OCTET * (needed - 1))
+ # Note we're already maximal in the needed == 1 case as while we'd like
+ # to add one more byte as a new label, we can't, as adding a new non-empty
+ # label requires at least 2 bytes.
+ new_labels = list(reversed(new_labels))
+ new_labels.extend(name.labels)
+ return Name(new_labels)
+
+
+def _pad_to_max_label(label, suffix_labels):
+ length = len(label)
+ # We have to subtract one here to account for the length byte of label.
+ remaining = 255 - _wire_length(suffix_labels) - length - 1
+ if remaining <= 0:
+ # Shouldn't happen!
+ return label
+ needed = min(63 - length, remaining)
+ return label + _MAXIMAL_OCTET * needed
+
+
+def _absolute_predecessor(name: Name, origin: Name, prefix_ok: bool) -> Name:
+ # This is the RFC 4471 predecessor algorithm using the "absolute method" of section
+ # 3.1.1.
+ #
+ # Our caller must ensure that the name and origin are absolute, and that name is a
+ # subdomain of origin.
+ if name == origin:
+ return _pad_to_max_name(name)
+ least_significant_label = name[0]
+ if least_significant_label == _MINIMAL_OCTET:
+ return name.parent()
+ least_octet = least_significant_label[-1]
+ suffix_labels = name.labels[1:]
+ if least_octet == _MINIMAL_OCTET_VALUE:
+ new_labels = [least_significant_label[:-1]]
+ else:
+ octets = bytearray(least_significant_label)
+ octet = octets[-1]
+ if octet == _LEFT_SQUARE_BRACKET_VALUE:
+ octet = _AT_SIGN_VALUE
+ else:
+ octet -= 1
+ octets[-1] = octet
+ least_significant_label = bytes(octets)
+ new_labels = [_pad_to_max_label(least_significant_label, suffix_labels)]
+ new_labels.extend(suffix_labels)
+ name = Name(new_labels)
+ if prefix_ok:
+ return _pad_to_max_name(name)
+ else:
+ return name
+
+
+def _absolute_successor(name: Name, origin: Name, prefix_ok: bool) -> Name:
+ # This is the RFC 4471 successor algorithm using the "absolute method" of section
+ # 3.1.2.
+ #
+ # Our caller must ensure that the name and origin are absolute, and that name is a
+ # subdomain of origin.
+ if prefix_ok:
+ # Try prefixing \000 as new label
+ try:
+ return _SUCCESSOR_PREFIX.concatenate(name)
+ except NameTooLong:
+ pass
+ while name != origin:
+ # Try extending the least significant label.
+ least_significant_label = name[0]
+ if len(least_significant_label) < 63:
+ # We may be able to extend the least label with a minimal additional byte.
+ # This is only "may" because we could have a maximal length name even though
+ # the least significant label isn't maximally long.
+ new_labels = [least_significant_label + _MINIMAL_OCTET]
+ new_labels.extend(name.labels[1:])
+ try:
+ return dns.name.Name(new_labels)
+ except dns.name.NameTooLong:
+ pass
+ # We can't extend the label either, so we'll try to increment the least
+ # signficant non-maximal byte in it.
+ octets = bytearray(least_significant_label)
+ # We do this reversed iteration with an explicit indexing variable because
+ # if we find something to increment, we're going to want to truncate everything
+ # to the right of it.
+ for i in range(len(octets) - 1, -1, -1):
+ octet = octets[i]
+ if octet == _MAXIMAL_OCTET_VALUE:
+ # We can't increment this, so keep looking.
+ continue
+ # Finally, something we can increment. We have to apply a special rule for
+ # incrementing "@", sending it to "[", because RFC 4034 6.1 says that when
+ # comparing names, uppercase letters compare as if they were their
+ # lower-case equivalents. If we increment "@" to "A", then it would compare
+ # as "a", which is after "[", "\", "]", "^", "_", and "`", so we would have
+ # skipped the most minimal successor, namely "[".
+ if octet == _AT_SIGN_VALUE:
+ octet = _LEFT_SQUARE_BRACKET_VALUE
+ else:
+ octet += 1
+ octets[i] = octet
+ # We can now truncate all of the maximal values we skipped (if any)
+ new_labels = [bytes(octets[: i + 1])]
+ new_labels.extend(name.labels[1:])
+ # We haven't changed the length of the name, so the Name constructor will
+ # always work.
+ return Name(new_labels)
+ # We couldn't increment, so chop off the least significant label and try
+ # again.
+ name = name.parent()
+
+ # We couldn't increment at all, so return the origin, as wrapping around is the
+ # DNSSEC way.
+ return origin
+
+
+def _handle_relativity_and_call(
+ function: Callable[[Name, Name, bool], Name],
+ name: Name,
+ origin: Name,
+ prefix_ok: bool,
+) -> Name:
+ # Make "name" absolute if needed, ensure that the origin is absolute,
+ # call function(), and then relativize the result if needed.
+ if not origin.is_absolute():
+ raise NeedAbsoluteNameOrOrigin
+ relative = not name.is_absolute()
+ if relative:
+ name = name.derelativize(origin)
+ elif not name.is_subdomain(origin):
+ raise NeedSubdomainOfOrigin
+ result_name = function(name, origin, prefix_ok)
+ if relative:
+ result_name = result_name.relativize(origin)
+ return result_name
diff --git a/.venv/lib/python3.12/site-packages/dns/namedict.py b/.venv/lib/python3.12/site-packages/dns/namedict.py
new file mode 100644
index 00000000..ca8b1978
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/namedict.py
@@ -0,0 +1,109 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+# Copyright (C) 2016 Coresec Systems AB
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND CORESEC SYSTEMS AB DISCLAIMS ALL
+# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL CORESEC
+# SYSTEMS AB BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS name dictionary"""
+
+# pylint seems to be confused about this one!
+from collections.abc import MutableMapping # pylint: disable=no-name-in-module
+
+import dns.name
+
+
+class NameDict(MutableMapping):
+ """A dictionary whose keys are dns.name.Name objects.
+
+ In addition to being like a regular Python dictionary, this
+ dictionary can also get the deepest match for a given key.
+ """
+
+ __slots__ = ["max_depth", "max_depth_items", "__store"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__()
+ self.__store = dict()
+ #: the maximum depth of the keys that have ever been added
+ self.max_depth = 0
+ #: the number of items of maximum depth
+ self.max_depth_items = 0
+ self.update(dict(*args, **kwargs))
+
+ def __update_max_depth(self, key):
+ if len(key) == self.max_depth:
+ self.max_depth_items = self.max_depth_items + 1
+ elif len(key) > self.max_depth:
+ self.max_depth = len(key)
+ self.max_depth_items = 1
+
+ def __getitem__(self, key):
+ return self.__store[key]
+
+ def __setitem__(self, key, value):
+ if not isinstance(key, dns.name.Name):
+ raise ValueError("NameDict key must be a name")
+ self.__store[key] = value
+ self.__update_max_depth(key)
+
+ def __delitem__(self, key):
+ self.__store.pop(key)
+ if len(key) == self.max_depth:
+ self.max_depth_items = self.max_depth_items - 1
+ if self.max_depth_items == 0:
+ self.max_depth = 0
+ for k in self.__store:
+ self.__update_max_depth(k)
+
+ def __iter__(self):
+ return iter(self.__store)
+
+ def __len__(self):
+ return len(self.__store)
+
+ def has_key(self, key):
+ return key in self.__store
+
+ def get_deepest_match(self, name):
+ """Find the deepest match to *name* in the dictionary.
+
+ The deepest match is the longest name in the dictionary which is
+ a superdomain of *name*. Note that *superdomain* includes matching
+ *name* itself.
+
+ *name*, a ``dns.name.Name``, the name to find.
+
+ Returns a ``(key, value)`` where *key* is the deepest
+ ``dns.name.Name``, and *value* is the value associated with *key*.
+ """
+
+ depth = len(name)
+ if depth > self.max_depth:
+ depth = self.max_depth
+ for i in range(-depth, 0):
+ n = dns.name.Name(name[i:])
+ if n in self:
+ return (n, self[n])
+ v = self[dns.name.empty]
+ return (dns.name.empty, v)
diff --git a/.venv/lib/python3.12/site-packages/dns/nameserver.py b/.venv/lib/python3.12/site-packages/dns/nameserver.py
new file mode 100644
index 00000000..b02a239b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/nameserver.py
@@ -0,0 +1,363 @@
+from typing import Optional, Union
+from urllib.parse import urlparse
+
+import dns.asyncbackend
+import dns.asyncquery
+import dns.inet
+import dns.message
+import dns.query
+
+
+class Nameserver:
+ def __init__(self):
+ pass
+
+ def __str__(self):
+ raise NotImplementedError
+
+ def kind(self) -> str:
+ raise NotImplementedError
+
+ def is_always_max_size(self) -> bool:
+ raise NotImplementedError
+
+ def answer_nameserver(self) -> str:
+ raise NotImplementedError
+
+ def answer_port(self) -> int:
+ raise NotImplementedError
+
+ def query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ raise NotImplementedError
+
+ async def async_query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool,
+ backend: dns.asyncbackend.Backend,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ raise NotImplementedError
+
+
+class AddressAndPortNameserver(Nameserver):
+ def __init__(self, address: str, port: int):
+ super().__init__()
+ self.address = address
+ self.port = port
+
+ def kind(self) -> str:
+ raise NotImplementedError
+
+ def is_always_max_size(self) -> bool:
+ return False
+
+ def __str__(self):
+ ns_kind = self.kind()
+ return f"{ns_kind}:{self.address}@{self.port}"
+
+ def answer_nameserver(self) -> str:
+ return self.address
+
+ def answer_port(self) -> int:
+ return self.port
+
+
+class Do53Nameserver(AddressAndPortNameserver):
+ def __init__(self, address: str, port: int = 53):
+ super().__init__(address, port)
+
+ def kind(self):
+ return "Do53"
+
+ def query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ if max_size:
+ response = dns.query.tcp(
+ request,
+ self.address,
+ timeout=timeout,
+ port=self.port,
+ source=source,
+ source_port=source_port,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ else:
+ response = dns.query.udp(
+ request,
+ self.address,
+ timeout=timeout,
+ port=self.port,
+ source=source,
+ source_port=source_port,
+ raise_on_truncation=True,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ ignore_errors=True,
+ ignore_unexpected=True,
+ )
+ return response
+
+ async def async_query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool,
+ backend: dns.asyncbackend.Backend,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ if max_size:
+ response = await dns.asyncquery.tcp(
+ request,
+ self.address,
+ timeout=timeout,
+ port=self.port,
+ source=source,
+ source_port=source_port,
+ backend=backend,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ else:
+ response = await dns.asyncquery.udp(
+ request,
+ self.address,
+ timeout=timeout,
+ port=self.port,
+ source=source,
+ source_port=source_port,
+ raise_on_truncation=True,
+ backend=backend,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ ignore_errors=True,
+ ignore_unexpected=True,
+ )
+ return response
+
+
+class DoHNameserver(Nameserver):
+ def __init__(
+ self,
+ url: str,
+ bootstrap_address: Optional[str] = None,
+ verify: Union[bool, str] = True,
+ want_get: bool = False,
+ http_version: dns.query.HTTPVersion = dns.query.HTTPVersion.DEFAULT,
+ ):
+ super().__init__()
+ self.url = url
+ self.bootstrap_address = bootstrap_address
+ self.verify = verify
+ self.want_get = want_get
+ self.http_version = http_version
+
+ def kind(self):
+ return "DoH"
+
+ def is_always_max_size(self) -> bool:
+ return True
+
+ def __str__(self):
+ return self.url
+
+ def answer_nameserver(self) -> str:
+ return self.url
+
+ def answer_port(self) -> int:
+ port = urlparse(self.url).port
+ if port is None:
+ port = 443
+ return port
+
+ def query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool = False,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ return dns.query.https(
+ request,
+ self.url,
+ timeout=timeout,
+ source=source,
+ source_port=source_port,
+ bootstrap_address=self.bootstrap_address,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ verify=self.verify,
+ post=(not self.want_get),
+ http_version=self.http_version,
+ )
+
+ async def async_query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool,
+ backend: dns.asyncbackend.Backend,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ return await dns.asyncquery.https(
+ request,
+ self.url,
+ timeout=timeout,
+ source=source,
+ source_port=source_port,
+ bootstrap_address=self.bootstrap_address,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ verify=self.verify,
+ post=(not self.want_get),
+ http_version=self.http_version,
+ )
+
+
+class DoTNameserver(AddressAndPortNameserver):
+ def __init__(
+ self,
+ address: str,
+ port: int = 853,
+ hostname: Optional[str] = None,
+ verify: Union[bool, str] = True,
+ ):
+ super().__init__(address, port)
+ self.hostname = hostname
+ self.verify = verify
+
+ def kind(self):
+ return "DoT"
+
+ def query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool = False,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ return dns.query.tls(
+ request,
+ self.address,
+ port=self.port,
+ timeout=timeout,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ server_hostname=self.hostname,
+ verify=self.verify,
+ )
+
+ async def async_query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool,
+ backend: dns.asyncbackend.Backend,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ return await dns.asyncquery.tls(
+ request,
+ self.address,
+ port=self.port,
+ timeout=timeout,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ server_hostname=self.hostname,
+ verify=self.verify,
+ )
+
+
+class DoQNameserver(AddressAndPortNameserver):
+ def __init__(
+ self,
+ address: str,
+ port: int = 853,
+ verify: Union[bool, str] = True,
+ server_hostname: Optional[str] = None,
+ ):
+ super().__init__(address, port)
+ self.verify = verify
+ self.server_hostname = server_hostname
+
+ def kind(self):
+ return "DoQ"
+
+ def query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool = False,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ return dns.query.quic(
+ request,
+ self.address,
+ port=self.port,
+ timeout=timeout,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ verify=self.verify,
+ server_hostname=self.server_hostname,
+ )
+
+ async def async_query(
+ self,
+ request: dns.message.QueryMessage,
+ timeout: float,
+ source: Optional[str],
+ source_port: int,
+ max_size: bool,
+ backend: dns.asyncbackend.Backend,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ ) -> dns.message.Message:
+ return await dns.asyncquery.quic(
+ request,
+ self.address,
+ port=self.port,
+ timeout=timeout,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ verify=self.verify,
+ server_hostname=self.server_hostname,
+ )
diff --git a/.venv/lib/python3.12/site-packages/dns/node.py b/.venv/lib/python3.12/site-packages/dns/node.py
new file mode 100644
index 00000000..de85a82d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/node.py
@@ -0,0 +1,359 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS nodes. A node is a set of rdatasets."""
+
+import enum
+import io
+from typing import Any, Dict, Optional
+
+import dns.immutable
+import dns.name
+import dns.rdataclass
+import dns.rdataset
+import dns.rdatatype
+import dns.renderer
+import dns.rrset
+
+_cname_types = {
+ dns.rdatatype.CNAME,
+}
+
+# "neutral" types can coexist with a CNAME and thus are not "other data"
+_neutral_types = {
+ dns.rdatatype.NSEC, # RFC 4035 section 2.5
+ dns.rdatatype.NSEC3, # This is not likely to happen, but not impossible!
+ dns.rdatatype.KEY, # RFC 4035 section 2.5, RFC 3007
+}
+
+
+def _matches_type_or_its_signature(rdtypes, rdtype, covers):
+ return rdtype in rdtypes or (rdtype == dns.rdatatype.RRSIG and covers in rdtypes)
+
+
+@enum.unique
+class NodeKind(enum.Enum):
+ """Rdatasets in nodes"""
+
+ REGULAR = 0 # a.k.a "other data"
+ NEUTRAL = 1
+ CNAME = 2
+
+ @classmethod
+ def classify(
+ cls, rdtype: dns.rdatatype.RdataType, covers: dns.rdatatype.RdataType
+ ) -> "NodeKind":
+ if _matches_type_or_its_signature(_cname_types, rdtype, covers):
+ return NodeKind.CNAME
+ elif _matches_type_or_its_signature(_neutral_types, rdtype, covers):
+ return NodeKind.NEUTRAL
+ else:
+ return NodeKind.REGULAR
+
+ @classmethod
+ def classify_rdataset(cls, rdataset: dns.rdataset.Rdataset) -> "NodeKind":
+ return cls.classify(rdataset.rdtype, rdataset.covers)
+
+
+class Node:
+ """A Node is a set of rdatasets.
+
+ A node is either a CNAME node or an "other data" node. A CNAME
+ node contains only CNAME, KEY, NSEC, and NSEC3 rdatasets along with their
+ covering RRSIG rdatasets. An "other data" node contains any
+ rdataset other than a CNAME or RRSIG(CNAME) rdataset. When
+ changes are made to a node, the CNAME or "other data" state is
+ always consistent with the update, i.e. the most recent change
+ wins. For example, if you have a node which contains a CNAME
+ rdataset, and then add an MX rdataset to it, then the CNAME
+ rdataset will be deleted. Likewise if you have a node containing
+ an MX rdataset and add a CNAME rdataset, the MX rdataset will be
+ deleted.
+ """
+
+ __slots__ = ["rdatasets"]
+
+ def __init__(self):
+ # the set of rdatasets, represented as a list.
+ self.rdatasets = []
+
+ def to_text(self, name: dns.name.Name, **kw: Dict[str, Any]) -> str:
+ """Convert a node to text format.
+
+ Each rdataset at the node is printed. Any keyword arguments
+ to this method are passed on to the rdataset's to_text() method.
+
+ *name*, a ``dns.name.Name``, the owner name of the
+ rdatasets.
+
+ Returns a ``str``.
+
+ """
+
+ s = io.StringIO()
+ for rds in self.rdatasets:
+ if len(rds) > 0:
+ s.write(rds.to_text(name, **kw)) # type: ignore[arg-type]
+ s.write("\n")
+ return s.getvalue()[:-1]
+
+ def __repr__(self):
+ return "<DNS node " + str(id(self)) + ">"
+
+ def __eq__(self, other):
+ #
+ # This is inefficient. Good thing we don't need to do it much.
+ #
+ for rd in self.rdatasets:
+ if rd not in other.rdatasets:
+ return False
+ for rd in other.rdatasets:
+ if rd not in self.rdatasets:
+ return False
+ return True
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __len__(self):
+ return len(self.rdatasets)
+
+ def __iter__(self):
+ return iter(self.rdatasets)
+
+ def _append_rdataset(self, rdataset):
+ """Append rdataset to the node with special handling for CNAME and
+ other data conditions.
+
+ Specifically, if the rdataset being appended has ``NodeKind.CNAME``,
+ then all rdatasets other than KEY, NSEC, NSEC3, and their covering
+ RRSIGs are deleted. If the rdataset being appended has
+ ``NodeKind.REGULAR`` then CNAME and RRSIG(CNAME) are deleted.
+ """
+ # Make having just one rdataset at the node fast.
+ if len(self.rdatasets) > 0:
+ kind = NodeKind.classify_rdataset(rdataset)
+ if kind == NodeKind.CNAME:
+ self.rdatasets = [
+ rds
+ for rds in self.rdatasets
+ if NodeKind.classify_rdataset(rds) != NodeKind.REGULAR
+ ]
+ elif kind == NodeKind.REGULAR:
+ self.rdatasets = [
+ rds
+ for rds in self.rdatasets
+ if NodeKind.classify_rdataset(rds) != NodeKind.CNAME
+ ]
+ # Otherwise the rdataset is NodeKind.NEUTRAL and we do not need to
+ # edit self.rdatasets.
+ self.rdatasets.append(rdataset)
+
+ def find_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> dns.rdataset.Rdataset:
+ """Find an rdataset matching the specified properties in the
+ current node.
+
+ *rdclass*, a ``dns.rdataclass.RdataClass``, the class of the rdataset.
+
+ *rdtype*, a ``dns.rdatatype.RdataType``, the type of the rdataset.
+
+ *covers*, a ``dns.rdatatype.RdataType``, the covered type.
+ Usually this value is ``dns.rdatatype.NONE``, but if the
+ rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
+ then the covers value will be the rdata type the SIG/RRSIG
+ covers. The library treats the SIG and RRSIG types as if they
+ were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
+ This makes RRSIGs much easier to work with than if RRSIGs
+ covering different rdata types were aggregated into a single
+ RRSIG rdataset.
+
+ *create*, a ``bool``. If True, create the rdataset if it is not found.
+
+ Raises ``KeyError`` if an rdataset of the desired type and class does
+ not exist and *create* is not ``True``.
+
+ Returns a ``dns.rdataset.Rdataset``.
+ """
+
+ for rds in self.rdatasets:
+ if rds.match(rdclass, rdtype, covers):
+ return rds
+ if not create:
+ raise KeyError
+ rds = dns.rdataset.Rdataset(rdclass, rdtype, covers)
+ self._append_rdataset(rds)
+ return rds
+
+ def get_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> Optional[dns.rdataset.Rdataset]:
+ """Get an rdataset matching the specified properties in the
+ current node.
+
+ None is returned if an rdataset of the specified type and
+ class does not exist and *create* is not ``True``.
+
+ *rdclass*, an ``int``, the class of the rdataset.
+
+ *rdtype*, an ``int``, the type of the rdataset.
+
+ *covers*, an ``int``, the covered type. Usually this value is
+ dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or
+ dns.rdatatype.RRSIG, then the covers value will be the rdata
+ type the SIG/RRSIG covers. The library treats the SIG and RRSIG
+ types as if they were a family of
+ types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This makes RRSIGs much
+ easier to work with than if RRSIGs covering different rdata
+ types were aggregated into a single RRSIG rdataset.
+
+ *create*, a ``bool``. If True, create the rdataset if it is not found.
+
+ Returns a ``dns.rdataset.Rdataset`` or ``None``.
+ """
+
+ try:
+ rds = self.find_rdataset(rdclass, rdtype, covers, create)
+ except KeyError:
+ rds = None
+ return rds
+
+ def delete_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ ) -> None:
+ """Delete the rdataset matching the specified properties in the
+ current node.
+
+ If a matching rdataset does not exist, it is not an error.
+
+ *rdclass*, an ``int``, the class of the rdataset.
+
+ *rdtype*, an ``int``, the type of the rdataset.
+
+ *covers*, an ``int``, the covered type.
+ """
+
+ rds = self.get_rdataset(rdclass, rdtype, covers)
+ if rds is not None:
+ self.rdatasets.remove(rds)
+
+ def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None:
+ """Replace an rdataset.
+
+ It is not an error if there is no rdataset matching *replacement*.
+
+ Ownership of the *replacement* object is transferred to the node;
+ in other words, this method does not store a copy of *replacement*
+ at the node, it stores *replacement* itself.
+
+ *replacement*, a ``dns.rdataset.Rdataset``.
+
+ Raises ``ValueError`` if *replacement* is not a
+ ``dns.rdataset.Rdataset``.
+ """
+
+ if not isinstance(replacement, dns.rdataset.Rdataset):
+ raise ValueError("replacement is not an rdataset")
+ if isinstance(replacement, dns.rrset.RRset):
+ # RRsets are not good replacements as the match() method
+ # is not compatible.
+ replacement = replacement.to_rdataset()
+ self.delete_rdataset(
+ replacement.rdclass, replacement.rdtype, replacement.covers
+ )
+ self._append_rdataset(replacement)
+
+ def classify(self) -> NodeKind:
+ """Classify a node.
+
+ A node which contains a CNAME or RRSIG(CNAME) is a
+ ``NodeKind.CNAME`` node.
+
+ A node which contains only "neutral" types, i.e. types allowed to
+ co-exist with a CNAME, is a ``NodeKind.NEUTRAL`` node. The neutral
+ types are NSEC, NSEC3, KEY, and their associated RRSIGS. An empty node
+ is also considered neutral.
+
+ A node which contains some rdataset which is not a CNAME, RRSIG(CNAME),
+ or a neutral type is a a ``NodeKind.REGULAR`` node. Regular nodes are
+ also commonly referred to as "other data".
+ """
+ for rdataset in self.rdatasets:
+ kind = NodeKind.classify(rdataset.rdtype, rdataset.covers)
+ if kind != NodeKind.NEUTRAL:
+ return kind
+ return NodeKind.NEUTRAL
+
+ def is_immutable(self) -> bool:
+ return False
+
+
+@dns.immutable.immutable
+class ImmutableNode(Node):
+ def __init__(self, node):
+ super().__init__()
+ self.rdatasets = tuple(
+ [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets]
+ )
+
+ def find_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> dns.rdataset.Rdataset:
+ if create:
+ raise TypeError("immutable")
+ return super().find_rdataset(rdclass, rdtype, covers, False)
+
+ def get_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> Optional[dns.rdataset.Rdataset]:
+ if create:
+ raise TypeError("immutable")
+ return super().get_rdataset(rdclass, rdtype, covers, False)
+
+ def delete_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ ) -> None:
+ raise TypeError("immutable")
+
+ def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None:
+ raise TypeError("immutable")
+
+ def is_immutable(self) -> bool:
+ return True
diff --git a/.venv/lib/python3.12/site-packages/dns/opcode.py b/.venv/lib/python3.12/site-packages/dns/opcode.py
new file mode 100644
index 00000000..78b43d2c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/opcode.py
@@ -0,0 +1,117 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Opcodes."""
+
+import dns.enum
+import dns.exception
+
+
+class Opcode(dns.enum.IntEnum):
+ #: Query
+ QUERY = 0
+ #: Inverse Query (historical)
+ IQUERY = 1
+ #: Server Status (unspecified and unimplemented anywhere)
+ STATUS = 2
+ #: Notify
+ NOTIFY = 4
+ #: Dynamic Update
+ UPDATE = 5
+
+ @classmethod
+ def _maximum(cls):
+ return 15
+
+ @classmethod
+ def _unknown_exception_class(cls):
+ return UnknownOpcode
+
+
+class UnknownOpcode(dns.exception.DNSException):
+ """An DNS opcode is unknown."""
+
+
+def from_text(text: str) -> Opcode:
+ """Convert text into an opcode.
+
+ *text*, a ``str``, the textual opcode
+
+ Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown.
+
+ Returns an ``int``.
+ """
+
+ return Opcode.from_text(text)
+
+
+def from_flags(flags: int) -> Opcode:
+ """Extract an opcode from DNS message flags.
+
+ *flags*, an ``int``, the DNS flags.
+
+ Returns an ``int``.
+ """
+
+ return Opcode((flags & 0x7800) >> 11)
+
+
+def to_flags(value: Opcode) -> int:
+ """Convert an opcode to a value suitable for ORing into DNS message
+ flags.
+
+ *value*, an ``int``, the DNS opcode value.
+
+ Returns an ``int``.
+ """
+
+ return (value << 11) & 0x7800
+
+
+def to_text(value: Opcode) -> str:
+ """Convert an opcode to text.
+
+ *value*, an ``int`` the opcode value,
+
+ Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown.
+
+ Returns a ``str``.
+ """
+
+ return Opcode.to_text(value)
+
+
+def is_update(flags: int) -> bool:
+ """Is the opcode in flags UPDATE?
+
+ *flags*, an ``int``, the DNS message flags.
+
+ Returns a ``bool``.
+ """
+
+ return from_flags(flags) == Opcode.UPDATE
+
+
+### BEGIN generated Opcode constants
+
+QUERY = Opcode.QUERY
+IQUERY = Opcode.IQUERY
+STATUS = Opcode.STATUS
+NOTIFY = Opcode.NOTIFY
+UPDATE = Opcode.UPDATE
+
+### END generated Opcode constants
diff --git a/.venv/lib/python3.12/site-packages/dns/py.typed b/.venv/lib/python3.12/site-packages/dns/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/py.typed
diff --git a/.venv/lib/python3.12/site-packages/dns/query.py b/.venv/lib/python3.12/site-packages/dns/query.py
new file mode 100644
index 00000000..0d8a977a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/query.py
@@ -0,0 +1,1665 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Talk to a DNS server."""
+
+import base64
+import contextlib
+import enum
+import errno
+import os
+import os.path
+import random
+import selectors
+import socket
+import struct
+import time
+import urllib.parse
+from typing import Any, Dict, Optional, Tuple, Union, cast
+
+import dns._features
+import dns.exception
+import dns.inet
+import dns.message
+import dns.name
+import dns.quic
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+import dns.serial
+import dns.transaction
+import dns.tsig
+import dns.xfr
+
+
+def _remaining(expiration):
+ if expiration is None:
+ return None
+ timeout = expiration - time.time()
+ if timeout <= 0.0:
+ raise dns.exception.Timeout
+ return timeout
+
+
+def _expiration_for_this_attempt(timeout, expiration):
+ if expiration is None:
+ return None
+ return min(time.time() + timeout, expiration)
+
+
+_have_httpx = dns._features.have("doh")
+if _have_httpx:
+ import httpcore._backends.sync
+ import httpx
+
+ _CoreNetworkBackend = httpcore.NetworkBackend
+ _CoreSyncStream = httpcore._backends.sync.SyncStream
+
+ class _NetworkBackend(_CoreNetworkBackend):
+ def __init__(self, resolver, local_port, bootstrap_address, family):
+ super().__init__()
+ self._local_port = local_port
+ self._resolver = resolver
+ self._bootstrap_address = bootstrap_address
+ self._family = family
+
+ def connect_tcp(
+ self, host, port, timeout, local_address, socket_options=None
+ ): # pylint: disable=signature-differs
+ addresses = []
+ _, expiration = _compute_times(timeout)
+ if dns.inet.is_address(host):
+ addresses.append(host)
+ elif self._bootstrap_address is not None:
+ addresses.append(self._bootstrap_address)
+ else:
+ timeout = _remaining(expiration)
+ family = self._family
+ if local_address:
+ family = dns.inet.af_for_address(local_address)
+ answers = self._resolver.resolve_name(
+ host, family=family, lifetime=timeout
+ )
+ addresses = answers.addresses()
+ for address in addresses:
+ af = dns.inet.af_for_address(address)
+ if local_address is not None or self._local_port != 0:
+ source = dns.inet.low_level_address_tuple(
+ (local_address, self._local_port), af
+ )
+ else:
+ source = None
+ sock = _make_socket(af, socket.SOCK_STREAM, source)
+ attempt_expiration = _expiration_for_this_attempt(2.0, expiration)
+ try:
+ _connect(
+ sock,
+ dns.inet.low_level_address_tuple((address, port), af),
+ attempt_expiration,
+ )
+ return _CoreSyncStream(sock)
+ except Exception:
+ pass
+ raise httpcore.ConnectError
+
+ def connect_unix_socket(
+ self, path, timeout, socket_options=None
+ ): # pylint: disable=signature-differs
+ raise NotImplementedError
+
+ class _HTTPTransport(httpx.HTTPTransport):
+ def __init__(
+ self,
+ *args,
+ local_port=0,
+ bootstrap_address=None,
+ resolver=None,
+ family=socket.AF_UNSPEC,
+ **kwargs,
+ ):
+ if resolver is None and bootstrap_address is None:
+ # pylint: disable=import-outside-toplevel,redefined-outer-name
+ import dns.resolver
+
+ resolver = dns.resolver.Resolver()
+ super().__init__(*args, **kwargs)
+ self._pool._network_backend = _NetworkBackend(
+ resolver, local_port, bootstrap_address, family
+ )
+
+else:
+
+ class _HTTPTransport: # type: ignore
+ def connect_tcp(self, host, port, timeout, local_address):
+ raise NotImplementedError
+
+
+have_doh = _have_httpx
+
+try:
+ import ssl
+except ImportError: # pragma: no cover
+
+ class ssl: # type: ignore
+ CERT_NONE = 0
+
+ class WantReadException(Exception):
+ pass
+
+ class WantWriteException(Exception):
+ pass
+
+ class SSLContext:
+ pass
+
+ class SSLSocket:
+ pass
+
+ @classmethod
+ def create_default_context(cls, *args, **kwargs):
+ raise Exception("no ssl support") # pylint: disable=broad-exception-raised
+
+
+# Function used to create a socket. Can be overridden if needed in special
+# situations.
+socket_factory = socket.socket
+
+
+class UnexpectedSource(dns.exception.DNSException):
+ """A DNS query response came from an unexpected address or port."""
+
+
+class BadResponse(dns.exception.FormError):
+ """A DNS query response does not respond to the question asked."""
+
+
+class NoDOH(dns.exception.DNSException):
+ """DNS over HTTPS (DOH) was requested but the httpx module is not
+ available."""
+
+
+class NoDOQ(dns.exception.DNSException):
+ """DNS over QUIC (DOQ) was requested but the aioquic module is not
+ available."""
+
+
+# for backwards compatibility
+TransferError = dns.xfr.TransferError
+
+
+def _compute_times(timeout):
+ now = time.time()
+ if timeout is None:
+ return (now, None)
+ else:
+ return (now, now + timeout)
+
+
+def _wait_for(fd, readable, writable, _, expiration):
+ # Use the selected selector class to wait for any of the specified
+ # events. An "expiration" absolute time is converted into a relative
+ # timeout.
+ #
+ # The unused parameter is 'error', which is always set when
+ # selecting for read or write, and we have no error-only selects.
+
+ if readable and isinstance(fd, ssl.SSLSocket) and fd.pending() > 0:
+ return True
+ sel = selectors.DefaultSelector()
+ events = 0
+ if readable:
+ events |= selectors.EVENT_READ
+ if writable:
+ events |= selectors.EVENT_WRITE
+ if events:
+ sel.register(fd, events)
+ if expiration is None:
+ timeout = None
+ else:
+ timeout = expiration - time.time()
+ if timeout <= 0.0:
+ raise dns.exception.Timeout
+ if not sel.select(timeout):
+ raise dns.exception.Timeout
+
+
+def _wait_for_readable(s, expiration):
+ _wait_for(s, True, False, True, expiration)
+
+
+def _wait_for_writable(s, expiration):
+ _wait_for(s, False, True, True, expiration)
+
+
+def _addresses_equal(af, a1, a2):
+ # Convert the first value of the tuple, which is a textual format
+ # address into binary form, so that we are not confused by different
+ # textual representations of the same address
+ try:
+ n1 = dns.inet.inet_pton(af, a1[0])
+ n2 = dns.inet.inet_pton(af, a2[0])
+ except dns.exception.SyntaxError:
+ return False
+ return n1 == n2 and a1[1:] == a2[1:]
+
+
+def _matches_destination(af, from_address, destination, ignore_unexpected):
+ # Check that from_address is appropriate for a response to a query
+ # sent to destination.
+ if not destination:
+ return True
+ if _addresses_equal(af, from_address, destination) or (
+ dns.inet.is_multicast(destination[0]) and from_address[1:] == destination[1:]
+ ):
+ return True
+ elif ignore_unexpected:
+ return False
+ raise UnexpectedSource(
+ f"got a response from {from_address} instead of " f"{destination}"
+ )
+
+
+def _destination_and_source(
+ where, port, source, source_port, where_must_be_address=True
+):
+ # Apply defaults and compute destination and source tuples
+ # suitable for use in connect(), sendto(), or bind().
+ af = None
+ destination = None
+ try:
+ af = dns.inet.af_for_address(where)
+ destination = where
+ except Exception:
+ if where_must_be_address:
+ raise
+ # URLs are ok so eat the exception
+ if source:
+ saf = dns.inet.af_for_address(source)
+ if af:
+ # We know the destination af, so source had better agree!
+ if saf != af:
+ raise ValueError(
+ "different address families for source and destination"
+ )
+ else:
+ # We didn't know the destination af, but we know the source,
+ # so that's our af.
+ af = saf
+ if source_port and not source:
+ # Caller has specified a source_port but not an address, so we
+ # need to return a source, and we need to use the appropriate
+ # wildcard address as the address.
+ try:
+ source = dns.inet.any_for_af(af)
+ except Exception:
+ # we catch this and raise ValueError for backwards compatibility
+ raise ValueError("source_port specified but address family is unknown")
+ # Convert high-level (address, port) tuples into low-level address
+ # tuples.
+ if destination:
+ destination = dns.inet.low_level_address_tuple((destination, port), af)
+ if source:
+ source = dns.inet.low_level_address_tuple((source, source_port), af)
+ return (af, destination, source)
+
+
+def _make_socket(af, type, source, ssl_context=None, server_hostname=None):
+ s = socket_factory(af, type)
+ try:
+ s.setblocking(False)
+ if source is not None:
+ s.bind(source)
+ if ssl_context:
+ # LGTM gets a false positive here, as our default context is OK
+ return ssl_context.wrap_socket(
+ s,
+ do_handshake_on_connect=False, # lgtm[py/insecure-protocol]
+ server_hostname=server_hostname,
+ )
+ else:
+ return s
+ except Exception:
+ s.close()
+ raise
+
+
+def _maybe_get_resolver(
+ resolver: Optional["dns.resolver.Resolver"],
+) -> "dns.resolver.Resolver":
+ # We need a separate method for this to avoid overriding the global
+ # variable "dns" with the as-yet undefined local variable "dns"
+ # in https().
+ if resolver is None:
+ # pylint: disable=import-outside-toplevel,redefined-outer-name
+ import dns.resolver
+
+ resolver = dns.resolver.Resolver()
+ return resolver
+
+
+class HTTPVersion(enum.IntEnum):
+ """Which version of HTTP should be used?
+
+ DEFAULT will select the first version from the list [2, 1.1, 3] that
+ is available.
+ """
+
+ DEFAULT = 0
+ HTTP_1 = 1
+ H1 = 1
+ HTTP_2 = 2
+ H2 = 2
+ HTTP_3 = 3
+ H3 = 3
+
+
+def https(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 443,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ session: Optional[Any] = None,
+ path: str = "/dns-query",
+ post: bool = True,
+ bootstrap_address: Optional[str] = None,
+ verify: Union[bool, str] = True,
+ resolver: Optional["dns.resolver.Resolver"] = None,
+ family: int = socket.AF_UNSPEC,
+ http_version: HTTPVersion = HTTPVersion.DEFAULT,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via DNS-over-HTTPS.
+
+ *q*, a ``dns.message.Message``, the query to send.
+
+ *where*, a ``str``, the nameserver IP address or the full URL. If an IP address is
+ given, the URL will be constructed using the following schema:
+ https://<IP-address>:<port>/<path>.
+
+ *timeout*, a ``float`` or ``None``, the number of seconds to wait before the query
+ times out. If ``None``, the default, wait forever.
+
+ *port*, a ``int``, the port to send the query to. The default is 443.
+
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying the source
+ address. The default is the wildcard address.
+
+ *source_port*, an ``int``, the port from which to send the message. The default is
+ 0.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset.
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the
+ received message.
+
+ *session*, an ``httpx.Client``. If provided, the client session to use to send the
+ queries.
+
+ *path*, a ``str``. If *where* is an IP address, then *path* will be used to
+ construct the URL to send the DNS query to.
+
+ *post*, a ``bool``. If ``True``, the default, POST method will be used.
+
+ *bootstrap_address*, a ``str``, the IP address to use to bypass resolution.
+
+ *verify*, a ``bool`` or ``str``. If a ``True``, then TLS certificate verification
+ of the server is done using the default CA bundle; if ``False``, then no
+ verification is done; if a `str` then it specifies the path to a certificate file or
+ directory which will be used for verification.
+
+ *resolver*, a ``dns.resolver.Resolver`` or ``None``, the resolver to use for
+ resolution of hostnames in URLs. If not specified, a new resolver with a default
+ configuration will be used; note this is *not* the default resolver as that resolver
+ might have been configured to use DoH causing a chicken-and-egg problem. This
+ parameter only has an effect if the HTTP library is httpx.
+
+ *family*, an ``int``, the address family. If socket.AF_UNSPEC (the default), both A
+ and AAAA records will be retrieved.
+
+ *http_version*, a ``dns.query.HTTPVersion``, indicating which HTTP version to use.
+
+ Returns a ``dns.message.Message``.
+ """
+
+ (af, _, the_source) = _destination_and_source(
+ where, port, source, source_port, False
+ )
+ if af is not None and dns.inet.is_address(where):
+ if af == socket.AF_INET:
+ url = f"https://{where}:{port}{path}"
+ elif af == socket.AF_INET6:
+ url = f"https://[{where}]:{port}{path}"
+ else:
+ url = where
+
+ extensions = {}
+ if bootstrap_address is None:
+ # pylint: disable=possibly-used-before-assignment
+ parsed = urllib.parse.urlparse(url)
+ if parsed.hostname is None:
+ raise ValueError("no hostname in URL")
+ if dns.inet.is_address(parsed.hostname):
+ bootstrap_address = parsed.hostname
+ extensions["sni_hostname"] = parsed.hostname
+ if parsed.port is not None:
+ port = parsed.port
+
+ if http_version == HTTPVersion.H3 or (
+ http_version == HTTPVersion.DEFAULT and not have_doh
+ ):
+ if bootstrap_address is None:
+ resolver = _maybe_get_resolver(resolver)
+ assert parsed.hostname is not None # for mypy
+ answers = resolver.resolve_name(parsed.hostname, family)
+ bootstrap_address = random.choice(list(answers.addresses()))
+ return _http3(
+ q,
+ bootstrap_address,
+ url,
+ timeout,
+ port,
+ source,
+ source_port,
+ one_rr_per_rrset,
+ ignore_trailing,
+ verify=verify,
+ post=post,
+ )
+
+ if not have_doh:
+ raise NoDOH # pragma: no cover
+ if session and not isinstance(session, httpx.Client):
+ raise ValueError("session parameter must be an httpx.Client")
+
+ wire = q.to_wire()
+ headers = {"accept": "application/dns-message"}
+
+ h1 = http_version in (HTTPVersion.H1, HTTPVersion.DEFAULT)
+ h2 = http_version in (HTTPVersion.H2, HTTPVersion.DEFAULT)
+
+ # set source port and source address
+
+ if the_source is None:
+ local_address = None
+ local_port = 0
+ else:
+ local_address = the_source[0]
+ local_port = the_source[1]
+
+ if session:
+ cm: contextlib.AbstractContextManager = contextlib.nullcontext(session)
+ else:
+ transport = _HTTPTransport(
+ local_address=local_address,
+ http1=h1,
+ http2=h2,
+ verify=verify,
+ local_port=local_port,
+ bootstrap_address=bootstrap_address,
+ resolver=resolver,
+ family=family,
+ )
+
+ cm = httpx.Client(http1=h1, http2=h2, verify=verify, transport=transport)
+ with cm as session:
+ # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH
+ # GET and POST examples
+ if post:
+ headers.update(
+ {
+ "content-type": "application/dns-message",
+ "content-length": str(len(wire)),
+ }
+ )
+ response = session.post(
+ url,
+ headers=headers,
+ content=wire,
+ timeout=timeout,
+ extensions=extensions,
+ )
+ else:
+ wire = base64.urlsafe_b64encode(wire).rstrip(b"=")
+ twire = wire.decode() # httpx does a repr() if we give it bytes
+ response = session.get(
+ url,
+ headers=headers,
+ timeout=timeout,
+ params={"dns": twire},
+ extensions=extensions,
+ )
+
+ # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH
+ # status codes
+ if response.status_code < 200 or response.status_code > 299:
+ raise ValueError(
+ f"{where} responded with status code {response.status_code}"
+ f"\nResponse body: {response.content}"
+ )
+ r = dns.message.from_wire(
+ response.content,
+ keyring=q.keyring,
+ request_mac=q.request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ r.time = response.elapsed.total_seconds()
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+
+
+def _find_header(headers: dns.quic.Headers, name: bytes) -> bytes:
+ if headers is None:
+ raise KeyError
+ for header, value in headers:
+ if header == name:
+ return value
+ raise KeyError
+
+
+def _check_status(headers: dns.quic.Headers, peer: str, wire: bytes) -> None:
+ value = _find_header(headers, b":status")
+ if value is None:
+ raise SyntaxError("no :status header in response")
+ status = int(value)
+ if status < 0:
+ raise SyntaxError("status is negative")
+ if status < 200 or status > 299:
+ error = ""
+ if len(wire) > 0:
+ try:
+ error = ": " + wire.decode()
+ except Exception:
+ pass
+ raise ValueError(f"{peer} responded with status code {status}{error}")
+
+
+def _http3(
+ q: dns.message.Message,
+ where: str,
+ url: str,
+ timeout: Optional[float] = None,
+ port: int = 853,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ verify: Union[bool, str] = True,
+ hostname: Optional[str] = None,
+ post: bool = True,
+) -> dns.message.Message:
+ if not dns.quic.have_quic:
+ raise NoDOH("DNS-over-HTTP3 is not available.") # pragma: no cover
+
+ url_parts = urllib.parse.urlparse(url)
+ hostname = url_parts.hostname
+ if url_parts.port is not None:
+ port = url_parts.port
+
+ q.id = 0
+ wire = q.to_wire()
+ manager = dns.quic.SyncQuicManager(
+ verify_mode=verify, server_name=hostname, h3=True
+ )
+
+ with manager:
+ connection = manager.connect(where, port, source, source_port)
+ (start, expiration) = _compute_times(timeout)
+ with connection.make_stream(timeout) as stream:
+ stream.send_h3(url, wire, post)
+ wire = stream.receive(_remaining(expiration))
+ _check_status(stream.headers(), where, wire)
+ finish = time.time()
+ r = dns.message.from_wire(
+ wire,
+ keyring=q.keyring,
+ request_mac=q.request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ r.time = max(finish - start, 0.0)
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+
+
+def _udp_recv(sock, max_size, expiration):
+ """Reads a datagram from the socket.
+ A Timeout exception will be raised if the operation is not completed
+ by the expiration time.
+ """
+ while True:
+ try:
+ return sock.recvfrom(max_size)
+ except BlockingIOError:
+ _wait_for_readable(sock, expiration)
+
+
+def _udp_send(sock, data, destination, expiration):
+ """Sends the specified datagram to destination over the socket.
+ A Timeout exception will be raised if the operation is not completed
+ by the expiration time.
+ """
+ while True:
+ try:
+ if destination:
+ return sock.sendto(data, destination)
+ else:
+ return sock.send(data)
+ except BlockingIOError: # pragma: no cover
+ _wait_for_writable(sock, expiration)
+
+
+def send_udp(
+ sock: Any,
+ what: Union[dns.message.Message, bytes],
+ destination: Any,
+ expiration: Optional[float] = None,
+) -> Tuple[int, float]:
+ """Send a DNS message to the specified UDP socket.
+
+ *sock*, a ``socket``.
+
+ *what*, a ``bytes`` or ``dns.message.Message``, the message to send.
+
+ *destination*, a destination tuple appropriate for the address family
+ of the socket, specifying where to send the query.
+
+ *expiration*, a ``float`` or ``None``, the absolute time at which
+ a timeout exception should be raised. If ``None``, no timeout will
+ occur.
+
+ Returns an ``(int, float)`` tuple of bytes sent and the sent time.
+ """
+
+ if isinstance(what, dns.message.Message):
+ what = what.to_wire()
+ sent_time = time.time()
+ n = _udp_send(sock, what, destination, expiration)
+ return (n, sent_time)
+
+
+def receive_udp(
+ sock: Any,
+ destination: Optional[Any] = None,
+ expiration: Optional[float] = None,
+ ignore_unexpected: bool = False,
+ one_rr_per_rrset: bool = False,
+ keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None,
+ request_mac: Optional[bytes] = b"",
+ ignore_trailing: bool = False,
+ raise_on_truncation: bool = False,
+ ignore_errors: bool = False,
+ query: Optional[dns.message.Message] = None,
+) -> Any:
+ """Read a DNS message from a UDP socket.
+
+ *sock*, a ``socket``.
+
+ *destination*, a destination tuple appropriate for the address family
+ of the socket, specifying where the message is expected to arrive from.
+ When receiving a response, this would be where the associated query was
+ sent.
+
+ *expiration*, a ``float`` or ``None``, the absolute time at which
+ a timeout exception should be raised. If ``None``, no timeout will
+ occur.
+
+ *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from
+ unexpected sources.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
+ RRset.
+
+ *keyring*, a ``dict``, the keyring to use for TSIG.
+
+ *request_mac*, a ``bytes`` or ``None``, the MAC of the request (for TSIG).
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing
+ junk at end of the received message.
+
+ *raise_on_truncation*, a ``bool``. If ``True``, raise an exception if
+ the TC bit is set.
+
+ Raises if the message is malformed, if network errors occur, of if
+ there is a timeout.
+
+ If *destination* is not ``None``, returns a ``(dns.message.Message, float)``
+ tuple of the received message and the received time.
+
+ If *destination* is ``None``, returns a
+ ``(dns.message.Message, float, tuple)``
+ tuple of the received message, the received time, and the address where
+ the message arrived from.
+
+ *ignore_errors*, a ``bool``. If various format errors or response
+ mismatches occur, ignore them and keep listening for a valid response.
+ The default is ``False``.
+
+ *query*, a ``dns.message.Message`` or ``None``. If not ``None`` and
+ *ignore_errors* is ``True``, check that the received message is a response
+ to this query, and if not keep listening for a valid response.
+ """
+
+ wire = b""
+ while True:
+ (wire, from_address) = _udp_recv(sock, 65535, expiration)
+ if not _matches_destination(
+ sock.family, from_address, destination, ignore_unexpected
+ ):
+ continue
+ received_time = time.time()
+ try:
+ r = dns.message.from_wire(
+ wire,
+ keyring=keyring,
+ request_mac=request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ raise_on_truncation=raise_on_truncation,
+ )
+ except dns.message.Truncated as e:
+ # If we got Truncated and not FORMERR, we at least got the header with TC
+ # set, and very likely the question section, so we'll re-raise if the
+ # message seems to be a response as we need to know when truncation happens.
+ # We need to check that it seems to be a response as we don't want a random
+ # injected message with TC set to cause us to bail out.
+ if (
+ ignore_errors
+ and query is not None
+ and not query.is_response(e.message())
+ ):
+ continue
+ else:
+ raise
+ except Exception:
+ if ignore_errors:
+ continue
+ else:
+ raise
+ if ignore_errors and query is not None and not query.is_response(r):
+ continue
+ if destination:
+ return (r, received_time)
+ else:
+ return (r, received_time, from_address)
+
+
+def udp(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 53,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ ignore_unexpected: bool = False,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ raise_on_truncation: bool = False,
+ sock: Optional[Any] = None,
+ ignore_errors: bool = False,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via UDP.
+
+ *q*, a ``dns.message.Message``, the query to send
+
+ *where*, a ``str`` containing an IPv4 or IPv6 address, where
+ to send the message.
+
+ *timeout*, a ``float`` or ``None``, the number of seconds to wait before the
+ query times out. If ``None``, the default, wait forever.
+
+ *port*, an ``int``, the port send the message to. The default is 53.
+
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+ the source address. The default is the wildcard address.
+
+ *source_port*, an ``int``, the port from which to send the message.
+ The default is 0.
+
+ *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from
+ unexpected sources.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
+ RRset.
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing
+ junk at end of the received message.
+
+ *raise_on_truncation*, a ``bool``. If ``True``, raise an exception if
+ the TC bit is set.
+
+ *sock*, a ``socket.socket``, or ``None``, the socket to use for the
+ query. If ``None``, the default, a socket is created. Note that
+ if a socket is provided, it must be a nonblocking datagram socket,
+ and the *source* and *source_port* are ignored.
+
+ *ignore_errors*, a ``bool``. If various format errors or response
+ mismatches occur, ignore them and keep listening for a valid response.
+ The default is ``False``.
+
+ Returns a ``dns.message.Message``.
+ """
+
+ wire = q.to_wire()
+ (af, destination, source) = _destination_and_source(
+ where, port, source, source_port
+ )
+ (begin_time, expiration) = _compute_times(timeout)
+ if sock:
+ cm: contextlib.AbstractContextManager = contextlib.nullcontext(sock)
+ else:
+ cm = _make_socket(af, socket.SOCK_DGRAM, source)
+ with cm as s:
+ send_udp(s, wire, destination, expiration)
+ (r, received_time) = receive_udp(
+ s,
+ destination,
+ expiration,
+ ignore_unexpected,
+ one_rr_per_rrset,
+ q.keyring,
+ q.mac,
+ ignore_trailing,
+ raise_on_truncation,
+ ignore_errors,
+ q,
+ )
+ r.time = received_time - begin_time
+ # We don't need to check q.is_response() if we are in ignore_errors mode
+ # as receive_udp() will have checked it.
+ if not (ignore_errors or q.is_response(r)):
+ raise BadResponse
+ return r
+ assert (
+ False # help mypy figure out we can't get here lgtm[py/unreachable-statement]
+ )
+
+
+def udp_with_fallback(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 53,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ ignore_unexpected: bool = False,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ udp_sock: Optional[Any] = None,
+ tcp_sock: Optional[Any] = None,
+ ignore_errors: bool = False,
+) -> Tuple[dns.message.Message, bool]:
+ """Return the response to the query, trying UDP first and falling back
+ to TCP if UDP results in a truncated response.
+
+ *q*, a ``dns.message.Message``, the query to send
+
+ *where*, a ``str`` containing an IPv4 or IPv6 address, where to send the message.
+
+ *timeout*, a ``float`` or ``None``, the number of seconds to wait before the query
+ times out. If ``None``, the default, wait forever.
+
+ *port*, an ``int``, the port send the message to. The default is 53.
+
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying the source
+ address. The default is the wildcard address.
+
+ *source_port*, an ``int``, the port from which to send the message. The default is
+ 0.
+
+ *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from unexpected
+ sources.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset.
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the
+ received message.
+
+ *udp_sock*, a ``socket.socket``, or ``None``, the socket to use for the UDP query.
+ If ``None``, the default, a socket is created. Note that if a socket is provided,
+ it must be a nonblocking datagram socket, and the *source* and *source_port* are
+ ignored for the UDP query.
+
+ *tcp_sock*, a ``socket.socket``, or ``None``, the connected socket to use for the
+ TCP query. If ``None``, the default, a socket is created. Note that if a socket is
+ provided, it must be a nonblocking connected stream socket, and *where*, *source*
+ and *source_port* are ignored for the TCP query.
+
+ *ignore_errors*, a ``bool``. If various format errors or response mismatches occur
+ while listening for UDP, ignore them and keep listening for a valid response. The
+ default is ``False``.
+
+ Returns a (``dns.message.Message``, tcp) tuple where tcp is ``True`` if and only if
+ TCP was used.
+ """
+ try:
+ response = udp(
+ q,
+ where,
+ timeout,
+ port,
+ source,
+ source_port,
+ ignore_unexpected,
+ one_rr_per_rrset,
+ ignore_trailing,
+ True,
+ udp_sock,
+ ignore_errors,
+ )
+ return (response, False)
+ except dns.message.Truncated:
+ response = tcp(
+ q,
+ where,
+ timeout,
+ port,
+ source,
+ source_port,
+ one_rr_per_rrset,
+ ignore_trailing,
+ tcp_sock,
+ )
+ return (response, True)
+
+
+def _net_read(sock, count, expiration):
+ """Read the specified number of bytes from sock. Keep trying until we
+ either get the desired amount, or we hit EOF.
+ A Timeout exception will be raised if the operation is not completed
+ by the expiration time.
+ """
+ s = b""
+ while count > 0:
+ try:
+ n = sock.recv(count)
+ if n == b"":
+ raise EOFError("EOF")
+ count -= len(n)
+ s += n
+ except (BlockingIOError, ssl.SSLWantReadError):
+ _wait_for_readable(sock, expiration)
+ except ssl.SSLWantWriteError: # pragma: no cover
+ _wait_for_writable(sock, expiration)
+ return s
+
+
+def _net_write(sock, data, expiration):
+ """Write the specified data to the socket.
+ A Timeout exception will be raised if the operation is not completed
+ by the expiration time.
+ """
+ current = 0
+ l = len(data)
+ while current < l:
+ try:
+ current += sock.send(data[current:])
+ except (BlockingIOError, ssl.SSLWantWriteError):
+ _wait_for_writable(sock, expiration)
+ except ssl.SSLWantReadError: # pragma: no cover
+ _wait_for_readable(sock, expiration)
+
+
+def send_tcp(
+ sock: Any,
+ what: Union[dns.message.Message, bytes],
+ expiration: Optional[float] = None,
+) -> Tuple[int, float]:
+ """Send a DNS message to the specified TCP socket.
+
+ *sock*, a ``socket``.
+
+ *what*, a ``bytes`` or ``dns.message.Message``, the message to send.
+
+ *expiration*, a ``float`` or ``None``, the absolute time at which
+ a timeout exception should be raised. If ``None``, no timeout will
+ occur.
+
+ Returns an ``(int, float)`` tuple of bytes sent and the sent time.
+ """
+
+ if isinstance(what, dns.message.Message):
+ tcpmsg = what.to_wire(prepend_length=True)
+ else:
+ # copying the wire into tcpmsg is inefficient, but lets us
+ # avoid writev() or doing a short write that would get pushed
+ # onto the net
+ tcpmsg = len(what).to_bytes(2, "big") + what
+ sent_time = time.time()
+ _net_write(sock, tcpmsg, expiration)
+ return (len(tcpmsg), sent_time)
+
+
+def receive_tcp(
+ sock: Any,
+ expiration: Optional[float] = None,
+ one_rr_per_rrset: bool = False,
+ keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None,
+ request_mac: Optional[bytes] = b"",
+ ignore_trailing: bool = False,
+) -> Tuple[dns.message.Message, float]:
+ """Read a DNS message from a TCP socket.
+
+ *sock*, a ``socket``.
+
+ *expiration*, a ``float`` or ``None``, the absolute time at which
+ a timeout exception should be raised. If ``None``, no timeout will
+ occur.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
+ RRset.
+
+ *keyring*, a ``dict``, the keyring to use for TSIG.
+
+ *request_mac*, a ``bytes`` or ``None``, the MAC of the request (for TSIG).
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing
+ junk at end of the received message.
+
+ Raises if the message is malformed, if network errors occur, of if
+ there is a timeout.
+
+ Returns a ``(dns.message.Message, float)`` tuple of the received message
+ and the received time.
+ """
+
+ ldata = _net_read(sock, 2, expiration)
+ (l,) = struct.unpack("!H", ldata)
+ wire = _net_read(sock, l, expiration)
+ received_time = time.time()
+ r = dns.message.from_wire(
+ wire,
+ keyring=keyring,
+ request_mac=request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ return (r, received_time)
+
+
+def _connect(s, address, expiration):
+ err = s.connect_ex(address)
+ if err == 0:
+ return
+ if err in (errno.EINPROGRESS, errno.EWOULDBLOCK, errno.EALREADY):
+ _wait_for_writable(s, expiration)
+ err = s.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
+ if err != 0:
+ raise OSError(err, os.strerror(err))
+
+
+def tcp(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 53,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ sock: Optional[Any] = None,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via TCP.
+
+ *q*, a ``dns.message.Message``, the query to send
+
+ *where*, a ``str`` containing an IPv4 or IPv6 address, where
+ to send the message.
+
+ *timeout*, a ``float`` or ``None``, the number of seconds to wait before the
+ query times out. If ``None``, the default, wait forever.
+
+ *port*, an ``int``, the port send the message to. The default is 53.
+
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+ the source address. The default is the wildcard address.
+
+ *source_port*, an ``int``, the port from which to send the message.
+ The default is 0.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
+ RRset.
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing
+ junk at end of the received message.
+
+ *sock*, a ``socket.socket``, or ``None``, the connected socket to use for the
+ query. If ``None``, the default, a socket is created. Note that
+ if a socket is provided, it must be a nonblocking connected stream
+ socket, and *where*, *port*, *source* and *source_port* are ignored.
+
+ Returns a ``dns.message.Message``.
+ """
+
+ wire = q.to_wire()
+ (begin_time, expiration) = _compute_times(timeout)
+ if sock:
+ cm: contextlib.AbstractContextManager = contextlib.nullcontext(sock)
+ else:
+ (af, destination, source) = _destination_and_source(
+ where, port, source, source_port
+ )
+ cm = _make_socket(af, socket.SOCK_STREAM, source)
+ with cm as s:
+ if not sock:
+ # pylint: disable=possibly-used-before-assignment
+ _connect(s, destination, expiration)
+ send_tcp(s, wire, expiration)
+ (r, received_time) = receive_tcp(
+ s, expiration, one_rr_per_rrset, q.keyring, q.mac, ignore_trailing
+ )
+ r.time = received_time - begin_time
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+ assert (
+ False # help mypy figure out we can't get here lgtm[py/unreachable-statement]
+ )
+
+
+def _tls_handshake(s, expiration):
+ while True:
+ try:
+ s.do_handshake()
+ return
+ except ssl.SSLWantReadError:
+ _wait_for_readable(s, expiration)
+ except ssl.SSLWantWriteError: # pragma: no cover
+ _wait_for_writable(s, expiration)
+
+
+def _make_dot_ssl_context(
+ server_hostname: Optional[str], verify: Union[bool, str]
+) -> ssl.SSLContext:
+ cafile: Optional[str] = None
+ capath: Optional[str] = None
+ if isinstance(verify, str):
+ if os.path.isfile(verify):
+ cafile = verify
+ elif os.path.isdir(verify):
+ capath = verify
+ else:
+ raise ValueError("invalid verify string")
+ ssl_context = ssl.create_default_context(cafile=cafile, capath=capath)
+ ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
+ if server_hostname is None:
+ ssl_context.check_hostname = False
+ ssl_context.set_alpn_protocols(["dot"])
+ if verify is False:
+ ssl_context.verify_mode = ssl.CERT_NONE
+ return ssl_context
+
+
+def tls(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 853,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ sock: Optional[ssl.SSLSocket] = None,
+ ssl_context: Optional[ssl.SSLContext] = None,
+ server_hostname: Optional[str] = None,
+ verify: Union[bool, str] = True,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via TLS.
+
+ *q*, a ``dns.message.Message``, the query to send
+
+ *where*, a ``str`` containing an IPv4 or IPv6 address, where
+ to send the message.
+
+ *timeout*, a ``float`` or ``None``, the number of seconds to wait before the
+ query times out. If ``None``, the default, wait forever.
+
+ *port*, an ``int``, the port send the message to. The default is 853.
+
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+ the source address. The default is the wildcard address.
+
+ *source_port*, an ``int``, the port from which to send the message.
+ The default is 0.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
+ RRset.
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing
+ junk at end of the received message.
+
+ *sock*, an ``ssl.SSLSocket``, or ``None``, the socket to use for
+ the query. If ``None``, the default, a socket is created. Note
+ that if a socket is provided, it must be a nonblocking connected
+ SSL stream socket, and *where*, *port*, *source*, *source_port*,
+ and *ssl_context* are ignored.
+
+ *ssl_context*, an ``ssl.SSLContext``, the context to use when establishing
+ a TLS connection. If ``None``, the default, creates one with the default
+ configuration.
+
+ *server_hostname*, a ``str`` containing the server's hostname. The
+ default is ``None``, which means that no hostname is known, and if an
+ SSL context is created, hostname checking will be disabled.
+
+ *verify*, a ``bool`` or ``str``. If a ``True``, then TLS certificate verification
+ of the server is done using the default CA bundle; if ``False``, then no
+ verification is done; if a `str` then it specifies the path to a certificate file or
+ directory which will be used for verification.
+
+ Returns a ``dns.message.Message``.
+
+ """
+
+ if sock:
+ #
+ # If a socket was provided, there's no special TLS handling needed.
+ #
+ return tcp(
+ q,
+ where,
+ timeout,
+ port,
+ source,
+ source_port,
+ one_rr_per_rrset,
+ ignore_trailing,
+ sock,
+ )
+
+ wire = q.to_wire()
+ (begin_time, expiration) = _compute_times(timeout)
+ (af, destination, source) = _destination_and_source(
+ where, port, source, source_port
+ )
+ if ssl_context is None and not sock:
+ ssl_context = _make_dot_ssl_context(server_hostname, verify)
+
+ with _make_socket(
+ af,
+ socket.SOCK_STREAM,
+ source,
+ ssl_context=ssl_context,
+ server_hostname=server_hostname,
+ ) as s:
+ _connect(s, destination, expiration)
+ _tls_handshake(s, expiration)
+ send_tcp(s, wire, expiration)
+ (r, received_time) = receive_tcp(
+ s, expiration, one_rr_per_rrset, q.keyring, q.mac, ignore_trailing
+ )
+ r.time = received_time - begin_time
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+ assert (
+ False # help mypy figure out we can't get here lgtm[py/unreachable-statement]
+ )
+
+
+def quic(
+ q: dns.message.Message,
+ where: str,
+ timeout: Optional[float] = None,
+ port: int = 853,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ one_rr_per_rrset: bool = False,
+ ignore_trailing: bool = False,
+ connection: Optional[dns.quic.SyncQuicConnection] = None,
+ verify: Union[bool, str] = True,
+ hostname: Optional[str] = None,
+ server_hostname: Optional[str] = None,
+) -> dns.message.Message:
+ """Return the response obtained after sending a query via DNS-over-QUIC.
+
+ *q*, a ``dns.message.Message``, the query to send.
+
+ *where*, a ``str``, the nameserver IP address.
+
+ *timeout*, a ``float`` or ``None``, the number of seconds to wait before the query
+ times out. If ``None``, the default, wait forever.
+
+ *port*, a ``int``, the port to send the query to. The default is 853.
+
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying the source
+ address. The default is the wildcard address.
+
+ *source_port*, an ``int``, the port from which to send the message. The default is
+ 0.
+
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset.
+
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the
+ received message.
+
+ *connection*, a ``dns.quic.SyncQuicConnection``. If provided, the connection to use
+ to send the query.
+
+ *verify*, a ``bool`` or ``str``. If a ``True``, then TLS certificate verification
+ of the server is done using the default CA bundle; if ``False``, then no
+ verification is done; if a `str` then it specifies the path to a certificate file or
+ directory which will be used for verification.
+
+ *hostname*, a ``str`` containing the server's hostname or ``None``. The default is
+ ``None``, which means that no hostname is known, and if an SSL context is created,
+ hostname checking will be disabled. This value is ignored if *url* is not
+ ``None``.
+
+ *server_hostname*, a ``str`` or ``None``. This item is for backwards compatibility
+ only, and has the same meaning as *hostname*.
+
+ Returns a ``dns.message.Message``.
+ """
+
+ if not dns.quic.have_quic:
+ raise NoDOQ("DNS-over-QUIC is not available.") # pragma: no cover
+
+ if server_hostname is not None and hostname is None:
+ hostname = server_hostname
+
+ q.id = 0
+ wire = q.to_wire()
+ the_connection: dns.quic.SyncQuicConnection
+ the_manager: dns.quic.SyncQuicManager
+ if connection:
+ manager: contextlib.AbstractContextManager = contextlib.nullcontext(None)
+ the_connection = connection
+ else:
+ manager = dns.quic.SyncQuicManager(verify_mode=verify, server_name=hostname)
+ the_manager = manager # for type checking happiness
+
+ with manager:
+ if not connection:
+ the_connection = the_manager.connect(where, port, source, source_port)
+ (start, expiration) = _compute_times(timeout)
+ with the_connection.make_stream(timeout) as stream:
+ stream.send(wire, True)
+ wire = stream.receive(_remaining(expiration))
+ finish = time.time()
+ r = dns.message.from_wire(
+ wire,
+ keyring=q.keyring,
+ request_mac=q.request_mac,
+ one_rr_per_rrset=one_rr_per_rrset,
+ ignore_trailing=ignore_trailing,
+ )
+ r.time = max(finish - start, 0.0)
+ if not q.is_response(r):
+ raise BadResponse
+ return r
+
+
+class UDPMode(enum.IntEnum):
+ """How should UDP be used in an IXFR from :py:func:`inbound_xfr()`?
+
+ NEVER means "never use UDP; always use TCP"
+ TRY_FIRST means "try to use UDP but fall back to TCP if needed"
+ ONLY means "raise ``dns.xfr.UseTCP`` if trying UDP does not succeed"
+ """
+
+ NEVER = 0
+ TRY_FIRST = 1
+ ONLY = 2
+
+
+def _inbound_xfr(
+ txn_manager: dns.transaction.TransactionManager,
+ s: socket.socket,
+ query: dns.message.Message,
+ serial: Optional[int],
+ timeout: Optional[float],
+ expiration: float,
+) -> Any:
+ """Given a socket, does the zone transfer."""
+ rdtype = query.question[0].rdtype
+ is_ixfr = rdtype == dns.rdatatype.IXFR
+ origin = txn_manager.from_wire_origin()
+ wire = query.to_wire()
+ is_udp = s.type == socket.SOCK_DGRAM
+ if is_udp:
+ _udp_send(s, wire, None, expiration)
+ else:
+ tcpmsg = struct.pack("!H", len(wire)) + wire
+ _net_write(s, tcpmsg, expiration)
+ with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound:
+ done = False
+ tsig_ctx = None
+ while not done:
+ (_, mexpiration) = _compute_times(timeout)
+ if mexpiration is None or (
+ expiration is not None and mexpiration > expiration
+ ):
+ mexpiration = expiration
+ if is_udp:
+ (rwire, _) = _udp_recv(s, 65535, mexpiration)
+ else:
+ ldata = _net_read(s, 2, mexpiration)
+ (l,) = struct.unpack("!H", ldata)
+ rwire = _net_read(s, l, mexpiration)
+ r = dns.message.from_wire(
+ rwire,
+ keyring=query.keyring,
+ request_mac=query.mac,
+ xfr=True,
+ origin=origin,
+ tsig_ctx=tsig_ctx,
+ multi=(not is_udp),
+ one_rr_per_rrset=is_ixfr,
+ )
+ done = inbound.process_message(r)
+ yield r
+ tsig_ctx = r.tsig_ctx
+ if query.keyring and not r.had_tsig:
+ raise dns.exception.FormError("missing TSIG")
+
+
+def xfr(
+ where: str,
+ zone: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.AXFR,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ timeout: Optional[float] = None,
+ port: int = 53,
+ keyring: Optional[Dict[dns.name.Name, dns.tsig.Key]] = None,
+ keyname: Optional[Union[dns.name.Name, str]] = None,
+ relativize: bool = True,
+ lifetime: Optional[float] = None,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ serial: int = 0,
+ use_udp: bool = False,
+ keyalgorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm,
+) -> Any:
+ """Return a generator for the responses to a zone transfer.
+
+ *where*, a ``str`` containing an IPv4 or IPv6 address, where
+ to send the message.
+
+ *zone*, a ``dns.name.Name`` or ``str``, the name of the zone to transfer.
+
+ *rdtype*, an ``int`` or ``str``, the type of zone transfer. The
+ default is ``dns.rdatatype.AXFR``. ``dns.rdatatype.IXFR`` can be
+ used to do an incremental transfer instead.
+
+ *rdclass*, an ``int`` or ``str``, the class of the zone transfer.
+ The default is ``dns.rdataclass.IN``.
+
+ *timeout*, a ``float``, the number of seconds to wait for each
+ response message. If None, the default, wait forever.
+
+ *port*, an ``int``, the port send the message to. The default is 53.
+
+ *keyring*, a ``dict``, the keyring to use for TSIG.
+
+ *keyname*, a ``dns.name.Name`` or ``str``, the name of the TSIG
+ key to use.
+
+ *relativize*, a ``bool``. If ``True``, all names in the zone will be
+ relativized to the zone origin. It is essential that the
+ relativize setting matches the one specified to
+ ``dns.zone.from_xfr()`` if using this generator to make a zone.
+
+ *lifetime*, a ``float``, the total number of seconds to spend
+ doing the transfer. If ``None``, the default, then there is no
+ limit on the time the transfer may take.
+
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+ the source address. The default is the wildcard address.
+
+ *source_port*, an ``int``, the port from which to send the message.
+ The default is 0.
+
+ *serial*, an ``int``, the SOA serial number to use as the base for
+ an IXFR diff sequence (only meaningful if *rdtype* is
+ ``dns.rdatatype.IXFR``).
+
+ *use_udp*, a ``bool``. If ``True``, use UDP (only meaningful for IXFR).
+
+ *keyalgorithm*, a ``dns.name.Name`` or ``str``, the TSIG algorithm to use.
+
+ Raises on errors, and so does the generator.
+
+ Returns a generator of ``dns.message.Message`` objects.
+ """
+
+ class DummyTransactionManager(dns.transaction.TransactionManager):
+ def __init__(self, origin, relativize):
+ self.info = (origin, relativize, dns.name.empty if relativize else origin)
+
+ def origin_information(self):
+ return self.info
+
+ def get_class(self) -> dns.rdataclass.RdataClass:
+ raise NotImplementedError # pragma: no cover
+
+ def reader(self):
+ raise NotImplementedError # pragma: no cover
+
+ def writer(self, replacement: bool = False) -> dns.transaction.Transaction:
+ class DummyTransaction:
+ def nop(self, *args, **kw):
+ pass
+
+ def __getattr__(self, _):
+ return self.nop
+
+ return cast(dns.transaction.Transaction, DummyTransaction())
+
+ if isinstance(zone, str):
+ zone = dns.name.from_text(zone)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ q = dns.message.make_query(zone, rdtype, rdclass)
+ if rdtype == dns.rdatatype.IXFR:
+ rrset = q.find_rrset(
+ q.authority, zone, dns.rdataclass.IN, dns.rdatatype.SOA, create=True
+ )
+ soa = dns.rdata.from_text("IN", "SOA", ". . %u 0 0 0 0" % serial)
+ rrset.add(soa, 0)
+ if keyring is not None:
+ q.use_tsig(keyring, keyname, algorithm=keyalgorithm)
+ (af, destination, source) = _destination_and_source(
+ where, port, source, source_port
+ )
+ (_, expiration) = _compute_times(lifetime)
+ tm = DummyTransactionManager(zone, relativize)
+ if use_udp and rdtype != dns.rdatatype.IXFR:
+ raise ValueError("cannot do a UDP AXFR")
+ sock_type = socket.SOCK_DGRAM if use_udp else socket.SOCK_STREAM
+ with _make_socket(af, sock_type, source) as s:
+ _connect(s, destination, expiration)
+ yield from _inbound_xfr(tm, s, q, serial, timeout, expiration)
+
+
+def inbound_xfr(
+ where: str,
+ txn_manager: dns.transaction.TransactionManager,
+ query: Optional[dns.message.Message] = None,
+ port: int = 53,
+ timeout: Optional[float] = None,
+ lifetime: Optional[float] = None,
+ source: Optional[str] = None,
+ source_port: int = 0,
+ udp_mode: UDPMode = UDPMode.NEVER,
+) -> None:
+ """Conduct an inbound transfer and apply it via a transaction from the
+ txn_manager.
+
+ *where*, a ``str`` containing an IPv4 or IPv6 address, where
+ to send the message.
+
+ *txn_manager*, a ``dns.transaction.TransactionManager``, the txn_manager
+ for this transfer (typically a ``dns.zone.Zone``).
+
+ *query*, the query to send. If not supplied, a default query is
+ constructed using information from the *txn_manager*.
+
+ *port*, an ``int``, the port send the message to. The default is 53.
+
+ *timeout*, a ``float``, the number of seconds to wait for each
+ response message. If None, the default, wait forever.
+
+ *lifetime*, a ``float``, the total number of seconds to spend
+ doing the transfer. If ``None``, the default, then there is no
+ limit on the time the transfer may take.
+
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+ the source address. The default is the wildcard address.
+
+ *source_port*, an ``int``, the port from which to send the message.
+ The default is 0.
+
+ *udp_mode*, a ``dns.query.UDPMode``, determines how UDP is used
+ for IXFRs. The default is ``dns.UDPMode.NEVER``, i.e. only use
+ TCP. Other possibilities are ``dns.UDPMode.TRY_FIRST``, which
+ means "try UDP but fallback to TCP if needed", and
+ ``dns.UDPMode.ONLY``, which means "try UDP and raise
+ ``dns.xfr.UseTCP`` if it does not succeed.
+
+ Raises on errors.
+ """
+ if query is None:
+ (query, serial) = dns.xfr.make_query(txn_manager)
+ else:
+ serial = dns.xfr.extract_serial_from_query(query)
+
+ (af, destination, source) = _destination_and_source(
+ where, port, source, source_port
+ )
+ (_, expiration) = _compute_times(lifetime)
+ if query.question[0].rdtype == dns.rdatatype.IXFR and udp_mode != UDPMode.NEVER:
+ with _make_socket(af, socket.SOCK_DGRAM, source) as s:
+ _connect(s, destination, expiration)
+ try:
+ for _ in _inbound_xfr(
+ txn_manager, s, query, serial, timeout, expiration
+ ):
+ pass
+ return
+ except dns.xfr.UseTCP:
+ if udp_mode == UDPMode.ONLY:
+ raise
+
+ with _make_socket(af, socket.SOCK_STREAM, source) as s:
+ _connect(s, destination, expiration)
+ for _ in _inbound_xfr(txn_manager, s, query, serial, timeout, expiration):
+ pass
diff --git a/.venv/lib/python3.12/site-packages/dns/quic/__init__.py b/.venv/lib/python3.12/site-packages/dns/quic/__init__.py
new file mode 100644
index 00000000..0750e729
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/quic/__init__.py
@@ -0,0 +1,80 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+from typing import List, Tuple
+
+import dns._features
+import dns.asyncbackend
+
+if dns._features.have("doq"):
+ import aioquic.quic.configuration # type: ignore
+
+ from dns._asyncbackend import NullContext
+ from dns.quic._asyncio import (
+ AsyncioQuicConnection,
+ AsyncioQuicManager,
+ AsyncioQuicStream,
+ )
+ from dns.quic._common import AsyncQuicConnection, AsyncQuicManager
+ from dns.quic._sync import SyncQuicConnection, SyncQuicManager, SyncQuicStream
+
+ have_quic = True
+
+ def null_factory(
+ *args, # pylint: disable=unused-argument
+ **kwargs, # pylint: disable=unused-argument
+ ):
+ return NullContext(None)
+
+ def _asyncio_manager_factory(
+ context, *args, **kwargs # pylint: disable=unused-argument
+ ):
+ return AsyncioQuicManager(*args, **kwargs)
+
+ # We have a context factory and a manager factory as for trio we need to have
+ # a nursery.
+
+ _async_factories = {"asyncio": (null_factory, _asyncio_manager_factory)}
+
+ if dns._features.have("trio"):
+ import trio
+
+ from dns.quic._trio import ( # pylint: disable=ungrouped-imports
+ TrioQuicConnection,
+ TrioQuicManager,
+ TrioQuicStream,
+ )
+
+ def _trio_context_factory():
+ return trio.open_nursery()
+
+ def _trio_manager_factory(context, *args, **kwargs):
+ return TrioQuicManager(context, *args, **kwargs)
+
+ _async_factories["trio"] = (_trio_context_factory, _trio_manager_factory)
+
+ def factories_for_backend(backend=None):
+ if backend is None:
+ backend = dns.asyncbackend.get_default_backend()
+ return _async_factories[backend.name()]
+
+else: # pragma: no cover
+ have_quic = False
+
+ from typing import Any
+
+ class AsyncQuicStream: # type: ignore
+ pass
+
+ class AsyncQuicConnection: # type: ignore
+ async def make_stream(self) -> Any:
+ raise NotImplementedError
+
+ class SyncQuicStream: # type: ignore
+ pass
+
+ class SyncQuicConnection: # type: ignore
+ def make_stream(self) -> Any:
+ raise NotImplementedError
+
+
+Headers = List[Tuple[bytes, bytes]]
diff --git a/.venv/lib/python3.12/site-packages/dns/quic/_asyncio.py b/.venv/lib/python3.12/site-packages/dns/quic/_asyncio.py
new file mode 100644
index 00000000..f87515da
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/quic/_asyncio.py
@@ -0,0 +1,267 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import asyncio
+import socket
+import ssl
+import struct
+import time
+
+import aioquic.quic.configuration # type: ignore
+import aioquic.quic.connection # type: ignore
+import aioquic.quic.events # type: ignore
+
+import dns.asyncbackend
+import dns.exception
+import dns.inet
+from dns.quic._common import (
+ QUIC_MAX_DATAGRAM,
+ AsyncQuicConnection,
+ AsyncQuicManager,
+ BaseQuicStream,
+ UnexpectedEOF,
+)
+
+
+class AsyncioQuicStream(BaseQuicStream):
+ def __init__(self, connection, stream_id):
+ super().__init__(connection, stream_id)
+ self._wake_up = asyncio.Condition()
+
+ async def _wait_for_wake_up(self):
+ async with self._wake_up:
+ await self._wake_up.wait()
+
+ async def wait_for(self, amount, expiration):
+ while True:
+ timeout = self._timeout_from_expiration(expiration)
+ if self._buffer.have(amount):
+ return
+ self._expecting = amount
+ try:
+ await asyncio.wait_for(self._wait_for_wake_up(), timeout)
+ except TimeoutError:
+ raise dns.exception.Timeout
+ self._expecting = 0
+
+ async def wait_for_end(self, expiration):
+ while True:
+ timeout = self._timeout_from_expiration(expiration)
+ if self._buffer.seen_end():
+ return
+ try:
+ await asyncio.wait_for(self._wait_for_wake_up(), timeout)
+ except TimeoutError:
+ raise dns.exception.Timeout
+
+ async def receive(self, timeout=None):
+ expiration = self._expiration_from_timeout(timeout)
+ if self._connection.is_h3():
+ await self.wait_for_end(expiration)
+ return self._buffer.get_all()
+ else:
+ await self.wait_for(2, expiration)
+ (size,) = struct.unpack("!H", self._buffer.get(2))
+ await self.wait_for(size, expiration)
+ return self._buffer.get(size)
+
+ async def send(self, datagram, is_end=False):
+ data = self._encapsulate(datagram)
+ await self._connection.write(self._stream_id, data, is_end)
+
+ async def _add_input(self, data, is_end):
+ if self._common_add_input(data, is_end):
+ async with self._wake_up:
+ self._wake_up.notify()
+
+ async def close(self):
+ self._close()
+
+ # Streams are async context managers
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ await self.close()
+ async with self._wake_up:
+ self._wake_up.notify()
+ return False
+
+
+class AsyncioQuicConnection(AsyncQuicConnection):
+ def __init__(self, connection, address, port, source, source_port, manager=None):
+ super().__init__(connection, address, port, source, source_port, manager)
+ self._socket = None
+ self._handshake_complete = asyncio.Event()
+ self._socket_created = asyncio.Event()
+ self._wake_timer = asyncio.Condition()
+ self._receiver_task = None
+ self._sender_task = None
+ self._wake_pending = False
+
+ async def _receiver(self):
+ try:
+ af = dns.inet.af_for_address(self._address)
+ backend = dns.asyncbackend.get_backend("asyncio")
+ # Note that peer is a low-level address tuple, but make_socket() wants
+ # a high-level address tuple, so we convert.
+ self._socket = await backend.make_socket(
+ af, socket.SOCK_DGRAM, 0, self._source, (self._peer[0], self._peer[1])
+ )
+ self._socket_created.set()
+ async with self._socket:
+ while not self._done:
+ (datagram, address) = await self._socket.recvfrom(
+ QUIC_MAX_DATAGRAM, None
+ )
+ if address[0] != self._peer[0] or address[1] != self._peer[1]:
+ continue
+ self._connection.receive_datagram(datagram, address, time.time())
+ # Wake up the timer in case the sender is sleeping, as there may be
+ # stuff to send now.
+ await self._wakeup()
+ except Exception:
+ pass
+ finally:
+ self._done = True
+ await self._wakeup()
+ self._handshake_complete.set()
+
+ async def _wakeup(self):
+ self._wake_pending = True
+ async with self._wake_timer:
+ self._wake_timer.notify_all()
+
+ async def _wait_for_wake_timer(self):
+ async with self._wake_timer:
+ if not self._wake_pending:
+ await self._wake_timer.wait()
+ self._wake_pending = False
+
+ async def _sender(self):
+ await self._socket_created.wait()
+ while not self._done:
+ datagrams = self._connection.datagrams_to_send(time.time())
+ for datagram, address in datagrams:
+ assert address == self._peer
+ await self._socket.sendto(datagram, self._peer, None)
+ (expiration, interval) = self._get_timer_values()
+ try:
+ await asyncio.wait_for(self._wait_for_wake_timer(), interval)
+ except Exception:
+ pass
+ self._handle_timer(expiration)
+ await self._handle_events()
+
+ async def _handle_events(self):
+ count = 0
+ while True:
+ event = self._connection.next_event()
+ if event is None:
+ return
+ if isinstance(event, aioquic.quic.events.StreamDataReceived):
+ if self.is_h3():
+ h3_events = self._h3_conn.handle_event(event)
+ for h3_event in h3_events:
+ if isinstance(h3_event, aioquic.h3.events.HeadersReceived):
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ if stream._headers is None:
+ stream._headers = h3_event.headers
+ elif stream._trailers is None:
+ stream._trailers = h3_event.headers
+ if h3_event.stream_ended:
+ await stream._add_input(b"", True)
+ elif isinstance(h3_event, aioquic.h3.events.DataReceived):
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ await stream._add_input(
+ h3_event.data, h3_event.stream_ended
+ )
+ else:
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ await stream._add_input(event.data, event.end_stream)
+ elif isinstance(event, aioquic.quic.events.HandshakeCompleted):
+ self._handshake_complete.set()
+ elif isinstance(event, aioquic.quic.events.ConnectionTerminated):
+ self._done = True
+ self._receiver_task.cancel()
+ elif isinstance(event, aioquic.quic.events.StreamReset):
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ await stream._add_input(b"", True)
+
+ count += 1
+ if count > 10:
+ # yield
+ count = 0
+ await asyncio.sleep(0)
+
+ async def write(self, stream, data, is_end=False):
+ self._connection.send_stream_data(stream, data, is_end)
+ await self._wakeup()
+
+ def run(self):
+ if self._closed:
+ return
+ self._receiver_task = asyncio.Task(self._receiver())
+ self._sender_task = asyncio.Task(self._sender())
+
+ async def make_stream(self, timeout=None):
+ try:
+ await asyncio.wait_for(self._handshake_complete.wait(), timeout)
+ except TimeoutError:
+ raise dns.exception.Timeout
+ if self._done:
+ raise UnexpectedEOF
+ stream_id = self._connection.get_next_available_stream_id(False)
+ stream = AsyncioQuicStream(self, stream_id)
+ self._streams[stream_id] = stream
+ return stream
+
+ async def close(self):
+ if not self._closed:
+ self._manager.closed(self._peer[0], self._peer[1])
+ self._closed = True
+ self._connection.close()
+ # sender might be blocked on this, so set it
+ self._socket_created.set()
+ await self._wakeup()
+ try:
+ await self._receiver_task
+ except asyncio.CancelledError:
+ pass
+ try:
+ await self._sender_task
+ except asyncio.CancelledError:
+ pass
+ await self._socket.close()
+
+
+class AsyncioQuicManager(AsyncQuicManager):
+ def __init__(
+ self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None, h3=False
+ ):
+ super().__init__(conf, verify_mode, AsyncioQuicConnection, server_name, h3)
+
+ def connect(
+ self, address, port=853, source=None, source_port=0, want_session_ticket=True
+ ):
+ (connection, start) = self._connect(
+ address, port, source, source_port, want_session_ticket
+ )
+ if start:
+ connection.run()
+ return connection
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ # Copy the iterator into a list as exiting things will mutate the connections
+ # table.
+ connections = list(self._connections.values())
+ for connection in connections:
+ await connection.close()
+ return False
diff --git a/.venv/lib/python3.12/site-packages/dns/quic/_common.py b/.venv/lib/python3.12/site-packages/dns/quic/_common.py
new file mode 100644
index 00000000..ce575b03
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/quic/_common.py
@@ -0,0 +1,339 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import base64
+import copy
+import functools
+import socket
+import struct
+import time
+import urllib
+from typing import Any, Optional
+
+import aioquic.h3.connection # type: ignore
+import aioquic.h3.events # type: ignore
+import aioquic.quic.configuration # type: ignore
+import aioquic.quic.connection # type: ignore
+
+import dns.inet
+
+QUIC_MAX_DATAGRAM = 2048
+MAX_SESSION_TICKETS = 8
+# If we hit the max sessions limit we will delete this many of the oldest connections.
+# The value must be a integer > 0 and <= MAX_SESSION_TICKETS.
+SESSIONS_TO_DELETE = MAX_SESSION_TICKETS // 4
+
+
+class UnexpectedEOF(Exception):
+ pass
+
+
+class Buffer:
+ def __init__(self):
+ self._buffer = b""
+ self._seen_end = False
+
+ def put(self, data, is_end):
+ if self._seen_end:
+ return
+ self._buffer += data
+ if is_end:
+ self._seen_end = True
+
+ def have(self, amount):
+ if len(self._buffer) >= amount:
+ return True
+ if self._seen_end:
+ raise UnexpectedEOF
+ return False
+
+ def seen_end(self):
+ return self._seen_end
+
+ def get(self, amount):
+ assert self.have(amount)
+ data = self._buffer[:amount]
+ self._buffer = self._buffer[amount:]
+ return data
+
+ def get_all(self):
+ assert self.seen_end()
+ data = self._buffer
+ self._buffer = b""
+ return data
+
+
+class BaseQuicStream:
+ def __init__(self, connection, stream_id):
+ self._connection = connection
+ self._stream_id = stream_id
+ self._buffer = Buffer()
+ self._expecting = 0
+ self._headers = None
+ self._trailers = None
+
+ def id(self):
+ return self._stream_id
+
+ def headers(self):
+ return self._headers
+
+ def trailers(self):
+ return self._trailers
+
+ def _expiration_from_timeout(self, timeout):
+ if timeout is not None:
+ expiration = time.time() + timeout
+ else:
+ expiration = None
+ return expiration
+
+ def _timeout_from_expiration(self, expiration):
+ if expiration is not None:
+ timeout = max(expiration - time.time(), 0.0)
+ else:
+ timeout = None
+ return timeout
+
+ # Subclass must implement receive() as sync / async and which returns a message
+ # or raises.
+
+ # Subclass must implement send() as sync / async and which takes a message and
+ # an EOF indicator.
+
+ def send_h3(self, url, datagram, post=True):
+ if not self._connection.is_h3():
+ raise SyntaxError("cannot send H3 to a non-H3 connection")
+ url_parts = urllib.parse.urlparse(url)
+ path = url_parts.path.encode()
+ if post:
+ method = b"POST"
+ else:
+ method = b"GET"
+ path += b"?dns=" + base64.urlsafe_b64encode(datagram).rstrip(b"=")
+ headers = [
+ (b":method", method),
+ (b":scheme", url_parts.scheme.encode()),
+ (b":authority", url_parts.netloc.encode()),
+ (b":path", path),
+ (b"accept", b"application/dns-message"),
+ ]
+ if post:
+ headers.extend(
+ [
+ (b"content-type", b"application/dns-message"),
+ (b"content-length", str(len(datagram)).encode()),
+ ]
+ )
+ self._connection.send_headers(self._stream_id, headers, not post)
+ if post:
+ self._connection.send_data(self._stream_id, datagram, True)
+
+ def _encapsulate(self, datagram):
+ if self._connection.is_h3():
+ return datagram
+ l = len(datagram)
+ return struct.pack("!H", l) + datagram
+
+ def _common_add_input(self, data, is_end):
+ self._buffer.put(data, is_end)
+ try:
+ return (
+ self._expecting > 0 and self._buffer.have(self._expecting)
+ ) or self._buffer.seen_end
+ except UnexpectedEOF:
+ return True
+
+ def _close(self):
+ self._connection.close_stream(self._stream_id)
+ self._buffer.put(b"", True) # send EOF in case we haven't seen it.
+
+
+class BaseQuicConnection:
+ def __init__(
+ self,
+ connection,
+ address,
+ port,
+ source=None,
+ source_port=0,
+ manager=None,
+ ):
+ self._done = False
+ self._connection = connection
+ self._address = address
+ self._port = port
+ self._closed = False
+ self._manager = manager
+ self._streams = {}
+ if manager.is_h3():
+ self._h3_conn = aioquic.h3.connection.H3Connection(connection, False)
+ else:
+ self._h3_conn = None
+ self._af = dns.inet.af_for_address(address)
+ self._peer = dns.inet.low_level_address_tuple((address, port))
+ if source is None and source_port != 0:
+ if self._af == socket.AF_INET:
+ source = "0.0.0.0"
+ elif self._af == socket.AF_INET6:
+ source = "::"
+ else:
+ raise NotImplementedError
+ if source:
+ self._source = (source, source_port)
+ else:
+ self._source = None
+
+ def is_h3(self):
+ return self._h3_conn is not None
+
+ def close_stream(self, stream_id):
+ del self._streams[stream_id]
+
+ def send_headers(self, stream_id, headers, is_end=False):
+ self._h3_conn.send_headers(stream_id, headers, is_end)
+
+ def send_data(self, stream_id, data, is_end=False):
+ self._h3_conn.send_data(stream_id, data, is_end)
+
+ def _get_timer_values(self, closed_is_special=True):
+ now = time.time()
+ expiration = self._connection.get_timer()
+ if expiration is None:
+ expiration = now + 3600 # arbitrary "big" value
+ interval = max(expiration - now, 0)
+ if self._closed and closed_is_special:
+ # lower sleep interval to avoid a race in the closing process
+ # which can lead to higher latency closing due to sleeping when
+ # we have events.
+ interval = min(interval, 0.05)
+ return (expiration, interval)
+
+ def _handle_timer(self, expiration):
+ now = time.time()
+ if expiration <= now:
+ self._connection.handle_timer(now)
+
+
+class AsyncQuicConnection(BaseQuicConnection):
+ async def make_stream(self, timeout: Optional[float] = None) -> Any:
+ pass
+
+
+class BaseQuicManager:
+ def __init__(
+ self, conf, verify_mode, connection_factory, server_name=None, h3=False
+ ):
+ self._connections = {}
+ self._connection_factory = connection_factory
+ self._session_tickets = {}
+ self._tokens = {}
+ self._h3 = h3
+ if conf is None:
+ verify_path = None
+ if isinstance(verify_mode, str):
+ verify_path = verify_mode
+ verify_mode = True
+ if h3:
+ alpn_protocols = ["h3"]
+ else:
+ alpn_protocols = ["doq", "doq-i03"]
+ conf = aioquic.quic.configuration.QuicConfiguration(
+ alpn_protocols=alpn_protocols,
+ verify_mode=verify_mode,
+ server_name=server_name,
+ )
+ if verify_path is not None:
+ conf.load_verify_locations(verify_path)
+ self._conf = conf
+
+ def _connect(
+ self,
+ address,
+ port=853,
+ source=None,
+ source_port=0,
+ want_session_ticket=True,
+ want_token=True,
+ ):
+ connection = self._connections.get((address, port))
+ if connection is not None:
+ return (connection, False)
+ conf = self._conf
+ if want_session_ticket:
+ try:
+ session_ticket = self._session_tickets.pop((address, port))
+ # We found a session ticket, so make a configuration that uses it.
+ conf = copy.copy(conf)
+ conf.session_ticket = session_ticket
+ except KeyError:
+ # No session ticket.
+ pass
+ # Whether or not we found a session ticket, we want a handler to save
+ # one.
+ session_ticket_handler = functools.partial(
+ self.save_session_ticket, address, port
+ )
+ else:
+ session_ticket_handler = None
+ if want_token:
+ try:
+ token = self._tokens.pop((address, port))
+ # We found a token, so make a configuration that uses it.
+ conf = copy.copy(conf)
+ conf.token = token
+ except KeyError:
+ # No token
+ pass
+ # Whether or not we found a token, we want a handler to save # one.
+ token_handler = functools.partial(self.save_token, address, port)
+ else:
+ token_handler = None
+
+ qconn = aioquic.quic.connection.QuicConnection(
+ configuration=conf,
+ session_ticket_handler=session_ticket_handler,
+ token_handler=token_handler,
+ )
+ lladdress = dns.inet.low_level_address_tuple((address, port))
+ qconn.connect(lladdress, time.time())
+ connection = self._connection_factory(
+ qconn, address, port, source, source_port, self
+ )
+ self._connections[(address, port)] = connection
+ return (connection, True)
+
+ def closed(self, address, port):
+ try:
+ del self._connections[(address, port)]
+ except KeyError:
+ pass
+
+ def is_h3(self):
+ return self._h3
+
+ def save_session_ticket(self, address, port, ticket):
+ # We rely on dictionaries keys() being in insertion order here. We
+ # can't just popitem() as that would be LIFO which is the opposite of
+ # what we want.
+ l = len(self._session_tickets)
+ if l >= MAX_SESSION_TICKETS:
+ keys_to_delete = list(self._session_tickets.keys())[0:SESSIONS_TO_DELETE]
+ for key in keys_to_delete:
+ del self._session_tickets[key]
+ self._session_tickets[(address, port)] = ticket
+
+ def save_token(self, address, port, token):
+ # We rely on dictionaries keys() being in insertion order here. We
+ # can't just popitem() as that would be LIFO which is the opposite of
+ # what we want.
+ l = len(self._tokens)
+ if l >= MAX_SESSION_TICKETS:
+ keys_to_delete = list(self._tokens.keys())[0:SESSIONS_TO_DELETE]
+ for key in keys_to_delete:
+ del self._tokens[key]
+ self._tokens[(address, port)] = token
+
+
+class AsyncQuicManager(BaseQuicManager):
+ def connect(self, address, port=853, source=None, source_port=0):
+ raise NotImplementedError
diff --git a/.venv/lib/python3.12/site-packages/dns/quic/_sync.py b/.venv/lib/python3.12/site-packages/dns/quic/_sync.py
new file mode 100644
index 00000000..473d1f48
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/quic/_sync.py
@@ -0,0 +1,295 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import selectors
+import socket
+import ssl
+import struct
+import threading
+import time
+
+import aioquic.quic.configuration # type: ignore
+import aioquic.quic.connection # type: ignore
+import aioquic.quic.events # type: ignore
+
+import dns.exception
+import dns.inet
+from dns.quic._common import (
+ QUIC_MAX_DATAGRAM,
+ BaseQuicConnection,
+ BaseQuicManager,
+ BaseQuicStream,
+ UnexpectedEOF,
+)
+
+# Function used to create a socket. Can be overridden if needed in special
+# situations.
+socket_factory = socket.socket
+
+
+class SyncQuicStream(BaseQuicStream):
+ def __init__(self, connection, stream_id):
+ super().__init__(connection, stream_id)
+ self._wake_up = threading.Condition()
+ self._lock = threading.Lock()
+
+ def wait_for(self, amount, expiration):
+ while True:
+ timeout = self._timeout_from_expiration(expiration)
+ with self._lock:
+ if self._buffer.have(amount):
+ return
+ self._expecting = amount
+ with self._wake_up:
+ if not self._wake_up.wait(timeout):
+ raise dns.exception.Timeout
+ self._expecting = 0
+
+ def wait_for_end(self, expiration):
+ while True:
+ timeout = self._timeout_from_expiration(expiration)
+ with self._lock:
+ if self._buffer.seen_end():
+ return
+ with self._wake_up:
+ if not self._wake_up.wait(timeout):
+ raise dns.exception.Timeout
+
+ def receive(self, timeout=None):
+ expiration = self._expiration_from_timeout(timeout)
+ if self._connection.is_h3():
+ self.wait_for_end(expiration)
+ with self._lock:
+ return self._buffer.get_all()
+ else:
+ self.wait_for(2, expiration)
+ with self._lock:
+ (size,) = struct.unpack("!H", self._buffer.get(2))
+ self.wait_for(size, expiration)
+ with self._lock:
+ return self._buffer.get(size)
+
+ def send(self, datagram, is_end=False):
+ data = self._encapsulate(datagram)
+ self._connection.write(self._stream_id, data, is_end)
+
+ def _add_input(self, data, is_end):
+ if self._common_add_input(data, is_end):
+ with self._wake_up:
+ self._wake_up.notify()
+
+ def close(self):
+ with self._lock:
+ self._close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.close()
+ with self._wake_up:
+ self._wake_up.notify()
+ return False
+
+
+class SyncQuicConnection(BaseQuicConnection):
+ def __init__(self, connection, address, port, source, source_port, manager):
+ super().__init__(connection, address, port, source, source_port, manager)
+ self._socket = socket_factory(self._af, socket.SOCK_DGRAM, 0)
+ if self._source is not None:
+ try:
+ self._socket.bind(
+ dns.inet.low_level_address_tuple(self._source, self._af)
+ )
+ except Exception:
+ self._socket.close()
+ raise
+ self._socket.connect(self._peer)
+ (self._send_wakeup, self._receive_wakeup) = socket.socketpair()
+ self._receive_wakeup.setblocking(False)
+ self._socket.setblocking(False)
+ self._handshake_complete = threading.Event()
+ self._worker_thread = None
+ self._lock = threading.Lock()
+
+ def _read(self):
+ count = 0
+ while count < 10:
+ count += 1
+ try:
+ datagram = self._socket.recv(QUIC_MAX_DATAGRAM)
+ except BlockingIOError:
+ return
+ with self._lock:
+ self._connection.receive_datagram(datagram, self._peer, time.time())
+
+ def _drain_wakeup(self):
+ while True:
+ try:
+ self._receive_wakeup.recv(32)
+ except BlockingIOError:
+ return
+
+ def _worker(self):
+ try:
+ sel = selectors.DefaultSelector()
+ sel.register(self._socket, selectors.EVENT_READ, self._read)
+ sel.register(self._receive_wakeup, selectors.EVENT_READ, self._drain_wakeup)
+ while not self._done:
+ (expiration, interval) = self._get_timer_values(False)
+ items = sel.select(interval)
+ for key, _ in items:
+ key.data()
+ with self._lock:
+ self._handle_timer(expiration)
+ self._handle_events()
+ with self._lock:
+ datagrams = self._connection.datagrams_to_send(time.time())
+ for datagram, _ in datagrams:
+ try:
+ self._socket.send(datagram)
+ except BlockingIOError:
+ # we let QUIC handle any lossage
+ pass
+ finally:
+ with self._lock:
+ self._done = True
+ self._socket.close()
+ # Ensure anyone waiting for this gets woken up.
+ self._handshake_complete.set()
+
+ def _handle_events(self):
+ while True:
+ with self._lock:
+ event = self._connection.next_event()
+ if event is None:
+ return
+ if isinstance(event, aioquic.quic.events.StreamDataReceived):
+ if self.is_h3():
+ h3_events = self._h3_conn.handle_event(event)
+ for h3_event in h3_events:
+ if isinstance(h3_event, aioquic.h3.events.HeadersReceived):
+ with self._lock:
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ if stream._headers is None:
+ stream._headers = h3_event.headers
+ elif stream._trailers is None:
+ stream._trailers = h3_event.headers
+ if h3_event.stream_ended:
+ stream._add_input(b"", True)
+ elif isinstance(h3_event, aioquic.h3.events.DataReceived):
+ with self._lock:
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ stream._add_input(h3_event.data, h3_event.stream_ended)
+ else:
+ with self._lock:
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ stream._add_input(event.data, event.end_stream)
+ elif isinstance(event, aioquic.quic.events.HandshakeCompleted):
+ self._handshake_complete.set()
+ elif isinstance(event, aioquic.quic.events.ConnectionTerminated):
+ with self._lock:
+ self._done = True
+ elif isinstance(event, aioquic.quic.events.StreamReset):
+ with self._lock:
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ stream._add_input(b"", True)
+
+ def write(self, stream, data, is_end=False):
+ with self._lock:
+ self._connection.send_stream_data(stream, data, is_end)
+ self._send_wakeup.send(b"\x01")
+
+ def send_headers(self, stream_id, headers, is_end=False):
+ with self._lock:
+ super().send_headers(stream_id, headers, is_end)
+ if is_end:
+ self._send_wakeup.send(b"\x01")
+
+ def send_data(self, stream_id, data, is_end=False):
+ with self._lock:
+ super().send_data(stream_id, data, is_end)
+ if is_end:
+ self._send_wakeup.send(b"\x01")
+
+ def run(self):
+ if self._closed:
+ return
+ self._worker_thread = threading.Thread(target=self._worker)
+ self._worker_thread.start()
+
+ def make_stream(self, timeout=None):
+ if not self._handshake_complete.wait(timeout):
+ raise dns.exception.Timeout
+ with self._lock:
+ if self._done:
+ raise UnexpectedEOF
+ stream_id = self._connection.get_next_available_stream_id(False)
+ stream = SyncQuicStream(self, stream_id)
+ self._streams[stream_id] = stream
+ return stream
+
+ def close_stream(self, stream_id):
+ with self._lock:
+ super().close_stream(stream_id)
+
+ def close(self):
+ with self._lock:
+ if self._closed:
+ return
+ self._manager.closed(self._peer[0], self._peer[1])
+ self._closed = True
+ self._connection.close()
+ self._send_wakeup.send(b"\x01")
+ self._worker_thread.join()
+
+
+class SyncQuicManager(BaseQuicManager):
+ def __init__(
+ self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None, h3=False
+ ):
+ super().__init__(conf, verify_mode, SyncQuicConnection, server_name, h3)
+ self._lock = threading.Lock()
+
+ def connect(
+ self,
+ address,
+ port=853,
+ source=None,
+ source_port=0,
+ want_session_ticket=True,
+ want_token=True,
+ ):
+ with self._lock:
+ (connection, start) = self._connect(
+ address, port, source, source_port, want_session_ticket, want_token
+ )
+ if start:
+ connection.run()
+ return connection
+
+ def closed(self, address, port):
+ with self._lock:
+ super().closed(address, port)
+
+ def save_session_ticket(self, address, port, ticket):
+ with self._lock:
+ super().save_session_ticket(address, port, ticket)
+
+ def save_token(self, address, port, token):
+ with self._lock:
+ super().save_token(address, port, token)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ # Copy the iterator into a list as exiting things will mutate the connections
+ # table.
+ connections = list(self._connections.values())
+ for connection in connections:
+ connection.close()
+ return False
diff --git a/.venv/lib/python3.12/site-packages/dns/quic/_trio.py b/.venv/lib/python3.12/site-packages/dns/quic/_trio.py
new file mode 100644
index 00000000..ae79f369
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/quic/_trio.py
@@ -0,0 +1,246 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import socket
+import ssl
+import struct
+import time
+
+import aioquic.quic.configuration # type: ignore
+import aioquic.quic.connection # type: ignore
+import aioquic.quic.events # type: ignore
+import trio
+
+import dns.exception
+import dns.inet
+from dns._asyncbackend import NullContext
+from dns.quic._common import (
+ QUIC_MAX_DATAGRAM,
+ AsyncQuicConnection,
+ AsyncQuicManager,
+ BaseQuicStream,
+ UnexpectedEOF,
+)
+
+
+class TrioQuicStream(BaseQuicStream):
+ def __init__(self, connection, stream_id):
+ super().__init__(connection, stream_id)
+ self._wake_up = trio.Condition()
+
+ async def wait_for(self, amount):
+ while True:
+ if self._buffer.have(amount):
+ return
+ self._expecting = amount
+ async with self._wake_up:
+ await self._wake_up.wait()
+ self._expecting = 0
+
+ async def wait_for_end(self):
+ while True:
+ if self._buffer.seen_end():
+ return
+ async with self._wake_up:
+ await self._wake_up.wait()
+
+ async def receive(self, timeout=None):
+ if timeout is None:
+ context = NullContext(None)
+ else:
+ context = trio.move_on_after(timeout)
+ with context:
+ if self._connection.is_h3():
+ await self.wait_for_end()
+ return self._buffer.get_all()
+ else:
+ await self.wait_for(2)
+ (size,) = struct.unpack("!H", self._buffer.get(2))
+ await self.wait_for(size)
+ return self._buffer.get(size)
+ raise dns.exception.Timeout
+
+ async def send(self, datagram, is_end=False):
+ data = self._encapsulate(datagram)
+ await self._connection.write(self._stream_id, data, is_end)
+
+ async def _add_input(self, data, is_end):
+ if self._common_add_input(data, is_end):
+ async with self._wake_up:
+ self._wake_up.notify()
+
+ async def close(self):
+ self._close()
+
+ # Streams are async context managers
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ await self.close()
+ async with self._wake_up:
+ self._wake_up.notify()
+ return False
+
+
+class TrioQuicConnection(AsyncQuicConnection):
+ def __init__(self, connection, address, port, source, source_port, manager=None):
+ super().__init__(connection, address, port, source, source_port, manager)
+ self._socket = trio.socket.socket(self._af, socket.SOCK_DGRAM, 0)
+ self._handshake_complete = trio.Event()
+ self._run_done = trio.Event()
+ self._worker_scope = None
+ self._send_pending = False
+
+ async def _worker(self):
+ try:
+ if self._source:
+ await self._socket.bind(
+ dns.inet.low_level_address_tuple(self._source, self._af)
+ )
+ await self._socket.connect(self._peer)
+ while not self._done:
+ (expiration, interval) = self._get_timer_values(False)
+ if self._send_pending:
+ # Do not block forever if sends are pending. Even though we
+ # have a wake-up mechanism if we've already started the blocking
+ # read, the possibility of context switching in send means that
+ # more writes can happen while we have no wake up context, so
+ # we need self._send_pending to avoid (effectively) a "lost wakeup"
+ # race.
+ interval = 0.0
+ with trio.CancelScope(
+ deadline=trio.current_time() + interval
+ ) as self._worker_scope:
+ datagram = await self._socket.recv(QUIC_MAX_DATAGRAM)
+ self._connection.receive_datagram(datagram, self._peer, time.time())
+ self._worker_scope = None
+ self._handle_timer(expiration)
+ await self._handle_events()
+ # We clear this now, before sending anything, as sending can cause
+ # context switches that do more sends. We want to know if that
+ # happens so we don't block a long time on the recv() above.
+ self._send_pending = False
+ datagrams = self._connection.datagrams_to_send(time.time())
+ for datagram, _ in datagrams:
+ await self._socket.send(datagram)
+ finally:
+ self._done = True
+ self._socket.close()
+ self._handshake_complete.set()
+
+ async def _handle_events(self):
+ count = 0
+ while True:
+ event = self._connection.next_event()
+ if event is None:
+ return
+ if isinstance(event, aioquic.quic.events.StreamDataReceived):
+ if self.is_h3():
+ h3_events = self._h3_conn.handle_event(event)
+ for h3_event in h3_events:
+ if isinstance(h3_event, aioquic.h3.events.HeadersReceived):
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ if stream._headers is None:
+ stream._headers = h3_event.headers
+ elif stream._trailers is None:
+ stream._trailers = h3_event.headers
+ if h3_event.stream_ended:
+ await stream._add_input(b"", True)
+ elif isinstance(h3_event, aioquic.h3.events.DataReceived):
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ await stream._add_input(
+ h3_event.data, h3_event.stream_ended
+ )
+ else:
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ await stream._add_input(event.data, event.end_stream)
+ elif isinstance(event, aioquic.quic.events.HandshakeCompleted):
+ self._handshake_complete.set()
+ elif isinstance(event, aioquic.quic.events.ConnectionTerminated):
+ self._done = True
+ self._socket.close()
+ elif isinstance(event, aioquic.quic.events.StreamReset):
+ stream = self._streams.get(event.stream_id)
+ if stream:
+ await stream._add_input(b"", True)
+ count += 1
+ if count > 10:
+ # yield
+ count = 0
+ await trio.sleep(0)
+
+ async def write(self, stream, data, is_end=False):
+ self._connection.send_stream_data(stream, data, is_end)
+ self._send_pending = True
+ if self._worker_scope is not None:
+ self._worker_scope.cancel()
+
+ async def run(self):
+ if self._closed:
+ return
+ async with trio.open_nursery() as nursery:
+ nursery.start_soon(self._worker)
+ self._run_done.set()
+
+ async def make_stream(self, timeout=None):
+ if timeout is None:
+ context = NullContext(None)
+ else:
+ context = trio.move_on_after(timeout)
+ with context:
+ await self._handshake_complete.wait()
+ if self._done:
+ raise UnexpectedEOF
+ stream_id = self._connection.get_next_available_stream_id(False)
+ stream = TrioQuicStream(self, stream_id)
+ self._streams[stream_id] = stream
+ return stream
+ raise dns.exception.Timeout
+
+ async def close(self):
+ if not self._closed:
+ self._manager.closed(self._peer[0], self._peer[1])
+ self._closed = True
+ self._connection.close()
+ self._send_pending = True
+ if self._worker_scope is not None:
+ self._worker_scope.cancel()
+ await self._run_done.wait()
+
+
+class TrioQuicManager(AsyncQuicManager):
+ def __init__(
+ self,
+ nursery,
+ conf=None,
+ verify_mode=ssl.CERT_REQUIRED,
+ server_name=None,
+ h3=False,
+ ):
+ super().__init__(conf, verify_mode, TrioQuicConnection, server_name, h3)
+ self._nursery = nursery
+
+ def connect(
+ self, address, port=853, source=None, source_port=0, want_session_ticket=True
+ ):
+ (connection, start) = self._connect(
+ address, port, source, source_port, want_session_ticket
+ )
+ if start:
+ self._nursery.start_soon(connection.run)
+ return connection
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ # Copy the iterator into a list as exiting things will mutate the connections
+ # table.
+ connections = list(self._connections.values())
+ for connection in connections:
+ await connection.close()
+ return False
diff --git a/.venv/lib/python3.12/site-packages/dns/rcode.py b/.venv/lib/python3.12/site-packages/dns/rcode.py
new file mode 100644
index 00000000..8e6386f8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rcode.py
@@ -0,0 +1,168 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Result Codes."""
+
+from typing import Tuple
+
+import dns.enum
+import dns.exception
+
+
+class Rcode(dns.enum.IntEnum):
+ #: No error
+ NOERROR = 0
+ #: Format error
+ FORMERR = 1
+ #: Server failure
+ SERVFAIL = 2
+ #: Name does not exist ("Name Error" in RFC 1025 terminology).
+ NXDOMAIN = 3
+ #: Not implemented
+ NOTIMP = 4
+ #: Refused
+ REFUSED = 5
+ #: Name exists.
+ YXDOMAIN = 6
+ #: RRset exists.
+ YXRRSET = 7
+ #: RRset does not exist.
+ NXRRSET = 8
+ #: Not authoritative.
+ NOTAUTH = 9
+ #: Name not in zone.
+ NOTZONE = 10
+ #: DSO-TYPE Not Implemented
+ DSOTYPENI = 11
+ #: Bad EDNS version.
+ BADVERS = 16
+ #: TSIG Signature Failure
+ BADSIG = 16
+ #: Key not recognized.
+ BADKEY = 17
+ #: Signature out of time window.
+ BADTIME = 18
+ #: Bad TKEY Mode.
+ BADMODE = 19
+ #: Duplicate key name.
+ BADNAME = 20
+ #: Algorithm not supported.
+ BADALG = 21
+ #: Bad Truncation
+ BADTRUNC = 22
+ #: Bad/missing Server Cookie
+ BADCOOKIE = 23
+
+ @classmethod
+ def _maximum(cls):
+ return 4095
+
+ @classmethod
+ def _unknown_exception_class(cls):
+ return UnknownRcode
+
+
+class UnknownRcode(dns.exception.DNSException):
+ """A DNS rcode is unknown."""
+
+
+def from_text(text: str) -> Rcode:
+ """Convert text into an rcode.
+
+ *text*, a ``str``, the textual rcode or an integer in textual form.
+
+ Raises ``dns.rcode.UnknownRcode`` if the rcode mnemonic is unknown.
+
+ Returns a ``dns.rcode.Rcode``.
+ """
+
+ return Rcode.from_text(text)
+
+
+def from_flags(flags: int, ednsflags: int) -> Rcode:
+ """Return the rcode value encoded by flags and ednsflags.
+
+ *flags*, an ``int``, the DNS flags field.
+
+ *ednsflags*, an ``int``, the EDNS flags field.
+
+ Raises ``ValueError`` if rcode is < 0 or > 4095
+
+ Returns a ``dns.rcode.Rcode``.
+ """
+
+ value = (flags & 0x000F) | ((ednsflags >> 20) & 0xFF0)
+ return Rcode.make(value)
+
+
+def to_flags(value: Rcode) -> Tuple[int, int]:
+ """Return a (flags, ednsflags) tuple which encodes the rcode.
+
+ *value*, a ``dns.rcode.Rcode``, the rcode.
+
+ Raises ``ValueError`` if rcode is < 0 or > 4095.
+
+ Returns an ``(int, int)`` tuple.
+ """
+
+ if value < 0 or value > 4095:
+ raise ValueError("rcode must be >= 0 and <= 4095")
+ v = value & 0xF
+ ev = (value & 0xFF0) << 20
+ return (v, ev)
+
+
+def to_text(value: Rcode, tsig: bool = False) -> str:
+ """Convert rcode into text.
+
+ *value*, a ``dns.rcode.Rcode``, the rcode.
+
+ Raises ``ValueError`` if rcode is < 0 or > 4095.
+
+ Returns a ``str``.
+ """
+
+ if tsig and value == Rcode.BADVERS:
+ return "BADSIG"
+ return Rcode.to_text(value)
+
+
+### BEGIN generated Rcode constants
+
+NOERROR = Rcode.NOERROR
+FORMERR = Rcode.FORMERR
+SERVFAIL = Rcode.SERVFAIL
+NXDOMAIN = Rcode.NXDOMAIN
+NOTIMP = Rcode.NOTIMP
+REFUSED = Rcode.REFUSED
+YXDOMAIN = Rcode.YXDOMAIN
+YXRRSET = Rcode.YXRRSET
+NXRRSET = Rcode.NXRRSET
+NOTAUTH = Rcode.NOTAUTH
+NOTZONE = Rcode.NOTZONE
+DSOTYPENI = Rcode.DSOTYPENI
+BADVERS = Rcode.BADVERS
+BADSIG = Rcode.BADSIG
+BADKEY = Rcode.BADKEY
+BADTIME = Rcode.BADTIME
+BADMODE = Rcode.BADMODE
+BADNAME = Rcode.BADNAME
+BADALG = Rcode.BADALG
+BADTRUNC = Rcode.BADTRUNC
+BADCOOKIE = Rcode.BADCOOKIE
+
+### END generated Rcode constants
diff --git a/.venv/lib/python3.12/site-packages/dns/rdata.py b/.venv/lib/python3.12/site-packages/dns/rdata.py
new file mode 100644
index 00000000..8099c26a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdata.py
@@ -0,0 +1,911 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS rdata."""
+
+import base64
+import binascii
+import inspect
+import io
+import itertools
+import random
+from importlib import import_module
+from typing import Any, Dict, Optional, Tuple, Union
+
+import dns.exception
+import dns.immutable
+import dns.ipv4
+import dns.ipv6
+import dns.name
+import dns.rdataclass
+import dns.rdatatype
+import dns.tokenizer
+import dns.ttl
+import dns.wire
+
+_chunksize = 32
+
+# We currently allow comparisons for rdata with relative names for backwards
+# compatibility, but in the future we will not, as these kinds of comparisons
+# can lead to subtle bugs if code is not carefully written.
+#
+# This switch allows the future behavior to be turned on so code can be
+# tested with it.
+_allow_relative_comparisons = True
+
+
+class NoRelativeRdataOrdering(dns.exception.DNSException):
+ """An attempt was made to do an ordered comparison of one or more
+ rdata with relative names. The only reliable way of sorting rdata
+ is to use non-relativized rdata.
+
+ """
+
+
+def _wordbreak(data, chunksize=_chunksize, separator=b" "):
+ """Break a binary string into chunks of chunksize characters separated by
+ a space.
+ """
+
+ if not chunksize:
+ return data.decode()
+ return separator.join(
+ [data[i : i + chunksize] for i in range(0, len(data), chunksize)]
+ ).decode()
+
+
+# pylint: disable=unused-argument
+
+
+def _hexify(data, chunksize=_chunksize, separator=b" ", **kw):
+ """Convert a binary string into its hex encoding, broken up into chunks
+ of chunksize characters separated by a separator.
+ """
+
+ return _wordbreak(binascii.hexlify(data), chunksize, separator)
+
+
+def _base64ify(data, chunksize=_chunksize, separator=b" ", **kw):
+ """Convert a binary string into its base64 encoding, broken up into chunks
+ of chunksize characters separated by a separator.
+ """
+
+ return _wordbreak(base64.b64encode(data), chunksize, separator)
+
+
+# pylint: enable=unused-argument
+
+__escaped = b'"\\'
+
+
+def _escapify(qstring):
+ """Escape the characters in a quoted string which need it."""
+
+ if isinstance(qstring, str):
+ qstring = qstring.encode()
+ if not isinstance(qstring, bytearray):
+ qstring = bytearray(qstring)
+
+ text = ""
+ for c in qstring:
+ if c in __escaped:
+ text += "\\" + chr(c)
+ elif c >= 0x20 and c < 0x7F:
+ text += chr(c)
+ else:
+ text += "\\%03d" % c
+ return text
+
+
+def _truncate_bitmap(what):
+ """Determine the index of greatest byte that isn't all zeros, and
+ return the bitmap that contains all the bytes less than that index.
+ """
+
+ for i in range(len(what) - 1, -1, -1):
+ if what[i] != 0:
+ return what[0 : i + 1]
+ return what[0:1]
+
+
+# So we don't have to edit all the rdata classes...
+_constify = dns.immutable.constify
+
+
+@dns.immutable.immutable
+class Rdata:
+ """Base class for all DNS rdata types."""
+
+ __slots__ = ["rdclass", "rdtype", "rdcomment"]
+
+ def __init__(self, rdclass, rdtype):
+ """Initialize an rdata.
+
+ *rdclass*, an ``int`` is the rdataclass of the Rdata.
+
+ *rdtype*, an ``int`` is the rdatatype of the Rdata.
+ """
+
+ self.rdclass = self._as_rdataclass(rdclass)
+ self.rdtype = self._as_rdatatype(rdtype)
+ self.rdcomment = None
+
+ def _get_all_slots(self):
+ return itertools.chain.from_iterable(
+ getattr(cls, "__slots__", []) for cls in self.__class__.__mro__
+ )
+
+ def __getstate__(self):
+ # We used to try to do a tuple of all slots here, but it
+ # doesn't work as self._all_slots isn't available at
+ # __setstate__() time. Before that we tried to store a tuple
+ # of __slots__, but that didn't work as it didn't store the
+ # slots defined by ancestors. This older way didn't fail
+ # outright, but ended up with partially broken objects, e.g.
+ # if you unpickled an A RR it wouldn't have rdclass and rdtype
+ # attributes, and would compare badly.
+ state = {}
+ for slot in self._get_all_slots():
+ state[slot] = getattr(self, slot)
+ return state
+
+ def __setstate__(self, state):
+ for slot, val in state.items():
+ object.__setattr__(self, slot, val)
+ if not hasattr(self, "rdcomment"):
+ # Pickled rdata from 2.0.x might not have a rdcomment, so add
+ # it if needed.
+ object.__setattr__(self, "rdcomment", None)
+
+ def covers(self) -> dns.rdatatype.RdataType:
+ """Return the type a Rdata covers.
+
+ DNS SIG/RRSIG rdatas apply to a specific type; this type is
+ returned by the covers() function. If the rdata type is not
+ SIG or RRSIG, dns.rdatatype.NONE is returned. This is useful when
+ creating rdatasets, allowing the rdataset to contain only RRSIGs
+ of a particular type, e.g. RRSIG(NS).
+
+ Returns a ``dns.rdatatype.RdataType``.
+ """
+
+ return dns.rdatatype.NONE
+
+ def extended_rdatatype(self) -> int:
+ """Return a 32-bit type value, the least significant 16 bits of
+ which are the ordinary DNS type, and the upper 16 bits of which are
+ the "covered" type, if any.
+
+ Returns an ``int``.
+ """
+
+ return self.covers() << 16 | self.rdtype
+
+ def to_text(
+ self,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ **kw: Dict[str, Any],
+ ) -> str:
+ """Convert an rdata to text format.
+
+ Returns a ``str``.
+ """
+
+ raise NotImplementedError # pragma: no cover
+
+ def _to_wire(
+ self,
+ file: Optional[Any],
+ compress: Optional[dns.name.CompressType] = None,
+ origin: Optional[dns.name.Name] = None,
+ canonicalize: bool = False,
+ ) -> None:
+ raise NotImplementedError # pragma: no cover
+
+ def to_wire(
+ self,
+ file: Optional[Any] = None,
+ compress: Optional[dns.name.CompressType] = None,
+ origin: Optional[dns.name.Name] = None,
+ canonicalize: bool = False,
+ ) -> Optional[bytes]:
+ """Convert an rdata to wire format.
+
+ Returns a ``bytes`` if no output file was specified, or ``None`` otherwise.
+ """
+
+ if file:
+ # We call _to_wire() and then return None explicitly instead of
+ # of just returning the None from _to_wire() as mypy's func-returns-value
+ # unhelpfully errors out with "error: "_to_wire" of "Rdata" does not return
+ # a value (it only ever returns None)"
+ self._to_wire(file, compress, origin, canonicalize)
+ return None
+ else:
+ f = io.BytesIO()
+ self._to_wire(f, compress, origin, canonicalize)
+ return f.getvalue()
+
+ def to_generic(
+ self, origin: Optional[dns.name.Name] = None
+ ) -> "dns.rdata.GenericRdata":
+ """Creates a dns.rdata.GenericRdata equivalent of this rdata.
+
+ Returns a ``dns.rdata.GenericRdata``.
+ """
+ return dns.rdata.GenericRdata(
+ self.rdclass, self.rdtype, self.to_wire(origin=origin)
+ )
+
+ def to_digestable(self, origin: Optional[dns.name.Name] = None) -> bytes:
+ """Convert rdata to a format suitable for digesting in hashes. This
+ is also the DNSSEC canonical form.
+
+ Returns a ``bytes``.
+ """
+ wire = self.to_wire(origin=origin, canonicalize=True)
+ assert wire is not None # for mypy
+ return wire
+
+ def __repr__(self):
+ covers = self.covers()
+ if covers == dns.rdatatype.NONE:
+ ctext = ""
+ else:
+ ctext = "(" + dns.rdatatype.to_text(covers) + ")"
+ return (
+ "<DNS "
+ + dns.rdataclass.to_text(self.rdclass)
+ + " "
+ + dns.rdatatype.to_text(self.rdtype)
+ + ctext
+ + " rdata: "
+ + str(self)
+ + ">"
+ )
+
+ def __str__(self):
+ return self.to_text()
+
+ def _cmp(self, other):
+ """Compare an rdata with another rdata of the same rdtype and
+ rdclass.
+
+ For rdata with only absolute names:
+ Return < 0 if self < other in the DNSSEC ordering, 0 if self
+ == other, and > 0 if self > other.
+ For rdata with at least one relative names:
+ The rdata sorts before any rdata with only absolute names.
+ When compared with another relative rdata, all names are
+ made absolute as if they were relative to the root, as the
+ proper origin is not available. While this creates a stable
+ ordering, it is NOT guaranteed to be the DNSSEC ordering.
+ In the future, all ordering comparisons for rdata with
+ relative names will be disallowed.
+ """
+ try:
+ our = self.to_digestable()
+ our_relative = False
+ except dns.name.NeedAbsoluteNameOrOrigin:
+ if _allow_relative_comparisons:
+ our = self.to_digestable(dns.name.root)
+ our_relative = True
+ try:
+ their = other.to_digestable()
+ their_relative = False
+ except dns.name.NeedAbsoluteNameOrOrigin:
+ if _allow_relative_comparisons:
+ their = other.to_digestable(dns.name.root)
+ their_relative = True
+ if _allow_relative_comparisons:
+ if our_relative != their_relative:
+ # For the purpose of comparison, all rdata with at least one
+ # relative name is less than an rdata with only absolute names.
+ if our_relative:
+ return -1
+ else:
+ return 1
+ elif our_relative or their_relative:
+ raise NoRelativeRdataOrdering
+ if our == their:
+ return 0
+ elif our > their:
+ return 1
+ else:
+ return -1
+
+ def __eq__(self, other):
+ if not isinstance(other, Rdata):
+ return False
+ if self.rdclass != other.rdclass or self.rdtype != other.rdtype:
+ return False
+ our_relative = False
+ their_relative = False
+ try:
+ our = self.to_digestable()
+ except dns.name.NeedAbsoluteNameOrOrigin:
+ our = self.to_digestable(dns.name.root)
+ our_relative = True
+ try:
+ their = other.to_digestable()
+ except dns.name.NeedAbsoluteNameOrOrigin:
+ their = other.to_digestable(dns.name.root)
+ their_relative = True
+ if our_relative != their_relative:
+ return False
+ return our == their
+
+ def __ne__(self, other):
+ if not isinstance(other, Rdata):
+ return True
+ if self.rdclass != other.rdclass or self.rdtype != other.rdtype:
+ return True
+ return not self.__eq__(other)
+
+ def __lt__(self, other):
+ if (
+ not isinstance(other, Rdata)
+ or self.rdclass != other.rdclass
+ or self.rdtype != other.rdtype
+ ):
+ return NotImplemented
+ return self._cmp(other) < 0
+
+ def __le__(self, other):
+ if (
+ not isinstance(other, Rdata)
+ or self.rdclass != other.rdclass
+ or self.rdtype != other.rdtype
+ ):
+ return NotImplemented
+ return self._cmp(other) <= 0
+
+ def __ge__(self, other):
+ if (
+ not isinstance(other, Rdata)
+ or self.rdclass != other.rdclass
+ or self.rdtype != other.rdtype
+ ):
+ return NotImplemented
+ return self._cmp(other) >= 0
+
+ def __gt__(self, other):
+ if (
+ not isinstance(other, Rdata)
+ or self.rdclass != other.rdclass
+ or self.rdtype != other.rdtype
+ ):
+ return NotImplemented
+ return self._cmp(other) > 0
+
+ def __hash__(self):
+ return hash(self.to_digestable(dns.name.root))
+
+ @classmethod
+ def from_text(
+ cls,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ tok: dns.tokenizer.Tokenizer,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ relativize_to: Optional[dns.name.Name] = None,
+ ) -> "Rdata":
+ raise NotImplementedError # pragma: no cover
+
+ @classmethod
+ def from_wire_parser(
+ cls,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ parser: dns.wire.Parser,
+ origin: Optional[dns.name.Name] = None,
+ ) -> "Rdata":
+ raise NotImplementedError # pragma: no cover
+
+ def replace(self, **kwargs: Any) -> "Rdata":
+ """
+ Create a new Rdata instance based on the instance replace was
+ invoked on. It is possible to pass different parameters to
+ override the corresponding properties of the base Rdata.
+
+ Any field specific to the Rdata type can be replaced, but the
+ *rdtype* and *rdclass* fields cannot.
+
+ Returns an instance of the same Rdata subclass as *self*.
+ """
+
+ # Get the constructor parameters.
+ parameters = inspect.signature(self.__init__).parameters # type: ignore
+
+ # Ensure that all of the arguments correspond to valid fields.
+ # Don't allow rdclass or rdtype to be changed, though.
+ for key in kwargs:
+ if key == "rdcomment":
+ continue
+ if key not in parameters:
+ raise AttributeError(
+ f"'{self.__class__.__name__}' object has no attribute '{key}'"
+ )
+ if key in ("rdclass", "rdtype"):
+ raise AttributeError(
+ f"Cannot overwrite '{self.__class__.__name__}' attribute '{key}'"
+ )
+
+ # Construct the parameter list. For each field, use the value in
+ # kwargs if present, and the current value otherwise.
+ args = (kwargs.get(key, getattr(self, key)) for key in parameters)
+
+ # Create, validate, and return the new object.
+ rd = self.__class__(*args)
+ # The comment is not set in the constructor, so give it special
+ # handling.
+ rdcomment = kwargs.get("rdcomment", self.rdcomment)
+ if rdcomment is not None:
+ object.__setattr__(rd, "rdcomment", rdcomment)
+ return rd
+
+ # Type checking and conversion helpers. These are class methods as
+ # they don't touch object state and may be useful to others.
+
+ @classmethod
+ def _as_rdataclass(cls, value):
+ return dns.rdataclass.RdataClass.make(value)
+
+ @classmethod
+ def _as_rdatatype(cls, value):
+ return dns.rdatatype.RdataType.make(value)
+
+ @classmethod
+ def _as_bytes(
+ cls,
+ value: Any,
+ encode: bool = False,
+ max_length: Optional[int] = None,
+ empty_ok: bool = True,
+ ) -> bytes:
+ if encode and isinstance(value, str):
+ bvalue = value.encode()
+ elif isinstance(value, bytearray):
+ bvalue = bytes(value)
+ elif isinstance(value, bytes):
+ bvalue = value
+ else:
+ raise ValueError("not bytes")
+ if max_length is not None and len(bvalue) > max_length:
+ raise ValueError("too long")
+ if not empty_ok and len(bvalue) == 0:
+ raise ValueError("empty bytes not allowed")
+ return bvalue
+
+ @classmethod
+ def _as_name(cls, value):
+ # Note that proper name conversion (e.g. with origin and IDNA
+ # awareness) is expected to be done via from_text. This is just
+ # a simple thing for people invoking the constructor directly.
+ if isinstance(value, str):
+ return dns.name.from_text(value)
+ elif not isinstance(value, dns.name.Name):
+ raise ValueError("not a name")
+ return value
+
+ @classmethod
+ def _as_uint8(cls, value):
+ if not isinstance(value, int):
+ raise ValueError("not an integer")
+ if value < 0 or value > 255:
+ raise ValueError("not a uint8")
+ return value
+
+ @classmethod
+ def _as_uint16(cls, value):
+ if not isinstance(value, int):
+ raise ValueError("not an integer")
+ if value < 0 or value > 65535:
+ raise ValueError("not a uint16")
+ return value
+
+ @classmethod
+ def _as_uint32(cls, value):
+ if not isinstance(value, int):
+ raise ValueError("not an integer")
+ if value < 0 or value > 4294967295:
+ raise ValueError("not a uint32")
+ return value
+
+ @classmethod
+ def _as_uint48(cls, value):
+ if not isinstance(value, int):
+ raise ValueError("not an integer")
+ if value < 0 or value > 281474976710655:
+ raise ValueError("not a uint48")
+ return value
+
+ @classmethod
+ def _as_int(cls, value, low=None, high=None):
+ if not isinstance(value, int):
+ raise ValueError("not an integer")
+ if low is not None and value < low:
+ raise ValueError("value too small")
+ if high is not None and value > high:
+ raise ValueError("value too large")
+ return value
+
+ @classmethod
+ def _as_ipv4_address(cls, value):
+ if isinstance(value, str):
+ return dns.ipv4.canonicalize(value)
+ elif isinstance(value, bytes):
+ return dns.ipv4.inet_ntoa(value)
+ else:
+ raise ValueError("not an IPv4 address")
+
+ @classmethod
+ def _as_ipv6_address(cls, value):
+ if isinstance(value, str):
+ return dns.ipv6.canonicalize(value)
+ elif isinstance(value, bytes):
+ return dns.ipv6.inet_ntoa(value)
+ else:
+ raise ValueError("not an IPv6 address")
+
+ @classmethod
+ def _as_bool(cls, value):
+ if isinstance(value, bool):
+ return value
+ else:
+ raise ValueError("not a boolean")
+
+ @classmethod
+ def _as_ttl(cls, value):
+ if isinstance(value, int):
+ return cls._as_int(value, 0, dns.ttl.MAX_TTL)
+ elif isinstance(value, str):
+ return dns.ttl.from_text(value)
+ else:
+ raise ValueError("not a TTL")
+
+ @classmethod
+ def _as_tuple(cls, value, as_value):
+ try:
+ # For user convenience, if value is a singleton of the list
+ # element type, wrap it in a tuple.
+ return (as_value(value),)
+ except Exception:
+ # Otherwise, check each element of the iterable *value*
+ # against *as_value*.
+ return tuple(as_value(v) for v in value)
+
+ # Processing order
+
+ @classmethod
+ def _processing_order(cls, iterable):
+ items = list(iterable)
+ random.shuffle(items)
+ return items
+
+
+@dns.immutable.immutable
+class GenericRdata(Rdata):
+ """Generic Rdata Class
+
+ This class is used for rdata types for which we have no better
+ implementation. It implements the DNS "unknown RRs" scheme.
+ """
+
+ __slots__ = ["data"]
+
+ def __init__(self, rdclass, rdtype, data):
+ super().__init__(rdclass, rdtype)
+ self.data = data
+
+ def to_text(
+ self,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ **kw: Dict[str, Any],
+ ) -> str:
+ return r"\# %d " % len(self.data) + _hexify(self.data, **kw)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ token = tok.get()
+ if not token.is_identifier() or token.value != r"\#":
+ raise dns.exception.SyntaxError(r"generic rdata does not start with \#")
+ length = tok.get_int()
+ hex = tok.concatenate_remaining_identifiers(True).encode()
+ data = binascii.unhexlify(hex)
+ if len(data) != length:
+ raise dns.exception.SyntaxError("generic rdata hex data has wrong length")
+ return cls(rdclass, rdtype, data)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(self.data)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ return cls(rdclass, rdtype, parser.get_remaining())
+
+
+_rdata_classes: Dict[Tuple[dns.rdataclass.RdataClass, dns.rdatatype.RdataType], Any] = (
+ {}
+)
+_module_prefix = "dns.rdtypes"
+_dynamic_load_allowed = True
+
+
+def get_rdata_class(rdclass, rdtype, use_generic=True):
+ cls = _rdata_classes.get((rdclass, rdtype))
+ if not cls:
+ cls = _rdata_classes.get((dns.rdatatype.ANY, rdtype))
+ if not cls and _dynamic_load_allowed:
+ rdclass_text = dns.rdataclass.to_text(rdclass)
+ rdtype_text = dns.rdatatype.to_text(rdtype)
+ rdtype_text = rdtype_text.replace("-", "_")
+ try:
+ mod = import_module(
+ ".".join([_module_prefix, rdclass_text, rdtype_text])
+ )
+ cls = getattr(mod, rdtype_text)
+ _rdata_classes[(rdclass, rdtype)] = cls
+ except ImportError:
+ try:
+ mod = import_module(".".join([_module_prefix, "ANY", rdtype_text]))
+ cls = getattr(mod, rdtype_text)
+ _rdata_classes[(dns.rdataclass.ANY, rdtype)] = cls
+ _rdata_classes[(rdclass, rdtype)] = cls
+ except ImportError:
+ pass
+ if not cls and use_generic:
+ cls = GenericRdata
+ _rdata_classes[(rdclass, rdtype)] = cls
+ return cls
+
+
+def load_all_types(disable_dynamic_load=True):
+ """Load all rdata types for which dnspython has a non-generic implementation.
+
+ Normally dnspython loads DNS rdatatype implementations on demand, but in some
+ specialized cases loading all types at an application-controlled time is preferred.
+
+ If *disable_dynamic_load*, a ``bool``, is ``True`` then dnspython will not attempt
+ to use its dynamic loading mechanism if an unknown type is subsequently encountered,
+ and will simply use the ``GenericRdata`` class.
+ """
+ # Load class IN and ANY types.
+ for rdtype in dns.rdatatype.RdataType:
+ get_rdata_class(dns.rdataclass.IN, rdtype, False)
+ # Load the one non-ANY implementation we have in CH. Everything
+ # else in CH is an ANY type, and we'll discover those on demand but won't
+ # have to import anything.
+ get_rdata_class(dns.rdataclass.CH, dns.rdatatype.A, False)
+ if disable_dynamic_load:
+ # Now disable dynamic loading so any subsequent unknown type immediately becomes
+ # GenericRdata without a load attempt.
+ global _dynamic_load_allowed
+ _dynamic_load_allowed = False
+
+
+def from_text(
+ rdclass: Union[dns.rdataclass.RdataClass, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ tok: Union[dns.tokenizer.Tokenizer, str],
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ relativize_to: Optional[dns.name.Name] = None,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+) -> Rdata:
+ """Build an rdata object from text format.
+
+ This function attempts to dynamically load a class which
+ implements the specified rdata class and type. If there is no
+ class-and-type-specific implementation, the GenericRdata class
+ is used.
+
+ Once a class is chosen, its from_text() class method is called
+ with the parameters to this function.
+
+ If *tok* is a ``str``, then a tokenizer is created and the string
+ is used as its input.
+
+ *rdclass*, a ``dns.rdataclass.RdataClass`` or ``str``, the rdataclass.
+
+ *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdatatype.
+
+ *tok*, a ``dns.tokenizer.Tokenizer`` or a ``str``.
+
+ *origin*, a ``dns.name.Name`` (or ``None``), the
+ origin to use for relative names.
+
+ *relativize*, a ``bool``. If true, name will be relativized.
+
+ *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use
+ when relativizing names. If not set, the *origin* value will be used.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder to use if a tokenizer needs to be created. If
+ ``None``, the default IDNA 2003 encoder/decoder is used. If a
+ tokenizer is not created, then the codec associated with the tokenizer
+ is the one that is used.
+
+ Returns an instance of the chosen Rdata subclass.
+
+ """
+ if isinstance(tok, str):
+ tok = dns.tokenizer.Tokenizer(tok, idna_codec=idna_codec)
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ cls = get_rdata_class(rdclass, rdtype)
+ with dns.exception.ExceptionWrapper(dns.exception.SyntaxError):
+ rdata = None
+ if cls != GenericRdata:
+ # peek at first token
+ token = tok.get()
+ tok.unget(token)
+ if token.is_identifier() and token.value == r"\#":
+ #
+ # Known type using the generic syntax. Extract the
+ # wire form from the generic syntax, and then run
+ # from_wire on it.
+ #
+ grdata = GenericRdata.from_text(
+ rdclass, rdtype, tok, origin, relativize, relativize_to
+ )
+ rdata = from_wire(
+ rdclass, rdtype, grdata.data, 0, len(grdata.data), origin
+ )
+ #
+ # If this comparison isn't equal, then there must have been
+ # compressed names in the wire format, which is an error,
+ # there being no reasonable context to decompress with.
+ #
+ rwire = rdata.to_wire()
+ if rwire != grdata.data:
+ raise dns.exception.SyntaxError(
+ "compressed data in "
+ "generic syntax form "
+ "of known rdatatype"
+ )
+ if rdata is None:
+ rdata = cls.from_text(
+ rdclass, rdtype, tok, origin, relativize, relativize_to
+ )
+ token = tok.get_eol_as_token()
+ if token.comment is not None:
+ object.__setattr__(rdata, "rdcomment", token.comment)
+ return rdata
+
+
+def from_wire_parser(
+ rdclass: Union[dns.rdataclass.RdataClass, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ parser: dns.wire.Parser,
+ origin: Optional[dns.name.Name] = None,
+) -> Rdata:
+ """Build an rdata object from wire format
+
+ This function attempts to dynamically load a class which
+ implements the specified rdata class and type. If there is no
+ class-and-type-specific implementation, the GenericRdata class
+ is used.
+
+ Once a class is chosen, its from_wire() class method is called
+ with the parameters to this function.
+
+ *rdclass*, a ``dns.rdataclass.RdataClass`` or ``str``, the rdataclass.
+
+ *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdatatype.
+
+ *parser*, a ``dns.wire.Parser``, the parser, which should be
+ restricted to the rdata length.
+
+ *origin*, a ``dns.name.Name`` (or ``None``). If not ``None``,
+ then names will be relativized to this origin.
+
+ Returns an instance of the chosen Rdata subclass.
+ """
+
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ cls = get_rdata_class(rdclass, rdtype)
+ with dns.exception.ExceptionWrapper(dns.exception.FormError):
+ return cls.from_wire_parser(rdclass, rdtype, parser, origin)
+
+
+def from_wire(
+ rdclass: Union[dns.rdataclass.RdataClass, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ wire: bytes,
+ current: int,
+ rdlen: int,
+ origin: Optional[dns.name.Name] = None,
+) -> Rdata:
+ """Build an rdata object from wire format
+
+ This function attempts to dynamically load a class which
+ implements the specified rdata class and type. If there is no
+ class-and-type-specific implementation, the GenericRdata class
+ is used.
+
+ Once a class is chosen, its from_wire() class method is called
+ with the parameters to this function.
+
+ *rdclass*, an ``int``, the rdataclass.
+
+ *rdtype*, an ``int``, the rdatatype.
+
+ *wire*, a ``bytes``, the wire-format message.
+
+ *current*, an ``int``, the offset in wire of the beginning of
+ the rdata.
+
+ *rdlen*, an ``int``, the length of the wire-format rdata
+
+ *origin*, a ``dns.name.Name`` (or ``None``). If not ``None``,
+ then names will be relativized to this origin.
+
+ Returns an instance of the chosen Rdata subclass.
+ """
+ parser = dns.wire.Parser(wire, current)
+ with parser.restrict_to(rdlen):
+ return from_wire_parser(rdclass, rdtype, parser, origin)
+
+
+class RdatatypeExists(dns.exception.DNSException):
+ """DNS rdatatype already exists."""
+
+ supp_kwargs = {"rdclass", "rdtype"}
+ fmt = (
+ "The rdata type with class {rdclass:d} and rdtype {rdtype:d} "
+ + "already exists."
+ )
+
+
+def register_type(
+ implementation: Any,
+ rdtype: int,
+ rdtype_text: str,
+ is_singleton: bool = False,
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+) -> None:
+ """Dynamically register a module to handle an rdatatype.
+
+ *implementation*, a module implementing the type in the usual dnspython
+ way.
+
+ *rdtype*, an ``int``, the rdatatype to register.
+
+ *rdtype_text*, a ``str``, the textual form of the rdatatype.
+
+ *is_singleton*, a ``bool``, indicating if the type is a singleton (i.e.
+ RRsets of the type can have only one member.)
+
+ *rdclass*, the rdataclass of the type, or ``dns.rdataclass.ANY`` if
+ it applies to all classes.
+ """
+
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ existing_cls = get_rdata_class(rdclass, rdtype)
+ if existing_cls != GenericRdata or dns.rdatatype.is_metatype(rdtype):
+ raise RdatatypeExists(rdclass=rdclass, rdtype=rdtype)
+ _rdata_classes[(rdclass, rdtype)] = getattr(
+ implementation, rdtype_text.replace("-", "_")
+ )
+ dns.rdatatype.register_type(rdtype, rdtype_text, is_singleton)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdataclass.py b/.venv/lib/python3.12/site-packages/dns/rdataclass.py
new file mode 100644
index 00000000..89b85a79
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdataclass.py
@@ -0,0 +1,118 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Rdata Classes."""
+
+import dns.enum
+import dns.exception
+
+
+class RdataClass(dns.enum.IntEnum):
+ """DNS Rdata Class"""
+
+ RESERVED0 = 0
+ IN = 1
+ INTERNET = IN
+ CH = 3
+ CHAOS = CH
+ HS = 4
+ HESIOD = HS
+ NONE = 254
+ ANY = 255
+
+ @classmethod
+ def _maximum(cls):
+ return 65535
+
+ @classmethod
+ def _short_name(cls):
+ return "class"
+
+ @classmethod
+ def _prefix(cls):
+ return "CLASS"
+
+ @classmethod
+ def _unknown_exception_class(cls):
+ return UnknownRdataclass
+
+
+_metaclasses = {RdataClass.NONE, RdataClass.ANY}
+
+
+class UnknownRdataclass(dns.exception.DNSException):
+ """A DNS class is unknown."""
+
+
+def from_text(text: str) -> RdataClass:
+ """Convert text into a DNS rdata class value.
+
+ The input text can be a defined DNS RR class mnemonic or
+ instance of the DNS generic class syntax.
+
+ For example, "IN" and "CLASS1" will both result in a value of 1.
+
+ Raises ``dns.rdatatype.UnknownRdataclass`` if the class is unknown.
+
+ Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535.
+
+ Returns a ``dns.rdataclass.RdataClass``.
+ """
+
+ return RdataClass.from_text(text)
+
+
+def to_text(value: RdataClass) -> str:
+ """Convert a DNS rdata class value to text.
+
+ If the value has a known mnemonic, it will be used, otherwise the
+ DNS generic class syntax will be used.
+
+ Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535.
+
+ Returns a ``str``.
+ """
+
+ return RdataClass.to_text(value)
+
+
+def is_metaclass(rdclass: RdataClass) -> bool:
+ """True if the specified class is a metaclass.
+
+ The currently defined metaclasses are ANY and NONE.
+
+ *rdclass* is a ``dns.rdataclass.RdataClass``.
+ """
+
+ if rdclass in _metaclasses:
+ return True
+ return False
+
+
+### BEGIN generated RdataClass constants
+
+RESERVED0 = RdataClass.RESERVED0
+IN = RdataClass.IN
+INTERNET = RdataClass.INTERNET
+CH = RdataClass.CH
+CHAOS = RdataClass.CHAOS
+HS = RdataClass.HS
+HESIOD = RdataClass.HESIOD
+NONE = RdataClass.NONE
+ANY = RdataClass.ANY
+
+### END generated RdataClass constants
diff --git a/.venv/lib/python3.12/site-packages/dns/rdataset.py b/.venv/lib/python3.12/site-packages/dns/rdataset.py
new file mode 100644
index 00000000..39cab236
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdataset.py
@@ -0,0 +1,512 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS rdatasets (an rdataset is a set of rdatas of a given type and class)"""
+
+import io
+import random
+import struct
+from typing import Any, Collection, Dict, List, Optional, Union, cast
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdataclass
+import dns.rdatatype
+import dns.renderer
+import dns.set
+import dns.ttl
+
+# define SimpleSet here for backwards compatibility
+SimpleSet = dns.set.Set
+
+
+class DifferingCovers(dns.exception.DNSException):
+ """An attempt was made to add a DNS SIG/RRSIG whose covered type
+ is not the same as that of the other rdatas in the rdataset."""
+
+
+class IncompatibleTypes(dns.exception.DNSException):
+ """An attempt was made to add DNS RR data of an incompatible type."""
+
+
+class Rdataset(dns.set.Set):
+ """A DNS rdataset."""
+
+ __slots__ = ["rdclass", "rdtype", "covers", "ttl"]
+
+ def __init__(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ ttl: int = 0,
+ ):
+ """Create a new rdataset of the specified class and type.
+
+ *rdclass*, a ``dns.rdataclass.RdataClass``, the rdataclass.
+
+ *rdtype*, an ``dns.rdatatype.RdataType``, the rdatatype.
+
+ *covers*, an ``dns.rdatatype.RdataType``, the covered rdatatype.
+
+ *ttl*, an ``int``, the TTL.
+ """
+
+ super().__init__()
+ self.rdclass = rdclass
+ self.rdtype: dns.rdatatype.RdataType = rdtype
+ self.covers: dns.rdatatype.RdataType = covers
+ self.ttl = ttl
+
+ def _clone(self):
+ obj = super()._clone()
+ obj.rdclass = self.rdclass
+ obj.rdtype = self.rdtype
+ obj.covers = self.covers
+ obj.ttl = self.ttl
+ return obj
+
+ def update_ttl(self, ttl: int) -> None:
+ """Perform TTL minimization.
+
+ Set the TTL of the rdataset to be the lesser of the set's current
+ TTL or the specified TTL. If the set contains no rdatas, set the TTL
+ to the specified TTL.
+
+ *ttl*, an ``int`` or ``str``.
+ """
+ ttl = dns.ttl.make(ttl)
+ if len(self) == 0:
+ self.ttl = ttl
+ elif ttl < self.ttl:
+ self.ttl = ttl
+
+ def add( # pylint: disable=arguments-differ,arguments-renamed
+ self, rd: dns.rdata.Rdata, ttl: Optional[int] = None
+ ) -> None:
+ """Add the specified rdata to the rdataset.
+
+ If the optional *ttl* parameter is supplied, then
+ ``self.update_ttl(ttl)`` will be called prior to adding the rdata.
+
+ *rd*, a ``dns.rdata.Rdata``, the rdata
+
+ *ttl*, an ``int``, the TTL.
+
+ Raises ``dns.rdataset.IncompatibleTypes`` if the type and class
+ do not match the type and class of the rdataset.
+
+ Raises ``dns.rdataset.DifferingCovers`` if the type is a signature
+ type and the covered type does not match that of the rdataset.
+ """
+
+ #
+ # If we're adding a signature, do some special handling to
+ # check that the signature covers the same type as the
+ # other rdatas in this rdataset. If this is the first rdata
+ # in the set, initialize the covers field.
+ #
+ if self.rdclass != rd.rdclass or self.rdtype != rd.rdtype:
+ raise IncompatibleTypes
+ if ttl is not None:
+ self.update_ttl(ttl)
+ if self.rdtype == dns.rdatatype.RRSIG or self.rdtype == dns.rdatatype.SIG:
+ covers = rd.covers()
+ if len(self) == 0 and self.covers == dns.rdatatype.NONE:
+ self.covers = covers
+ elif self.covers != covers:
+ raise DifferingCovers
+ if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0:
+ self.clear()
+ super().add(rd)
+
+ def union_update(self, other):
+ self.update_ttl(other.ttl)
+ super().union_update(other)
+
+ def intersection_update(self, other):
+ self.update_ttl(other.ttl)
+ super().intersection_update(other)
+
+ def update(self, other):
+ """Add all rdatas in other to self.
+
+ *other*, a ``dns.rdataset.Rdataset``, the rdataset from which
+ to update.
+ """
+
+ self.update_ttl(other.ttl)
+ super().update(other)
+
+ def _rdata_repr(self):
+ def maybe_truncate(s):
+ if len(s) > 100:
+ return s[:100] + "..."
+ return s
+
+ return "[" + ", ".join(f"<{maybe_truncate(str(rr))}>" for rr in self) + "]"
+
+ def __repr__(self):
+ if self.covers == 0:
+ ctext = ""
+ else:
+ ctext = "(" + dns.rdatatype.to_text(self.covers) + ")"
+ return (
+ "<DNS "
+ + dns.rdataclass.to_text(self.rdclass)
+ + " "
+ + dns.rdatatype.to_text(self.rdtype)
+ + ctext
+ + " rdataset: "
+ + self._rdata_repr()
+ + ">"
+ )
+
+ def __str__(self):
+ return self.to_text()
+
+ def __eq__(self, other):
+ if not isinstance(other, Rdataset):
+ return False
+ if (
+ self.rdclass != other.rdclass
+ or self.rdtype != other.rdtype
+ or self.covers != other.covers
+ ):
+ return False
+ return super().__eq__(other)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def to_text(
+ self,
+ name: Optional[dns.name.Name] = None,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ override_rdclass: Optional[dns.rdataclass.RdataClass] = None,
+ want_comments: bool = False,
+ **kw: Dict[str, Any],
+ ) -> str:
+ """Convert the rdataset into DNS zone file format.
+
+ See ``dns.name.Name.choose_relativity`` for more information
+ on how *origin* and *relativize* determine the way names
+ are emitted.
+
+ Any additional keyword arguments are passed on to the rdata
+ ``to_text()`` method.
+
+ *name*, a ``dns.name.Name``. If name is not ``None``, emit RRs with
+ *name* as the owner name.
+
+ *origin*, a ``dns.name.Name`` or ``None``, the origin for relative
+ names.
+
+ *relativize*, a ``bool``. If ``True``, names will be relativized
+ to *origin*.
+
+ *override_rdclass*, a ``dns.rdataclass.RdataClass`` or ``None``.
+ If not ``None``, use this class instead of the Rdataset's class.
+
+ *want_comments*, a ``bool``. If ``True``, emit comments for rdata
+ which have them. The default is ``False``.
+ """
+
+ if name is not None:
+ name = name.choose_relativity(origin, relativize)
+ ntext = str(name)
+ pad = " "
+ else:
+ ntext = ""
+ pad = ""
+ s = io.StringIO()
+ if override_rdclass is not None:
+ rdclass = override_rdclass
+ else:
+ rdclass = self.rdclass
+ if len(self) == 0:
+ #
+ # Empty rdatasets are used for the question section, and in
+ # some dynamic updates, so we don't need to print out the TTL
+ # (which is meaningless anyway).
+ #
+ s.write(
+ f"{ntext}{pad}{dns.rdataclass.to_text(rdclass)} "
+ f"{dns.rdatatype.to_text(self.rdtype)}\n"
+ )
+ else:
+ for rd in self:
+ extra = ""
+ if want_comments:
+ if rd.rdcomment:
+ extra = f" ;{rd.rdcomment}"
+ s.write(
+ "%s%s%d %s %s %s%s\n"
+ % (
+ ntext,
+ pad,
+ self.ttl,
+ dns.rdataclass.to_text(rdclass),
+ dns.rdatatype.to_text(self.rdtype),
+ rd.to_text(origin=origin, relativize=relativize, **kw),
+ extra,
+ )
+ )
+ #
+ # We strip off the final \n for the caller's convenience in printing
+ #
+ return s.getvalue()[:-1]
+
+ def to_wire(
+ self,
+ name: dns.name.Name,
+ file: Any,
+ compress: Optional[dns.name.CompressType] = None,
+ origin: Optional[dns.name.Name] = None,
+ override_rdclass: Optional[dns.rdataclass.RdataClass] = None,
+ want_shuffle: bool = True,
+ ) -> int:
+ """Convert the rdataset to wire format.
+
+ *name*, a ``dns.name.Name`` is the owner name to use.
+
+ *file* is the file where the name is emitted (typically a
+ BytesIO file).
+
+ *compress*, a ``dict``, is the compression table to use. If
+ ``None`` (the default), names will not be compressed.
+
+ *origin* is a ``dns.name.Name`` or ``None``. If the name is
+ relative and origin is not ``None``, then *origin* will be appended
+ to it.
+
+ *override_rdclass*, an ``int``, is used as the class instead of the
+ class of the rdataset. This is useful when rendering rdatasets
+ associated with dynamic updates.
+
+ *want_shuffle*, a ``bool``. If ``True``, then the order of the
+ Rdatas within the Rdataset will be shuffled before rendering.
+
+ Returns an ``int``, the number of records emitted.
+ """
+
+ if override_rdclass is not None:
+ rdclass = override_rdclass
+ want_shuffle = False
+ else:
+ rdclass = self.rdclass
+ if len(self) == 0:
+ name.to_wire(file, compress, origin)
+ file.write(struct.pack("!HHIH", self.rdtype, rdclass, 0, 0))
+ return 1
+ else:
+ l: Union[Rdataset, List[dns.rdata.Rdata]]
+ if want_shuffle:
+ l = list(self)
+ random.shuffle(l)
+ else:
+ l = self
+ for rd in l:
+ name.to_wire(file, compress, origin)
+ file.write(struct.pack("!HHI", self.rdtype, rdclass, self.ttl))
+ with dns.renderer.prefixed_length(file, 2):
+ rd.to_wire(file, compress, origin)
+ return len(self)
+
+ def match(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType,
+ ) -> bool:
+ """Returns ``True`` if this rdataset matches the specified class,
+ type, and covers.
+ """
+ if self.rdclass == rdclass and self.rdtype == rdtype and self.covers == covers:
+ return True
+ return False
+
+ def processing_order(self) -> List[dns.rdata.Rdata]:
+ """Return rdatas in a valid processing order according to the type's
+ specification. For example, MX records are in preference order from
+ lowest to highest preferences, with items of the same preference
+ shuffled.
+
+ For types that do not define a processing order, the rdatas are
+ simply shuffled.
+ """
+ if len(self) == 0:
+ return []
+ else:
+ return self[0]._processing_order(iter(self))
+
+
+@dns.immutable.immutable
+class ImmutableRdataset(Rdataset): # lgtm[py/missing-equals]
+ """An immutable DNS rdataset."""
+
+ _clone_class = Rdataset
+
+ def __init__(self, rdataset: Rdataset):
+ """Create an immutable rdataset from the specified rdataset."""
+
+ super().__init__(
+ rdataset.rdclass, rdataset.rdtype, rdataset.covers, rdataset.ttl
+ )
+ self.items = dns.immutable.Dict(rdataset.items)
+
+ def update_ttl(self, ttl):
+ raise TypeError("immutable")
+
+ def add(self, rd, ttl=None):
+ raise TypeError("immutable")
+
+ def union_update(self, other):
+ raise TypeError("immutable")
+
+ def intersection_update(self, other):
+ raise TypeError("immutable")
+
+ def update(self, other):
+ raise TypeError("immutable")
+
+ def __delitem__(self, i):
+ raise TypeError("immutable")
+
+ # lgtm complains about these not raising ArithmeticError, but there is
+ # precedent for overrides of these methods in other classes to raise
+ # TypeError, and it seems like the better exception.
+
+ def __ior__(self, other): # lgtm[py/unexpected-raise-in-special-method]
+ raise TypeError("immutable")
+
+ def __iand__(self, other): # lgtm[py/unexpected-raise-in-special-method]
+ raise TypeError("immutable")
+
+ def __iadd__(self, other): # lgtm[py/unexpected-raise-in-special-method]
+ raise TypeError("immutable")
+
+ def __isub__(self, other): # lgtm[py/unexpected-raise-in-special-method]
+ raise TypeError("immutable")
+
+ def clear(self):
+ raise TypeError("immutable")
+
+ def __copy__(self):
+ return ImmutableRdataset(super().copy())
+
+ def copy(self):
+ return ImmutableRdataset(super().copy())
+
+ def union(self, other):
+ return ImmutableRdataset(super().union(other))
+
+ def intersection(self, other):
+ return ImmutableRdataset(super().intersection(other))
+
+ def difference(self, other):
+ return ImmutableRdataset(super().difference(other))
+
+ def symmetric_difference(self, other):
+ return ImmutableRdataset(super().symmetric_difference(other))
+
+
+def from_text_list(
+ rdclass: Union[dns.rdataclass.RdataClass, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ ttl: int,
+ text_rdatas: Collection[str],
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ relativize_to: Optional[dns.name.Name] = None,
+) -> Rdataset:
+ """Create an rdataset with the specified class, type, and TTL, and with
+ the specified list of rdatas in text format.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder to use; if ``None``, the default IDNA 2003
+ encoder/decoder is used.
+
+ *origin*, a ``dns.name.Name`` (or ``None``), the
+ origin to use for relative names.
+
+ *relativize*, a ``bool``. If true, name will be relativized.
+
+ *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use
+ when relativizing names. If not set, the *origin* value will be used.
+
+ Returns a ``dns.rdataset.Rdataset`` object.
+ """
+
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ r = Rdataset(rdclass, rdtype)
+ r.update_ttl(ttl)
+ for t in text_rdatas:
+ rd = dns.rdata.from_text(
+ r.rdclass, r.rdtype, t, origin, relativize, relativize_to, idna_codec
+ )
+ r.add(rd)
+ return r
+
+
+def from_text(
+ rdclass: Union[dns.rdataclass.RdataClass, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ ttl: int,
+ *text_rdatas: Any,
+) -> Rdataset:
+ """Create an rdataset with the specified class, type, and TTL, and with
+ the specified rdatas in text format.
+
+ Returns a ``dns.rdataset.Rdataset`` object.
+ """
+
+ return from_text_list(rdclass, rdtype, ttl, cast(Collection[str], text_rdatas))
+
+
+def from_rdata_list(ttl: int, rdatas: Collection[dns.rdata.Rdata]) -> Rdataset:
+ """Create an rdataset with the specified TTL, and with
+ the specified list of rdata objects.
+
+ Returns a ``dns.rdataset.Rdataset`` object.
+ """
+
+ if len(rdatas) == 0:
+ raise ValueError("rdata list must not be empty")
+ r = None
+ for rd in rdatas:
+ if r is None:
+ r = Rdataset(rd.rdclass, rd.rdtype)
+ r.update_ttl(ttl)
+ r.add(rd)
+ assert r is not None
+ return r
+
+
+def from_rdata(ttl: int, *rdatas: Any) -> Rdataset:
+ """Create an rdataset with the specified TTL, and with
+ the specified rdata objects.
+
+ Returns a ``dns.rdataset.Rdataset`` object.
+ """
+
+ return from_rdata_list(ttl, cast(Collection[dns.rdata.Rdata], rdatas))
diff --git a/.venv/lib/python3.12/site-packages/dns/rdatatype.py b/.venv/lib/python3.12/site-packages/dns/rdatatype.py
new file mode 100644
index 00000000..aa9e561c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdatatype.py
@@ -0,0 +1,336 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Rdata Types."""
+
+from typing import Dict
+
+import dns.enum
+import dns.exception
+
+
+class RdataType(dns.enum.IntEnum):
+ """DNS Rdata Type"""
+
+ TYPE0 = 0
+ NONE = 0
+ A = 1
+ NS = 2
+ MD = 3
+ MF = 4
+ CNAME = 5
+ SOA = 6
+ MB = 7
+ MG = 8
+ MR = 9
+ NULL = 10
+ WKS = 11
+ PTR = 12
+ HINFO = 13
+ MINFO = 14
+ MX = 15
+ TXT = 16
+ RP = 17
+ AFSDB = 18
+ X25 = 19
+ ISDN = 20
+ RT = 21
+ NSAP = 22
+ NSAP_PTR = 23
+ SIG = 24
+ KEY = 25
+ PX = 26
+ GPOS = 27
+ AAAA = 28
+ LOC = 29
+ NXT = 30
+ SRV = 33
+ NAPTR = 35
+ KX = 36
+ CERT = 37
+ A6 = 38
+ DNAME = 39
+ OPT = 41
+ APL = 42
+ DS = 43
+ SSHFP = 44
+ IPSECKEY = 45
+ RRSIG = 46
+ NSEC = 47
+ DNSKEY = 48
+ DHCID = 49
+ NSEC3 = 50
+ NSEC3PARAM = 51
+ TLSA = 52
+ SMIMEA = 53
+ HIP = 55
+ NINFO = 56
+ CDS = 59
+ CDNSKEY = 60
+ OPENPGPKEY = 61
+ CSYNC = 62
+ ZONEMD = 63
+ SVCB = 64
+ HTTPS = 65
+ SPF = 99
+ UNSPEC = 103
+ NID = 104
+ L32 = 105
+ L64 = 106
+ LP = 107
+ EUI48 = 108
+ EUI64 = 109
+ TKEY = 249
+ TSIG = 250
+ IXFR = 251
+ AXFR = 252
+ MAILB = 253
+ MAILA = 254
+ ANY = 255
+ URI = 256
+ CAA = 257
+ AVC = 258
+ AMTRELAY = 260
+ RESINFO = 261
+ WALLET = 262
+ TA = 32768
+ DLV = 32769
+
+ @classmethod
+ def _maximum(cls):
+ return 65535
+
+ @classmethod
+ def _short_name(cls):
+ return "type"
+
+ @classmethod
+ def _prefix(cls):
+ return "TYPE"
+
+ @classmethod
+ def _extra_from_text(cls, text):
+ if text.find("-") >= 0:
+ try:
+ return cls[text.replace("-", "_")]
+ except KeyError: # pragma: no cover
+ pass
+ return _registered_by_text.get(text)
+
+ @classmethod
+ def _extra_to_text(cls, value, current_text):
+ if current_text is None:
+ return _registered_by_value.get(value)
+ if current_text.find("_") >= 0:
+ return current_text.replace("_", "-")
+ return current_text
+
+ @classmethod
+ def _unknown_exception_class(cls):
+ return UnknownRdatatype
+
+
+_registered_by_text: Dict[str, RdataType] = {}
+_registered_by_value: Dict[RdataType, str] = {}
+
+_metatypes = {RdataType.OPT}
+
+_singletons = {
+ RdataType.SOA,
+ RdataType.NXT,
+ RdataType.DNAME,
+ RdataType.NSEC,
+ RdataType.CNAME,
+}
+
+
+class UnknownRdatatype(dns.exception.DNSException):
+ """DNS resource record type is unknown."""
+
+
+def from_text(text: str) -> RdataType:
+ """Convert text into a DNS rdata type value.
+
+ The input text can be a defined DNS RR type mnemonic or
+ instance of the DNS generic type syntax.
+
+ For example, "NS" and "TYPE2" will both result in a value of 2.
+
+ Raises ``dns.rdatatype.UnknownRdatatype`` if the type is unknown.
+
+ Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535.
+
+ Returns a ``dns.rdatatype.RdataType``.
+ """
+
+ return RdataType.from_text(text)
+
+
+def to_text(value: RdataType) -> str:
+ """Convert a DNS rdata type value to text.
+
+ If the value has a known mnemonic, it will be used, otherwise the
+ DNS generic type syntax will be used.
+
+ Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535.
+
+ Returns a ``str``.
+ """
+
+ return RdataType.to_text(value)
+
+
+def is_metatype(rdtype: RdataType) -> bool:
+ """True if the specified type is a metatype.
+
+ *rdtype* is a ``dns.rdatatype.RdataType``.
+
+ The currently defined metatypes are TKEY, TSIG, IXFR, AXFR, MAILA,
+ MAILB, ANY, and OPT.
+
+ Returns a ``bool``.
+ """
+
+ return (256 > rdtype >= 128) or rdtype in _metatypes
+
+
+def is_singleton(rdtype: RdataType) -> bool:
+ """Is the specified type a singleton type?
+
+ Singleton types can only have a single rdata in an rdataset, or a single
+ RR in an RRset.
+
+ The currently defined singleton types are CNAME, DNAME, NSEC, NXT, and
+ SOA.
+
+ *rdtype* is an ``int``.
+
+ Returns a ``bool``.
+ """
+
+ if rdtype in _singletons:
+ return True
+ return False
+
+
+# pylint: disable=redefined-outer-name
+def register_type(
+ rdtype: RdataType, rdtype_text: str, is_singleton: bool = False
+) -> None:
+ """Dynamically register an rdatatype.
+
+ *rdtype*, a ``dns.rdatatype.RdataType``, the rdatatype to register.
+
+ *rdtype_text*, a ``str``, the textual form of the rdatatype.
+
+ *is_singleton*, a ``bool``, indicating if the type is a singleton (i.e.
+ RRsets of the type can have only one member.)
+ """
+
+ _registered_by_text[rdtype_text] = rdtype
+ _registered_by_value[rdtype] = rdtype_text
+ if is_singleton:
+ _singletons.add(rdtype)
+
+
+### BEGIN generated RdataType constants
+
+TYPE0 = RdataType.TYPE0
+NONE = RdataType.NONE
+A = RdataType.A
+NS = RdataType.NS
+MD = RdataType.MD
+MF = RdataType.MF
+CNAME = RdataType.CNAME
+SOA = RdataType.SOA
+MB = RdataType.MB
+MG = RdataType.MG
+MR = RdataType.MR
+NULL = RdataType.NULL
+WKS = RdataType.WKS
+PTR = RdataType.PTR
+HINFO = RdataType.HINFO
+MINFO = RdataType.MINFO
+MX = RdataType.MX
+TXT = RdataType.TXT
+RP = RdataType.RP
+AFSDB = RdataType.AFSDB
+X25 = RdataType.X25
+ISDN = RdataType.ISDN
+RT = RdataType.RT
+NSAP = RdataType.NSAP
+NSAP_PTR = RdataType.NSAP_PTR
+SIG = RdataType.SIG
+KEY = RdataType.KEY
+PX = RdataType.PX
+GPOS = RdataType.GPOS
+AAAA = RdataType.AAAA
+LOC = RdataType.LOC
+NXT = RdataType.NXT
+SRV = RdataType.SRV
+NAPTR = RdataType.NAPTR
+KX = RdataType.KX
+CERT = RdataType.CERT
+A6 = RdataType.A6
+DNAME = RdataType.DNAME
+OPT = RdataType.OPT
+APL = RdataType.APL
+DS = RdataType.DS
+SSHFP = RdataType.SSHFP
+IPSECKEY = RdataType.IPSECKEY
+RRSIG = RdataType.RRSIG
+NSEC = RdataType.NSEC
+DNSKEY = RdataType.DNSKEY
+DHCID = RdataType.DHCID
+NSEC3 = RdataType.NSEC3
+NSEC3PARAM = RdataType.NSEC3PARAM
+TLSA = RdataType.TLSA
+SMIMEA = RdataType.SMIMEA
+HIP = RdataType.HIP
+NINFO = RdataType.NINFO
+CDS = RdataType.CDS
+CDNSKEY = RdataType.CDNSKEY
+OPENPGPKEY = RdataType.OPENPGPKEY
+CSYNC = RdataType.CSYNC
+ZONEMD = RdataType.ZONEMD
+SVCB = RdataType.SVCB
+HTTPS = RdataType.HTTPS
+SPF = RdataType.SPF
+UNSPEC = RdataType.UNSPEC
+NID = RdataType.NID
+L32 = RdataType.L32
+L64 = RdataType.L64
+LP = RdataType.LP
+EUI48 = RdataType.EUI48
+EUI64 = RdataType.EUI64
+TKEY = RdataType.TKEY
+TSIG = RdataType.TSIG
+IXFR = RdataType.IXFR
+AXFR = RdataType.AXFR
+MAILB = RdataType.MAILB
+MAILA = RdataType.MAILA
+ANY = RdataType.ANY
+URI = RdataType.URI
+CAA = RdataType.CAA
+AVC = RdataType.AVC
+AMTRELAY = RdataType.AMTRELAY
+RESINFO = RdataType.RESINFO
+WALLET = RdataType.WALLET
+TA = RdataType.TA
+DLV = RdataType.DLV
+
+### END generated RdataType constants
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AFSDB.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AFSDB.py
new file mode 100644
index 00000000..06a3b970
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AFSDB.py
@@ -0,0 +1,45 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.mxbase
+
+
+@dns.immutable.immutable
+class AFSDB(dns.rdtypes.mxbase.UncompressedDowncasingMX):
+ """AFSDB record"""
+
+ # Use the property mechanism to make "subtype" an alias for the
+ # "preference" attribute, and "hostname" an alias for the "exchange"
+ # attribute.
+ #
+ # This lets us inherit the UncompressedMX implementation but lets
+ # the caller use appropriate attribute names for the rdata type.
+ #
+ # We probably lose some performance vs. a cut-and-paste
+ # implementation, but this way we don't copy code, and that's
+ # good.
+
+ @property
+ def subtype(self):
+ "the AFSDB subtype"
+ return self.preference
+
+ @property
+ def hostname(self):
+ "the AFSDB hostname"
+ return self.exchange
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AMTRELAY.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AMTRELAY.py
new file mode 100644
index 00000000..ed2b072b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AMTRELAY.py
@@ -0,0 +1,91 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdtypes.util
+
+
+class Relay(dns.rdtypes.util.Gateway):
+ name = "AMTRELAY relay"
+
+ @property
+ def relay(self):
+ return self.gateway
+
+
+@dns.immutable.immutable
+class AMTRELAY(dns.rdata.Rdata):
+ """AMTRELAY record"""
+
+ # see: RFC 8777
+
+ __slots__ = ["precedence", "discovery_optional", "relay_type", "relay"]
+
+ def __init__(
+ self, rdclass, rdtype, precedence, discovery_optional, relay_type, relay
+ ):
+ super().__init__(rdclass, rdtype)
+ relay = Relay(relay_type, relay)
+ self.precedence = self._as_uint8(precedence)
+ self.discovery_optional = self._as_bool(discovery_optional)
+ self.relay_type = relay.type
+ self.relay = relay.relay
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ relay = Relay(self.relay_type, self.relay).to_text(origin, relativize)
+ return "%d %d %d %s" % (
+ self.precedence,
+ self.discovery_optional,
+ self.relay_type,
+ relay,
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ precedence = tok.get_uint8()
+ discovery_optional = tok.get_uint8()
+ if discovery_optional > 1:
+ raise dns.exception.SyntaxError("expecting 0 or 1")
+ discovery_optional = bool(discovery_optional)
+ relay_type = tok.get_uint8()
+ if relay_type > 0x7F:
+ raise dns.exception.SyntaxError("expecting an integer <= 127")
+ relay = Relay.from_text(relay_type, tok, origin, relativize, relativize_to)
+ return cls(
+ rdclass, rdtype, precedence, discovery_optional, relay_type, relay.relay
+ )
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ relay_type = self.relay_type | (self.discovery_optional << 7)
+ header = struct.pack("!BB", self.precedence, relay_type)
+ file.write(header)
+ Relay(self.relay_type, self.relay).to_wire(file, compress, origin, canonicalize)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (precedence, relay_type) = parser.get_struct("!BB")
+ discovery_optional = bool(relay_type >> 7)
+ relay_type &= 0x7F
+ relay = Relay.from_wire_parser(relay_type, parser, origin)
+ return cls(
+ rdclass, rdtype, precedence, discovery_optional, relay_type, relay.relay
+ )
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AVC.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AVC.py
new file mode 100644
index 00000000..a27ae2d6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/AVC.py
@@ -0,0 +1,26 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2016 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.txtbase
+
+
+@dns.immutable.immutable
+class AVC(dns.rdtypes.txtbase.TXTBase):
+ """AVC record"""
+
+ # See: IANA dns parameters for AVC
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CAA.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CAA.py
new file mode 100644
index 00000000..2e6a7e7e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CAA.py
@@ -0,0 +1,71 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class CAA(dns.rdata.Rdata):
+ """CAA (Certification Authority Authorization) record"""
+
+ # see: RFC 6844
+
+ __slots__ = ["flags", "tag", "value"]
+
+ def __init__(self, rdclass, rdtype, flags, tag, value):
+ super().__init__(rdclass, rdtype)
+ self.flags = self._as_uint8(flags)
+ self.tag = self._as_bytes(tag, True, 255)
+ if not tag.isalnum():
+ raise ValueError("tag is not alphanumeric")
+ self.value = self._as_bytes(value)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return '%u %s "%s"' % (
+ self.flags,
+ dns.rdata._escapify(self.tag),
+ dns.rdata._escapify(self.value),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ flags = tok.get_uint8()
+ tag = tok.get_string().encode()
+ value = tok.get_string().encode()
+ return cls(rdclass, rdtype, flags, tag, value)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(struct.pack("!B", self.flags))
+ l = len(self.tag)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.tag)
+ file.write(self.value)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ flags = parser.get_uint8()
+ tag = parser.get_counted_bytes()
+ value = parser.get_remaining()
+ return cls(rdclass, rdtype, flags, tag, value)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CDNSKEY.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CDNSKEY.py
new file mode 100644
index 00000000..b613409f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CDNSKEY.py
@@ -0,0 +1,33 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.dnskeybase # lgtm[py/import-and-import-from]
+
+# pylint: disable=unused-import
+from dns.rdtypes.dnskeybase import ( # noqa: F401 lgtm[py/unused-import]
+ REVOKE,
+ SEP,
+ ZONE,
+)
+
+# pylint: enable=unused-import
+
+
+@dns.immutable.immutable
+class CDNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase):
+ """CDNSKEY record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CDS.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CDS.py
new file mode 100644
index 00000000..8312b972
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CDS.py
@@ -0,0 +1,29 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.dsbase
+
+
+@dns.immutable.immutable
+class CDS(dns.rdtypes.dsbase.DSBase):
+ """CDS record"""
+
+ _digest_length_by_type = {
+ **dns.rdtypes.dsbase.DSBase._digest_length_by_type,
+ 0: 1, # delete, RFC 8078 Sec. 4 (including Errata ID 5049)
+ }
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CERT.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CERT.py
new file mode 100644
index 00000000..f369cc85
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CERT.py
@@ -0,0 +1,116 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import struct
+
+import dns.dnssectypes
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+_ctype_by_value = {
+ 1: "PKIX",
+ 2: "SPKI",
+ 3: "PGP",
+ 4: "IPKIX",
+ 5: "ISPKI",
+ 6: "IPGP",
+ 7: "ACPKIX",
+ 8: "IACPKIX",
+ 253: "URI",
+ 254: "OID",
+}
+
+_ctype_by_name = {
+ "PKIX": 1,
+ "SPKI": 2,
+ "PGP": 3,
+ "IPKIX": 4,
+ "ISPKI": 5,
+ "IPGP": 6,
+ "ACPKIX": 7,
+ "IACPKIX": 8,
+ "URI": 253,
+ "OID": 254,
+}
+
+
+def _ctype_from_text(what):
+ v = _ctype_by_name.get(what)
+ if v is not None:
+ return v
+ return int(what)
+
+
+def _ctype_to_text(what):
+ v = _ctype_by_value.get(what)
+ if v is not None:
+ return v
+ return str(what)
+
+
+@dns.immutable.immutable
+class CERT(dns.rdata.Rdata):
+ """CERT record"""
+
+ # see RFC 4398
+
+ __slots__ = ["certificate_type", "key_tag", "algorithm", "certificate"]
+
+ def __init__(
+ self, rdclass, rdtype, certificate_type, key_tag, algorithm, certificate
+ ):
+ super().__init__(rdclass, rdtype)
+ self.certificate_type = self._as_uint16(certificate_type)
+ self.key_tag = self._as_uint16(key_tag)
+ self.algorithm = self._as_uint8(algorithm)
+ self.certificate = self._as_bytes(certificate)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ certificate_type = _ctype_to_text(self.certificate_type)
+ return "%s %d %s %s" % (
+ certificate_type,
+ self.key_tag,
+ dns.dnssectypes.Algorithm.to_text(self.algorithm),
+ dns.rdata._base64ify(self.certificate, **kw),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ certificate_type = _ctype_from_text(tok.get_string())
+ key_tag = tok.get_uint16()
+ algorithm = dns.dnssectypes.Algorithm.from_text(tok.get_string())
+ b64 = tok.concatenate_remaining_identifiers().encode()
+ certificate = base64.b64decode(b64)
+ return cls(rdclass, rdtype, certificate_type, key_tag, algorithm, certificate)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ prefix = struct.pack(
+ "!HHB", self.certificate_type, self.key_tag, self.algorithm
+ )
+ file.write(prefix)
+ file.write(self.certificate)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (certificate_type, key_tag, algorithm) = parser.get_struct("!HHB")
+ certificate = parser.get_remaining()
+ return cls(rdclass, rdtype, certificate_type, key_tag, algorithm, certificate)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CNAME.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CNAME.py
new file mode 100644
index 00000000..665e407c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CNAME.py
@@ -0,0 +1,28 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.nsbase
+
+
+@dns.immutable.immutable
+class CNAME(dns.rdtypes.nsbase.NSBase):
+ """CNAME record
+
+ Note: although CNAME is officially a singleton type, dnspython allows
+ non-singleton CNAME rdatasets because such sets have been commonly
+ used by BIND and other nameservers for load balancing."""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CSYNC.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CSYNC.py
new file mode 100644
index 00000000..2f972f6e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/CSYNC.py
@@ -0,0 +1,68 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011, 2016 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdatatype
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class Bitmap(dns.rdtypes.util.Bitmap):
+ type_name = "CSYNC"
+
+
+@dns.immutable.immutable
+class CSYNC(dns.rdata.Rdata):
+ """CSYNC record"""
+
+ __slots__ = ["serial", "flags", "windows"]
+
+ def __init__(self, rdclass, rdtype, serial, flags, windows):
+ super().__init__(rdclass, rdtype)
+ self.serial = self._as_uint32(serial)
+ self.flags = self._as_uint16(flags)
+ if not isinstance(windows, Bitmap):
+ windows = Bitmap(windows)
+ self.windows = tuple(windows.windows)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ text = Bitmap(self.windows).to_text()
+ return "%d %d%s" % (self.serial, self.flags, text)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ serial = tok.get_uint32()
+ flags = tok.get_uint16()
+ bitmap = Bitmap.from_text(tok)
+ return cls(rdclass, rdtype, serial, flags, bitmap)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(struct.pack("!IH", self.serial, self.flags))
+ Bitmap(self.windows).to_wire(file)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (serial, flags) = parser.get_struct("!IH")
+ bitmap = Bitmap.from_wire_parser(parser)
+ return cls(rdclass, rdtype, serial, flags, bitmap)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DLV.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DLV.py
new file mode 100644
index 00000000..6c134f18
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DLV.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.dsbase
+
+
+@dns.immutable.immutable
+class DLV(dns.rdtypes.dsbase.DSBase):
+ """DLV record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DNAME.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DNAME.py
new file mode 100644
index 00000000..bbf9186c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DNAME.py
@@ -0,0 +1,27 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.nsbase
+
+
+@dns.immutable.immutable
+class DNAME(dns.rdtypes.nsbase.UncompressedNS):
+ """DNAME record"""
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ self.target.to_wire(file, None, origin, canonicalize)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DNSKEY.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DNSKEY.py
new file mode 100644
index 00000000..6d961a9f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DNSKEY.py
@@ -0,0 +1,33 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.dnskeybase # lgtm[py/import-and-import-from]
+
+# pylint: disable=unused-import
+from dns.rdtypes.dnskeybase import ( # noqa: F401 lgtm[py/unused-import]
+ REVOKE,
+ SEP,
+ ZONE,
+)
+
+# pylint: enable=unused-import
+
+
+@dns.immutable.immutable
+class DNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase):
+ """DNSKEY record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DS.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DS.py
new file mode 100644
index 00000000..58b3108d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/DS.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.dsbase
+
+
+@dns.immutable.immutable
+class DS(dns.rdtypes.dsbase.DSBase):
+ """DS record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/EUI48.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/EUI48.py
new file mode 100644
index 00000000..c843be50
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/EUI48.py
@@ -0,0 +1,30 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2015 Red Hat, Inc.
+# Author: Petr Spacek <pspacek@redhat.com>
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.euibase
+
+
+@dns.immutable.immutable
+class EUI48(dns.rdtypes.euibase.EUIBase):
+ """EUI48 record"""
+
+ # see: rfc7043.txt
+
+ byte_len = 6 # 0123456789ab (in hex)
+ text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/EUI64.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/EUI64.py
new file mode 100644
index 00000000..f6d7e257
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/EUI64.py
@@ -0,0 +1,30 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2015 Red Hat, Inc.
+# Author: Petr Spacek <pspacek@redhat.com>
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.euibase
+
+
+@dns.immutable.immutable
+class EUI64(dns.rdtypes.euibase.EUIBase):
+ """EUI64 record"""
+
+ # see: rfc7043.txt
+
+ byte_len = 8 # 0123456789abcdef (in hex)
+ text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab-cd-ef
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/GPOS.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/GPOS.py
new file mode 100644
index 00000000..d79f4a06
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/GPOS.py
@@ -0,0 +1,126 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+def _validate_float_string(what):
+ if len(what) == 0:
+ raise dns.exception.FormError
+ if what[0] == b"-"[0] or what[0] == b"+"[0]:
+ what = what[1:]
+ if what.isdigit():
+ return
+ try:
+ (left, right) = what.split(b".")
+ except ValueError:
+ raise dns.exception.FormError
+ if left == b"" and right == b"":
+ raise dns.exception.FormError
+ if not left == b"" and not left.decode().isdigit():
+ raise dns.exception.FormError
+ if not right == b"" and not right.decode().isdigit():
+ raise dns.exception.FormError
+
+
+@dns.immutable.immutable
+class GPOS(dns.rdata.Rdata):
+ """GPOS record"""
+
+ # see: RFC 1712
+
+ __slots__ = ["latitude", "longitude", "altitude"]
+
+ def __init__(self, rdclass, rdtype, latitude, longitude, altitude):
+ super().__init__(rdclass, rdtype)
+ if isinstance(latitude, float) or isinstance(latitude, int):
+ latitude = str(latitude)
+ if isinstance(longitude, float) or isinstance(longitude, int):
+ longitude = str(longitude)
+ if isinstance(altitude, float) or isinstance(altitude, int):
+ altitude = str(altitude)
+ latitude = self._as_bytes(latitude, True, 255)
+ longitude = self._as_bytes(longitude, True, 255)
+ altitude = self._as_bytes(altitude, True, 255)
+ _validate_float_string(latitude)
+ _validate_float_string(longitude)
+ _validate_float_string(altitude)
+ self.latitude = latitude
+ self.longitude = longitude
+ self.altitude = altitude
+ flat = self.float_latitude
+ if flat < -90.0 or flat > 90.0:
+ raise dns.exception.FormError("bad latitude")
+ flong = self.float_longitude
+ if flong < -180.0 or flong > 180.0:
+ raise dns.exception.FormError("bad longitude")
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return (
+ f"{self.latitude.decode()} {self.longitude.decode()} "
+ f"{self.altitude.decode()}"
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ latitude = tok.get_string()
+ longitude = tok.get_string()
+ altitude = tok.get_string()
+ return cls(rdclass, rdtype, latitude, longitude, altitude)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ l = len(self.latitude)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.latitude)
+ l = len(self.longitude)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.longitude)
+ l = len(self.altitude)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.altitude)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ latitude = parser.get_counted_bytes()
+ longitude = parser.get_counted_bytes()
+ altitude = parser.get_counted_bytes()
+ return cls(rdclass, rdtype, latitude, longitude, altitude)
+
+ @property
+ def float_latitude(self):
+ "latitude as a floating point value"
+ return float(self.latitude)
+
+ @property
+ def float_longitude(self):
+ "longitude as a floating point value"
+ return float(self.longitude)
+
+ @property
+ def float_altitude(self):
+ "altitude as a floating point value"
+ return float(self.altitude)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/HINFO.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/HINFO.py
new file mode 100644
index 00000000..06ad3487
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/HINFO.py
@@ -0,0 +1,64 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class HINFO(dns.rdata.Rdata):
+ """HINFO record"""
+
+ # see: RFC 1035
+
+ __slots__ = ["cpu", "os"]
+
+ def __init__(self, rdclass, rdtype, cpu, os):
+ super().__init__(rdclass, rdtype)
+ self.cpu = self._as_bytes(cpu, True, 255)
+ self.os = self._as_bytes(os, True, 255)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return f'"{dns.rdata._escapify(self.cpu)}" "{dns.rdata._escapify(self.os)}"'
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ cpu = tok.get_string(max_length=255)
+ os = tok.get_string(max_length=255)
+ return cls(rdclass, rdtype, cpu, os)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ l = len(self.cpu)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.cpu)
+ l = len(self.os)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.os)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ cpu = parser.get_counted_bytes()
+ os = parser.get_counted_bytes()
+ return cls(rdclass, rdtype, cpu, os)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/HIP.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/HIP.py
new file mode 100644
index 00000000..f3157da7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/HIP.py
@@ -0,0 +1,85 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2010, 2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import binascii
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+
+
+@dns.immutable.immutable
+class HIP(dns.rdata.Rdata):
+ """HIP record"""
+
+ # see: RFC 5205
+
+ __slots__ = ["hit", "algorithm", "key", "servers"]
+
+ def __init__(self, rdclass, rdtype, hit, algorithm, key, servers):
+ super().__init__(rdclass, rdtype)
+ self.hit = self._as_bytes(hit, True, 255)
+ self.algorithm = self._as_uint8(algorithm)
+ self.key = self._as_bytes(key, True)
+ self.servers = self._as_tuple(servers, self._as_name)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ hit = binascii.hexlify(self.hit).decode()
+ key = base64.b64encode(self.key).replace(b"\n", b"").decode()
+ text = ""
+ servers = []
+ for server in self.servers:
+ servers.append(server.choose_relativity(origin, relativize))
+ if len(servers) > 0:
+ text += " " + " ".join(x.to_unicode() for x in servers)
+ return "%u %s %s%s" % (self.algorithm, hit, key, text)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ algorithm = tok.get_uint8()
+ hit = binascii.unhexlify(tok.get_string().encode())
+ key = base64.b64decode(tok.get_string().encode())
+ servers = []
+ for token in tok.get_remaining():
+ server = tok.as_name(token, origin, relativize, relativize_to)
+ servers.append(server)
+ return cls(rdclass, rdtype, hit, algorithm, key, servers)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ lh = len(self.hit)
+ lk = len(self.key)
+ file.write(struct.pack("!BBH", lh, self.algorithm, lk))
+ file.write(self.hit)
+ file.write(self.key)
+ for server in self.servers:
+ server.to_wire(file, None, origin, False)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (lh, algorithm, lk) = parser.get_struct("!BBH")
+ hit = parser.get_bytes(lh)
+ key = parser.get_bytes(lk)
+ servers = []
+ while parser.remaining() > 0:
+ server = parser.get_name(origin)
+ servers.append(server)
+ return cls(rdclass, rdtype, hit, algorithm, key, servers)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/ISDN.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/ISDN.py
new file mode 100644
index 00000000..6428a0a8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/ISDN.py
@@ -0,0 +1,78 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class ISDN(dns.rdata.Rdata):
+ """ISDN record"""
+
+ # see: RFC 1183
+
+ __slots__ = ["address", "subaddress"]
+
+ def __init__(self, rdclass, rdtype, address, subaddress):
+ super().__init__(rdclass, rdtype)
+ self.address = self._as_bytes(address, True, 255)
+ self.subaddress = self._as_bytes(subaddress, True, 255)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ if self.subaddress:
+ return (
+ f'"{dns.rdata._escapify(self.address)}" '
+ f'"{dns.rdata._escapify(self.subaddress)}"'
+ )
+ else:
+ return f'"{dns.rdata._escapify(self.address)}"'
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ address = tok.get_string()
+ tokens = tok.get_remaining(max_tokens=1)
+ if len(tokens) >= 1:
+ subaddress = tokens[0].unescape().value
+ else:
+ subaddress = ""
+ return cls(rdclass, rdtype, address, subaddress)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ l = len(self.address)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.address)
+ l = len(self.subaddress)
+ if l > 0:
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.subaddress)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ address = parser.get_counted_bytes()
+ if parser.remaining() > 0:
+ subaddress = parser.get_counted_bytes()
+ else:
+ subaddress = b""
+ return cls(rdclass, rdtype, address, subaddress)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/L32.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/L32.py
new file mode 100644
index 00000000..09804c2d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/L32.py
@@ -0,0 +1,41 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import struct
+
+import dns.immutable
+import dns.rdata
+
+
+@dns.immutable.immutable
+class L32(dns.rdata.Rdata):
+ """L32 record"""
+
+ # see: rfc6742.txt
+
+ __slots__ = ["preference", "locator32"]
+
+ def __init__(self, rdclass, rdtype, preference, locator32):
+ super().__init__(rdclass, rdtype)
+ self.preference = self._as_uint16(preference)
+ self.locator32 = self._as_ipv4_address(locator32)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return f"{self.preference} {self.locator32}"
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ preference = tok.get_uint16()
+ nodeid = tok.get_identifier()
+ return cls(rdclass, rdtype, preference, nodeid)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(struct.pack("!H", self.preference))
+ file.write(dns.ipv4.inet_aton(self.locator32))
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ preference = parser.get_uint16()
+ locator32 = parser.get_remaining()
+ return cls(rdclass, rdtype, preference, locator32)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/L64.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/L64.py
new file mode 100644
index 00000000..fb76808e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/L64.py
@@ -0,0 +1,47 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import struct
+
+import dns.immutable
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class L64(dns.rdata.Rdata):
+ """L64 record"""
+
+ # see: rfc6742.txt
+
+ __slots__ = ["preference", "locator64"]
+
+ def __init__(self, rdclass, rdtype, preference, locator64):
+ super().__init__(rdclass, rdtype)
+ self.preference = self._as_uint16(preference)
+ if isinstance(locator64, bytes):
+ if len(locator64) != 8:
+ raise ValueError("invalid locator64")
+ self.locator64 = dns.rdata._hexify(locator64, 4, b":")
+ else:
+ dns.rdtypes.util.parse_formatted_hex(locator64, 4, 4, ":")
+ self.locator64 = locator64
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return f"{self.preference} {self.locator64}"
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ preference = tok.get_uint16()
+ locator64 = tok.get_identifier()
+ return cls(rdclass, rdtype, preference, locator64)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(struct.pack("!H", self.preference))
+ file.write(dns.rdtypes.util.parse_formatted_hex(self.locator64, 4, 4, ":"))
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ preference = parser.get_uint16()
+ locator64 = parser.get_remaining()
+ return cls(rdclass, rdtype, preference, locator64)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/LOC.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/LOC.py
new file mode 100644
index 00000000..1153cf03
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/LOC.py
@@ -0,0 +1,353 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+
+_pows = tuple(10**i for i in range(0, 11))
+
+# default values are in centimeters
+_default_size = 100.0
+_default_hprec = 1000000.0
+_default_vprec = 1000.0
+
+# for use by from_wire()
+_MAX_LATITUDE = 0x80000000 + 90 * 3600000
+_MIN_LATITUDE = 0x80000000 - 90 * 3600000
+_MAX_LONGITUDE = 0x80000000 + 180 * 3600000
+_MIN_LONGITUDE = 0x80000000 - 180 * 3600000
+
+
+def _exponent_of(what, desc):
+ if what == 0:
+ return 0
+ exp = None
+ for i, pow in enumerate(_pows):
+ if what < pow:
+ exp = i - 1
+ break
+ if exp is None or exp < 0:
+ raise dns.exception.SyntaxError(f"{desc} value out of bounds")
+ return exp
+
+
+def _float_to_tuple(what):
+ if what < 0:
+ sign = -1
+ what *= -1
+ else:
+ sign = 1
+ what = round(what * 3600000)
+ degrees = int(what // 3600000)
+ what -= degrees * 3600000
+ minutes = int(what // 60000)
+ what -= minutes * 60000
+ seconds = int(what // 1000)
+ what -= int(seconds * 1000)
+ what = int(what)
+ return (degrees, minutes, seconds, what, sign)
+
+
+def _tuple_to_float(what):
+ value = float(what[0])
+ value += float(what[1]) / 60.0
+ value += float(what[2]) / 3600.0
+ value += float(what[3]) / 3600000.0
+ return float(what[4]) * value
+
+
+def _encode_size(what, desc):
+ what = int(what)
+ exponent = _exponent_of(what, desc) & 0xF
+ base = what // pow(10, exponent) & 0xF
+ return base * 16 + exponent
+
+
+def _decode_size(what, desc):
+ exponent = what & 0x0F
+ if exponent > 9:
+ raise dns.exception.FormError(f"bad {desc} exponent")
+ base = (what & 0xF0) >> 4
+ if base > 9:
+ raise dns.exception.FormError(f"bad {desc} base")
+ return base * pow(10, exponent)
+
+
+def _check_coordinate_list(value, low, high):
+ if value[0] < low or value[0] > high:
+ raise ValueError(f"not in range [{low}, {high}]")
+ if value[1] < 0 or value[1] > 59:
+ raise ValueError("bad minutes value")
+ if value[2] < 0 or value[2] > 59:
+ raise ValueError("bad seconds value")
+ if value[3] < 0 or value[3] > 999:
+ raise ValueError("bad milliseconds value")
+ if value[4] != 1 and value[4] != -1:
+ raise ValueError("bad hemisphere value")
+
+
+@dns.immutable.immutable
+class LOC(dns.rdata.Rdata):
+ """LOC record"""
+
+ # see: RFC 1876
+
+ __slots__ = [
+ "latitude",
+ "longitude",
+ "altitude",
+ "size",
+ "horizontal_precision",
+ "vertical_precision",
+ ]
+
+ def __init__(
+ self,
+ rdclass,
+ rdtype,
+ latitude,
+ longitude,
+ altitude,
+ size=_default_size,
+ hprec=_default_hprec,
+ vprec=_default_vprec,
+ ):
+ """Initialize a LOC record instance.
+
+ The parameters I{latitude} and I{longitude} may be either a 4-tuple
+ of integers specifying (degrees, minutes, seconds, milliseconds),
+ or they may be floating point values specifying the number of
+ degrees. The other parameters are floats. Size, horizontal precision,
+ and vertical precision are specified in centimeters."""
+
+ super().__init__(rdclass, rdtype)
+ if isinstance(latitude, int):
+ latitude = float(latitude)
+ if isinstance(latitude, float):
+ latitude = _float_to_tuple(latitude)
+ _check_coordinate_list(latitude, -90, 90)
+ self.latitude = tuple(latitude)
+ if isinstance(longitude, int):
+ longitude = float(longitude)
+ if isinstance(longitude, float):
+ longitude = _float_to_tuple(longitude)
+ _check_coordinate_list(longitude, -180, 180)
+ self.longitude = tuple(longitude)
+ self.altitude = float(altitude)
+ self.size = float(size)
+ self.horizontal_precision = float(hprec)
+ self.vertical_precision = float(vprec)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ if self.latitude[4] > 0:
+ lat_hemisphere = "N"
+ else:
+ lat_hemisphere = "S"
+ if self.longitude[4] > 0:
+ long_hemisphere = "E"
+ else:
+ long_hemisphere = "W"
+ text = "%d %d %d.%03d %s %d %d %d.%03d %s %0.2fm" % (
+ self.latitude[0],
+ self.latitude[1],
+ self.latitude[2],
+ self.latitude[3],
+ lat_hemisphere,
+ self.longitude[0],
+ self.longitude[1],
+ self.longitude[2],
+ self.longitude[3],
+ long_hemisphere,
+ self.altitude / 100.0,
+ )
+
+ # do not print default values
+ if (
+ self.size != _default_size
+ or self.horizontal_precision != _default_hprec
+ or self.vertical_precision != _default_vprec
+ ):
+ text += (
+ f" {self.size / 100.0:0.2f}m {self.horizontal_precision / 100.0:0.2f}m"
+ f" {self.vertical_precision / 100.0:0.2f}m"
+ )
+ return text
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ latitude = [0, 0, 0, 0, 1]
+ longitude = [0, 0, 0, 0, 1]
+ size = _default_size
+ hprec = _default_hprec
+ vprec = _default_vprec
+
+ latitude[0] = tok.get_int()
+ t = tok.get_string()
+ if t.isdigit():
+ latitude[1] = int(t)
+ t = tok.get_string()
+ if "." in t:
+ (seconds, milliseconds) = t.split(".")
+ if not seconds.isdigit():
+ raise dns.exception.SyntaxError("bad latitude seconds value")
+ latitude[2] = int(seconds)
+ l = len(milliseconds)
+ if l == 0 or l > 3 or not milliseconds.isdigit():
+ raise dns.exception.SyntaxError("bad latitude milliseconds value")
+ if l == 1:
+ m = 100
+ elif l == 2:
+ m = 10
+ else:
+ m = 1
+ latitude[3] = m * int(milliseconds)
+ t = tok.get_string()
+ elif t.isdigit():
+ latitude[2] = int(t)
+ t = tok.get_string()
+ if t == "S":
+ latitude[4] = -1
+ elif t != "N":
+ raise dns.exception.SyntaxError("bad latitude hemisphere value")
+
+ longitude[0] = tok.get_int()
+ t = tok.get_string()
+ if t.isdigit():
+ longitude[1] = int(t)
+ t = tok.get_string()
+ if "." in t:
+ (seconds, milliseconds) = t.split(".")
+ if not seconds.isdigit():
+ raise dns.exception.SyntaxError("bad longitude seconds value")
+ longitude[2] = int(seconds)
+ l = len(milliseconds)
+ if l == 0 or l > 3 or not milliseconds.isdigit():
+ raise dns.exception.SyntaxError("bad longitude milliseconds value")
+ if l == 1:
+ m = 100
+ elif l == 2:
+ m = 10
+ else:
+ m = 1
+ longitude[3] = m * int(milliseconds)
+ t = tok.get_string()
+ elif t.isdigit():
+ longitude[2] = int(t)
+ t = tok.get_string()
+ if t == "W":
+ longitude[4] = -1
+ elif t != "E":
+ raise dns.exception.SyntaxError("bad longitude hemisphere value")
+
+ t = tok.get_string()
+ if t[-1] == "m":
+ t = t[0:-1]
+ altitude = float(t) * 100.0 # m -> cm
+
+ tokens = tok.get_remaining(max_tokens=3)
+ if len(tokens) >= 1:
+ value = tokens[0].unescape().value
+ if value[-1] == "m":
+ value = value[0:-1]
+ size = float(value) * 100.0 # m -> cm
+ if len(tokens) >= 2:
+ value = tokens[1].unescape().value
+ if value[-1] == "m":
+ value = value[0:-1]
+ hprec = float(value) * 100.0 # m -> cm
+ if len(tokens) >= 3:
+ value = tokens[2].unescape().value
+ if value[-1] == "m":
+ value = value[0:-1]
+ vprec = float(value) * 100.0 # m -> cm
+
+ # Try encoding these now so we raise if they are bad
+ _encode_size(size, "size")
+ _encode_size(hprec, "horizontal precision")
+ _encode_size(vprec, "vertical precision")
+
+ return cls(rdclass, rdtype, latitude, longitude, altitude, size, hprec, vprec)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ milliseconds = (
+ self.latitude[0] * 3600000
+ + self.latitude[1] * 60000
+ + self.latitude[2] * 1000
+ + self.latitude[3]
+ ) * self.latitude[4]
+ latitude = 0x80000000 + milliseconds
+ milliseconds = (
+ self.longitude[0] * 3600000
+ + self.longitude[1] * 60000
+ + self.longitude[2] * 1000
+ + self.longitude[3]
+ ) * self.longitude[4]
+ longitude = 0x80000000 + milliseconds
+ altitude = int(self.altitude) + 10000000
+ size = _encode_size(self.size, "size")
+ hprec = _encode_size(self.horizontal_precision, "horizontal precision")
+ vprec = _encode_size(self.vertical_precision, "vertical precision")
+ wire = struct.pack(
+ "!BBBBIII", 0, size, hprec, vprec, latitude, longitude, altitude
+ )
+ file.write(wire)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (
+ version,
+ size,
+ hprec,
+ vprec,
+ latitude,
+ longitude,
+ altitude,
+ ) = parser.get_struct("!BBBBIII")
+ if version != 0:
+ raise dns.exception.FormError("LOC version not zero")
+ if latitude < _MIN_LATITUDE or latitude > _MAX_LATITUDE:
+ raise dns.exception.FormError("bad latitude")
+ if latitude > 0x80000000:
+ latitude = (latitude - 0x80000000) / 3600000
+ else:
+ latitude = -1 * (0x80000000 - latitude) / 3600000
+ if longitude < _MIN_LONGITUDE or longitude > _MAX_LONGITUDE:
+ raise dns.exception.FormError("bad longitude")
+ if longitude > 0x80000000:
+ longitude = (longitude - 0x80000000) / 3600000
+ else:
+ longitude = -1 * (0x80000000 - longitude) / 3600000
+ altitude = float(altitude) - 10000000.0
+ size = _decode_size(size, "size")
+ hprec = _decode_size(hprec, "horizontal precision")
+ vprec = _decode_size(vprec, "vertical precision")
+ return cls(rdclass, rdtype, latitude, longitude, altitude, size, hprec, vprec)
+
+ @property
+ def float_latitude(self):
+ "latitude as a floating point value"
+ return _tuple_to_float(self.latitude)
+
+ @property
+ def float_longitude(self):
+ "longitude as a floating point value"
+ return _tuple_to_float(self.longitude)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/LP.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/LP.py
new file mode 100644
index 00000000..312663f1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/LP.py
@@ -0,0 +1,42 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import struct
+
+import dns.immutable
+import dns.rdata
+
+
+@dns.immutable.immutable
+class LP(dns.rdata.Rdata):
+ """LP record"""
+
+ # see: rfc6742.txt
+
+ __slots__ = ["preference", "fqdn"]
+
+ def __init__(self, rdclass, rdtype, preference, fqdn):
+ super().__init__(rdclass, rdtype)
+ self.preference = self._as_uint16(preference)
+ self.fqdn = self._as_name(fqdn)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ fqdn = self.fqdn.choose_relativity(origin, relativize)
+ return "%d %s" % (self.preference, fqdn)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ preference = tok.get_uint16()
+ fqdn = tok.get_name(origin, relativize, relativize_to)
+ return cls(rdclass, rdtype, preference, fqdn)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(struct.pack("!H", self.preference))
+ self.fqdn.to_wire(file, compress, origin, canonicalize)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ preference = parser.get_uint16()
+ fqdn = parser.get_name(origin)
+ return cls(rdclass, rdtype, preference, fqdn)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/MX.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/MX.py
new file mode 100644
index 00000000..0c300c5a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/MX.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.mxbase
+
+
+@dns.immutable.immutable
+class MX(dns.rdtypes.mxbase.MXBase):
+ """MX record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NID.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NID.py
new file mode 100644
index 00000000..2f649178
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NID.py
@@ -0,0 +1,47 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import struct
+
+import dns.immutable
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class NID(dns.rdata.Rdata):
+ """NID record"""
+
+ # see: rfc6742.txt
+
+ __slots__ = ["preference", "nodeid"]
+
+ def __init__(self, rdclass, rdtype, preference, nodeid):
+ super().__init__(rdclass, rdtype)
+ self.preference = self._as_uint16(preference)
+ if isinstance(nodeid, bytes):
+ if len(nodeid) != 8:
+ raise ValueError("invalid nodeid")
+ self.nodeid = dns.rdata._hexify(nodeid, 4, b":")
+ else:
+ dns.rdtypes.util.parse_formatted_hex(nodeid, 4, 4, ":")
+ self.nodeid = nodeid
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return f"{self.preference} {self.nodeid}"
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ preference = tok.get_uint16()
+ nodeid = tok.get_identifier()
+ return cls(rdclass, rdtype, preference, nodeid)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(struct.pack("!H", self.preference))
+ file.write(dns.rdtypes.util.parse_formatted_hex(self.nodeid, 4, 4, ":"))
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ preference = parser.get_uint16()
+ nodeid = parser.get_remaining()
+ return cls(rdclass, rdtype, preference, nodeid)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NINFO.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NINFO.py
new file mode 100644
index 00000000..b177bddb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NINFO.py
@@ -0,0 +1,26 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.txtbase
+
+
+@dns.immutable.immutable
+class NINFO(dns.rdtypes.txtbase.TXTBase):
+ """NINFO record"""
+
+ # see: draft-reid-dnsext-zs-01
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NS.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NS.py
new file mode 100644
index 00000000..c3f34ce9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NS.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.nsbase
+
+
+@dns.immutable.immutable
+class NS(dns.rdtypes.nsbase.NSBase):
+ """NS record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC.py
new file mode 100644
index 00000000..3c78b722
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC.py
@@ -0,0 +1,67 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdatatype
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class Bitmap(dns.rdtypes.util.Bitmap):
+ type_name = "NSEC"
+
+
+@dns.immutable.immutable
+class NSEC(dns.rdata.Rdata):
+ """NSEC record"""
+
+ __slots__ = ["next", "windows"]
+
+ def __init__(self, rdclass, rdtype, next, windows):
+ super().__init__(rdclass, rdtype)
+ self.next = self._as_name(next)
+ if not isinstance(windows, Bitmap):
+ windows = Bitmap(windows)
+ self.windows = tuple(windows.windows)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ next = self.next.choose_relativity(origin, relativize)
+ text = Bitmap(self.windows).to_text()
+ return f"{next}{text}"
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ next = tok.get_name(origin, relativize, relativize_to)
+ windows = Bitmap.from_text(tok)
+ return cls(rdclass, rdtype, next, windows)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ # Note that NSEC downcasing, originally mandated by RFC 4034
+ # section 6.2 was removed by RFC 6840 section 5.1.
+ self.next.to_wire(file, None, origin, False)
+ Bitmap(self.windows).to_wire(file)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ next = parser.get_name(origin)
+ bitmap = Bitmap.from_wire_parser(parser)
+ return cls(rdclass, rdtype, next, bitmap)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC3.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC3.py
new file mode 100644
index 00000000..d71302b7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC3.py
@@ -0,0 +1,126 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import binascii
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.rdtypes.util
+
+b32_hex_to_normal = bytes.maketrans(
+ b"0123456789ABCDEFGHIJKLMNOPQRSTUV", b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+)
+b32_normal_to_hex = bytes.maketrans(
+ b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", b"0123456789ABCDEFGHIJKLMNOPQRSTUV"
+)
+
+# hash algorithm constants
+SHA1 = 1
+
+# flag constants
+OPTOUT = 1
+
+
+@dns.immutable.immutable
+class Bitmap(dns.rdtypes.util.Bitmap):
+ type_name = "NSEC3"
+
+
+@dns.immutable.immutable
+class NSEC3(dns.rdata.Rdata):
+ """NSEC3 record"""
+
+ __slots__ = ["algorithm", "flags", "iterations", "salt", "next", "windows"]
+
+ def __init__(
+ self, rdclass, rdtype, algorithm, flags, iterations, salt, next, windows
+ ):
+ super().__init__(rdclass, rdtype)
+ self.algorithm = self._as_uint8(algorithm)
+ self.flags = self._as_uint8(flags)
+ self.iterations = self._as_uint16(iterations)
+ self.salt = self._as_bytes(salt, True, 255)
+ self.next = self._as_bytes(next, True, 255)
+ if not isinstance(windows, Bitmap):
+ windows = Bitmap(windows)
+ self.windows = tuple(windows.windows)
+
+ def _next_text(self):
+ next = base64.b32encode(self.next).translate(b32_normal_to_hex).lower().decode()
+ next = next.rstrip("=")
+ return next
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ next = self._next_text()
+ if self.salt == b"":
+ salt = "-"
+ else:
+ salt = binascii.hexlify(self.salt).decode()
+ text = Bitmap(self.windows).to_text()
+ return "%u %u %u %s %s%s" % (
+ self.algorithm,
+ self.flags,
+ self.iterations,
+ salt,
+ next,
+ text,
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ algorithm = tok.get_uint8()
+ flags = tok.get_uint8()
+ iterations = tok.get_uint16()
+ salt = tok.get_string()
+ if salt == "-":
+ salt = b""
+ else:
+ salt = binascii.unhexlify(salt.encode("ascii"))
+ next = tok.get_string().encode("ascii").upper().translate(b32_hex_to_normal)
+ if next.endswith(b"="):
+ raise binascii.Error("Incorrect padding")
+ if len(next) % 8 != 0:
+ next += b"=" * (8 - len(next) % 8)
+ next = base64.b32decode(next)
+ bitmap = Bitmap.from_text(tok)
+ return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next, bitmap)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ l = len(self.salt)
+ file.write(struct.pack("!BBHB", self.algorithm, self.flags, self.iterations, l))
+ file.write(self.salt)
+ l = len(self.next)
+ file.write(struct.pack("!B", l))
+ file.write(self.next)
+ Bitmap(self.windows).to_wire(file)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (algorithm, flags, iterations) = parser.get_struct("!BBH")
+ salt = parser.get_counted_bytes()
+ next = parser.get_counted_bytes()
+ bitmap = Bitmap.from_wire_parser(parser)
+ return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next, bitmap)
+
+ def next_name(self, origin=None):
+ return dns.name.from_text(self._next_text(), origin)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py
new file mode 100644
index 00000000..d1e62ebc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py
@@ -0,0 +1,69 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+
+
+@dns.immutable.immutable
+class NSEC3PARAM(dns.rdata.Rdata):
+ """NSEC3PARAM record"""
+
+ __slots__ = ["algorithm", "flags", "iterations", "salt"]
+
+ def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt):
+ super().__init__(rdclass, rdtype)
+ self.algorithm = self._as_uint8(algorithm)
+ self.flags = self._as_uint8(flags)
+ self.iterations = self._as_uint16(iterations)
+ self.salt = self._as_bytes(salt, True, 255)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ if self.salt == b"":
+ salt = "-"
+ else:
+ salt = binascii.hexlify(self.salt).decode()
+ return "%u %u %u %s" % (self.algorithm, self.flags, self.iterations, salt)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ algorithm = tok.get_uint8()
+ flags = tok.get_uint8()
+ iterations = tok.get_uint16()
+ salt = tok.get_string()
+ if salt == "-":
+ salt = ""
+ else:
+ salt = binascii.unhexlify(salt.encode())
+ return cls(rdclass, rdtype, algorithm, flags, iterations, salt)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ l = len(self.salt)
+ file.write(struct.pack("!BBHB", self.algorithm, self.flags, self.iterations, l))
+ file.write(self.salt)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (algorithm, flags, iterations) = parser.get_struct("!BBH")
+ salt = parser.get_counted_bytes()
+ return cls(rdclass, rdtype, algorithm, flags, iterations, salt)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py
new file mode 100644
index 00000000..4d7a4b6c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py
@@ -0,0 +1,53 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2016 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class OPENPGPKEY(dns.rdata.Rdata):
+ """OPENPGPKEY record"""
+
+ # see: RFC 7929
+
+ def __init__(self, rdclass, rdtype, key):
+ super().__init__(rdclass, rdtype)
+ self.key = self._as_bytes(key)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return dns.rdata._base64ify(self.key, chunksize=None, **kw)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ b64 = tok.concatenate_remaining_identifiers().encode()
+ key = base64.b64decode(b64)
+ return cls(rdclass, rdtype, key)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(self.key)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ key = parser.get_remaining()
+ return cls(rdclass, rdtype, key)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/OPT.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/OPT.py
new file mode 100644
index 00000000..d343dfa5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/OPT.py
@@ -0,0 +1,77 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.edns
+import dns.exception
+import dns.immutable
+import dns.rdata
+
+# We don't implement from_text, and that's ok.
+# pylint: disable=abstract-method
+
+
+@dns.immutable.immutable
+class OPT(dns.rdata.Rdata):
+ """OPT record"""
+
+ __slots__ = ["options"]
+
+ def __init__(self, rdclass, rdtype, options):
+ """Initialize an OPT rdata.
+
+ *rdclass*, an ``int`` is the rdataclass of the Rdata,
+ which is also the payload size.
+
+ *rdtype*, an ``int`` is the rdatatype of the Rdata.
+
+ *options*, a tuple of ``bytes``
+ """
+
+ super().__init__(rdclass, rdtype)
+
+ def as_option(option):
+ if not isinstance(option, dns.edns.Option):
+ raise ValueError("option is not a dns.edns.option")
+ return option
+
+ self.options = self._as_tuple(options, as_option)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ for opt in self.options:
+ owire = opt.to_wire()
+ file.write(struct.pack("!HH", opt.otype, len(owire)))
+ file.write(owire)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return " ".join(opt.to_text() for opt in self.options)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ options = []
+ while parser.remaining() > 0:
+ (otype, olen) = parser.get_struct("!HH")
+ with parser.restrict_to(olen):
+ opt = dns.edns.option_from_wire_parser(otype, parser)
+ options.append(opt)
+ return cls(rdclass, rdtype, options)
+
+ @property
+ def payload(self):
+ "payload size"
+ return self.rdclass
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/PTR.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/PTR.py
new file mode 100644
index 00000000..98c36167
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/PTR.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.nsbase
+
+
+@dns.immutable.immutable
+class PTR(dns.rdtypes.nsbase.NSBase):
+ """PTR record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RESINFO.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RESINFO.py
new file mode 100644
index 00000000..76c8ea2a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RESINFO.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.txtbase
+
+
+@dns.immutable.immutable
+class RESINFO(dns.rdtypes.txtbase.TXTBase):
+ """RESINFO record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RP.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RP.py
new file mode 100644
index 00000000..a66cfc50
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RP.py
@@ -0,0 +1,58 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+
+
+@dns.immutable.immutable
+class RP(dns.rdata.Rdata):
+ """RP record"""
+
+ # see: RFC 1183
+
+ __slots__ = ["mbox", "txt"]
+
+ def __init__(self, rdclass, rdtype, mbox, txt):
+ super().__init__(rdclass, rdtype)
+ self.mbox = self._as_name(mbox)
+ self.txt = self._as_name(txt)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ mbox = self.mbox.choose_relativity(origin, relativize)
+ txt = self.txt.choose_relativity(origin, relativize)
+ return f"{str(mbox)} {str(txt)}"
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ mbox = tok.get_name(origin, relativize, relativize_to)
+ txt = tok.get_name(origin, relativize, relativize_to)
+ return cls(rdclass, rdtype, mbox, txt)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ self.mbox.to_wire(file, None, origin, canonicalize)
+ self.txt.to_wire(file, None, origin, canonicalize)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ mbox = parser.get_name(origin)
+ txt = parser.get_name(origin)
+ return cls(rdclass, rdtype, mbox, txt)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RRSIG.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RRSIG.py
new file mode 100644
index 00000000..8beb4237
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RRSIG.py
@@ -0,0 +1,157 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import calendar
+import struct
+import time
+
+import dns.dnssectypes
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+
+
+class BadSigTime(dns.exception.DNSException):
+ """Time in DNS SIG or RRSIG resource record cannot be parsed."""
+
+
+def sigtime_to_posixtime(what):
+ if len(what) <= 10 and what.isdigit():
+ return int(what)
+ if len(what) != 14:
+ raise BadSigTime
+ year = int(what[0:4])
+ month = int(what[4:6])
+ day = int(what[6:8])
+ hour = int(what[8:10])
+ minute = int(what[10:12])
+ second = int(what[12:14])
+ return calendar.timegm((year, month, day, hour, minute, second, 0, 0, 0))
+
+
+def posixtime_to_sigtime(what):
+ return time.strftime("%Y%m%d%H%M%S", time.gmtime(what))
+
+
+@dns.immutable.immutable
+class RRSIG(dns.rdata.Rdata):
+ """RRSIG record"""
+
+ __slots__ = [
+ "type_covered",
+ "algorithm",
+ "labels",
+ "original_ttl",
+ "expiration",
+ "inception",
+ "key_tag",
+ "signer",
+ "signature",
+ ]
+
+ def __init__(
+ self,
+ rdclass,
+ rdtype,
+ type_covered,
+ algorithm,
+ labels,
+ original_ttl,
+ expiration,
+ inception,
+ key_tag,
+ signer,
+ signature,
+ ):
+ super().__init__(rdclass, rdtype)
+ self.type_covered = self._as_rdatatype(type_covered)
+ self.algorithm = dns.dnssectypes.Algorithm.make(algorithm)
+ self.labels = self._as_uint8(labels)
+ self.original_ttl = self._as_ttl(original_ttl)
+ self.expiration = self._as_uint32(expiration)
+ self.inception = self._as_uint32(inception)
+ self.key_tag = self._as_uint16(key_tag)
+ self.signer = self._as_name(signer)
+ self.signature = self._as_bytes(signature)
+
+ def covers(self):
+ return self.type_covered
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return "%s %d %d %d %s %s %d %s %s" % (
+ dns.rdatatype.to_text(self.type_covered),
+ self.algorithm,
+ self.labels,
+ self.original_ttl,
+ posixtime_to_sigtime(self.expiration),
+ posixtime_to_sigtime(self.inception),
+ self.key_tag,
+ self.signer.choose_relativity(origin, relativize),
+ dns.rdata._base64ify(self.signature, **kw),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ type_covered = dns.rdatatype.from_text(tok.get_string())
+ algorithm = dns.dnssectypes.Algorithm.from_text(tok.get_string())
+ labels = tok.get_int()
+ original_ttl = tok.get_ttl()
+ expiration = sigtime_to_posixtime(tok.get_string())
+ inception = sigtime_to_posixtime(tok.get_string())
+ key_tag = tok.get_int()
+ signer = tok.get_name(origin, relativize, relativize_to)
+ b64 = tok.concatenate_remaining_identifiers().encode()
+ signature = base64.b64decode(b64)
+ return cls(
+ rdclass,
+ rdtype,
+ type_covered,
+ algorithm,
+ labels,
+ original_ttl,
+ expiration,
+ inception,
+ key_tag,
+ signer,
+ signature,
+ )
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ header = struct.pack(
+ "!HBBIIIH",
+ self.type_covered,
+ self.algorithm,
+ self.labels,
+ self.original_ttl,
+ self.expiration,
+ self.inception,
+ self.key_tag,
+ )
+ file.write(header)
+ self.signer.to_wire(file, None, origin, canonicalize)
+ file.write(self.signature)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ header = parser.get_struct("!HBBIIIH")
+ signer = parser.get_name(origin)
+ signature = parser.get_remaining()
+ return cls(rdclass, rdtype, *header, signer, signature)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RT.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RT.py
new file mode 100644
index 00000000..5a4d45cf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/RT.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.mxbase
+
+
+@dns.immutable.immutable
+class RT(dns.rdtypes.mxbase.UncompressedDowncasingMX):
+ """RT record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SMIMEA.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SMIMEA.py
new file mode 100644
index 00000000..55d87bf8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SMIMEA.py
@@ -0,0 +1,9 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.immutable
+import dns.rdtypes.tlsabase
+
+
+@dns.immutable.immutable
+class SMIMEA(dns.rdtypes.tlsabase.TLSABase):
+ """SMIMEA record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SOA.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SOA.py
new file mode 100644
index 00000000..09aa8321
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SOA.py
@@ -0,0 +1,86 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+
+
+@dns.immutable.immutable
+class SOA(dns.rdata.Rdata):
+ """SOA record"""
+
+ # see: RFC 1035
+
+ __slots__ = ["mname", "rname", "serial", "refresh", "retry", "expire", "minimum"]
+
+ def __init__(
+ self, rdclass, rdtype, mname, rname, serial, refresh, retry, expire, minimum
+ ):
+ super().__init__(rdclass, rdtype)
+ self.mname = self._as_name(mname)
+ self.rname = self._as_name(rname)
+ self.serial = self._as_uint32(serial)
+ self.refresh = self._as_ttl(refresh)
+ self.retry = self._as_ttl(retry)
+ self.expire = self._as_ttl(expire)
+ self.minimum = self._as_ttl(minimum)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ mname = self.mname.choose_relativity(origin, relativize)
+ rname = self.rname.choose_relativity(origin, relativize)
+ return "%s %s %d %d %d %d %d" % (
+ mname,
+ rname,
+ self.serial,
+ self.refresh,
+ self.retry,
+ self.expire,
+ self.minimum,
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ mname = tok.get_name(origin, relativize, relativize_to)
+ rname = tok.get_name(origin, relativize, relativize_to)
+ serial = tok.get_uint32()
+ refresh = tok.get_ttl()
+ retry = tok.get_ttl()
+ expire = tok.get_ttl()
+ minimum = tok.get_ttl()
+ return cls(
+ rdclass, rdtype, mname, rname, serial, refresh, retry, expire, minimum
+ )
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ self.mname.to_wire(file, compress, origin, canonicalize)
+ self.rname.to_wire(file, compress, origin, canonicalize)
+ five_ints = struct.pack(
+ "!IIIII", self.serial, self.refresh, self.retry, self.expire, self.minimum
+ )
+ file.write(five_ints)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ mname = parser.get_name(origin)
+ rname = parser.get_name(origin)
+ return cls(rdclass, rdtype, mname, rname, *parser.get_struct("!IIIII"))
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SPF.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SPF.py
new file mode 100644
index 00000000..1df3b705
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SPF.py
@@ -0,0 +1,26 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.txtbase
+
+
+@dns.immutable.immutable
+class SPF(dns.rdtypes.txtbase.TXTBase):
+ """SPF record"""
+
+ # see: RFC 4408
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SSHFP.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SSHFP.py
new file mode 100644
index 00000000..d2c4b073
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/SSHFP.py
@@ -0,0 +1,68 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+import struct
+
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+
+
+@dns.immutable.immutable
+class SSHFP(dns.rdata.Rdata):
+ """SSHFP record"""
+
+ # See RFC 4255
+
+ __slots__ = ["algorithm", "fp_type", "fingerprint"]
+
+ def __init__(self, rdclass, rdtype, algorithm, fp_type, fingerprint):
+ super().__init__(rdclass, rdtype)
+ self.algorithm = self._as_uint8(algorithm)
+ self.fp_type = self._as_uint8(fp_type)
+ self.fingerprint = self._as_bytes(fingerprint, True)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ kw = kw.copy()
+ chunksize = kw.pop("chunksize", 128)
+ return "%d %d %s" % (
+ self.algorithm,
+ self.fp_type,
+ dns.rdata._hexify(self.fingerprint, chunksize=chunksize, **kw),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ algorithm = tok.get_uint8()
+ fp_type = tok.get_uint8()
+ fingerprint = tok.concatenate_remaining_identifiers().encode()
+ fingerprint = binascii.unhexlify(fingerprint)
+ return cls(rdclass, rdtype, algorithm, fp_type, fingerprint)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ header = struct.pack("!BB", self.algorithm, self.fp_type)
+ file.write(header)
+ file.write(self.fingerprint)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ header = parser.get_struct("BB")
+ fingerprint = parser.get_remaining()
+ return cls(rdclass, rdtype, header[0], header[1], fingerprint)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TKEY.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TKEY.py
new file mode 100644
index 00000000..75f62249
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TKEY.py
@@ -0,0 +1,142 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+
+
+@dns.immutable.immutable
+class TKEY(dns.rdata.Rdata):
+ """TKEY Record"""
+
+ __slots__ = [
+ "algorithm",
+ "inception",
+ "expiration",
+ "mode",
+ "error",
+ "key",
+ "other",
+ ]
+
+ def __init__(
+ self,
+ rdclass,
+ rdtype,
+ algorithm,
+ inception,
+ expiration,
+ mode,
+ error,
+ key,
+ other=b"",
+ ):
+ super().__init__(rdclass, rdtype)
+ self.algorithm = self._as_name(algorithm)
+ self.inception = self._as_uint32(inception)
+ self.expiration = self._as_uint32(expiration)
+ self.mode = self._as_uint16(mode)
+ self.error = self._as_uint16(error)
+ self.key = self._as_bytes(key)
+ self.other = self._as_bytes(other)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ _algorithm = self.algorithm.choose_relativity(origin, relativize)
+ text = "%s %u %u %u %u %s" % (
+ str(_algorithm),
+ self.inception,
+ self.expiration,
+ self.mode,
+ self.error,
+ dns.rdata._base64ify(self.key, 0),
+ )
+ if len(self.other) > 0:
+ text += f" {dns.rdata._base64ify(self.other, 0)}"
+
+ return text
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ algorithm = tok.get_name(relativize=False)
+ inception = tok.get_uint32()
+ expiration = tok.get_uint32()
+ mode = tok.get_uint16()
+ error = tok.get_uint16()
+ key_b64 = tok.get_string().encode()
+ key = base64.b64decode(key_b64)
+ other_b64 = tok.concatenate_remaining_identifiers(True).encode()
+ other = base64.b64decode(other_b64)
+
+ return cls(
+ rdclass, rdtype, algorithm, inception, expiration, mode, error, key, other
+ )
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ self.algorithm.to_wire(file, compress, origin)
+ file.write(
+ struct.pack("!IIHH", self.inception, self.expiration, self.mode, self.error)
+ )
+ file.write(struct.pack("!H", len(self.key)))
+ file.write(self.key)
+ file.write(struct.pack("!H", len(self.other)))
+ if len(self.other) > 0:
+ file.write(self.other)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ algorithm = parser.get_name(origin)
+ inception, expiration, mode, error = parser.get_struct("!IIHH")
+ key = parser.get_counted_bytes(2)
+ other = parser.get_counted_bytes(2)
+
+ return cls(
+ rdclass, rdtype, algorithm, inception, expiration, mode, error, key, other
+ )
+
+ # Constants for the mode field - from RFC 2930:
+ # 2.5 The Mode Field
+ #
+ # The mode field specifies the general scheme for key agreement or
+ # the purpose of the TKEY DNS message. Servers and resolvers
+ # supporting this specification MUST implement the Diffie-Hellman key
+ # agreement mode and the key deletion mode for queries. All other
+ # modes are OPTIONAL. A server supporting TKEY that receives a TKEY
+ # request with a mode it does not support returns the BADMODE error.
+ # The following values of the Mode octet are defined, available, or
+ # reserved:
+ #
+ # Value Description
+ # ----- -----------
+ # 0 - reserved, see section 7
+ # 1 server assignment
+ # 2 Diffie-Hellman exchange
+ # 3 GSS-API negotiation
+ # 4 resolver assignment
+ # 5 key deletion
+ # 6-65534 - available, see section 7
+ # 65535 - reserved, see section 7
+ SERVER_ASSIGNMENT = 1
+ DIFFIE_HELLMAN_EXCHANGE = 2
+ GSSAPI_NEGOTIATION = 3
+ RESOLVER_ASSIGNMENT = 4
+ KEY_DELETION = 5
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TLSA.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TLSA.py
new file mode 100644
index 00000000..4dffc553
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TLSA.py
@@ -0,0 +1,9 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.immutable
+import dns.rdtypes.tlsabase
+
+
+@dns.immutable.immutable
+class TLSA(dns.rdtypes.tlsabase.TLSABase):
+ """TLSA record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TSIG.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TSIG.py
new file mode 100644
index 00000000..79423826
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TSIG.py
@@ -0,0 +1,160 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rcode
+import dns.rdata
+
+
+@dns.immutable.immutable
+class TSIG(dns.rdata.Rdata):
+ """TSIG record"""
+
+ __slots__ = [
+ "algorithm",
+ "time_signed",
+ "fudge",
+ "mac",
+ "original_id",
+ "error",
+ "other",
+ ]
+
+ def __init__(
+ self,
+ rdclass,
+ rdtype,
+ algorithm,
+ time_signed,
+ fudge,
+ mac,
+ original_id,
+ error,
+ other,
+ ):
+ """Initialize a TSIG rdata.
+
+ *rdclass*, an ``int`` is the rdataclass of the Rdata.
+
+ *rdtype*, an ``int`` is the rdatatype of the Rdata.
+
+ *algorithm*, a ``dns.name.Name``.
+
+ *time_signed*, an ``int``.
+
+ *fudge*, an ``int`.
+
+ *mac*, a ``bytes``
+
+ *original_id*, an ``int``
+
+ *error*, an ``int``
+
+ *other*, a ``bytes``
+ """
+
+ super().__init__(rdclass, rdtype)
+ self.algorithm = self._as_name(algorithm)
+ self.time_signed = self._as_uint48(time_signed)
+ self.fudge = self._as_uint16(fudge)
+ self.mac = self._as_bytes(mac)
+ self.original_id = self._as_uint16(original_id)
+ self.error = dns.rcode.Rcode.make(error)
+ self.other = self._as_bytes(other)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ algorithm = self.algorithm.choose_relativity(origin, relativize)
+ error = dns.rcode.to_text(self.error, True)
+ text = (
+ f"{algorithm} {self.time_signed} {self.fudge} "
+ + f"{len(self.mac)} {dns.rdata._base64ify(self.mac, 0)} "
+ + f"{self.original_id} {error} {len(self.other)}"
+ )
+ if self.other:
+ text += f" {dns.rdata._base64ify(self.other, 0)}"
+ return text
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ algorithm = tok.get_name(relativize=False)
+ time_signed = tok.get_uint48()
+ fudge = tok.get_uint16()
+ mac_len = tok.get_uint16()
+ mac = base64.b64decode(tok.get_string())
+ if len(mac) != mac_len:
+ raise SyntaxError("invalid MAC")
+ original_id = tok.get_uint16()
+ error = dns.rcode.from_text(tok.get_string())
+ other_len = tok.get_uint16()
+ if other_len > 0:
+ other = base64.b64decode(tok.get_string())
+ if len(other) != other_len:
+ raise SyntaxError("invalid other data")
+ else:
+ other = b""
+ return cls(
+ rdclass,
+ rdtype,
+ algorithm,
+ time_signed,
+ fudge,
+ mac,
+ original_id,
+ error,
+ other,
+ )
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ self.algorithm.to_wire(file, None, origin, False)
+ file.write(
+ struct.pack(
+ "!HIHH",
+ (self.time_signed >> 32) & 0xFFFF,
+ self.time_signed & 0xFFFFFFFF,
+ self.fudge,
+ len(self.mac),
+ )
+ )
+ file.write(self.mac)
+ file.write(struct.pack("!HHH", self.original_id, self.error, len(self.other)))
+ file.write(self.other)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ algorithm = parser.get_name()
+ time_signed = parser.get_uint48()
+ fudge = parser.get_uint16()
+ mac = parser.get_counted_bytes(2)
+ (original_id, error) = parser.get_struct("!HH")
+ other = parser.get_counted_bytes(2)
+ return cls(
+ rdclass,
+ rdtype,
+ algorithm,
+ time_signed,
+ fudge,
+ mac,
+ original_id,
+ error,
+ other,
+ )
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TXT.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TXT.py
new file mode 100644
index 00000000..6d4dae27
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/TXT.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.txtbase
+
+
+@dns.immutable.immutable
+class TXT(dns.rdtypes.txtbase.TXTBase):
+ """TXT record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/URI.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/URI.py
new file mode 100644
index 00000000..2efbb305
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/URI.py
@@ -0,0 +1,79 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class URI(dns.rdata.Rdata):
+ """URI record"""
+
+ # see RFC 7553
+
+ __slots__ = ["priority", "weight", "target"]
+
+ def __init__(self, rdclass, rdtype, priority, weight, target):
+ super().__init__(rdclass, rdtype)
+ self.priority = self._as_uint16(priority)
+ self.weight = self._as_uint16(weight)
+ self.target = self._as_bytes(target, True)
+ if len(self.target) == 0:
+ raise dns.exception.SyntaxError("URI target cannot be empty")
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return '%d %d "%s"' % (self.priority, self.weight, self.target.decode())
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ priority = tok.get_uint16()
+ weight = tok.get_uint16()
+ target = tok.get().unescape()
+ if not (target.is_quoted_string() or target.is_identifier()):
+ raise dns.exception.SyntaxError("URI target must be a string")
+ return cls(rdclass, rdtype, priority, weight, target.value)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ two_ints = struct.pack("!HH", self.priority, self.weight)
+ file.write(two_ints)
+ file.write(self.target)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (priority, weight) = parser.get_struct("!HH")
+ target = parser.get_remaining()
+ if len(target) == 0:
+ raise dns.exception.FormError("URI target may not be empty")
+ return cls(rdclass, rdtype, priority, weight, target)
+
+ def _processing_priority(self):
+ return self.priority
+
+ def _processing_weight(self):
+ return self.weight
+
+ @classmethod
+ def _processing_order(cls, iterable):
+ return dns.rdtypes.util.weighted_processing_order(iterable)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/WALLET.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/WALLET.py
new file mode 100644
index 00000000..ff464763
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/WALLET.py
@@ -0,0 +1,9 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.immutable
+import dns.rdtypes.txtbase
+
+
+@dns.immutable.immutable
+class WALLET(dns.rdtypes.txtbase.TXTBase):
+ """WALLET record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/X25.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/X25.py
new file mode 100644
index 00000000..2436ddb6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/X25.py
@@ -0,0 +1,57 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class X25(dns.rdata.Rdata):
+ """X25 record"""
+
+ # see RFC 1183
+
+ __slots__ = ["address"]
+
+ def __init__(self, rdclass, rdtype, address):
+ super().__init__(rdclass, rdtype)
+ self.address = self._as_bytes(address, True, 255)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return f'"{dns.rdata._escapify(self.address)}"'
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ address = tok.get_string()
+ return cls(rdclass, rdtype, address)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ l = len(self.address)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(self.address)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ address = parser.get_counted_bytes()
+ return cls(rdclass, rdtype, address)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/ZONEMD.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/ZONEMD.py
new file mode 100644
index 00000000..c90e3ee1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/ZONEMD.py
@@ -0,0 +1,66 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import binascii
+import struct
+
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.zonetypes
+
+
+@dns.immutable.immutable
+class ZONEMD(dns.rdata.Rdata):
+ """ZONEMD record"""
+
+ # See RFC 8976
+
+ __slots__ = ["serial", "scheme", "hash_algorithm", "digest"]
+
+ def __init__(self, rdclass, rdtype, serial, scheme, hash_algorithm, digest):
+ super().__init__(rdclass, rdtype)
+ self.serial = self._as_uint32(serial)
+ self.scheme = dns.zonetypes.DigestScheme.make(scheme)
+ self.hash_algorithm = dns.zonetypes.DigestHashAlgorithm.make(hash_algorithm)
+ self.digest = self._as_bytes(digest)
+
+ if self.scheme == 0: # reserved, RFC 8976 Sec. 5.2
+ raise ValueError("scheme 0 is reserved")
+ if self.hash_algorithm == 0: # reserved, RFC 8976 Sec. 5.3
+ raise ValueError("hash_algorithm 0 is reserved")
+
+ hasher = dns.zonetypes._digest_hashers.get(self.hash_algorithm)
+ if hasher and hasher().digest_size != len(self.digest):
+ raise ValueError("digest length inconsistent with hash algorithm")
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ kw = kw.copy()
+ chunksize = kw.pop("chunksize", 128)
+ return "%d %d %d %s" % (
+ self.serial,
+ self.scheme,
+ self.hash_algorithm,
+ dns.rdata._hexify(self.digest, chunksize=chunksize, **kw),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ serial = tok.get_uint32()
+ scheme = tok.get_uint8()
+ hash_algorithm = tok.get_uint8()
+ digest = tok.concatenate_remaining_identifiers().encode()
+ digest = binascii.unhexlify(digest)
+ return cls(rdclass, rdtype, serial, scheme, hash_algorithm, digest)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ header = struct.pack("!IBB", self.serial, self.scheme, self.hash_algorithm)
+ file.write(header)
+ file.write(self.digest)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ header = parser.get_struct("!IBB")
+ digest = parser.get_remaining()
+ return cls(rdclass, rdtype, header[0], header[1], header[2], digest)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/__init__.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/__init__.py
new file mode 100644
index 00000000..647b215b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/ANY/__init__.py
@@ -0,0 +1,70 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Class ANY (generic) rdata type classes."""
+
+__all__ = [
+ "AFSDB",
+ "AMTRELAY",
+ "AVC",
+ "CAA",
+ "CDNSKEY",
+ "CDS",
+ "CERT",
+ "CNAME",
+ "CSYNC",
+ "DLV",
+ "DNAME",
+ "DNSKEY",
+ "DS",
+ "EUI48",
+ "EUI64",
+ "GPOS",
+ "HINFO",
+ "HIP",
+ "ISDN",
+ "L32",
+ "L64",
+ "LOC",
+ "LP",
+ "MX",
+ "NID",
+ "NINFO",
+ "NS",
+ "NSEC",
+ "NSEC3",
+ "NSEC3PARAM",
+ "OPENPGPKEY",
+ "OPT",
+ "PTR",
+ "RESINFO",
+ "RP",
+ "RRSIG",
+ "RT",
+ "SMIMEA",
+ "SOA",
+ "SPF",
+ "SSHFP",
+ "TKEY",
+ "TLSA",
+ "TSIG",
+ "TXT",
+ "URI",
+ "WALLET",
+ "X25",
+ "ZONEMD",
+]
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/CH/A.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/CH/A.py
new file mode 100644
index 00000000..832e8d3a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/CH/A.py
@@ -0,0 +1,59 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.immutable
+import dns.rdtypes.mxbase
+
+
+@dns.immutable.immutable
+class A(dns.rdata.Rdata):
+ """A record for Chaosnet"""
+
+ # domain: the domain of the address
+ # address: the 16-bit address
+
+ __slots__ = ["domain", "address"]
+
+ def __init__(self, rdclass, rdtype, domain, address):
+ super().__init__(rdclass, rdtype)
+ self.domain = self._as_name(domain)
+ self.address = self._as_uint16(address)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ domain = self.domain.choose_relativity(origin, relativize)
+ return f"{domain} {self.address:o}"
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ domain = tok.get_name(origin, relativize, relativize_to)
+ address = tok.get_uint16(base=8)
+ return cls(rdclass, rdtype, domain, address)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ self.domain.to_wire(file, compress, origin, canonicalize)
+ pref = struct.pack("!H", self.address)
+ file.write(pref)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ domain = parser.get_name(origin)
+ address = parser.get_uint16()
+ return cls(rdclass, rdtype, domain, address)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/CH/__init__.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/CH/__init__.py
new file mode 100644
index 00000000..0760c26c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/CH/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Class CH rdata type classes."""
+
+__all__ = [
+ "A",
+]
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/A.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/A.py
new file mode 100644
index 00000000..e09d6110
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/A.py
@@ -0,0 +1,51 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.exception
+import dns.immutable
+import dns.ipv4
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class A(dns.rdata.Rdata):
+ """A record."""
+
+ __slots__ = ["address"]
+
+ def __init__(self, rdclass, rdtype, address):
+ super().__init__(rdclass, rdtype)
+ self.address = self._as_ipv4_address(address)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return self.address
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ address = tok.get_identifier()
+ return cls(rdclass, rdtype, address)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(dns.ipv4.inet_aton(self.address))
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ address = parser.get_remaining()
+ return cls(rdclass, rdtype, address)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/AAAA.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/AAAA.py
new file mode 100644
index 00000000..0cd139e7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/AAAA.py
@@ -0,0 +1,51 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.exception
+import dns.immutable
+import dns.ipv6
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class AAAA(dns.rdata.Rdata):
+ """AAAA record."""
+
+ __slots__ = ["address"]
+
+ def __init__(self, rdclass, rdtype, address):
+ super().__init__(rdclass, rdtype)
+ self.address = self._as_ipv6_address(address)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return self.address
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ address = tok.get_identifier()
+ return cls(rdclass, rdtype, address)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(dns.ipv6.inet_aton(self.address))
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ address = parser.get_remaining()
+ return cls(rdclass, rdtype, address)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/APL.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/APL.py
new file mode 100644
index 00000000..44cb3fef
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/APL.py
@@ -0,0 +1,150 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+import codecs
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.ipv4
+import dns.ipv6
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class APLItem:
+ """An APL list item."""
+
+ __slots__ = ["family", "negation", "address", "prefix"]
+
+ def __init__(self, family, negation, address, prefix):
+ self.family = dns.rdata.Rdata._as_uint16(family)
+ self.negation = dns.rdata.Rdata._as_bool(negation)
+ if self.family == 1:
+ self.address = dns.rdata.Rdata._as_ipv4_address(address)
+ self.prefix = dns.rdata.Rdata._as_int(prefix, 0, 32)
+ elif self.family == 2:
+ self.address = dns.rdata.Rdata._as_ipv6_address(address)
+ self.prefix = dns.rdata.Rdata._as_int(prefix, 0, 128)
+ else:
+ self.address = dns.rdata.Rdata._as_bytes(address, max_length=127)
+ self.prefix = dns.rdata.Rdata._as_uint8(prefix)
+
+ def __str__(self):
+ if self.negation:
+ return "!%d:%s/%s" % (self.family, self.address, self.prefix)
+ else:
+ return "%d:%s/%s" % (self.family, self.address, self.prefix)
+
+ def to_wire(self, file):
+ if self.family == 1:
+ address = dns.ipv4.inet_aton(self.address)
+ elif self.family == 2:
+ address = dns.ipv6.inet_aton(self.address)
+ else:
+ address = binascii.unhexlify(self.address)
+ #
+ # Truncate least significant zero bytes.
+ #
+ last = 0
+ for i in range(len(address) - 1, -1, -1):
+ if address[i] != 0:
+ last = i + 1
+ break
+ address = address[0:last]
+ l = len(address)
+ assert l < 128
+ if self.negation:
+ l |= 0x80
+ header = struct.pack("!HBB", self.family, self.prefix, l)
+ file.write(header)
+ file.write(address)
+
+
+@dns.immutable.immutable
+class APL(dns.rdata.Rdata):
+ """APL record."""
+
+ # see: RFC 3123
+
+ __slots__ = ["items"]
+
+ def __init__(self, rdclass, rdtype, items):
+ super().__init__(rdclass, rdtype)
+ for item in items:
+ if not isinstance(item, APLItem):
+ raise ValueError("item not an APLItem")
+ self.items = tuple(items)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return " ".join(map(str, self.items))
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ items = []
+ for token in tok.get_remaining():
+ item = token.unescape().value
+ if item[0] == "!":
+ negation = True
+ item = item[1:]
+ else:
+ negation = False
+ (family, rest) = item.split(":", 1)
+ family = int(family)
+ (address, prefix) = rest.split("/", 1)
+ prefix = int(prefix)
+ item = APLItem(family, negation, address, prefix)
+ items.append(item)
+
+ return cls(rdclass, rdtype, items)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ for item in self.items:
+ item.to_wire(file)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ items = []
+ while parser.remaining() > 0:
+ header = parser.get_struct("!HBB")
+ afdlen = header[2]
+ if afdlen > 127:
+ negation = True
+ afdlen -= 128
+ else:
+ negation = False
+ address = parser.get_bytes(afdlen)
+ l = len(address)
+ if header[0] == 1:
+ if l < 4:
+ address += b"\x00" * (4 - l)
+ elif header[0] == 2:
+ if l < 16:
+ address += b"\x00" * (16 - l)
+ else:
+ #
+ # This isn't really right according to the RFC, but it
+ # seems better than throwing an exception
+ #
+ address = codecs.encode(address, "hex_codec")
+ item = APLItem(header[0], negation, address, header[1])
+ items.append(item)
+ return cls(rdclass, rdtype, items)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/DHCID.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/DHCID.py
new file mode 100644
index 00000000..723492fa
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/DHCID.py
@@ -0,0 +1,54 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+
+
+@dns.immutable.immutable
+class DHCID(dns.rdata.Rdata):
+ """DHCID record"""
+
+ # see: RFC 4701
+
+ __slots__ = ["data"]
+
+ def __init__(self, rdclass, rdtype, data):
+ super().__init__(rdclass, rdtype)
+ self.data = self._as_bytes(data)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return dns.rdata._base64ify(self.data, **kw)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ b64 = tok.concatenate_remaining_identifiers().encode()
+ data = base64.b64decode(b64)
+ return cls(rdclass, rdtype, data)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(self.data)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ data = parser.get_remaining()
+ return cls(rdclass, rdtype, data)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/HTTPS.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/HTTPS.py
new file mode 100644
index 00000000..15464cbd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/HTTPS.py
@@ -0,0 +1,9 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.immutable
+import dns.rdtypes.svcbbase
+
+
+@dns.immutable.immutable
+class HTTPS(dns.rdtypes.svcbbase.SVCBBase):
+ """HTTPS record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/IPSECKEY.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/IPSECKEY.py
new file mode 100644
index 00000000..e3a66157
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/IPSECKEY.py
@@ -0,0 +1,91 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdtypes.util
+
+
+class Gateway(dns.rdtypes.util.Gateway):
+ name = "IPSECKEY gateway"
+
+
+@dns.immutable.immutable
+class IPSECKEY(dns.rdata.Rdata):
+ """IPSECKEY record"""
+
+ # see: RFC 4025
+
+ __slots__ = ["precedence", "gateway_type", "algorithm", "gateway", "key"]
+
+ def __init__(
+ self, rdclass, rdtype, precedence, gateway_type, algorithm, gateway, key
+ ):
+ super().__init__(rdclass, rdtype)
+ gateway = Gateway(gateway_type, gateway)
+ self.precedence = self._as_uint8(precedence)
+ self.gateway_type = gateway.type
+ self.algorithm = self._as_uint8(algorithm)
+ self.gateway = gateway.gateway
+ self.key = self._as_bytes(key)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ gateway = Gateway(self.gateway_type, self.gateway).to_text(origin, relativize)
+ return "%d %d %d %s %s" % (
+ self.precedence,
+ self.gateway_type,
+ self.algorithm,
+ gateway,
+ dns.rdata._base64ify(self.key, **kw),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ precedence = tok.get_uint8()
+ gateway_type = tok.get_uint8()
+ algorithm = tok.get_uint8()
+ gateway = Gateway.from_text(
+ gateway_type, tok, origin, relativize, relativize_to
+ )
+ b64 = tok.concatenate_remaining_identifiers().encode()
+ key = base64.b64decode(b64)
+ return cls(
+ rdclass, rdtype, precedence, gateway_type, algorithm, gateway.gateway, key
+ )
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ header = struct.pack("!BBB", self.precedence, self.gateway_type, self.algorithm)
+ file.write(header)
+ Gateway(self.gateway_type, self.gateway).to_wire(
+ file, compress, origin, canonicalize
+ )
+ file.write(self.key)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ header = parser.get_struct("!BBB")
+ gateway_type = header[1]
+ gateway = Gateway.from_wire_parser(gateway_type, parser, origin)
+ key = parser.get_remaining()
+ return cls(
+ rdclass, rdtype, header[0], gateway_type, header[2], gateway.gateway, key
+ )
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/KX.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/KX.py
new file mode 100644
index 00000000..6073df47
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/KX.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.mxbase
+
+
+@dns.immutable.immutable
+class KX(dns.rdtypes.mxbase.UncompressedDowncasingMX):
+ """KX record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NAPTR.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NAPTR.py
new file mode 100644
index 00000000..195d1cba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NAPTR.py
@@ -0,0 +1,110 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdtypes.util
+
+
+def _write_string(file, s):
+ l = len(s)
+ assert l < 256
+ file.write(struct.pack("!B", l))
+ file.write(s)
+
+
+@dns.immutable.immutable
+class NAPTR(dns.rdata.Rdata):
+ """NAPTR record"""
+
+ # see: RFC 3403
+
+ __slots__ = ["order", "preference", "flags", "service", "regexp", "replacement"]
+
+ def __init__(
+ self, rdclass, rdtype, order, preference, flags, service, regexp, replacement
+ ):
+ super().__init__(rdclass, rdtype)
+ self.flags = self._as_bytes(flags, True, 255)
+ self.service = self._as_bytes(service, True, 255)
+ self.regexp = self._as_bytes(regexp, True, 255)
+ self.order = self._as_uint16(order)
+ self.preference = self._as_uint16(preference)
+ self.replacement = self._as_name(replacement)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ replacement = self.replacement.choose_relativity(origin, relativize)
+ return '%d %d "%s" "%s" "%s" %s' % (
+ self.order,
+ self.preference,
+ dns.rdata._escapify(self.flags),
+ dns.rdata._escapify(self.service),
+ dns.rdata._escapify(self.regexp),
+ replacement,
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ order = tok.get_uint16()
+ preference = tok.get_uint16()
+ flags = tok.get_string()
+ service = tok.get_string()
+ regexp = tok.get_string()
+ replacement = tok.get_name(origin, relativize, relativize_to)
+ return cls(
+ rdclass, rdtype, order, preference, flags, service, regexp, replacement
+ )
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ two_ints = struct.pack("!HH", self.order, self.preference)
+ file.write(two_ints)
+ _write_string(file, self.flags)
+ _write_string(file, self.service)
+ _write_string(file, self.regexp)
+ self.replacement.to_wire(file, compress, origin, canonicalize)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (order, preference) = parser.get_struct("!HH")
+ strings = []
+ for _ in range(3):
+ s = parser.get_counted_bytes()
+ strings.append(s)
+ replacement = parser.get_name(origin)
+ return cls(
+ rdclass,
+ rdtype,
+ order,
+ preference,
+ strings[0],
+ strings[1],
+ strings[2],
+ replacement,
+ )
+
+ def _processing_priority(self):
+ return (self.order, self.preference)
+
+ @classmethod
+ def _processing_order(cls, iterable):
+ return dns.rdtypes.util.priority_processing_order(iterable)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NSAP.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NSAP.py
new file mode 100644
index 00000000..d55edb73
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NSAP.py
@@ -0,0 +1,60 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class NSAP(dns.rdata.Rdata):
+ """NSAP record."""
+
+ # see: RFC 1706
+
+ __slots__ = ["address"]
+
+ def __init__(self, rdclass, rdtype, address):
+ super().__init__(rdclass, rdtype)
+ self.address = self._as_bytes(address)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return f"0x{binascii.hexlify(self.address).decode()}"
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ address = tok.get_string()
+ if address[0:2] != "0x":
+ raise dns.exception.SyntaxError("string does not start with 0x")
+ address = address[2:].replace(".", "")
+ if len(address) % 2 != 0:
+ raise dns.exception.SyntaxError("hexstring has odd length")
+ address = binascii.unhexlify(address.encode())
+ return cls(rdclass, rdtype, address)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(self.address)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ address = parser.get_remaining()
+ return cls(rdclass, rdtype, address)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NSAP_PTR.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NSAP_PTR.py
new file mode 100644
index 00000000..ce1c6632
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/NSAP_PTR.py
@@ -0,0 +1,24 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.immutable
+import dns.rdtypes.nsbase
+
+
+@dns.immutable.immutable
+class NSAP_PTR(dns.rdtypes.nsbase.UncompressedNS):
+ """NSAP-PTR record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/PX.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/PX.py
new file mode 100644
index 00000000..cdca1532
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/PX.py
@@ -0,0 +1,73 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class PX(dns.rdata.Rdata):
+ """PX record."""
+
+ # see: RFC 2163
+
+ __slots__ = ["preference", "map822", "mapx400"]
+
+ def __init__(self, rdclass, rdtype, preference, map822, mapx400):
+ super().__init__(rdclass, rdtype)
+ self.preference = self._as_uint16(preference)
+ self.map822 = self._as_name(map822)
+ self.mapx400 = self._as_name(mapx400)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ map822 = self.map822.choose_relativity(origin, relativize)
+ mapx400 = self.mapx400.choose_relativity(origin, relativize)
+ return "%d %s %s" % (self.preference, map822, mapx400)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ preference = tok.get_uint16()
+ map822 = tok.get_name(origin, relativize, relativize_to)
+ mapx400 = tok.get_name(origin, relativize, relativize_to)
+ return cls(rdclass, rdtype, preference, map822, mapx400)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ pref = struct.pack("!H", self.preference)
+ file.write(pref)
+ self.map822.to_wire(file, None, origin, canonicalize)
+ self.mapx400.to_wire(file, None, origin, canonicalize)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ preference = parser.get_uint16()
+ map822 = parser.get_name(origin)
+ mapx400 = parser.get_name(origin)
+ return cls(rdclass, rdtype, preference, map822, mapx400)
+
+ def _processing_priority(self):
+ return self.preference
+
+ @classmethod
+ def _processing_order(cls, iterable):
+ return dns.rdtypes.util.priority_processing_order(iterable)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/SRV.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/SRV.py
new file mode 100644
index 00000000..5adef98f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/SRV.py
@@ -0,0 +1,75 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class SRV(dns.rdata.Rdata):
+ """SRV record"""
+
+ # see: RFC 2782
+
+ __slots__ = ["priority", "weight", "port", "target"]
+
+ def __init__(self, rdclass, rdtype, priority, weight, port, target):
+ super().__init__(rdclass, rdtype)
+ self.priority = self._as_uint16(priority)
+ self.weight = self._as_uint16(weight)
+ self.port = self._as_uint16(port)
+ self.target = self._as_name(target)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ target = self.target.choose_relativity(origin, relativize)
+ return "%d %d %d %s" % (self.priority, self.weight, self.port, target)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ priority = tok.get_uint16()
+ weight = tok.get_uint16()
+ port = tok.get_uint16()
+ target = tok.get_name(origin, relativize, relativize_to)
+ return cls(rdclass, rdtype, priority, weight, port, target)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ three_ints = struct.pack("!HHH", self.priority, self.weight, self.port)
+ file.write(three_ints)
+ self.target.to_wire(file, compress, origin, canonicalize)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ (priority, weight, port) = parser.get_struct("!HHH")
+ target = parser.get_name(origin)
+ return cls(rdclass, rdtype, priority, weight, port, target)
+
+ def _processing_priority(self):
+ return self.priority
+
+ def _processing_weight(self):
+ return self.weight
+
+ @classmethod
+ def _processing_order(cls, iterable):
+ return dns.rdtypes.util.weighted_processing_order(iterable)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/SVCB.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/SVCB.py
new file mode 100644
index 00000000..ff3e9327
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/SVCB.py
@@ -0,0 +1,9 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.immutable
+import dns.rdtypes.svcbbase
+
+
+@dns.immutable.immutable
+class SVCB(dns.rdtypes.svcbbase.SVCBBase):
+ """SVCB record"""
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/WKS.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/WKS.py
new file mode 100644
index 00000000..881a7849
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/WKS.py
@@ -0,0 +1,100 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import socket
+import struct
+
+import dns.immutable
+import dns.ipv4
+import dns.rdata
+
+try:
+ _proto_tcp = socket.getprotobyname("tcp")
+ _proto_udp = socket.getprotobyname("udp")
+except OSError:
+ # Fall back to defaults in case /etc/protocols is unavailable.
+ _proto_tcp = 6
+ _proto_udp = 17
+
+
+@dns.immutable.immutable
+class WKS(dns.rdata.Rdata):
+ """WKS record"""
+
+ # see: RFC 1035
+
+ __slots__ = ["address", "protocol", "bitmap"]
+
+ def __init__(self, rdclass, rdtype, address, protocol, bitmap):
+ super().__init__(rdclass, rdtype)
+ self.address = self._as_ipv4_address(address)
+ self.protocol = self._as_uint8(protocol)
+ self.bitmap = self._as_bytes(bitmap)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ bits = []
+ for i, byte in enumerate(self.bitmap):
+ for j in range(0, 8):
+ if byte & (0x80 >> j):
+ bits.append(str(i * 8 + j))
+ text = " ".join(bits)
+ return "%s %d %s" % (self.address, self.protocol, text)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ address = tok.get_string()
+ protocol = tok.get_string()
+ if protocol.isdigit():
+ protocol = int(protocol)
+ else:
+ protocol = socket.getprotobyname(protocol)
+ bitmap = bytearray()
+ for token in tok.get_remaining():
+ value = token.unescape().value
+ if value.isdigit():
+ serv = int(value)
+ else:
+ if protocol != _proto_udp and protocol != _proto_tcp:
+ raise NotImplementedError("protocol must be TCP or UDP")
+ if protocol == _proto_udp:
+ protocol_text = "udp"
+ else:
+ protocol_text = "tcp"
+ serv = socket.getservbyname(value, protocol_text)
+ i = serv // 8
+ l = len(bitmap)
+ if l < i + 1:
+ for _ in range(l, i + 1):
+ bitmap.append(0)
+ bitmap[i] = bitmap[i] | (0x80 >> (serv % 8))
+ bitmap = dns.rdata._truncate_bitmap(bitmap)
+ return cls(rdclass, rdtype, address, protocol, bitmap)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(dns.ipv4.inet_aton(self.address))
+ protocol = struct.pack("!B", self.protocol)
+ file.write(protocol)
+ file.write(self.bitmap)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ address = parser.get_bytes(4)
+ protocol = parser.get_uint8()
+ bitmap = parser.get_remaining()
+ return cls(rdclass, rdtype, address, protocol, bitmap)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/__init__.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/__init__.py
new file mode 100644
index 00000000..dcec4dd2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/IN/__init__.py
@@ -0,0 +1,35 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Class IN rdata type classes."""
+
+__all__ = [
+ "A",
+ "AAAA",
+ "APL",
+ "DHCID",
+ "HTTPS",
+ "IPSECKEY",
+ "KX",
+ "NAPTR",
+ "NSAP",
+ "NSAP_PTR",
+ "PX",
+ "SRV",
+ "SVCB",
+ "WKS",
+]
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/__init__.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/__init__.py
new file mode 100644
index 00000000..3997f84c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/__init__.py
@@ -0,0 +1,33 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS rdata type classes"""
+
+__all__ = [
+ "ANY",
+ "IN",
+ "CH",
+ "dnskeybase",
+ "dsbase",
+ "euibase",
+ "mxbase",
+ "nsbase",
+ "svcbbase",
+ "tlsabase",
+ "txtbase",
+ "util",
+]
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/dnskeybase.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/dnskeybase.py
new file mode 100644
index 00000000..db300f8b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/dnskeybase.py
@@ -0,0 +1,87 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import enum
+import struct
+
+import dns.dnssectypes
+import dns.exception
+import dns.immutable
+import dns.rdata
+
+# wildcard import
+__all__ = ["SEP", "REVOKE", "ZONE"] # noqa: F822
+
+
+class Flag(enum.IntFlag):
+ SEP = 0x0001
+ REVOKE = 0x0080
+ ZONE = 0x0100
+
+
+@dns.immutable.immutable
+class DNSKEYBase(dns.rdata.Rdata):
+ """Base class for rdata that is like a DNSKEY record"""
+
+ __slots__ = ["flags", "protocol", "algorithm", "key"]
+
+ def __init__(self, rdclass, rdtype, flags, protocol, algorithm, key):
+ super().__init__(rdclass, rdtype)
+ self.flags = Flag(self._as_uint16(flags))
+ self.protocol = self._as_uint8(protocol)
+ self.algorithm = dns.dnssectypes.Algorithm.make(algorithm)
+ self.key = self._as_bytes(key)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return "%d %d %d %s" % (
+ self.flags,
+ self.protocol,
+ self.algorithm,
+ dns.rdata._base64ify(self.key, **kw),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ flags = tok.get_uint16()
+ protocol = tok.get_uint8()
+ algorithm = tok.get_string()
+ b64 = tok.concatenate_remaining_identifiers().encode()
+ key = base64.b64decode(b64)
+ return cls(rdclass, rdtype, flags, protocol, algorithm, key)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ header = struct.pack("!HBB", self.flags, self.protocol, self.algorithm)
+ file.write(header)
+ file.write(self.key)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ header = parser.get_struct("!HBB")
+ key = parser.get_remaining()
+ return cls(rdclass, rdtype, header[0], header[1], header[2], key)
+
+
+### BEGIN generated Flag constants
+
+SEP = Flag.SEP
+REVOKE = Flag.REVOKE
+ZONE = Flag.ZONE
+
+### END generated Flag constants
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/dsbase.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/dsbase.py
new file mode 100644
index 00000000..cd21f026
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/dsbase.py
@@ -0,0 +1,85 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2010, 2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+import struct
+
+import dns.dnssectypes
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+
+
+@dns.immutable.immutable
+class DSBase(dns.rdata.Rdata):
+ """Base class for rdata that is like a DS record"""
+
+ __slots__ = ["key_tag", "algorithm", "digest_type", "digest"]
+
+ # Digest types registry:
+ # https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml
+ _digest_length_by_type = {
+ 1: 20, # SHA-1, RFC 3658 Sec. 2.4
+ 2: 32, # SHA-256, RFC 4509 Sec. 2.2
+ 3: 32, # GOST R 34.11-94, RFC 5933 Sec. 4 in conjunction with RFC 4490 Sec. 2.1
+ 4: 48, # SHA-384, RFC 6605 Sec. 2
+ }
+
+ def __init__(self, rdclass, rdtype, key_tag, algorithm, digest_type, digest):
+ super().__init__(rdclass, rdtype)
+ self.key_tag = self._as_uint16(key_tag)
+ self.algorithm = dns.dnssectypes.Algorithm.make(algorithm)
+ self.digest_type = dns.dnssectypes.DSDigest.make(self._as_uint8(digest_type))
+ self.digest = self._as_bytes(digest)
+ try:
+ if len(self.digest) != self._digest_length_by_type[self.digest_type]:
+ raise ValueError("digest length inconsistent with digest type")
+ except KeyError:
+ if self.digest_type == 0: # reserved, RFC 3658 Sec. 2.4
+ raise ValueError("digest type 0 is reserved")
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ kw = kw.copy()
+ chunksize = kw.pop("chunksize", 128)
+ return "%d %d %d %s" % (
+ self.key_tag,
+ self.algorithm,
+ self.digest_type,
+ dns.rdata._hexify(self.digest, chunksize=chunksize, **kw),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ key_tag = tok.get_uint16()
+ algorithm = tok.get_string()
+ digest_type = tok.get_uint8()
+ digest = tok.concatenate_remaining_identifiers().encode()
+ digest = binascii.unhexlify(digest)
+ return cls(rdclass, rdtype, key_tag, algorithm, digest_type, digest)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ header = struct.pack("!HBB", self.key_tag, self.algorithm, self.digest_type)
+ file.write(header)
+ file.write(self.digest)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ header = parser.get_struct("!HBB")
+ digest = parser.get_remaining()
+ return cls(rdclass, rdtype, header[0], header[1], header[2], digest)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/euibase.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/euibase.py
new file mode 100644
index 00000000..a39c166b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/euibase.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2015 Red Hat, Inc.
+# Author: Petr Spacek <pspacek@redhat.com>
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+
+import dns.immutable
+import dns.rdata
+
+
+@dns.immutable.immutable
+class EUIBase(dns.rdata.Rdata):
+ """EUIxx record"""
+
+ # see: rfc7043.txt
+
+ __slots__ = ["eui"]
+ # define these in subclasses
+ # byte_len = 6 # 0123456789ab (in hex)
+ # text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab
+
+ def __init__(self, rdclass, rdtype, eui):
+ super().__init__(rdclass, rdtype)
+ self.eui = self._as_bytes(eui)
+ if len(self.eui) != self.byte_len:
+ raise dns.exception.FormError(
+ f"EUI{self.byte_len * 8} rdata has to have {self.byte_len} bytes"
+ )
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ return dns.rdata._hexify(self.eui, chunksize=2, separator=b"-", **kw)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ text = tok.get_string()
+ if len(text) != cls.text_len:
+ raise dns.exception.SyntaxError(
+ f"Input text must have {cls.text_len} characters"
+ )
+ for i in range(2, cls.byte_len * 3 - 1, 3):
+ if text[i] != "-":
+ raise dns.exception.SyntaxError(f"Dash expected at position {i}")
+ text = text.replace("-", "")
+ try:
+ data = binascii.unhexlify(text.encode())
+ except (ValueError, TypeError) as ex:
+ raise dns.exception.SyntaxError(f"Hex decoding error: {str(ex)}")
+ return cls(rdclass, rdtype, data)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(self.eui)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ eui = parser.get_bytes(cls.byte_len)
+ return cls(rdclass, rdtype, eui)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/mxbase.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/mxbase.py
new file mode 100644
index 00000000..6d5e3d87
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/mxbase.py
@@ -0,0 +1,87 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""MX-like base classes."""
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class MXBase(dns.rdata.Rdata):
+ """Base class for rdata that is like an MX record."""
+
+ __slots__ = ["preference", "exchange"]
+
+ def __init__(self, rdclass, rdtype, preference, exchange):
+ super().__init__(rdclass, rdtype)
+ self.preference = self._as_uint16(preference)
+ self.exchange = self._as_name(exchange)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ exchange = self.exchange.choose_relativity(origin, relativize)
+ return "%d %s" % (self.preference, exchange)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ preference = tok.get_uint16()
+ exchange = tok.get_name(origin, relativize, relativize_to)
+ return cls(rdclass, rdtype, preference, exchange)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ pref = struct.pack("!H", self.preference)
+ file.write(pref)
+ self.exchange.to_wire(file, compress, origin, canonicalize)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ preference = parser.get_uint16()
+ exchange = parser.get_name(origin)
+ return cls(rdclass, rdtype, preference, exchange)
+
+ def _processing_priority(self):
+ return self.preference
+
+ @classmethod
+ def _processing_order(cls, iterable):
+ return dns.rdtypes.util.priority_processing_order(iterable)
+
+
+@dns.immutable.immutable
+class UncompressedMX(MXBase):
+ """Base class for rdata that is like an MX record, but whose name
+ is not compressed when converted to DNS wire format, and whose
+ digestable form is not downcased."""
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ super()._to_wire(file, None, origin, False)
+
+
+@dns.immutable.immutable
+class UncompressedDowncasingMX(MXBase):
+ """Base class for rdata that is like an MX record, but whose name
+ is not compressed when convert to DNS wire format."""
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ super()._to_wire(file, None, origin, canonicalize)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/nsbase.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/nsbase.py
new file mode 100644
index 00000000..904224f0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/nsbase.py
@@ -0,0 +1,63 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""NS-like base classes."""
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+
+
+@dns.immutable.immutable
+class NSBase(dns.rdata.Rdata):
+ """Base class for rdata that is like an NS record."""
+
+ __slots__ = ["target"]
+
+ def __init__(self, rdclass, rdtype, target):
+ super().__init__(rdclass, rdtype)
+ self.target = self._as_name(target)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ target = self.target.choose_relativity(origin, relativize)
+ return str(target)
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ target = tok.get_name(origin, relativize, relativize_to)
+ return cls(rdclass, rdtype, target)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ self.target.to_wire(file, compress, origin, canonicalize)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ target = parser.get_name(origin)
+ return cls(rdclass, rdtype, target)
+
+
+@dns.immutable.immutable
+class UncompressedNS(NSBase):
+ """Base class for rdata that is like an NS record, but whose name
+ is not compressed when convert to DNS wire format, and whose
+ digestable form is not downcased."""
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ self.target.to_wire(file, None, origin, False)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/svcbbase.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/svcbbase.py
new file mode 100644
index 00000000..a2b15b92
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/svcbbase.py
@@ -0,0 +1,585 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import base64
+import enum
+import struct
+
+import dns.enum
+import dns.exception
+import dns.immutable
+import dns.ipv4
+import dns.ipv6
+import dns.name
+import dns.rdata
+import dns.rdtypes.util
+import dns.renderer
+import dns.tokenizer
+import dns.wire
+
+# Until there is an RFC, this module is experimental and may be changed in
+# incompatible ways.
+
+
+class UnknownParamKey(dns.exception.DNSException):
+ """Unknown SVCB ParamKey"""
+
+
+class ParamKey(dns.enum.IntEnum):
+ """SVCB ParamKey"""
+
+ MANDATORY = 0
+ ALPN = 1
+ NO_DEFAULT_ALPN = 2
+ PORT = 3
+ IPV4HINT = 4
+ ECH = 5
+ IPV6HINT = 6
+ DOHPATH = 7
+ OHTTP = 8
+
+ @classmethod
+ def _maximum(cls):
+ return 65535
+
+ @classmethod
+ def _short_name(cls):
+ return "SVCBParamKey"
+
+ @classmethod
+ def _prefix(cls):
+ return "KEY"
+
+ @classmethod
+ def _unknown_exception_class(cls):
+ return UnknownParamKey
+
+
+class Emptiness(enum.IntEnum):
+ NEVER = 0
+ ALWAYS = 1
+ ALLOWED = 2
+
+
+def _validate_key(key):
+ force_generic = False
+ if isinstance(key, bytes):
+ # We decode to latin-1 so we get 0-255 as valid and do NOT interpret
+ # UTF-8 sequences
+ key = key.decode("latin-1")
+ if isinstance(key, str):
+ if key.lower().startswith("key"):
+ force_generic = True
+ if key[3:].startswith("0") and len(key) != 4:
+ # key has leading zeros
+ raise ValueError("leading zeros in key")
+ key = key.replace("-", "_")
+ return (ParamKey.make(key), force_generic)
+
+
+def key_to_text(key):
+ return ParamKey.to_text(key).replace("_", "-").lower()
+
+
+# Like rdata escapify, but escapes ',' too.
+
+_escaped = b'",\\'
+
+
+def _escapify(qstring):
+ text = ""
+ for c in qstring:
+ if c in _escaped:
+ text += "\\" + chr(c)
+ elif c >= 0x20 and c < 0x7F:
+ text += chr(c)
+ else:
+ text += "\\%03d" % c
+ return text
+
+
+def _unescape(value):
+ if value == "":
+ return value
+ unescaped = b""
+ l = len(value)
+ i = 0
+ while i < l:
+ c = value[i]
+ i += 1
+ if c == "\\":
+ if i >= l: # pragma: no cover (can't happen via tokenizer get())
+ raise dns.exception.UnexpectedEnd
+ c = value[i]
+ i += 1
+ if c.isdigit():
+ if i >= l:
+ raise dns.exception.UnexpectedEnd
+ c2 = value[i]
+ i += 1
+ if i >= l:
+ raise dns.exception.UnexpectedEnd
+ c3 = value[i]
+ i += 1
+ if not (c2.isdigit() and c3.isdigit()):
+ raise dns.exception.SyntaxError
+ codepoint = int(c) * 100 + int(c2) * 10 + int(c3)
+ if codepoint > 255:
+ raise dns.exception.SyntaxError
+ unescaped += b"%c" % (codepoint)
+ continue
+ unescaped += c.encode()
+ return unescaped
+
+
+def _split(value):
+ l = len(value)
+ i = 0
+ items = []
+ unescaped = b""
+ while i < l:
+ c = value[i]
+ i += 1
+ if c == ord("\\"):
+ if i >= l: # pragma: no cover (can't happen via tokenizer get())
+ raise dns.exception.UnexpectedEnd
+ c = value[i]
+ i += 1
+ unescaped += b"%c" % (c)
+ elif c == ord(","):
+ items.append(unescaped)
+ unescaped = b""
+ else:
+ unescaped += b"%c" % (c)
+ items.append(unescaped)
+ return items
+
+
+@dns.immutable.immutable
+class Param:
+ """Abstract base class for SVCB parameters"""
+
+ @classmethod
+ def emptiness(cls):
+ return Emptiness.NEVER
+
+
+@dns.immutable.immutable
+class GenericParam(Param):
+ """Generic SVCB parameter"""
+
+ def __init__(self, value):
+ self.value = dns.rdata.Rdata._as_bytes(value, True)
+
+ @classmethod
+ def emptiness(cls):
+ return Emptiness.ALLOWED
+
+ @classmethod
+ def from_value(cls, value):
+ if value is None or len(value) == 0:
+ return None
+ else:
+ return cls(_unescape(value))
+
+ def to_text(self):
+ return '"' + dns.rdata._escapify(self.value) + '"'
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ value = parser.get_bytes(parser.remaining())
+ if len(value) == 0:
+ return None
+ else:
+ return cls(value)
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ file.write(self.value)
+
+
+@dns.immutable.immutable
+class MandatoryParam(Param):
+ def __init__(self, keys):
+ # check for duplicates
+ keys = sorted([_validate_key(key)[0] for key in keys])
+ prior_k = None
+ for k in keys:
+ if k == prior_k:
+ raise ValueError(f"duplicate key {k:d}")
+ prior_k = k
+ if k == ParamKey.MANDATORY:
+ raise ValueError("listed the mandatory key as mandatory")
+ self.keys = tuple(keys)
+
+ @classmethod
+ def from_value(cls, value):
+ keys = [k.encode() for k in value.split(",")]
+ return cls(keys)
+
+ def to_text(self):
+ return '"' + ",".join([key_to_text(key) for key in self.keys]) + '"'
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ keys = []
+ last_key = -1
+ while parser.remaining() > 0:
+ key = parser.get_uint16()
+ if key < last_key:
+ raise dns.exception.FormError("manadatory keys not ascending")
+ last_key = key
+ keys.append(key)
+ return cls(keys)
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ for key in self.keys:
+ file.write(struct.pack("!H", key))
+
+
+@dns.immutable.immutable
+class ALPNParam(Param):
+ def __init__(self, ids):
+ self.ids = dns.rdata.Rdata._as_tuple(
+ ids, lambda x: dns.rdata.Rdata._as_bytes(x, True, 255, False)
+ )
+
+ @classmethod
+ def from_value(cls, value):
+ return cls(_split(_unescape(value)))
+
+ def to_text(self):
+ value = ",".join([_escapify(id) for id in self.ids])
+ return '"' + dns.rdata._escapify(value.encode()) + '"'
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ ids = []
+ while parser.remaining() > 0:
+ id = parser.get_counted_bytes()
+ ids.append(id)
+ return cls(ids)
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ for id in self.ids:
+ file.write(struct.pack("!B", len(id)))
+ file.write(id)
+
+
+@dns.immutable.immutable
+class NoDefaultALPNParam(Param):
+ # We don't ever expect to instantiate this class, but we need
+ # a from_value() and a from_wire_parser(), so we just return None
+ # from the class methods when things are OK.
+
+ @classmethod
+ def emptiness(cls):
+ return Emptiness.ALWAYS
+
+ @classmethod
+ def from_value(cls, value):
+ if value is None or value == "":
+ return None
+ else:
+ raise ValueError("no-default-alpn with non-empty value")
+
+ def to_text(self):
+ raise NotImplementedError # pragma: no cover
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ if parser.remaining() != 0:
+ raise dns.exception.FormError
+ return None
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ raise NotImplementedError # pragma: no cover
+
+
+@dns.immutable.immutable
+class PortParam(Param):
+ def __init__(self, port):
+ self.port = dns.rdata.Rdata._as_uint16(port)
+
+ @classmethod
+ def from_value(cls, value):
+ value = int(value)
+ return cls(value)
+
+ def to_text(self):
+ return f'"{self.port}"'
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ port = parser.get_uint16()
+ return cls(port)
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ file.write(struct.pack("!H", self.port))
+
+
+@dns.immutable.immutable
+class IPv4HintParam(Param):
+ def __init__(self, addresses):
+ self.addresses = dns.rdata.Rdata._as_tuple(
+ addresses, dns.rdata.Rdata._as_ipv4_address
+ )
+
+ @classmethod
+ def from_value(cls, value):
+ addresses = value.split(",")
+ return cls(addresses)
+
+ def to_text(self):
+ return '"' + ",".join(self.addresses) + '"'
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ addresses = []
+ while parser.remaining() > 0:
+ ip = parser.get_bytes(4)
+ addresses.append(dns.ipv4.inet_ntoa(ip))
+ return cls(addresses)
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ for address in self.addresses:
+ file.write(dns.ipv4.inet_aton(address))
+
+
+@dns.immutable.immutable
+class IPv6HintParam(Param):
+ def __init__(self, addresses):
+ self.addresses = dns.rdata.Rdata._as_tuple(
+ addresses, dns.rdata.Rdata._as_ipv6_address
+ )
+
+ @classmethod
+ def from_value(cls, value):
+ addresses = value.split(",")
+ return cls(addresses)
+
+ def to_text(self):
+ return '"' + ",".join(self.addresses) + '"'
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ addresses = []
+ while parser.remaining() > 0:
+ ip = parser.get_bytes(16)
+ addresses.append(dns.ipv6.inet_ntoa(ip))
+ return cls(addresses)
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ for address in self.addresses:
+ file.write(dns.ipv6.inet_aton(address))
+
+
+@dns.immutable.immutable
+class ECHParam(Param):
+ def __init__(self, ech):
+ self.ech = dns.rdata.Rdata._as_bytes(ech, True)
+
+ @classmethod
+ def from_value(cls, value):
+ if "\\" in value:
+ raise ValueError("escape in ECH value")
+ value = base64.b64decode(value.encode())
+ return cls(value)
+
+ def to_text(self):
+ b64 = base64.b64encode(self.ech).decode("ascii")
+ return f'"{b64}"'
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ value = parser.get_bytes(parser.remaining())
+ return cls(value)
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ file.write(self.ech)
+
+
+@dns.immutable.immutable
+class OHTTPParam(Param):
+ # We don't ever expect to instantiate this class, but we need
+ # a from_value() and a from_wire_parser(), so we just return None
+ # from the class methods when things are OK.
+
+ @classmethod
+ def emptiness(cls):
+ return Emptiness.ALWAYS
+
+ @classmethod
+ def from_value(cls, value):
+ if value is None or value == "":
+ return None
+ else:
+ raise ValueError("ohttp with non-empty value")
+
+ def to_text(self):
+ raise NotImplementedError # pragma: no cover
+
+ @classmethod
+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613
+ if parser.remaining() != 0:
+ raise dns.exception.FormError
+ return None
+
+ def to_wire(self, file, origin=None): # pylint: disable=W0613
+ raise NotImplementedError # pragma: no cover
+
+
+_class_for_key = {
+ ParamKey.MANDATORY: MandatoryParam,
+ ParamKey.ALPN: ALPNParam,
+ ParamKey.NO_DEFAULT_ALPN: NoDefaultALPNParam,
+ ParamKey.PORT: PortParam,
+ ParamKey.IPV4HINT: IPv4HintParam,
+ ParamKey.ECH: ECHParam,
+ ParamKey.IPV6HINT: IPv6HintParam,
+ ParamKey.OHTTP: OHTTPParam,
+}
+
+
+def _validate_and_define(params, key, value):
+ (key, force_generic) = _validate_key(_unescape(key))
+ if key in params:
+ raise SyntaxError(f'duplicate key "{key:d}"')
+ cls = _class_for_key.get(key, GenericParam)
+ emptiness = cls.emptiness()
+ if value is None:
+ if emptiness == Emptiness.NEVER:
+ raise SyntaxError("value cannot be empty")
+ value = cls.from_value(value)
+ else:
+ if force_generic:
+ value = cls.from_wire_parser(dns.wire.Parser(_unescape(value)))
+ else:
+ value = cls.from_value(value)
+ params[key] = value
+
+
+@dns.immutable.immutable
+class SVCBBase(dns.rdata.Rdata):
+ """Base class for SVCB-like records"""
+
+ # see: draft-ietf-dnsop-svcb-https-11
+
+ __slots__ = ["priority", "target", "params"]
+
+ def __init__(self, rdclass, rdtype, priority, target, params):
+ super().__init__(rdclass, rdtype)
+ self.priority = self._as_uint16(priority)
+ self.target = self._as_name(target)
+ for k, v in params.items():
+ k = ParamKey.make(k)
+ if not isinstance(v, Param) and v is not None:
+ raise ValueError(f"{k:d} not a Param")
+ self.params = dns.immutable.Dict(params)
+ # Make sure any parameter listed as mandatory is present in the
+ # record.
+ mandatory = params.get(ParamKey.MANDATORY)
+ if mandatory:
+ for key in mandatory.keys:
+ # Note we have to say "not in" as we have None as a value
+ # so a get() and a not None test would be wrong.
+ if key not in params:
+ raise ValueError(f"key {key:d} declared mandatory but not present")
+ # The no-default-alpn parameter requires the alpn parameter.
+ if ParamKey.NO_DEFAULT_ALPN in params:
+ if ParamKey.ALPN not in params:
+ raise ValueError("no-default-alpn present, but alpn missing")
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ target = self.target.choose_relativity(origin, relativize)
+ params = []
+ for key in sorted(self.params.keys()):
+ value = self.params[key]
+ if value is None:
+ params.append(key_to_text(key))
+ else:
+ kv = key_to_text(key) + "=" + value.to_text()
+ params.append(kv)
+ if len(params) > 0:
+ space = " "
+ else:
+ space = ""
+ return "%d %s%s%s" % (self.priority, target, space, " ".join(params))
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ priority = tok.get_uint16()
+ target = tok.get_name(origin, relativize, relativize_to)
+ if priority == 0:
+ token = tok.get()
+ if not token.is_eol_or_eof():
+ raise SyntaxError("parameters in AliasMode")
+ tok.unget(token)
+ params = {}
+ while True:
+ token = tok.get()
+ if token.is_eol_or_eof():
+ tok.unget(token)
+ break
+ if token.ttype != dns.tokenizer.IDENTIFIER:
+ raise SyntaxError("parameter is not an identifier")
+ equals = token.value.find("=")
+ if equals == len(token.value) - 1:
+ # 'key=', so next token should be a quoted string without
+ # any intervening whitespace.
+ key = token.value[:-1]
+ token = tok.get(want_leading=True)
+ if token.ttype != dns.tokenizer.QUOTED_STRING:
+ raise SyntaxError("whitespace after =")
+ value = token.value
+ elif equals > 0:
+ # key=value
+ key = token.value[:equals]
+ value = token.value[equals + 1 :]
+ elif equals == 0:
+ # =key
+ raise SyntaxError('parameter cannot start with "="')
+ else:
+ # key
+ key = token.value
+ value = None
+ _validate_and_define(params, key, value)
+ return cls(rdclass, rdtype, priority, target, params)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ file.write(struct.pack("!H", self.priority))
+ self.target.to_wire(file, None, origin, False)
+ for key in sorted(self.params):
+ file.write(struct.pack("!H", key))
+ value = self.params[key]
+ with dns.renderer.prefixed_length(file, 2):
+ # Note that we're still writing a length of zero if the value is None
+ if value is not None:
+ value.to_wire(file, origin)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ priority = parser.get_uint16()
+ target = parser.get_name(origin)
+ if priority == 0 and parser.remaining() != 0:
+ raise dns.exception.FormError("parameters in AliasMode")
+ params = {}
+ prior_key = -1
+ while parser.remaining() > 0:
+ key = parser.get_uint16()
+ if key < prior_key:
+ raise dns.exception.FormError("keys not in order")
+ prior_key = key
+ vlen = parser.get_uint16()
+ pcls = _class_for_key.get(key, GenericParam)
+ with parser.restrict_to(vlen):
+ value = pcls.from_wire_parser(parser, origin)
+ params[key] = value
+ return cls(rdclass, rdtype, priority, target, params)
+
+ def _processing_priority(self):
+ return self.priority
+
+ @classmethod
+ def _processing_order(cls, iterable):
+ return dns.rdtypes.util.priority_processing_order(iterable)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/tlsabase.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/tlsabase.py
new file mode 100644
index 00000000..a059d2c4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/tlsabase.py
@@ -0,0 +1,71 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+import struct
+
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+
+
+@dns.immutable.immutable
+class TLSABase(dns.rdata.Rdata):
+ """Base class for TLSA and SMIMEA records"""
+
+ # see: RFC 6698
+
+ __slots__ = ["usage", "selector", "mtype", "cert"]
+
+ def __init__(self, rdclass, rdtype, usage, selector, mtype, cert):
+ super().__init__(rdclass, rdtype)
+ self.usage = self._as_uint8(usage)
+ self.selector = self._as_uint8(selector)
+ self.mtype = self._as_uint8(mtype)
+ self.cert = self._as_bytes(cert)
+
+ def to_text(self, origin=None, relativize=True, **kw):
+ kw = kw.copy()
+ chunksize = kw.pop("chunksize", 128)
+ return "%d %d %d %s" % (
+ self.usage,
+ self.selector,
+ self.mtype,
+ dns.rdata._hexify(self.cert, chunksize=chunksize, **kw),
+ )
+
+ @classmethod
+ def from_text(
+ cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ usage = tok.get_uint8()
+ selector = tok.get_uint8()
+ mtype = tok.get_uint8()
+ cert = tok.concatenate_remaining_identifiers().encode()
+ cert = binascii.unhexlify(cert)
+ return cls(rdclass, rdtype, usage, selector, mtype, cert)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ header = struct.pack("!BBB", self.usage, self.selector, self.mtype)
+ file.write(header)
+ file.write(self.cert)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ header = parser.get_struct("BBB")
+ cert = parser.get_remaining()
+ return cls(rdclass, rdtype, header[0], header[1], header[2], cert)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/txtbase.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/txtbase.py
new file mode 100644
index 00000000..73db6d9e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/txtbase.py
@@ -0,0 +1,106 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""TXT-like base class."""
+
+from typing import Any, Dict, Iterable, Optional, Tuple, Union
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.renderer
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class TXTBase(dns.rdata.Rdata):
+ """Base class for rdata that is like a TXT record (see RFC 1035)."""
+
+ __slots__ = ["strings"]
+
+ def __init__(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ strings: Iterable[Union[bytes, str]],
+ ):
+ """Initialize a TXT-like rdata.
+
+ *rdclass*, an ``int`` is the rdataclass of the Rdata.
+
+ *rdtype*, an ``int`` is the rdatatype of the Rdata.
+
+ *strings*, a tuple of ``bytes``
+ """
+ super().__init__(rdclass, rdtype)
+ self.strings: Tuple[bytes] = self._as_tuple(
+ strings, lambda x: self._as_bytes(x, True, 255)
+ )
+ if len(self.strings) == 0:
+ raise ValueError("the list of strings must not be empty")
+
+ def to_text(
+ self,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ **kw: Dict[str, Any],
+ ) -> str:
+ txt = ""
+ prefix = ""
+ for s in self.strings:
+ txt += f'{prefix}"{dns.rdata._escapify(s)}"'
+ prefix = " "
+ return txt
+
+ @classmethod
+ def from_text(
+ cls,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ tok: dns.tokenizer.Tokenizer,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ relativize_to: Optional[dns.name.Name] = None,
+ ) -> dns.rdata.Rdata:
+ strings = []
+ for token in tok.get_remaining():
+ token = token.unescape_to_bytes()
+ # The 'if' below is always true in the current code, but we
+ # are leaving this check in in case things change some day.
+ if not (
+ token.is_quoted_string() or token.is_identifier()
+ ): # pragma: no cover
+ raise dns.exception.SyntaxError("expected a string")
+ if len(token.value) > 255:
+ raise dns.exception.SyntaxError("string too long")
+ strings.append(token.value)
+ if len(strings) == 0:
+ raise dns.exception.UnexpectedEnd
+ return cls(rdclass, rdtype, strings)
+
+ def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ for s in self.strings:
+ with dns.renderer.prefixed_length(file, 1):
+ file.write(s)
+
+ @classmethod
+ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+ strings = []
+ while parser.remaining() > 0:
+ s = parser.get_counted_bytes()
+ strings.append(s)
+ return cls(rdclass, rdtype, strings)
diff --git a/.venv/lib/python3.12/site-packages/dns/rdtypes/util.py b/.venv/lib/python3.12/site-packages/dns/rdtypes/util.py
new file mode 100644
index 00000000..653a0bf2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rdtypes/util.py
@@ -0,0 +1,257 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import collections
+import random
+import struct
+from typing import Any, List
+
+import dns.exception
+import dns.ipv4
+import dns.ipv6
+import dns.name
+import dns.rdata
+
+
+class Gateway:
+ """A helper class for the IPSECKEY gateway and AMTRELAY relay fields"""
+
+ name = ""
+
+ def __init__(self, type, gateway=None):
+ self.type = dns.rdata.Rdata._as_uint8(type)
+ self.gateway = gateway
+ self._check()
+
+ @classmethod
+ def _invalid_type(cls, gateway_type):
+ return f"invalid {cls.name} type: {gateway_type}"
+
+ def _check(self):
+ if self.type == 0:
+ if self.gateway not in (".", None):
+ raise SyntaxError(f"invalid {self.name} for type 0")
+ self.gateway = None
+ elif self.type == 1:
+ # check that it's OK
+ dns.ipv4.inet_aton(self.gateway)
+ elif self.type == 2:
+ # check that it's OK
+ dns.ipv6.inet_aton(self.gateway)
+ elif self.type == 3:
+ if not isinstance(self.gateway, dns.name.Name):
+ raise SyntaxError(f"invalid {self.name}; not a name")
+ else:
+ raise SyntaxError(self._invalid_type(self.type))
+
+ def to_text(self, origin=None, relativize=True):
+ if self.type == 0:
+ return "."
+ elif self.type in (1, 2):
+ return self.gateway
+ elif self.type == 3:
+ return str(self.gateway.choose_relativity(origin, relativize))
+ else:
+ raise ValueError(self._invalid_type(self.type)) # pragma: no cover
+
+ @classmethod
+ def from_text(
+ cls, gateway_type, tok, origin=None, relativize=True, relativize_to=None
+ ):
+ if gateway_type in (0, 1, 2):
+ gateway = tok.get_string()
+ elif gateway_type == 3:
+ gateway = tok.get_name(origin, relativize, relativize_to)
+ else:
+ raise dns.exception.SyntaxError(
+ cls._invalid_type(gateway_type)
+ ) # pragma: no cover
+ return cls(gateway_type, gateway)
+
+ # pylint: disable=unused-argument
+ def to_wire(self, file, compress=None, origin=None, canonicalize=False):
+ if self.type == 0:
+ pass
+ elif self.type == 1:
+ file.write(dns.ipv4.inet_aton(self.gateway))
+ elif self.type == 2:
+ file.write(dns.ipv6.inet_aton(self.gateway))
+ elif self.type == 3:
+ self.gateway.to_wire(file, None, origin, False)
+ else:
+ raise ValueError(self._invalid_type(self.type)) # pragma: no cover
+
+ # pylint: enable=unused-argument
+
+ @classmethod
+ def from_wire_parser(cls, gateway_type, parser, origin=None):
+ if gateway_type == 0:
+ gateway = None
+ elif gateway_type == 1:
+ gateway = dns.ipv4.inet_ntoa(parser.get_bytes(4))
+ elif gateway_type == 2:
+ gateway = dns.ipv6.inet_ntoa(parser.get_bytes(16))
+ elif gateway_type == 3:
+ gateway = parser.get_name(origin)
+ else:
+ raise dns.exception.FormError(cls._invalid_type(gateway_type))
+ return cls(gateway_type, gateway)
+
+
+class Bitmap:
+ """A helper class for the NSEC/NSEC3/CSYNC type bitmaps"""
+
+ type_name = ""
+
+ def __init__(self, windows=None):
+ last_window = -1
+ self.windows = windows
+ for window, bitmap in self.windows:
+ if not isinstance(window, int):
+ raise ValueError(f"bad {self.type_name} window type")
+ if window <= last_window:
+ raise ValueError(f"bad {self.type_name} window order")
+ if window > 256:
+ raise ValueError(f"bad {self.type_name} window number")
+ last_window = window
+ if not isinstance(bitmap, bytes):
+ raise ValueError(f"bad {self.type_name} octets type")
+ if len(bitmap) == 0 or len(bitmap) > 32:
+ raise ValueError(f"bad {self.type_name} octets")
+
+ def to_text(self) -> str:
+ text = ""
+ for window, bitmap in self.windows:
+ bits = []
+ for i, byte in enumerate(bitmap):
+ for j in range(0, 8):
+ if byte & (0x80 >> j):
+ rdtype = window * 256 + i * 8 + j
+ bits.append(dns.rdatatype.to_text(rdtype))
+ text += " " + " ".join(bits)
+ return text
+
+ @classmethod
+ def from_text(cls, tok: "dns.tokenizer.Tokenizer") -> "Bitmap":
+ rdtypes = []
+ for token in tok.get_remaining():
+ rdtype = dns.rdatatype.from_text(token.unescape().value)
+ if rdtype == 0:
+ raise dns.exception.SyntaxError(f"{cls.type_name} with bit 0")
+ rdtypes.append(rdtype)
+ return cls.from_rdtypes(rdtypes)
+
+ @classmethod
+ def from_rdtypes(cls, rdtypes: List[dns.rdatatype.RdataType]) -> "Bitmap":
+ rdtypes = sorted(rdtypes)
+ window = 0
+ octets = 0
+ prior_rdtype = 0
+ bitmap = bytearray(b"\0" * 32)
+ windows = []
+ for rdtype in rdtypes:
+ if rdtype == prior_rdtype:
+ continue
+ prior_rdtype = rdtype
+ new_window = rdtype // 256
+ if new_window != window:
+ if octets != 0:
+ windows.append((window, bytes(bitmap[0:octets])))
+ bitmap = bytearray(b"\0" * 32)
+ window = new_window
+ offset = rdtype % 256
+ byte = offset // 8
+ bit = offset % 8
+ octets = byte + 1
+ bitmap[byte] = bitmap[byte] | (0x80 >> bit)
+ if octets != 0:
+ windows.append((window, bytes(bitmap[0:octets])))
+ return cls(windows)
+
+ def to_wire(self, file: Any) -> None:
+ for window, bitmap in self.windows:
+ file.write(struct.pack("!BB", window, len(bitmap)))
+ file.write(bitmap)
+
+ @classmethod
+ def from_wire_parser(cls, parser: "dns.wire.Parser") -> "Bitmap":
+ windows = []
+ while parser.remaining() > 0:
+ window = parser.get_uint8()
+ bitmap = parser.get_counted_bytes()
+ windows.append((window, bitmap))
+ return cls(windows)
+
+
+def _priority_table(items):
+ by_priority = collections.defaultdict(list)
+ for rdata in items:
+ by_priority[rdata._processing_priority()].append(rdata)
+ return by_priority
+
+
+def priority_processing_order(iterable):
+ items = list(iterable)
+ if len(items) == 1:
+ return items
+ by_priority = _priority_table(items)
+ ordered = []
+ for k in sorted(by_priority.keys()):
+ rdatas = by_priority[k]
+ random.shuffle(rdatas)
+ ordered.extend(rdatas)
+ return ordered
+
+
+_no_weight = 0.1
+
+
+def weighted_processing_order(iterable):
+ items = list(iterable)
+ if len(items) == 1:
+ return items
+ by_priority = _priority_table(items)
+ ordered = []
+ for k in sorted(by_priority.keys()):
+ rdatas = by_priority[k]
+ total = sum(rdata._processing_weight() or _no_weight for rdata in rdatas)
+ while len(rdatas) > 1:
+ r = random.uniform(0, total)
+ for n, rdata in enumerate(rdatas): # noqa: B007
+ weight = rdata._processing_weight() or _no_weight
+ if weight > r:
+ break
+ r -= weight
+ total -= weight
+ ordered.append(rdata) # pylint: disable=undefined-loop-variable
+ del rdatas[n] # pylint: disable=undefined-loop-variable
+ ordered.append(rdatas[0])
+ return ordered
+
+
+def parse_formatted_hex(formatted, num_chunks, chunk_size, separator):
+ if len(formatted) != num_chunks * (chunk_size + 1) - 1:
+ raise ValueError("invalid formatted hex string")
+ value = b""
+ for _ in range(num_chunks):
+ chunk = formatted[0:chunk_size]
+ value += int(chunk, 16).to_bytes(chunk_size // 2, "big")
+ formatted = formatted[chunk_size:]
+ if len(formatted) > 0 and formatted[0] != separator:
+ raise ValueError("invalid formatted hex string")
+ formatted = formatted[1:]
+ return value
diff --git a/.venv/lib/python3.12/site-packages/dns/renderer.py b/.venv/lib/python3.12/site-packages/dns/renderer.py
new file mode 100644
index 00000000..a77481f6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/renderer.py
@@ -0,0 +1,346 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Help for building DNS wire format messages"""
+
+import contextlib
+import io
+import random
+import struct
+import time
+
+import dns.exception
+import dns.tsig
+
+QUESTION = 0
+ANSWER = 1
+AUTHORITY = 2
+ADDITIONAL = 3
+
+
+@contextlib.contextmanager
+def prefixed_length(output, length_length):
+ output.write(b"\00" * length_length)
+ start = output.tell()
+ yield
+ end = output.tell()
+ length = end - start
+ if length > 0:
+ try:
+ output.seek(start - length_length)
+ try:
+ output.write(length.to_bytes(length_length, "big"))
+ except OverflowError:
+ raise dns.exception.FormError
+ finally:
+ output.seek(end)
+
+
+class Renderer:
+ """Helper class for building DNS wire-format messages.
+
+ Most applications can use the higher-level L{dns.message.Message}
+ class and its to_wire() method to generate wire-format messages.
+ This class is for those applications which need finer control
+ over the generation of messages.
+
+ Typical use::
+
+ r = dns.renderer.Renderer(id=1, flags=0x80, max_size=512)
+ r.add_question(qname, qtype, qclass)
+ r.add_rrset(dns.renderer.ANSWER, rrset_1)
+ r.add_rrset(dns.renderer.ANSWER, rrset_2)
+ r.add_rrset(dns.renderer.AUTHORITY, ns_rrset)
+ r.add_rrset(dns.renderer.ADDITIONAL, ad_rrset_1)
+ r.add_rrset(dns.renderer.ADDITIONAL, ad_rrset_2)
+ r.add_edns(0, 0, 4096)
+ r.write_header()
+ r.add_tsig(keyname, secret, 300, 1, 0, '', request_mac)
+ wire = r.get_wire()
+
+ If padding is going to be used, then the OPT record MUST be
+ written after everything else in the additional section except for
+ the TSIG (if any).
+
+ output, an io.BytesIO, where rendering is written
+
+ id: the message id
+
+ flags: the message flags
+
+ max_size: the maximum size of the message
+
+ origin: the origin to use when rendering relative names
+
+ compress: the compression table
+
+ section: an int, the section currently being rendered
+
+ counts: list of the number of RRs in each section
+
+ mac: the MAC of the rendered message (if TSIG was used)
+ """
+
+ def __init__(self, id=None, flags=0, max_size=65535, origin=None):
+ """Initialize a new renderer."""
+
+ self.output = io.BytesIO()
+ if id is None:
+ self.id = random.randint(0, 65535)
+ else:
+ self.id = id
+ self.flags = flags
+ self.max_size = max_size
+ self.origin = origin
+ self.compress = {}
+ self.section = QUESTION
+ self.counts = [0, 0, 0, 0]
+ self.output.write(b"\x00" * 12)
+ self.mac = ""
+ self.reserved = 0
+ self.was_padded = False
+
+ def _rollback(self, where):
+ """Truncate the output buffer at offset *where*, and remove any
+ compression table entries that pointed beyond the truncation
+ point.
+ """
+
+ self.output.seek(where)
+ self.output.truncate()
+ keys_to_delete = []
+ for k, v in self.compress.items():
+ if v >= where:
+ keys_to_delete.append(k)
+ for k in keys_to_delete:
+ del self.compress[k]
+
+ def _set_section(self, section):
+ """Set the renderer's current section.
+
+ Sections must be rendered order: QUESTION, ANSWER, AUTHORITY,
+ ADDITIONAL. Sections may be empty.
+
+ Raises dns.exception.FormError if an attempt was made to set
+ a section value less than the current section.
+ """
+
+ if self.section != section:
+ if self.section > section:
+ raise dns.exception.FormError
+ self.section = section
+
+ @contextlib.contextmanager
+ def _track_size(self):
+ start = self.output.tell()
+ yield start
+ if self.output.tell() > self.max_size:
+ self._rollback(start)
+ raise dns.exception.TooBig
+
+ @contextlib.contextmanager
+ def _temporarily_seek_to(self, where):
+ current = self.output.tell()
+ try:
+ self.output.seek(where)
+ yield
+ finally:
+ self.output.seek(current)
+
+ def add_question(self, qname, rdtype, rdclass=dns.rdataclass.IN):
+ """Add a question to the message."""
+
+ self._set_section(QUESTION)
+ with self._track_size():
+ qname.to_wire(self.output, self.compress, self.origin)
+ self.output.write(struct.pack("!HH", rdtype, rdclass))
+ self.counts[QUESTION] += 1
+
+ def add_rrset(self, section, rrset, **kw):
+ """Add the rrset to the specified section.
+
+ Any keyword arguments are passed on to the rdataset's to_wire()
+ routine.
+ """
+
+ self._set_section(section)
+ with self._track_size():
+ n = rrset.to_wire(self.output, self.compress, self.origin, **kw)
+ self.counts[section] += n
+
+ def add_rdataset(self, section, name, rdataset, **kw):
+ """Add the rdataset to the specified section, using the specified
+ name as the owner name.
+
+ Any keyword arguments are passed on to the rdataset's to_wire()
+ routine.
+ """
+
+ self._set_section(section)
+ with self._track_size():
+ n = rdataset.to_wire(name, self.output, self.compress, self.origin, **kw)
+ self.counts[section] += n
+
+ def add_opt(self, opt, pad=0, opt_size=0, tsig_size=0):
+ """Add *opt* to the additional section, applying padding if desired. The
+ padding will take the specified precomputed OPT size and TSIG size into
+ account.
+
+ Note that we don't have reliable way of knowing how big a GSS-TSIG digest
+ might be, so we we might not get an even multiple of the pad in that case."""
+ if pad:
+ ttl = opt.ttl
+ assert opt_size >= 11
+ opt_rdata = opt[0]
+ size_without_padding = self.output.tell() + opt_size + tsig_size
+ remainder = size_without_padding % pad
+ if remainder:
+ pad = b"\x00" * (pad - remainder)
+ else:
+ pad = b""
+ options = list(opt_rdata.options)
+ options.append(dns.edns.GenericOption(dns.edns.OptionType.PADDING, pad))
+ opt = dns.message.Message._make_opt(ttl, opt_rdata.rdclass, options)
+ self.was_padded = True
+ self.add_rrset(ADDITIONAL, opt)
+
+ def add_edns(self, edns, ednsflags, payload, options=None):
+ """Add an EDNS OPT record to the message."""
+
+ # make sure the EDNS version in ednsflags agrees with edns
+ ednsflags &= 0xFF00FFFF
+ ednsflags |= edns << 16
+ opt = dns.message.Message._make_opt(ednsflags, payload, options)
+ self.add_opt(opt)
+
+ def add_tsig(
+ self,
+ keyname,
+ secret,
+ fudge,
+ id,
+ tsig_error,
+ other_data,
+ request_mac,
+ algorithm=dns.tsig.default_algorithm,
+ ):
+ """Add a TSIG signature to the message."""
+
+ s = self.output.getvalue()
+
+ if isinstance(secret, dns.tsig.Key):
+ key = secret
+ else:
+ key = dns.tsig.Key(keyname, secret, algorithm)
+ tsig = dns.message.Message._make_tsig(
+ keyname, algorithm, 0, fudge, b"", id, tsig_error, other_data
+ )
+ (tsig, _) = dns.tsig.sign(s, key, tsig[0], int(time.time()), request_mac)
+ self._write_tsig(tsig, keyname)
+
+ def add_multi_tsig(
+ self,
+ ctx,
+ keyname,
+ secret,
+ fudge,
+ id,
+ tsig_error,
+ other_data,
+ request_mac,
+ algorithm=dns.tsig.default_algorithm,
+ ):
+ """Add a TSIG signature to the message. Unlike add_tsig(), this can be
+ used for a series of consecutive DNS envelopes, e.g. for a zone
+ transfer over TCP [RFC2845, 4.4].
+
+ For the first message in the sequence, give ctx=None. For each
+ subsequent message, give the ctx that was returned from the
+ add_multi_tsig() call for the previous message."""
+
+ s = self.output.getvalue()
+
+ if isinstance(secret, dns.tsig.Key):
+ key = secret
+ else:
+ key = dns.tsig.Key(keyname, secret, algorithm)
+ tsig = dns.message.Message._make_tsig(
+ keyname, algorithm, 0, fudge, b"", id, tsig_error, other_data
+ )
+ (tsig, ctx) = dns.tsig.sign(
+ s, key, tsig[0], int(time.time()), request_mac, ctx, True
+ )
+ self._write_tsig(tsig, keyname)
+ return ctx
+
+ def _write_tsig(self, tsig, keyname):
+ if self.was_padded:
+ compress = None
+ else:
+ compress = self.compress
+ self._set_section(ADDITIONAL)
+ with self._track_size():
+ keyname.to_wire(self.output, compress, self.origin)
+ self.output.write(
+ struct.pack("!HHI", dns.rdatatype.TSIG, dns.rdataclass.ANY, 0)
+ )
+ with prefixed_length(self.output, 2):
+ tsig.to_wire(self.output)
+
+ self.counts[ADDITIONAL] += 1
+ with self._temporarily_seek_to(10):
+ self.output.write(struct.pack("!H", self.counts[ADDITIONAL]))
+
+ def write_header(self):
+ """Write the DNS message header.
+
+ Writing the DNS message header is done after all sections
+ have been rendered, but before the optional TSIG signature
+ is added.
+ """
+
+ with self._temporarily_seek_to(0):
+ self.output.write(
+ struct.pack(
+ "!HHHHHH",
+ self.id,
+ self.flags,
+ self.counts[0],
+ self.counts[1],
+ self.counts[2],
+ self.counts[3],
+ )
+ )
+
+ def get_wire(self):
+ """Return the wire format message."""
+
+ return self.output.getvalue()
+
+ def reserve(self, size: int) -> None:
+ """Reserve *size* bytes."""
+ if size < 0:
+ raise ValueError("reserved amount must be non-negative")
+ if size > self.max_size:
+ raise ValueError("cannot reserve more than the maximum size")
+ self.reserved += size
+ self.max_size -= size
+
+ def release_reserved(self) -> None:
+ """Release the reserved bytes."""
+ self.max_size += self.reserved
+ self.reserved = 0
diff --git a/.venv/lib/python3.12/site-packages/dns/resolver.py b/.venv/lib/python3.12/site-packages/dns/resolver.py
new file mode 100644
index 00000000..3ba76e31
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/resolver.py
@@ -0,0 +1,2053 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS stub resolver."""
+
+import contextlib
+import random
+import socket
+import sys
+import threading
+import time
+import warnings
+from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
+from urllib.parse import urlparse
+
+import dns._ddr
+import dns.edns
+import dns.exception
+import dns.flags
+import dns.inet
+import dns.ipv4
+import dns.ipv6
+import dns.message
+import dns.name
+import dns.rdata
+import dns.nameserver
+import dns.query
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+import dns.rdtypes.svcbbase
+import dns.reversename
+import dns.tsig
+
+if sys.platform == "win32": # pragma: no cover
+ import dns.win32util
+
+
+class NXDOMAIN(dns.exception.DNSException):
+ """The DNS query name does not exist."""
+
+ supp_kwargs = {"qnames", "responses"}
+ fmt = None # we have our own __str__ implementation
+
+ # pylint: disable=arguments-differ
+
+ # We do this as otherwise mypy complains about unexpected keyword argument
+ # idna_exception
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def _check_kwargs(self, qnames, responses=None):
+ if not isinstance(qnames, (list, tuple, set)):
+ raise AttributeError("qnames must be a list, tuple or set")
+ if len(qnames) == 0:
+ raise AttributeError("qnames must contain at least one element")
+ if responses is None:
+ responses = {}
+ elif not isinstance(responses, dict):
+ raise AttributeError("responses must be a dict(qname=response)")
+ kwargs = dict(qnames=qnames, responses=responses)
+ return kwargs
+
+ def __str__(self) -> str:
+ if "qnames" not in self.kwargs:
+ return super().__str__()
+ qnames = self.kwargs["qnames"]
+ if len(qnames) > 1:
+ msg = "None of DNS query names exist"
+ else:
+ msg = "The DNS query name does not exist"
+ qnames = ", ".join(map(str, qnames))
+ return f"{msg}: {qnames}"
+
+ @property
+ def canonical_name(self):
+ """Return the unresolved canonical name."""
+ if "qnames" not in self.kwargs:
+ raise TypeError("parametrized exception required")
+ for qname in self.kwargs["qnames"]:
+ response = self.kwargs["responses"][qname]
+ try:
+ cname = response.canonical_name()
+ if cname != qname:
+ return cname
+ except Exception: # pragma: no cover
+ # We can just eat this exception as it means there was
+ # something wrong with the response.
+ pass
+ return self.kwargs["qnames"][0]
+
+ def __add__(self, e_nx):
+ """Augment by results from another NXDOMAIN exception."""
+ qnames0 = list(self.kwargs.get("qnames", []))
+ responses0 = dict(self.kwargs.get("responses", {}))
+ responses1 = e_nx.kwargs.get("responses", {})
+ for qname1 in e_nx.kwargs.get("qnames", []):
+ if qname1 not in qnames0:
+ qnames0.append(qname1)
+ if qname1 in responses1:
+ responses0[qname1] = responses1[qname1]
+ return NXDOMAIN(qnames=qnames0, responses=responses0)
+
+ def qnames(self):
+ """All of the names that were tried.
+
+ Returns a list of ``dns.name.Name``.
+ """
+ return self.kwargs["qnames"]
+
+ def responses(self):
+ """A map from queried names to their NXDOMAIN responses.
+
+ Returns a dict mapping a ``dns.name.Name`` to a
+ ``dns.message.Message``.
+ """
+ return self.kwargs["responses"]
+
+ def response(self, qname):
+ """The response for query *qname*.
+
+ Returns a ``dns.message.Message``.
+ """
+ return self.kwargs["responses"][qname]
+
+
+class YXDOMAIN(dns.exception.DNSException):
+ """The DNS query name is too long after DNAME substitution."""
+
+
+ErrorTuple = Tuple[
+ Optional[str],
+ bool,
+ int,
+ Union[Exception, str],
+ Optional[dns.message.Message],
+]
+
+
+def _errors_to_text(errors: List[ErrorTuple]) -> List[str]:
+ """Turn a resolution errors trace into a list of text."""
+ texts = []
+ for err in errors:
+ texts.append(f"Server {err[0]} answered {err[3]}")
+ return texts
+
+
+class LifetimeTimeout(dns.exception.Timeout):
+ """The resolution lifetime expired."""
+
+ msg = "The resolution lifetime expired."
+ fmt = f"{msg[:-1]} after {{timeout:.3f}} seconds: {{errors}}"
+ supp_kwargs = {"timeout", "errors"}
+
+ # We do this as otherwise mypy complains about unexpected keyword argument
+ # idna_exception
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def _fmt_kwargs(self, **kwargs):
+ srv_msgs = _errors_to_text(kwargs["errors"])
+ return super()._fmt_kwargs(
+ timeout=kwargs["timeout"], errors="; ".join(srv_msgs)
+ )
+
+
+# We added more detail to resolution timeouts, but they are still
+# subclasses of dns.exception.Timeout for backwards compatibility. We also
+# keep dns.resolver.Timeout defined for backwards compatibility.
+Timeout = LifetimeTimeout
+
+
+class NoAnswer(dns.exception.DNSException):
+ """The DNS response does not contain an answer to the question."""
+
+ fmt = "The DNS response does not contain an answer to the question: {query}"
+ supp_kwargs = {"response"}
+
+ # We do this as otherwise mypy complains about unexpected keyword argument
+ # idna_exception
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def _fmt_kwargs(self, **kwargs):
+ return super()._fmt_kwargs(query=kwargs["response"].question)
+
+ def response(self):
+ return self.kwargs["response"]
+
+
+class NoNameservers(dns.exception.DNSException):
+ """All nameservers failed to answer the query.
+
+ errors: list of servers and respective errors
+ The type of errors is
+ [(server IP address, any object convertible to string)].
+ Non-empty errors list will add explanatory message ()
+ """
+
+ msg = "All nameservers failed to answer the query."
+ fmt = f"{msg[:-1]} {{query}}: {{errors}}"
+ supp_kwargs = {"request", "errors"}
+
+ # We do this as otherwise mypy complains about unexpected keyword argument
+ # idna_exception
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def _fmt_kwargs(self, **kwargs):
+ srv_msgs = _errors_to_text(kwargs["errors"])
+ return super()._fmt_kwargs(
+ query=kwargs["request"].question, errors="; ".join(srv_msgs)
+ )
+
+
+class NotAbsolute(dns.exception.DNSException):
+ """An absolute domain name is required but a relative name was provided."""
+
+
+class NoRootSOA(dns.exception.DNSException):
+ """There is no SOA RR at the DNS root name. This should never happen!"""
+
+
+class NoMetaqueries(dns.exception.DNSException):
+ """DNS metaqueries are not allowed."""
+
+
+class NoResolverConfiguration(dns.exception.DNSException):
+ """Resolver configuration could not be read or specified no nameservers."""
+
+
+class Answer:
+ """DNS stub resolver answer.
+
+ Instances of this class bundle up the result of a successful DNS
+ resolution.
+
+ For convenience, the answer object implements much of the sequence
+ protocol, forwarding to its ``rrset`` attribute. E.g.
+ ``for a in answer`` is equivalent to ``for a in answer.rrset``.
+ ``answer[i]`` is equivalent to ``answer.rrset[i]``, and
+ ``answer[i:j]`` is equivalent to ``answer.rrset[i:j]``.
+
+ Note that CNAMEs or DNAMEs in the response may mean that answer
+ RRset's name might not be the query name.
+ """
+
+ def __init__(
+ self,
+ qname: dns.name.Name,
+ rdtype: dns.rdatatype.RdataType,
+ rdclass: dns.rdataclass.RdataClass,
+ response: dns.message.QueryMessage,
+ nameserver: Optional[str] = None,
+ port: Optional[int] = None,
+ ) -> None:
+ self.qname = qname
+ self.rdtype = rdtype
+ self.rdclass = rdclass
+ self.response = response
+ self.nameserver = nameserver
+ self.port = port
+ self.chaining_result = response.resolve_chaining()
+ # Copy some attributes out of chaining_result for backwards
+ # compatibility and convenience.
+ self.canonical_name = self.chaining_result.canonical_name
+ self.rrset = self.chaining_result.answer
+ self.expiration = time.time() + self.chaining_result.minimum_ttl
+
+ def __getattr__(self, attr): # pragma: no cover
+ if attr == "name":
+ return self.rrset.name
+ elif attr == "ttl":
+ return self.rrset.ttl
+ elif attr == "covers":
+ return self.rrset.covers
+ elif attr == "rdclass":
+ return self.rrset.rdclass
+ elif attr == "rdtype":
+ return self.rrset.rdtype
+ else:
+ raise AttributeError(attr)
+
+ def __len__(self) -> int:
+ return self.rrset and len(self.rrset) or 0
+
+ def __iter__(self) -> Iterator[dns.rdata.Rdata]:
+ return self.rrset and iter(self.rrset) or iter(tuple())
+
+ def __getitem__(self, i):
+ if self.rrset is None:
+ raise IndexError
+ return self.rrset[i]
+
+ def __delitem__(self, i):
+ if self.rrset is None:
+ raise IndexError
+ del self.rrset[i]
+
+
+class Answers(dict):
+ """A dict of DNS stub resolver answers, indexed by type."""
+
+
+class HostAnswers(Answers):
+ """A dict of DNS stub resolver answers to a host name lookup, indexed by
+ type.
+ """
+
+ @classmethod
+ def make(
+ cls,
+ v6: Optional[Answer] = None,
+ v4: Optional[Answer] = None,
+ add_empty: bool = True,
+ ) -> "HostAnswers":
+ answers = HostAnswers()
+ if v6 is not None and (add_empty or v6.rrset):
+ answers[dns.rdatatype.AAAA] = v6
+ if v4 is not None and (add_empty or v4.rrset):
+ answers[dns.rdatatype.A] = v4
+ return answers
+
+ # Returns pairs of (address, family) from this result, potentially
+ # filtering by address family.
+ def addresses_and_families(
+ self, family: int = socket.AF_UNSPEC
+ ) -> Iterator[Tuple[str, int]]:
+ if family == socket.AF_UNSPEC:
+ yield from self.addresses_and_families(socket.AF_INET6)
+ yield from self.addresses_and_families(socket.AF_INET)
+ return
+ elif family == socket.AF_INET6:
+ answer = self.get(dns.rdatatype.AAAA)
+ elif family == socket.AF_INET:
+ answer = self.get(dns.rdatatype.A)
+ else: # pragma: no cover
+ raise NotImplementedError(f"unknown address family {family}")
+ if answer:
+ for rdata in answer:
+ yield (rdata.address, family)
+
+ # Returns addresses from this result, potentially filtering by
+ # address family.
+ def addresses(self, family: int = socket.AF_UNSPEC) -> Iterator[str]:
+ return (pair[0] for pair in self.addresses_and_families(family))
+
+ # Returns the canonical name from this result.
+ def canonical_name(self) -> dns.name.Name:
+ answer = self.get(dns.rdatatype.AAAA, self.get(dns.rdatatype.A))
+ return answer.canonical_name
+
+
+class CacheStatistics:
+ """Cache Statistics"""
+
+ def __init__(self, hits: int = 0, misses: int = 0) -> None:
+ self.hits = hits
+ self.misses = misses
+
+ def reset(self) -> None:
+ self.hits = 0
+ self.misses = 0
+
+ def clone(self) -> "CacheStatistics":
+ return CacheStatistics(self.hits, self.misses)
+
+
+class CacheBase:
+ def __init__(self) -> None:
+ self.lock = threading.Lock()
+ self.statistics = CacheStatistics()
+
+ def reset_statistics(self) -> None:
+ """Reset all statistics to zero."""
+ with self.lock:
+ self.statistics.reset()
+
+ def hits(self) -> int:
+ """How many hits has the cache had?"""
+ with self.lock:
+ return self.statistics.hits
+
+ def misses(self) -> int:
+ """How many misses has the cache had?"""
+ with self.lock:
+ return self.statistics.misses
+
+ def get_statistics_snapshot(self) -> CacheStatistics:
+ """Return a consistent snapshot of all the statistics.
+
+ If running with multiple threads, it's better to take a
+ snapshot than to call statistics methods such as hits() and
+ misses() individually.
+ """
+ with self.lock:
+ return self.statistics.clone()
+
+
+CacheKey = Tuple[dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass]
+
+
+class Cache(CacheBase):
+ """Simple thread-safe DNS answer cache."""
+
+ def __init__(self, cleaning_interval: float = 300.0) -> None:
+ """*cleaning_interval*, a ``float`` is the number of seconds between
+ periodic cleanings.
+ """
+
+ super().__init__()
+ self.data: Dict[CacheKey, Answer] = {}
+ self.cleaning_interval = cleaning_interval
+ self.next_cleaning: float = time.time() + self.cleaning_interval
+
+ def _maybe_clean(self) -> None:
+ """Clean the cache if it's time to do so."""
+
+ now = time.time()
+ if self.next_cleaning <= now:
+ keys_to_delete = []
+ for k, v in self.data.items():
+ if v.expiration <= now:
+ keys_to_delete.append(k)
+ for k in keys_to_delete:
+ del self.data[k]
+ now = time.time()
+ self.next_cleaning = now + self.cleaning_interval
+
+ def get(self, key: CacheKey) -> Optional[Answer]:
+ """Get the answer associated with *key*.
+
+ Returns None if no answer is cached for the key.
+
+ *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``
+ tuple whose values are the query name, rdtype, and rdclass respectively.
+
+ Returns a ``dns.resolver.Answer`` or ``None``.
+ """
+
+ with self.lock:
+ self._maybe_clean()
+ v = self.data.get(key)
+ if v is None or v.expiration <= time.time():
+ self.statistics.misses += 1
+ return None
+ self.statistics.hits += 1
+ return v
+
+ def put(self, key: CacheKey, value: Answer) -> None:
+ """Associate key and value in the cache.
+
+ *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``
+ tuple whose values are the query name, rdtype, and rdclass respectively.
+
+ *value*, a ``dns.resolver.Answer``, the answer.
+ """
+
+ with self.lock:
+ self._maybe_clean()
+ self.data[key] = value
+
+ def flush(self, key: Optional[CacheKey] = None) -> None:
+ """Flush the cache.
+
+ If *key* is not ``None``, only that item is flushed. Otherwise the entire cache
+ is flushed.
+
+ *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``
+ tuple whose values are the query name, rdtype, and rdclass respectively.
+ """
+
+ with self.lock:
+ if key is not None:
+ if key in self.data:
+ del self.data[key]
+ else:
+ self.data = {}
+ self.next_cleaning = time.time() + self.cleaning_interval
+
+
+class LRUCacheNode:
+ """LRUCache node."""
+
+ def __init__(self, key, value):
+ self.key = key
+ self.value = value
+ self.hits = 0
+ self.prev = self
+ self.next = self
+
+ def link_after(self, node: "LRUCacheNode") -> None:
+ self.prev = node
+ self.next = node.next
+ node.next.prev = self
+ node.next = self
+
+ def unlink(self) -> None:
+ self.next.prev = self.prev
+ self.prev.next = self.next
+
+
+class LRUCache(CacheBase):
+ """Thread-safe, bounded, least-recently-used DNS answer cache.
+
+ This cache is better than the simple cache (above) if you're
+ running a web crawler or other process that does a lot of
+ resolutions. The LRUCache has a maximum number of nodes, and when
+ it is full, the least-recently used node is removed to make space
+ for a new one.
+ """
+
+ def __init__(self, max_size: int = 100000) -> None:
+ """*max_size*, an ``int``, is the maximum number of nodes to cache;
+ it must be greater than 0.
+ """
+
+ super().__init__()
+ self.data: Dict[CacheKey, LRUCacheNode] = {}
+ self.set_max_size(max_size)
+ self.sentinel: LRUCacheNode = LRUCacheNode(None, None)
+ self.sentinel.prev = self.sentinel
+ self.sentinel.next = self.sentinel
+
+ def set_max_size(self, max_size: int) -> None:
+ if max_size < 1:
+ max_size = 1
+ self.max_size = max_size
+
+ def get(self, key: CacheKey) -> Optional[Answer]:
+ """Get the answer associated with *key*.
+
+ Returns None if no answer is cached for the key.
+
+ *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``
+ tuple whose values are the query name, rdtype, and rdclass respectively.
+
+ Returns a ``dns.resolver.Answer`` or ``None``.
+ """
+
+ with self.lock:
+ node = self.data.get(key)
+ if node is None:
+ self.statistics.misses += 1
+ return None
+ # Unlink because we're either going to move the node to the front
+ # of the LRU list or we're going to free it.
+ node.unlink()
+ if node.value.expiration <= time.time():
+ del self.data[node.key]
+ self.statistics.misses += 1
+ return None
+ node.link_after(self.sentinel)
+ self.statistics.hits += 1
+ node.hits += 1
+ return node.value
+
+ def get_hits_for_key(self, key: CacheKey) -> int:
+ """Return the number of cache hits associated with the specified key."""
+ with self.lock:
+ node = self.data.get(key)
+ if node is None or node.value.expiration <= time.time():
+ return 0
+ else:
+ return node.hits
+
+ def put(self, key: CacheKey, value: Answer) -> None:
+ """Associate key and value in the cache.
+
+ *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``
+ tuple whose values are the query name, rdtype, and rdclass respectively.
+
+ *value*, a ``dns.resolver.Answer``, the answer.
+ """
+
+ with self.lock:
+ node = self.data.get(key)
+ if node is not None:
+ node.unlink()
+ del self.data[node.key]
+ while len(self.data) >= self.max_size:
+ gnode = self.sentinel.prev
+ gnode.unlink()
+ del self.data[gnode.key]
+ node = LRUCacheNode(key, value)
+ node.link_after(self.sentinel)
+ self.data[key] = node
+
+ def flush(self, key: Optional[CacheKey] = None) -> None:
+ """Flush the cache.
+
+ If *key* is not ``None``, only that item is flushed. Otherwise the entire cache
+ is flushed.
+
+ *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``
+ tuple whose values are the query name, rdtype, and rdclass respectively.
+ """
+
+ with self.lock:
+ if key is not None:
+ node = self.data.get(key)
+ if node is not None:
+ node.unlink()
+ del self.data[node.key]
+ else:
+ gnode = self.sentinel.next
+ while gnode != self.sentinel:
+ next = gnode.next
+ gnode.unlink()
+ gnode = next
+ self.data = {}
+
+
+class _Resolution:
+ """Helper class for dns.resolver.Resolver.resolve().
+
+ All of the "business logic" of resolution is encapsulated in this
+ class, allowing us to have multiple resolve() implementations
+ using different I/O schemes without copying all of the
+ complicated logic.
+
+ This class is a "friend" to dns.resolver.Resolver and manipulates
+ resolver data structures directly.
+ """
+
+ def __init__(
+ self,
+ resolver: "BaseResolver",
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ rdclass: Union[dns.rdataclass.RdataClass, str],
+ tcp: bool,
+ raise_on_no_answer: bool,
+ search: Optional[bool],
+ ) -> None:
+ if isinstance(qname, str):
+ qname = dns.name.from_text(qname, None)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ if dns.rdatatype.is_metatype(rdtype):
+ raise NoMetaqueries
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ if dns.rdataclass.is_metaclass(rdclass):
+ raise NoMetaqueries
+ self.resolver = resolver
+ self.qnames_to_try = resolver._get_qnames_to_try(qname, search)
+ self.qnames = self.qnames_to_try[:]
+ self.rdtype = rdtype
+ self.rdclass = rdclass
+ self.tcp = tcp
+ self.raise_on_no_answer = raise_on_no_answer
+ self.nxdomain_responses: Dict[dns.name.Name, dns.message.QueryMessage] = {}
+ # Initialize other things to help analysis tools
+ self.qname = dns.name.empty
+ self.nameservers: List[dns.nameserver.Nameserver] = []
+ self.current_nameservers: List[dns.nameserver.Nameserver] = []
+ self.errors: List[ErrorTuple] = []
+ self.nameserver: Optional[dns.nameserver.Nameserver] = None
+ self.tcp_attempt = False
+ self.retry_with_tcp = False
+ self.request: Optional[dns.message.QueryMessage] = None
+ self.backoff = 0.0
+
+ def next_request(
+ self,
+ ) -> Tuple[Optional[dns.message.QueryMessage], Optional[Answer]]:
+ """Get the next request to send, and check the cache.
+
+ Returns a (request, answer) tuple. At most one of request or
+ answer will not be None.
+ """
+
+ # We return a tuple instead of Union[Message,Answer] as it lets
+ # the caller avoid isinstance().
+
+ while len(self.qnames) > 0:
+ self.qname = self.qnames.pop(0)
+
+ # Do we know the answer?
+ if self.resolver.cache:
+ answer = self.resolver.cache.get(
+ (self.qname, self.rdtype, self.rdclass)
+ )
+ if answer is not None:
+ if answer.rrset is None and self.raise_on_no_answer:
+ raise NoAnswer(response=answer.response)
+ else:
+ return (None, answer)
+ answer = self.resolver.cache.get(
+ (self.qname, dns.rdatatype.ANY, self.rdclass)
+ )
+ if answer is not None and answer.response.rcode() == dns.rcode.NXDOMAIN:
+ # cached NXDOMAIN; record it and continue to next
+ # name.
+ self.nxdomain_responses[self.qname] = answer.response
+ continue
+
+ # Build the request
+ request = dns.message.make_query(self.qname, self.rdtype, self.rdclass)
+ if self.resolver.keyname is not None:
+ request.use_tsig(
+ self.resolver.keyring,
+ self.resolver.keyname,
+ algorithm=self.resolver.keyalgorithm,
+ )
+ request.use_edns(
+ self.resolver.edns,
+ self.resolver.ednsflags,
+ self.resolver.payload,
+ options=self.resolver.ednsoptions,
+ )
+ if self.resolver.flags is not None:
+ request.flags = self.resolver.flags
+
+ self.nameservers = self.resolver._enrich_nameservers(
+ self.resolver._nameservers,
+ self.resolver.nameserver_ports,
+ self.resolver.port,
+ )
+ if self.resolver.rotate:
+ random.shuffle(self.nameservers)
+ self.current_nameservers = self.nameservers[:]
+ self.errors = []
+ self.nameserver = None
+ self.tcp_attempt = False
+ self.retry_with_tcp = False
+ self.request = request
+ self.backoff = 0.10
+
+ return (request, None)
+
+ #
+ # We've tried everything and only gotten NXDOMAINs. (We know
+ # it's only NXDOMAINs as anything else would have returned
+ # before now.)
+ #
+ raise NXDOMAIN(qnames=self.qnames_to_try, responses=self.nxdomain_responses)
+
+ def next_nameserver(self) -> Tuple[dns.nameserver.Nameserver, bool, float]:
+ if self.retry_with_tcp:
+ assert self.nameserver is not None
+ assert not self.nameserver.is_always_max_size()
+ self.tcp_attempt = True
+ self.retry_with_tcp = False
+ return (self.nameserver, True, 0)
+
+ backoff = 0.0
+ if not self.current_nameservers:
+ if len(self.nameservers) == 0:
+ # Out of things to try!
+ raise NoNameservers(request=self.request, errors=self.errors)
+ self.current_nameservers = self.nameservers[:]
+ backoff = self.backoff
+ self.backoff = min(self.backoff * 2, 2)
+
+ self.nameserver = self.current_nameservers.pop(0)
+ self.tcp_attempt = self.tcp or self.nameserver.is_always_max_size()
+ return (self.nameserver, self.tcp_attempt, backoff)
+
+ def query_result(
+ self, response: Optional[dns.message.Message], ex: Optional[Exception]
+ ) -> Tuple[Optional[Answer], bool]:
+ #
+ # returns an (answer: Answer, end_loop: bool) tuple.
+ #
+ assert self.nameserver is not None
+ if ex:
+ # Exception during I/O or from_wire()
+ assert response is None
+ self.errors.append(
+ (
+ str(self.nameserver),
+ self.tcp_attempt,
+ self.nameserver.answer_port(),
+ ex,
+ response,
+ )
+ )
+ if (
+ isinstance(ex, dns.exception.FormError)
+ or isinstance(ex, EOFError)
+ or isinstance(ex, OSError)
+ or isinstance(ex, NotImplementedError)
+ ):
+ # This nameserver is no good, take it out of the mix.
+ self.nameservers.remove(self.nameserver)
+ elif isinstance(ex, dns.message.Truncated):
+ if self.tcp_attempt:
+ # Truncation with TCP is no good!
+ self.nameservers.remove(self.nameserver)
+ else:
+ self.retry_with_tcp = True
+ return (None, False)
+ # We got an answer!
+ assert response is not None
+ assert isinstance(response, dns.message.QueryMessage)
+ rcode = response.rcode()
+ if rcode == dns.rcode.NOERROR:
+ try:
+ answer = Answer(
+ self.qname,
+ self.rdtype,
+ self.rdclass,
+ response,
+ self.nameserver.answer_nameserver(),
+ self.nameserver.answer_port(),
+ )
+ except Exception as e:
+ self.errors.append(
+ (
+ str(self.nameserver),
+ self.tcp_attempt,
+ self.nameserver.answer_port(),
+ e,
+ response,
+ )
+ )
+ # The nameserver is no good, take it out of the mix.
+ self.nameservers.remove(self.nameserver)
+ return (None, False)
+ if self.resolver.cache:
+ self.resolver.cache.put((self.qname, self.rdtype, self.rdclass), answer)
+ if answer.rrset is None and self.raise_on_no_answer:
+ raise NoAnswer(response=answer.response)
+ return (answer, True)
+ elif rcode == dns.rcode.NXDOMAIN:
+ # Further validate the response by making an Answer, even
+ # if we aren't going to cache it.
+ try:
+ answer = Answer(
+ self.qname, dns.rdatatype.ANY, dns.rdataclass.IN, response
+ )
+ except Exception as e:
+ self.errors.append(
+ (
+ str(self.nameserver),
+ self.tcp_attempt,
+ self.nameserver.answer_port(),
+ e,
+ response,
+ )
+ )
+ # The nameserver is no good, take it out of the mix.
+ self.nameservers.remove(self.nameserver)
+ return (None, False)
+ self.nxdomain_responses[self.qname] = response
+ if self.resolver.cache:
+ self.resolver.cache.put(
+ (self.qname, dns.rdatatype.ANY, self.rdclass), answer
+ )
+ # Make next_nameserver() return None, so caller breaks its
+ # inner loop and calls next_request().
+ return (None, True)
+ elif rcode == dns.rcode.YXDOMAIN:
+ yex = YXDOMAIN()
+ self.errors.append(
+ (
+ str(self.nameserver),
+ self.tcp_attempt,
+ self.nameserver.answer_port(),
+ yex,
+ response,
+ )
+ )
+ raise yex
+ else:
+ #
+ # We got a response, but we're not happy with the
+ # rcode in it.
+ #
+ if rcode != dns.rcode.SERVFAIL or not self.resolver.retry_servfail:
+ self.nameservers.remove(self.nameserver)
+ self.errors.append(
+ (
+ str(self.nameserver),
+ self.tcp_attempt,
+ self.nameserver.answer_port(),
+ dns.rcode.to_text(rcode),
+ response,
+ )
+ )
+ return (None, False)
+
+
+class BaseResolver:
+ """DNS stub resolver."""
+
+ # We initialize in reset()
+ #
+ # pylint: disable=attribute-defined-outside-init
+
+ domain: dns.name.Name
+ nameserver_ports: Dict[str, int]
+ port: int
+ search: List[dns.name.Name]
+ use_search_by_default: bool
+ timeout: float
+ lifetime: float
+ keyring: Optional[Any]
+ keyname: Optional[Union[dns.name.Name, str]]
+ keyalgorithm: Union[dns.name.Name, str]
+ edns: int
+ ednsflags: int
+ ednsoptions: Optional[List[dns.edns.Option]]
+ payload: int
+ cache: Any
+ flags: Optional[int]
+ retry_servfail: bool
+ rotate: bool
+ ndots: Optional[int]
+ _nameservers: Sequence[Union[str, dns.nameserver.Nameserver]]
+
+ def __init__(
+ self, filename: str = "/etc/resolv.conf", configure: bool = True
+ ) -> None:
+ """*filename*, a ``str`` or file object, specifying a file
+ in standard /etc/resolv.conf format. This parameter is meaningful
+ only when *configure* is true and the platform is POSIX.
+
+ *configure*, a ``bool``. If True (the default), the resolver
+ instance is configured in the normal fashion for the operating
+ system the resolver is running on. (I.e. by reading a
+ /etc/resolv.conf file on POSIX systems and from the registry
+ on Windows systems.)
+ """
+
+ self.reset()
+ if configure:
+ if sys.platform == "win32": # pragma: no cover
+ self.read_registry()
+ elif filename:
+ self.read_resolv_conf(filename)
+
+ def reset(self) -> None:
+ """Reset all resolver configuration to the defaults."""
+
+ self.domain = dns.name.Name(dns.name.from_text(socket.gethostname())[1:])
+ if len(self.domain) == 0: # pragma: no cover
+ self.domain = dns.name.root
+ self._nameservers = []
+ self.nameserver_ports = {}
+ self.port = 53
+ self.search = []
+ self.use_search_by_default = False
+ self.timeout = 2.0
+ self.lifetime = 5.0
+ self.keyring = None
+ self.keyname = None
+ self.keyalgorithm = dns.tsig.default_algorithm
+ self.edns = -1
+ self.ednsflags = 0
+ self.ednsoptions = None
+ self.payload = 0
+ self.cache = None
+ self.flags = None
+ self.retry_servfail = False
+ self.rotate = False
+ self.ndots = None
+
+ def read_resolv_conf(self, f: Any) -> None:
+ """Process *f* as a file in the /etc/resolv.conf format. If f is
+ a ``str``, it is used as the name of the file to open; otherwise it
+ is treated as the file itself.
+
+ Interprets the following items:
+
+ - nameserver - name server IP address
+
+ - domain - local domain name
+
+ - search - search list for host-name lookup
+
+ - options - supported options are rotate, timeout, edns0, and ndots
+
+ """
+
+ nameservers = []
+ if isinstance(f, str):
+ try:
+ cm: contextlib.AbstractContextManager = open(f)
+ except OSError:
+ # /etc/resolv.conf doesn't exist, can't be read, etc.
+ raise NoResolverConfiguration(f"cannot open {f}")
+ else:
+ cm = contextlib.nullcontext(f)
+ with cm as f:
+ for l in f:
+ if len(l) == 0 or l[0] == "#" or l[0] == ";":
+ continue
+ tokens = l.split()
+
+ # Any line containing less than 2 tokens is malformed
+ if len(tokens) < 2:
+ continue
+
+ if tokens[0] == "nameserver":
+ nameservers.append(tokens[1])
+ elif tokens[0] == "domain":
+ self.domain = dns.name.from_text(tokens[1])
+ # domain and search are exclusive
+ self.search = []
+ elif tokens[0] == "search":
+ # the last search wins
+ self.search = []
+ for suffix in tokens[1:]:
+ self.search.append(dns.name.from_text(suffix))
+ # We don't set domain as it is not used if
+ # len(self.search) > 0
+ elif tokens[0] == "options":
+ for opt in tokens[1:]:
+ if opt == "rotate":
+ self.rotate = True
+ elif opt == "edns0":
+ self.use_edns()
+ elif "timeout" in opt:
+ try:
+ self.timeout = int(opt.split(":")[1])
+ except (ValueError, IndexError):
+ pass
+ elif "ndots" in opt:
+ try:
+ self.ndots = int(opt.split(":")[1])
+ except (ValueError, IndexError):
+ pass
+ if len(nameservers) == 0:
+ raise NoResolverConfiguration("no nameservers")
+ # Assigning directly instead of appending means we invoke the
+ # setter logic, with additonal checking and enrichment.
+ self.nameservers = nameservers
+
+ def read_registry(self) -> None: # pragma: no cover
+ """Extract resolver configuration from the Windows registry."""
+ try:
+ info = dns.win32util.get_dns_info() # type: ignore
+ if info.domain is not None:
+ self.domain = info.domain
+ self.nameservers = info.nameservers
+ self.search = info.search
+ except AttributeError:
+ raise NotImplementedError
+
+ def _compute_timeout(
+ self,
+ start: float,
+ lifetime: Optional[float] = None,
+ errors: Optional[List[ErrorTuple]] = None,
+ ) -> float:
+ lifetime = self.lifetime if lifetime is None else lifetime
+ now = time.time()
+ duration = now - start
+ if errors is None:
+ errors = []
+ if duration < 0:
+ if duration < -1:
+ # Time going backwards is bad. Just give up.
+ raise LifetimeTimeout(timeout=duration, errors=errors)
+ else:
+ # Time went backwards, but only a little. This can
+ # happen, e.g. under vmware with older linux kernels.
+ # Pretend it didn't happen.
+ duration = 0
+ if duration >= lifetime:
+ raise LifetimeTimeout(timeout=duration, errors=errors)
+ return min(lifetime - duration, self.timeout)
+
+ def _get_qnames_to_try(
+ self, qname: dns.name.Name, search: Optional[bool]
+ ) -> List[dns.name.Name]:
+ # This is a separate method so we can unit test the search
+ # rules without requiring the Internet.
+ if search is None:
+ search = self.use_search_by_default
+ qnames_to_try = []
+ if qname.is_absolute():
+ qnames_to_try.append(qname)
+ else:
+ abs_qname = qname.concatenate(dns.name.root)
+ if search:
+ if len(self.search) > 0:
+ # There is a search list, so use it exclusively
+ search_list = self.search[:]
+ elif self.domain != dns.name.root and self.domain is not None:
+ # We have some notion of a domain that isn't the root, so
+ # use it as the search list.
+ search_list = [self.domain]
+ else:
+ search_list = []
+ # Figure out the effective ndots (default is 1)
+ if self.ndots is None:
+ ndots = 1
+ else:
+ ndots = self.ndots
+ for suffix in search_list:
+ qnames_to_try.append(qname + suffix)
+ if len(qname) > ndots:
+ # The name has at least ndots dots, so we should try an
+ # absolute query first.
+ qnames_to_try.insert(0, abs_qname)
+ else:
+ # The name has less than ndots dots, so we should search
+ # first, then try the absolute name.
+ qnames_to_try.append(abs_qname)
+ else:
+ qnames_to_try.append(abs_qname)
+ return qnames_to_try
+
+ def use_tsig(
+ self,
+ keyring: Any,
+ keyname: Optional[Union[dns.name.Name, str]] = None,
+ algorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm,
+ ) -> None:
+ """Add a TSIG signature to each query.
+
+ The parameters are passed to ``dns.message.Message.use_tsig()``;
+ see its documentation for details.
+ """
+
+ self.keyring = keyring
+ self.keyname = keyname
+ self.keyalgorithm = algorithm
+
+ def use_edns(
+ self,
+ edns: Optional[Union[int, bool]] = 0,
+ ednsflags: int = 0,
+ payload: int = dns.message.DEFAULT_EDNS_PAYLOAD,
+ options: Optional[List[dns.edns.Option]] = None,
+ ) -> None:
+ """Configure EDNS behavior.
+
+ *edns*, an ``int``, is the EDNS level to use. Specifying
+ ``None``, ``False``, or ``-1`` means "do not use EDNS", and in this case
+ the other parameters are ignored. Specifying ``True`` is
+ equivalent to specifying 0, i.e. "use EDNS0".
+
+ *ednsflags*, an ``int``, the EDNS flag values.
+
+ *payload*, an ``int``, is the EDNS sender's payload field, which is the
+ maximum size of UDP datagram the sender can handle. I.e. how big
+ a response to this message can be.
+
+ *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS
+ options.
+ """
+
+ if edns is None or edns is False:
+ edns = -1
+ elif edns is True:
+ edns = 0
+ self.edns = edns
+ self.ednsflags = ednsflags
+ self.payload = payload
+ self.ednsoptions = options
+
+ def set_flags(self, flags: int) -> None:
+ """Overrides the default flags with your own.
+
+ *flags*, an ``int``, the message flags to use.
+ """
+
+ self.flags = flags
+
+ @classmethod
+ def _enrich_nameservers(
+ cls,
+ nameservers: Sequence[Union[str, dns.nameserver.Nameserver]],
+ nameserver_ports: Dict[str, int],
+ default_port: int,
+ ) -> List[dns.nameserver.Nameserver]:
+ enriched_nameservers = []
+ if isinstance(nameservers, list):
+ for nameserver in nameservers:
+ enriched_nameserver: dns.nameserver.Nameserver
+ if isinstance(nameserver, dns.nameserver.Nameserver):
+ enriched_nameserver = nameserver
+ elif dns.inet.is_address(nameserver):
+ port = nameserver_ports.get(nameserver, default_port)
+ enriched_nameserver = dns.nameserver.Do53Nameserver(
+ nameserver, port
+ )
+ else:
+ try:
+ if urlparse(nameserver).scheme != "https":
+ raise NotImplementedError
+ except Exception:
+ raise ValueError(
+ f"nameserver {nameserver} is not a "
+ "dns.nameserver.Nameserver instance or text form, "
+ "IP address, nor a valid https URL"
+ )
+ enriched_nameserver = dns.nameserver.DoHNameserver(nameserver)
+ enriched_nameservers.append(enriched_nameserver)
+ else:
+ raise ValueError(
+ f"nameservers must be a list or tuple (not a {type(nameservers)})"
+ )
+ return enriched_nameservers
+
+ @property
+ def nameservers(
+ self,
+ ) -> Sequence[Union[str, dns.nameserver.Nameserver]]:
+ return self._nameservers
+
+ @nameservers.setter
+ def nameservers(
+ self, nameservers: Sequence[Union[str, dns.nameserver.Nameserver]]
+ ) -> None:
+ """
+ *nameservers*, a ``list`` of nameservers, where a nameserver is either
+ a string interpretable as a nameserver, or a ``dns.nameserver.Nameserver``
+ instance.
+
+ Raises ``ValueError`` if *nameservers* is not a list of nameservers.
+ """
+ # We just call _enrich_nameservers() for checking
+ self._enrich_nameservers(nameservers, self.nameserver_ports, self.port)
+ self._nameservers = nameservers
+
+
+class Resolver(BaseResolver):
+ """DNS stub resolver."""
+
+ def resolve(
+ self,
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ tcp: bool = False,
+ source: Optional[str] = None,
+ raise_on_no_answer: bool = True,
+ source_port: int = 0,
+ lifetime: Optional[float] = None,
+ search: Optional[bool] = None,
+ ) -> Answer: # pylint: disable=arguments-differ
+ """Query nameservers to find the answer to the question.
+
+ The *qname*, *rdtype*, and *rdclass* parameters may be objects
+ of the appropriate type, or strings that can be converted into objects
+ of the appropriate type.
+
+ *qname*, a ``dns.name.Name`` or ``str``, the query name.
+
+ *rdtype*, an ``int`` or ``str``, the query type.
+
+ *rdclass*, an ``int`` or ``str``, the query class.
+
+ *tcp*, a ``bool``. If ``True``, use TCP to make the query.
+
+ *source*, a ``str`` or ``None``. If not ``None``, bind to this IP
+ address when making queries.
+
+ *raise_on_no_answer*, a ``bool``. If ``True``, raise
+ ``dns.resolver.NoAnswer`` if there's no answer to the question.
+
+ *source_port*, an ``int``, the port from which to send the message.
+
+ *lifetime*, a ``float``, how many seconds a query should run
+ before timing out.
+
+ *search*, a ``bool`` or ``None``, determines whether the
+ search list configured in the system's resolver configuration
+ are used for relative names, and whether the resolver's domain
+ may be added to relative names. The default is ``None``,
+ which causes the value of the resolver's
+ ``use_search_by_default`` attribute to be used.
+
+ Raises ``dns.resolver.LifetimeTimeout`` if no answers could be found
+ in the specified lifetime.
+
+ Raises ``dns.resolver.NXDOMAIN`` if the query name does not exist.
+
+ Raises ``dns.resolver.YXDOMAIN`` if the query name is too long after
+ DNAME substitution.
+
+ Raises ``dns.resolver.NoAnswer`` if *raise_on_no_answer* is
+ ``True`` and the query name exists but has no RRset of the
+ desired type and class.
+
+ Raises ``dns.resolver.NoNameservers`` if no non-broken
+ nameservers are available to answer the question.
+
+ Returns a ``dns.resolver.Answer`` instance.
+
+ """
+
+ resolution = _Resolution(
+ self, qname, rdtype, rdclass, tcp, raise_on_no_answer, search
+ )
+ start = time.time()
+ while True:
+ (request, answer) = resolution.next_request()
+ # Note we need to say "if answer is not None" and not just
+ # "if answer" because answer implements __len__, and python
+ # will call that. We want to return if we have an answer
+ # object, including in cases where its length is 0.
+ if answer is not None:
+ # cache hit!
+ return answer
+ assert request is not None # needed for type checking
+ done = False
+ while not done:
+ (nameserver, tcp, backoff) = resolution.next_nameserver()
+ if backoff:
+ time.sleep(backoff)
+ timeout = self._compute_timeout(start, lifetime, resolution.errors)
+ try:
+ response = nameserver.query(
+ request,
+ timeout=timeout,
+ source=source,
+ source_port=source_port,
+ max_size=tcp,
+ )
+ except Exception as ex:
+ (_, done) = resolution.query_result(None, ex)
+ continue
+ (answer, done) = resolution.query_result(response, None)
+ # Note we need to say "if answer is not None" and not just
+ # "if answer" because answer implements __len__, and python
+ # will call that. We want to return if we have an answer
+ # object, including in cases where its length is 0.
+ if answer is not None:
+ return answer
+
+ def query(
+ self,
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ tcp: bool = False,
+ source: Optional[str] = None,
+ raise_on_no_answer: bool = True,
+ source_port: int = 0,
+ lifetime: Optional[float] = None,
+ ) -> Answer: # pragma: no cover
+ """Query nameservers to find the answer to the question.
+
+ This method calls resolve() with ``search=True``, and is
+ provided for backwards compatibility with prior versions of
+ dnspython. See the documentation for the resolve() method for
+ further details.
+ """
+ warnings.warn(
+ "please use dns.resolver.Resolver.resolve() instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.resolve(
+ qname,
+ rdtype,
+ rdclass,
+ tcp,
+ source,
+ raise_on_no_answer,
+ source_port,
+ lifetime,
+ True,
+ )
+
+ def resolve_address(self, ipaddr: str, *args: Any, **kwargs: Any) -> Answer:
+ """Use a resolver to run a reverse query for PTR records.
+
+ This utilizes the resolve() method to perform a PTR lookup on the
+ specified IP address.
+
+ *ipaddr*, a ``str``, the IPv4 or IPv6 address you want to get
+ the PTR record for.
+
+ All other arguments that can be passed to the resolve() function
+ except for rdtype and rdclass are also supported by this
+ function.
+ """
+ # We make a modified kwargs for type checking happiness, as otherwise
+ # we get a legit warning about possibly having rdtype and rdclass
+ # in the kwargs more than once.
+ modified_kwargs: Dict[str, Any] = {}
+ modified_kwargs.update(kwargs)
+ modified_kwargs["rdtype"] = dns.rdatatype.PTR
+ modified_kwargs["rdclass"] = dns.rdataclass.IN
+ return self.resolve(
+ dns.reversename.from_address(ipaddr), *args, **modified_kwargs
+ )
+
+ def resolve_name(
+ self,
+ name: Union[dns.name.Name, str],
+ family: int = socket.AF_UNSPEC,
+ **kwargs: Any,
+ ) -> HostAnswers:
+ """Use a resolver to query for address records.
+
+ This utilizes the resolve() method to perform A and/or AAAA lookups on
+ the specified name.
+
+ *qname*, a ``dns.name.Name`` or ``str``, the name to resolve.
+
+ *family*, an ``int``, the address family. If socket.AF_UNSPEC
+ (the default), both A and AAAA records will be retrieved.
+
+ All other arguments that can be passed to the resolve() function
+ except for rdtype and rdclass are also supported by this
+ function.
+ """
+ # We make a modified kwargs for type checking happiness, as otherwise
+ # we get a legit warning about possibly having rdtype and rdclass
+ # in the kwargs more than once.
+ modified_kwargs: Dict[str, Any] = {}
+ modified_kwargs.update(kwargs)
+ modified_kwargs.pop("rdtype", None)
+ modified_kwargs["rdclass"] = dns.rdataclass.IN
+
+ if family == socket.AF_INET:
+ v4 = self.resolve(name, dns.rdatatype.A, **modified_kwargs)
+ return HostAnswers.make(v4=v4)
+ elif family == socket.AF_INET6:
+ v6 = self.resolve(name, dns.rdatatype.AAAA, **modified_kwargs)
+ return HostAnswers.make(v6=v6)
+ elif family != socket.AF_UNSPEC: # pragma: no cover
+ raise NotImplementedError(f"unknown address family {family}")
+
+ raise_on_no_answer = modified_kwargs.pop("raise_on_no_answer", True)
+ lifetime = modified_kwargs.pop("lifetime", None)
+ start = time.time()
+ v6 = self.resolve(
+ name,
+ dns.rdatatype.AAAA,
+ raise_on_no_answer=False,
+ lifetime=self._compute_timeout(start, lifetime),
+ **modified_kwargs,
+ )
+ # Note that setting name ensures we query the same name
+ # for A as we did for AAAA. (This is just in case search lists
+ # are active by default in the resolver configuration and
+ # we might be talking to a server that says NXDOMAIN when it
+ # wants to say NOERROR no data.
+ name = v6.qname
+ v4 = self.resolve(
+ name,
+ dns.rdatatype.A,
+ raise_on_no_answer=False,
+ lifetime=self._compute_timeout(start, lifetime),
+ **modified_kwargs,
+ )
+ answers = HostAnswers.make(v6=v6, v4=v4, add_empty=not raise_on_no_answer)
+ if not answers:
+ raise NoAnswer(response=v6.response)
+ return answers
+
+ # pylint: disable=redefined-outer-name
+
+ def canonical_name(self, name: Union[dns.name.Name, str]) -> dns.name.Name:
+ """Determine the canonical name of *name*.
+
+ The canonical name is the name the resolver uses for queries
+ after all CNAME and DNAME renamings have been applied.
+
+ *name*, a ``dns.name.Name`` or ``str``, the query name.
+
+ This method can raise any exception that ``resolve()`` can
+ raise, other than ``dns.resolver.NoAnswer`` and
+ ``dns.resolver.NXDOMAIN``.
+
+ Returns a ``dns.name.Name``.
+ """
+ try:
+ answer = self.resolve(name, raise_on_no_answer=False)
+ canonical_name = answer.canonical_name
+ except dns.resolver.NXDOMAIN as e:
+ canonical_name = e.canonical_name
+ return canonical_name
+
+ # pylint: enable=redefined-outer-name
+
+ def try_ddr(self, lifetime: float = 5.0) -> None:
+ """Try to update the resolver's nameservers using Discovery of Designated
+ Resolvers (DDR). If successful, the resolver will subsequently use
+ DNS-over-HTTPS or DNS-over-TLS for future queries.
+
+ *lifetime*, a float, is the maximum time to spend attempting DDR. The default
+ is 5 seconds.
+
+ If the SVCB query is successful and results in a non-empty list of nameservers,
+ then the resolver's nameservers are set to the returned servers in priority
+ order.
+
+ The current implementation does not use any address hints from the SVCB record,
+ nor does it resolve addresses for the SCVB target name, rather it assumes that
+ the bootstrap nameserver will always be one of the addresses and uses it.
+ A future revision to the code may offer fuller support. The code verifies that
+ the bootstrap nameserver is in the Subject Alternative Name field of the
+ TLS certficate.
+ """
+ try:
+ expiration = time.time() + lifetime
+ answer = self.resolve(
+ dns._ddr._local_resolver_name, "SVCB", lifetime=lifetime
+ )
+ timeout = dns.query._remaining(expiration)
+ nameservers = dns._ddr._get_nameservers_sync(answer, timeout)
+ if len(nameservers) > 0:
+ self.nameservers = nameservers
+ except Exception: # pragma: no cover
+ pass
+
+
+#: The default resolver.
+default_resolver: Optional[Resolver] = None
+
+
+def get_default_resolver() -> Resolver:
+ """Get the default resolver, initializing it if necessary."""
+ if default_resolver is None:
+ reset_default_resolver()
+ assert default_resolver is not None
+ return default_resolver
+
+
+def reset_default_resolver() -> None:
+ """Re-initialize default resolver.
+
+ Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX
+ systems) will be re-read immediately.
+ """
+
+ global default_resolver
+ default_resolver = Resolver()
+
+
+def resolve(
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ tcp: bool = False,
+ source: Optional[str] = None,
+ raise_on_no_answer: bool = True,
+ source_port: int = 0,
+ lifetime: Optional[float] = None,
+ search: Optional[bool] = None,
+) -> Answer: # pragma: no cover
+ """Query nameservers to find the answer to the question.
+
+ This is a convenience function that uses the default resolver
+ object to make the query.
+
+ See ``dns.resolver.Resolver.resolve`` for more information on the
+ parameters.
+ """
+
+ return get_default_resolver().resolve(
+ qname,
+ rdtype,
+ rdclass,
+ tcp,
+ source,
+ raise_on_no_answer,
+ source_port,
+ lifetime,
+ search,
+ )
+
+
+def query(
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ tcp: bool = False,
+ source: Optional[str] = None,
+ raise_on_no_answer: bool = True,
+ source_port: int = 0,
+ lifetime: Optional[float] = None,
+) -> Answer: # pragma: no cover
+ """Query nameservers to find the answer to the question.
+
+ This method calls resolve() with ``search=True``, and is
+ provided for backwards compatibility with prior versions of
+ dnspython. See the documentation for the resolve() method for
+ further details.
+ """
+ warnings.warn(
+ "please use dns.resolver.resolve() instead", DeprecationWarning, stacklevel=2
+ )
+ return resolve(
+ qname,
+ rdtype,
+ rdclass,
+ tcp,
+ source,
+ raise_on_no_answer,
+ source_port,
+ lifetime,
+ True,
+ )
+
+
+def resolve_address(ipaddr: str, *args: Any, **kwargs: Any) -> Answer:
+ """Use a resolver to run a reverse query for PTR records.
+
+ See ``dns.resolver.Resolver.resolve_address`` for more information on the
+ parameters.
+ """
+
+ return get_default_resolver().resolve_address(ipaddr, *args, **kwargs)
+
+
+def resolve_name(
+ name: Union[dns.name.Name, str], family: int = socket.AF_UNSPEC, **kwargs: Any
+) -> HostAnswers:
+ """Use a resolver to query for address records.
+
+ See ``dns.resolver.Resolver.resolve_name`` for more information on the
+ parameters.
+ """
+
+ return get_default_resolver().resolve_name(name, family, **kwargs)
+
+
+def canonical_name(name: Union[dns.name.Name, str]) -> dns.name.Name:
+ """Determine the canonical name of *name*.
+
+ See ``dns.resolver.Resolver.canonical_name`` for more information on the
+ parameters and possible exceptions.
+ """
+
+ return get_default_resolver().canonical_name(name)
+
+
+def try_ddr(lifetime: float = 5.0) -> None: # pragma: no cover
+ """Try to update the default resolver's nameservers using Discovery of Designated
+ Resolvers (DDR). If successful, the resolver will subsequently use
+ DNS-over-HTTPS or DNS-over-TLS for future queries.
+
+ See :py:func:`dns.resolver.Resolver.try_ddr` for more information.
+ """
+ return get_default_resolver().try_ddr(lifetime)
+
+
+def zone_for_name(
+ name: Union[dns.name.Name, str],
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+ tcp: bool = False,
+ resolver: Optional[Resolver] = None,
+ lifetime: Optional[float] = None,
+) -> dns.name.Name:
+ """Find the name of the zone which contains the specified name.
+
+ *name*, an absolute ``dns.name.Name`` or ``str``, the query name.
+
+ *rdclass*, an ``int``, the query class.
+
+ *tcp*, a ``bool``. If ``True``, use TCP to make the query.
+
+ *resolver*, a ``dns.resolver.Resolver`` or ``None``, the resolver to use.
+ If ``None``, the default, then the default resolver is used.
+
+ *lifetime*, a ``float``, the total time to allow for the queries needed
+ to determine the zone. If ``None``, the default, then only the individual
+ query limits of the resolver apply.
+
+ Raises ``dns.resolver.NoRootSOA`` if there is no SOA RR at the DNS
+ root. (This is only likely to happen if you're using non-default
+ root servers in your network and they are misconfigured.)
+
+ Raises ``dns.resolver.LifetimeTimeout`` if the answer could not be
+ found in the allotted lifetime.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, dns.name.root)
+ if resolver is None:
+ resolver = get_default_resolver()
+ if not name.is_absolute():
+ raise NotAbsolute(name)
+ start = time.time()
+ expiration: Optional[float]
+ if lifetime is not None:
+ expiration = start + lifetime
+ else:
+ expiration = None
+ while 1:
+ try:
+ rlifetime: Optional[float]
+ if expiration is not None:
+ rlifetime = expiration - time.time()
+ if rlifetime <= 0:
+ rlifetime = 0
+ else:
+ rlifetime = None
+ answer = resolver.resolve(
+ name, dns.rdatatype.SOA, rdclass, tcp, lifetime=rlifetime
+ )
+ assert answer.rrset is not None
+ if answer.rrset.name == name:
+ return name
+ # otherwise we were CNAMEd or DNAMEd and need to look higher
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
+ if isinstance(e, dns.resolver.NXDOMAIN):
+ response = e.responses().get(name)
+ else:
+ response = e.response() # pylint: disable=no-value-for-parameter
+ if response:
+ for rrs in response.authority:
+ if rrs.rdtype == dns.rdatatype.SOA and rrs.rdclass == rdclass:
+ (nr, _, _) = rrs.name.fullcompare(name)
+ if nr == dns.name.NAMERELN_SUPERDOMAIN:
+ # We're doing a proper superdomain check as
+ # if the name were equal we ought to have gotten
+ # it in the answer section! We are ignoring the
+ # possibility that the authority is insane and
+ # is including multiple SOA RRs for different
+ # authorities.
+ return rrs.name
+ # we couldn't extract anything useful from the response (e.g. it's
+ # a type 3 NXDOMAIN)
+ try:
+ name = name.parent()
+ except dns.name.NoParent:
+ raise NoRootSOA
+
+
+def make_resolver_at(
+ where: Union[dns.name.Name, str],
+ port: int = 53,
+ family: int = socket.AF_UNSPEC,
+ resolver: Optional[Resolver] = None,
+) -> Resolver:
+ """Make a stub resolver using the specified destination as the full resolver.
+
+ *where*, a ``dns.name.Name`` or ``str`` the domain name or IP address of the
+ full resolver.
+
+ *port*, an ``int``, the port to use. If not specified, the default is 53.
+
+ *family*, an ``int``, the address family to use. This parameter is used if
+ *where* is not an address. The default is ``socket.AF_UNSPEC`` in which case
+ the first address returned by ``resolve_name()`` will be used, otherwise the
+ first address of the specified family will be used.
+
+ *resolver*, a ``dns.resolver.Resolver`` or ``None``, the resolver to use for
+ resolution of hostnames. If not specified, the default resolver will be used.
+
+ Returns a ``dns.resolver.Resolver`` or raises an exception.
+ """
+ if resolver is None:
+ resolver = get_default_resolver()
+ nameservers: List[Union[str, dns.nameserver.Nameserver]] = []
+ if isinstance(where, str) and dns.inet.is_address(where):
+ nameservers.append(dns.nameserver.Do53Nameserver(where, port))
+ else:
+ for address in resolver.resolve_name(where, family).addresses():
+ nameservers.append(dns.nameserver.Do53Nameserver(address, port))
+ res = dns.resolver.Resolver(configure=False)
+ res.nameservers = nameservers
+ return res
+
+
+def resolve_at(
+ where: Union[dns.name.Name, str],
+ qname: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
+ rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ tcp: bool = False,
+ source: Optional[str] = None,
+ raise_on_no_answer: bool = True,
+ source_port: int = 0,
+ lifetime: Optional[float] = None,
+ search: Optional[bool] = None,
+ port: int = 53,
+ family: int = socket.AF_UNSPEC,
+ resolver: Optional[Resolver] = None,
+) -> Answer:
+ """Query nameservers to find the answer to the question.
+
+ This is a convenience function that calls ``dns.resolver.make_resolver_at()`` to
+ make a resolver, and then uses it to resolve the query.
+
+ See ``dns.resolver.Resolver.resolve`` for more information on the resolution
+ parameters, and ``dns.resolver.make_resolver_at`` for information about the resolver
+ parameters *where*, *port*, *family*, and *resolver*.
+
+ If making more than one query, it is more efficient to call
+ ``dns.resolver.make_resolver_at()`` and then use that resolver for the queries
+ instead of calling ``resolve_at()`` multiple times.
+ """
+ return make_resolver_at(where, port, family, resolver).resolve(
+ qname,
+ rdtype,
+ rdclass,
+ tcp,
+ source,
+ raise_on_no_answer,
+ source_port,
+ lifetime,
+ search,
+ )
+
+
+#
+# Support for overriding the system resolver for all python code in the
+# running process.
+#
+
+_protocols_for_socktype = {
+ socket.SOCK_DGRAM: [socket.SOL_UDP],
+ socket.SOCK_STREAM: [socket.SOL_TCP],
+}
+
+_resolver = None
+_original_getaddrinfo = socket.getaddrinfo
+_original_getnameinfo = socket.getnameinfo
+_original_getfqdn = socket.getfqdn
+_original_gethostbyname = socket.gethostbyname
+_original_gethostbyname_ex = socket.gethostbyname_ex
+_original_gethostbyaddr = socket.gethostbyaddr
+
+
+def _getaddrinfo(
+ host=None, service=None, family=socket.AF_UNSPEC, socktype=0, proto=0, flags=0
+):
+ if flags & socket.AI_NUMERICHOST != 0:
+ # Short circuit directly into the system's getaddrinfo(). We're
+ # not adding any value in this case, and this avoids infinite loops
+ # because dns.query.* needs to call getaddrinfo() for IPv6 scoping
+ # reasons. We will also do this short circuit below if we
+ # discover that the host is an address literal.
+ return _original_getaddrinfo(host, service, family, socktype, proto, flags)
+ if flags & (socket.AI_ADDRCONFIG | socket.AI_V4MAPPED) != 0:
+ # Not implemented. We raise a gaierror as opposed to a
+ # NotImplementedError as it helps callers handle errors more
+ # appropriately. [Issue #316]
+ #
+ # We raise EAI_FAIL as opposed to EAI_SYSTEM because there is
+ # no EAI_SYSTEM on Windows [Issue #416]. We didn't go for
+ # EAI_BADFLAGS as the flags aren't bad, we just don't
+ # implement them.
+ raise socket.gaierror(
+ socket.EAI_FAIL, "Non-recoverable failure in name resolution"
+ )
+ if host is None and service is None:
+ raise socket.gaierror(socket.EAI_NONAME, "Name or service not known")
+ addrs = []
+ canonical_name = None # pylint: disable=redefined-outer-name
+ # Is host None or an address literal? If so, use the system's
+ # getaddrinfo().
+ if host is None:
+ return _original_getaddrinfo(host, service, family, socktype, proto, flags)
+ try:
+ # We don't care about the result of af_for_address(), we're just
+ # calling it so it raises an exception if host is not an IPv4 or
+ # IPv6 address.
+ dns.inet.af_for_address(host)
+ return _original_getaddrinfo(host, service, family, socktype, proto, flags)
+ except Exception:
+ pass
+ # Something needs resolution!
+ try:
+ answers = _resolver.resolve_name(host, family)
+ addrs = answers.addresses_and_families()
+ canonical_name = answers.canonical_name().to_text(True)
+ except dns.resolver.NXDOMAIN:
+ raise socket.gaierror(socket.EAI_NONAME, "Name or service not known")
+ except Exception:
+ # We raise EAI_AGAIN here as the failure may be temporary
+ # (e.g. a timeout) and EAI_SYSTEM isn't defined on Windows.
+ # [Issue #416]
+ raise socket.gaierror(socket.EAI_AGAIN, "Temporary failure in name resolution")
+ port = None
+ try:
+ # Is it a port literal?
+ if service is None:
+ port = 0
+ else:
+ port = int(service)
+ except Exception:
+ if flags & socket.AI_NUMERICSERV == 0:
+ try:
+ port = socket.getservbyname(service)
+ except Exception:
+ pass
+ if port is None:
+ raise socket.gaierror(socket.EAI_NONAME, "Name or service not known")
+ tuples = []
+ if socktype == 0:
+ socktypes = [socket.SOCK_DGRAM, socket.SOCK_STREAM]
+ else:
+ socktypes = [socktype]
+ if flags & socket.AI_CANONNAME != 0:
+ cname = canonical_name
+ else:
+ cname = ""
+ for addr, af in addrs:
+ for socktype in socktypes:
+ for proto in _protocols_for_socktype[socktype]:
+ addr_tuple = dns.inet.low_level_address_tuple((addr, port), af)
+ tuples.append((af, socktype, proto, cname, addr_tuple))
+ if len(tuples) == 0:
+ raise socket.gaierror(socket.EAI_NONAME, "Name or service not known")
+ return tuples
+
+
+def _getnameinfo(sockaddr, flags=0):
+ host = sockaddr[0]
+ port = sockaddr[1]
+ if len(sockaddr) == 4:
+ scope = sockaddr[3]
+ family = socket.AF_INET6
+ else:
+ scope = None
+ family = socket.AF_INET
+ tuples = _getaddrinfo(host, port, family, socket.SOCK_STREAM, socket.SOL_TCP, 0)
+ if len(tuples) > 1:
+ raise OSError("sockaddr resolved to multiple addresses")
+ addr = tuples[0][4][0]
+ if flags & socket.NI_DGRAM:
+ pname = "udp"
+ else:
+ pname = "tcp"
+ qname = dns.reversename.from_address(addr)
+ if flags & socket.NI_NUMERICHOST == 0:
+ try:
+ answer = _resolver.resolve(qname, "PTR")
+ hostname = answer.rrset[0].target.to_text(True)
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ if flags & socket.NI_NAMEREQD:
+ raise socket.gaierror(socket.EAI_NONAME, "Name or service not known")
+ hostname = addr
+ if scope is not None:
+ hostname += "%" + str(scope)
+ else:
+ hostname = addr
+ if scope is not None:
+ hostname += "%" + str(scope)
+ if flags & socket.NI_NUMERICSERV:
+ service = str(port)
+ else:
+ service = socket.getservbyport(port, pname)
+ return (hostname, service)
+
+
+def _getfqdn(name=None):
+ if name is None:
+ name = socket.gethostname()
+ try:
+ (name, _, _) = _gethostbyaddr(name)
+ # Python's version checks aliases too, but our gethostbyname
+ # ignores them, so we do so here as well.
+ except Exception: # pragma: no cover
+ pass
+ return name
+
+
+def _gethostbyname(name):
+ return _gethostbyname_ex(name)[2][0]
+
+
+def _gethostbyname_ex(name):
+ aliases = []
+ addresses = []
+ tuples = _getaddrinfo(
+ name, 0, socket.AF_INET, socket.SOCK_STREAM, socket.SOL_TCP, socket.AI_CANONNAME
+ )
+ canonical = tuples[0][3]
+ for item in tuples:
+ addresses.append(item[4][0])
+ # XXX we just ignore aliases
+ return (canonical, aliases, addresses)
+
+
+def _gethostbyaddr(ip):
+ try:
+ dns.ipv6.inet_aton(ip)
+ sockaddr = (ip, 80, 0, 0)
+ family = socket.AF_INET6
+ except Exception:
+ try:
+ dns.ipv4.inet_aton(ip)
+ except Exception:
+ raise socket.gaierror(socket.EAI_NONAME, "Name or service not known")
+ sockaddr = (ip, 80)
+ family = socket.AF_INET
+ (name, _) = _getnameinfo(sockaddr, socket.NI_NAMEREQD)
+ aliases = []
+ addresses = []
+ tuples = _getaddrinfo(
+ name, 0, family, socket.SOCK_STREAM, socket.SOL_TCP, socket.AI_CANONNAME
+ )
+ canonical = tuples[0][3]
+ # We only want to include an address from the tuples if it's the
+ # same as the one we asked about. We do this comparison in binary
+ # to avoid any differences in text representations.
+ bin_ip = dns.inet.inet_pton(family, ip)
+ for item in tuples:
+ addr = item[4][0]
+ bin_addr = dns.inet.inet_pton(family, addr)
+ if bin_ip == bin_addr:
+ addresses.append(addr)
+ # XXX we just ignore aliases
+ return (canonical, aliases, addresses)
+
+
+def override_system_resolver(resolver: Optional[Resolver] = None) -> None:
+ """Override the system resolver routines in the socket module with
+ versions which use dnspython's resolver.
+
+ This can be useful in testing situations where you want to control
+ the resolution behavior of python code without having to change
+ the system's resolver settings (e.g. /etc/resolv.conf).
+
+ The resolver to use may be specified; if it's not, the default
+ resolver will be used.
+
+ resolver, a ``dns.resolver.Resolver`` or ``None``, the resolver to use.
+ """
+
+ if resolver is None:
+ resolver = get_default_resolver()
+ global _resolver
+ _resolver = resolver
+ socket.getaddrinfo = _getaddrinfo
+ socket.getnameinfo = _getnameinfo
+ socket.getfqdn = _getfqdn
+ socket.gethostbyname = _gethostbyname
+ socket.gethostbyname_ex = _gethostbyname_ex
+ socket.gethostbyaddr = _gethostbyaddr
+
+
+def restore_system_resolver() -> None:
+ """Undo the effects of prior override_system_resolver()."""
+
+ global _resolver
+ _resolver = None
+ socket.getaddrinfo = _original_getaddrinfo
+ socket.getnameinfo = _original_getnameinfo
+ socket.getfqdn = _original_getfqdn
+ socket.gethostbyname = _original_gethostbyname
+ socket.gethostbyname_ex = _original_gethostbyname_ex
+ socket.gethostbyaddr = _original_gethostbyaddr
diff --git a/.venv/lib/python3.12/site-packages/dns/reversename.py b/.venv/lib/python3.12/site-packages/dns/reversename.py
new file mode 100644
index 00000000..8236c711
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/reversename.py
@@ -0,0 +1,105 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Reverse Map Names."""
+
+import binascii
+
+import dns.ipv4
+import dns.ipv6
+import dns.name
+
+ipv4_reverse_domain = dns.name.from_text("in-addr.arpa.")
+ipv6_reverse_domain = dns.name.from_text("ip6.arpa.")
+
+
+def from_address(
+ text: str,
+ v4_origin: dns.name.Name = ipv4_reverse_domain,
+ v6_origin: dns.name.Name = ipv6_reverse_domain,
+) -> dns.name.Name:
+ """Convert an IPv4 or IPv6 address in textual form into a Name object whose
+ value is the reverse-map domain name of the address.
+
+ *text*, a ``str``, is an IPv4 or IPv6 address in textual form
+ (e.g. '127.0.0.1', '::1')
+
+ *v4_origin*, a ``dns.name.Name`` to append to the labels corresponding to
+ the address if the address is an IPv4 address, instead of the default
+ (in-addr.arpa.)
+
+ *v6_origin*, a ``dns.name.Name`` to append to the labels corresponding to
+ the address if the address is an IPv6 address, instead of the default
+ (ip6.arpa.)
+
+ Raises ``dns.exception.SyntaxError`` if the address is badly formed.
+
+ Returns a ``dns.name.Name``.
+ """
+
+ try:
+ v6 = dns.ipv6.inet_aton(text)
+ if dns.ipv6.is_mapped(v6):
+ parts = ["%d" % byte for byte in v6[12:]]
+ origin = v4_origin
+ else:
+ parts = [x for x in str(binascii.hexlify(v6).decode())]
+ origin = v6_origin
+ except Exception:
+ parts = ["%d" % byte for byte in dns.ipv4.inet_aton(text)]
+ origin = v4_origin
+ return dns.name.from_text(".".join(reversed(parts)), origin=origin)
+
+
+def to_address(
+ name: dns.name.Name,
+ v4_origin: dns.name.Name = ipv4_reverse_domain,
+ v6_origin: dns.name.Name = ipv6_reverse_domain,
+) -> str:
+ """Convert a reverse map domain name into textual address form.
+
+ *name*, a ``dns.name.Name``, an IPv4 or IPv6 address in reverse-map name
+ form.
+
+ *v4_origin*, a ``dns.name.Name`` representing the top-level domain for
+ IPv4 addresses, instead of the default (in-addr.arpa.)
+
+ *v6_origin*, a ``dns.name.Name`` representing the top-level domain for
+ IPv4 addresses, instead of the default (ip6.arpa.)
+
+ Raises ``dns.exception.SyntaxError`` if the name does not have a
+ reverse-map form.
+
+ Returns a ``str``.
+ """
+
+ if name.is_subdomain(v4_origin):
+ name = name.relativize(v4_origin)
+ text = b".".join(reversed(name.labels))
+ # run through inet_ntoa() to check syntax and make pretty.
+ return dns.ipv4.inet_ntoa(dns.ipv4.inet_aton(text))
+ elif name.is_subdomain(v6_origin):
+ name = name.relativize(v6_origin)
+ labels = list(reversed(name.labels))
+ parts = []
+ for i in range(0, len(labels), 4):
+ parts.append(b"".join(labels[i : i + 4]))
+ text = b":".join(parts)
+ # run through inet_ntoa() to check syntax and make pretty.
+ return dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(text))
+ else:
+ raise dns.exception.SyntaxError("unknown reverse-map address family")
diff --git a/.venv/lib/python3.12/site-packages/dns/rrset.py b/.venv/lib/python3.12/site-packages/dns/rrset.py
new file mode 100644
index 00000000..6f39b108
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/rrset.py
@@ -0,0 +1,285 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS RRsets (an RRset is a named rdataset)"""
+
+from typing import Any, Collection, Dict, Optional, Union, cast
+
+import dns.name
+import dns.rdataclass
+import dns.rdataset
+import dns.renderer
+
+
+class RRset(dns.rdataset.Rdataset):
+ """A DNS RRset (named rdataset).
+
+ RRset inherits from Rdataset, and RRsets can be treated as
+ Rdatasets in most cases. There are, however, a few notable
+ exceptions. RRsets have different to_wire() and to_text() method
+ arguments, reflecting the fact that RRsets always have an owner
+ name.
+ """
+
+ __slots__ = ["name", "deleting"]
+
+ def __init__(
+ self,
+ name: dns.name.Name,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ deleting: Optional[dns.rdataclass.RdataClass] = None,
+ ):
+ """Create a new RRset."""
+
+ super().__init__(rdclass, rdtype, covers)
+ self.name = name
+ self.deleting = deleting
+
+ def _clone(self):
+ obj = super()._clone()
+ obj.name = self.name
+ obj.deleting = self.deleting
+ return obj
+
+ def __repr__(self):
+ if self.covers == 0:
+ ctext = ""
+ else:
+ ctext = "(" + dns.rdatatype.to_text(self.covers) + ")"
+ if self.deleting is not None:
+ dtext = " delete=" + dns.rdataclass.to_text(self.deleting)
+ else:
+ dtext = ""
+ return (
+ "<DNS "
+ + str(self.name)
+ + " "
+ + dns.rdataclass.to_text(self.rdclass)
+ + " "
+ + dns.rdatatype.to_text(self.rdtype)
+ + ctext
+ + dtext
+ + " RRset: "
+ + self._rdata_repr()
+ + ">"
+ )
+
+ def __str__(self):
+ return self.to_text()
+
+ def __eq__(self, other):
+ if isinstance(other, RRset):
+ if self.name != other.name:
+ return False
+ elif not isinstance(other, dns.rdataset.Rdataset):
+ return False
+ return super().__eq__(other)
+
+ def match(self, *args: Any, **kwargs: Any) -> bool: # type: ignore[override]
+ """Does this rrset match the specified attributes?
+
+ Behaves as :py:func:`full_match()` if the first argument is a
+ ``dns.name.Name``, and as :py:func:`dns.rdataset.Rdataset.match()`
+ otherwise.
+
+ (This behavior fixes a design mistake where the signature of this
+ method became incompatible with that of its superclass. The fix
+ makes RRsets matchable as Rdatasets while preserving backwards
+ compatibility.)
+ """
+ if isinstance(args[0], dns.name.Name):
+ return self.full_match(*args, **kwargs) # type: ignore[arg-type]
+ else:
+ return super().match(*args, **kwargs) # type: ignore[arg-type]
+
+ def full_match(
+ self,
+ name: dns.name.Name,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType,
+ deleting: Optional[dns.rdataclass.RdataClass] = None,
+ ) -> bool:
+ """Returns ``True`` if this rrset matches the specified name, class,
+ type, covers, and deletion state.
+ """
+ if not super().match(rdclass, rdtype, covers):
+ return False
+ if self.name != name or self.deleting != deleting:
+ return False
+ return True
+
+ # pylint: disable=arguments-differ
+
+ def to_text( # type: ignore[override]
+ self,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ **kw: Dict[str, Any],
+ ) -> str:
+ """Convert the RRset into DNS zone file format.
+
+ See ``dns.name.Name.choose_relativity`` for more information
+ on how *origin* and *relativize* determine the way names
+ are emitted.
+
+ Any additional keyword arguments are passed on to the rdata
+ ``to_text()`` method.
+
+ *origin*, a ``dns.name.Name`` or ``None``, the origin for relative
+ names.
+
+ *relativize*, a ``bool``. If ``True``, names will be relativized
+ to *origin*.
+ """
+
+ return super().to_text(
+ self.name, origin, relativize, self.deleting, **kw # type: ignore
+ )
+
+ def to_wire( # type: ignore[override]
+ self,
+ file: Any,
+ compress: Optional[dns.name.CompressType] = None, # type: ignore
+ origin: Optional[dns.name.Name] = None,
+ **kw: Dict[str, Any],
+ ) -> int:
+ """Convert the RRset to wire format.
+
+ All keyword arguments are passed to ``dns.rdataset.to_wire()``; see
+ that function for details.
+
+ Returns an ``int``, the number of records emitted.
+ """
+
+ return super().to_wire(
+ self.name, file, compress, origin, self.deleting, **kw # type:ignore
+ )
+
+ # pylint: enable=arguments-differ
+
+ def to_rdataset(self) -> dns.rdataset.Rdataset:
+ """Convert an RRset into an Rdataset.
+
+ Returns a ``dns.rdataset.Rdataset``.
+ """
+ return dns.rdataset.from_rdata_list(self.ttl, list(self))
+
+
+def from_text_list(
+ name: Union[dns.name.Name, str],
+ ttl: int,
+ rdclass: Union[dns.rdataclass.RdataClass, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ text_rdatas: Collection[str],
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = True,
+ relativize_to: Optional[dns.name.Name] = None,
+) -> RRset:
+ """Create an RRset with the specified name, TTL, class, and type, and with
+ the specified list of rdatas in text format.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder to use; if ``None``, the default IDNA 2003
+ encoder/decoder is used.
+
+ *origin*, a ``dns.name.Name`` (or ``None``), the
+ origin to use for relative names.
+
+ *relativize*, a ``bool``. If true, name will be relativized.
+
+ *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use
+ when relativizing names. If not set, the *origin* value will be used.
+
+ Returns a ``dns.rrset.RRset`` object.
+ """
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None, idna_codec=idna_codec)
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ r = RRset(name, rdclass, rdtype)
+ r.update_ttl(ttl)
+ for t in text_rdatas:
+ rd = dns.rdata.from_text(
+ r.rdclass, r.rdtype, t, origin, relativize, relativize_to, idna_codec
+ )
+ r.add(rd)
+ return r
+
+
+def from_text(
+ name: Union[dns.name.Name, str],
+ ttl: int,
+ rdclass: Union[dns.rdataclass.RdataClass, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ *text_rdatas: Any,
+) -> RRset:
+ """Create an RRset with the specified name, TTL, class, and type and with
+ the specified rdatas in text format.
+
+ Returns a ``dns.rrset.RRset`` object.
+ """
+
+ return from_text_list(
+ name, ttl, rdclass, rdtype, cast(Collection[str], text_rdatas)
+ )
+
+
+def from_rdata_list(
+ name: Union[dns.name.Name, str],
+ ttl: int,
+ rdatas: Collection[dns.rdata.Rdata],
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+) -> RRset:
+ """Create an RRset with the specified name and TTL, and with
+ the specified list of rdata objects.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder to use; if ``None``, the default IDNA 2003
+ encoder/decoder is used.
+
+ Returns a ``dns.rrset.RRset`` object.
+
+ """
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None, idna_codec=idna_codec)
+
+ if len(rdatas) == 0:
+ raise ValueError("rdata list must not be empty")
+ r = None
+ for rd in rdatas:
+ if r is None:
+ r = RRset(name, rd.rdclass, rd.rdtype)
+ r.update_ttl(ttl)
+ r.add(rd)
+ assert r is not None
+ return r
+
+
+def from_rdata(name: Union[dns.name.Name, str], ttl: int, *rdatas: Any) -> RRset:
+ """Create an RRset with the specified name and TTL, and with
+ the specified rdata objects.
+
+ Returns a ``dns.rrset.RRset`` object.
+ """
+
+ return from_rdata_list(name, ttl, cast(Collection[dns.rdata.Rdata], rdatas))
diff --git a/.venv/lib/python3.12/site-packages/dns/serial.py b/.venv/lib/python3.12/site-packages/dns/serial.py
new file mode 100644
index 00000000..3417299b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/serial.py
@@ -0,0 +1,118 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+"""Serial Number Arthimetic from RFC 1982"""
+
+
+class Serial:
+ def __init__(self, value: int, bits: int = 32):
+ self.value = value % 2**bits
+ self.bits = bits
+
+ def __repr__(self):
+ return f"dns.serial.Serial({self.value}, {self.bits})"
+
+ def __eq__(self, other):
+ if isinstance(other, int):
+ other = Serial(other, self.bits)
+ elif not isinstance(other, Serial) or other.bits != self.bits:
+ return NotImplemented
+ return self.value == other.value
+
+ def __ne__(self, other):
+ if isinstance(other, int):
+ other = Serial(other, self.bits)
+ elif not isinstance(other, Serial) or other.bits != self.bits:
+ return NotImplemented
+ return self.value != other.value
+
+ def __lt__(self, other):
+ if isinstance(other, int):
+ other = Serial(other, self.bits)
+ elif not isinstance(other, Serial) or other.bits != self.bits:
+ return NotImplemented
+ if self.value < other.value and other.value - self.value < 2 ** (self.bits - 1):
+ return True
+ elif self.value > other.value and self.value - other.value > 2 ** (
+ self.bits - 1
+ ):
+ return True
+ else:
+ return False
+
+ def __le__(self, other):
+ return self == other or self < other
+
+ def __gt__(self, other):
+ if isinstance(other, int):
+ other = Serial(other, self.bits)
+ elif not isinstance(other, Serial) or other.bits != self.bits:
+ return NotImplemented
+ if self.value < other.value and other.value - self.value > 2 ** (self.bits - 1):
+ return True
+ elif self.value > other.value and self.value - other.value < 2 ** (
+ self.bits - 1
+ ):
+ return True
+ else:
+ return False
+
+ def __ge__(self, other):
+ return self == other or self > other
+
+ def __add__(self, other):
+ v = self.value
+ if isinstance(other, Serial):
+ delta = other.value
+ elif isinstance(other, int):
+ delta = other
+ else:
+ raise ValueError
+ if abs(delta) > (2 ** (self.bits - 1) - 1):
+ raise ValueError
+ v += delta
+ v = v % 2**self.bits
+ return Serial(v, self.bits)
+
+ def __iadd__(self, other):
+ v = self.value
+ if isinstance(other, Serial):
+ delta = other.value
+ elif isinstance(other, int):
+ delta = other
+ else:
+ raise ValueError
+ if abs(delta) > (2 ** (self.bits - 1) - 1):
+ raise ValueError
+ v += delta
+ v = v % 2**self.bits
+ self.value = v
+ return self
+
+ def __sub__(self, other):
+ v = self.value
+ if isinstance(other, Serial):
+ delta = other.value
+ elif isinstance(other, int):
+ delta = other
+ else:
+ raise ValueError
+ if abs(delta) > (2 ** (self.bits - 1) - 1):
+ raise ValueError
+ v -= delta
+ v = v % 2**self.bits
+ return Serial(v, self.bits)
+
+ def __isub__(self, other):
+ v = self.value
+ if isinstance(other, Serial):
+ delta = other.value
+ elif isinstance(other, int):
+ delta = other
+ else:
+ raise ValueError
+ if abs(delta) > (2 ** (self.bits - 1) - 1):
+ raise ValueError
+ v -= delta
+ v = v % 2**self.bits
+ self.value = v
+ return self
diff --git a/.venv/lib/python3.12/site-packages/dns/set.py b/.venv/lib/python3.12/site-packages/dns/set.py
new file mode 100644
index 00000000..ae8f0dd5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/set.py
@@ -0,0 +1,308 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import itertools
+
+
+class Set:
+ """A simple set class.
+
+ This class was originally used to deal with python not having a set class, and
+ originally the class used lists in its implementation. The ordered and indexable
+ nature of RRsets and Rdatasets is unfortunately widely used in dnspython
+ applications, so for backwards compatibility sets continue to be a custom class, now
+ based on an ordered dictionary.
+ """
+
+ __slots__ = ["items"]
+
+ def __init__(self, items=None):
+ """Initialize the set.
+
+ *items*, an iterable or ``None``, the initial set of items.
+ """
+
+ self.items = dict()
+ if items is not None:
+ for item in items:
+ # This is safe for how we use set, but if other code
+ # subclasses it could be a legitimate issue.
+ self.add(item) # lgtm[py/init-calls-subclass]
+
+ def __repr__(self):
+ return f"dns.set.Set({repr(list(self.items.keys()))})" # pragma: no cover
+
+ def add(self, item):
+ """Add an item to the set."""
+
+ if item not in self.items:
+ self.items[item] = None
+
+ def remove(self, item):
+ """Remove an item from the set."""
+
+ try:
+ del self.items[item]
+ except KeyError:
+ raise ValueError
+
+ def discard(self, item):
+ """Remove an item from the set if present."""
+
+ self.items.pop(item, None)
+
+ def pop(self):
+ """Remove an arbitrary item from the set."""
+ (k, _) = self.items.popitem()
+ return k
+
+ def _clone(self) -> "Set":
+ """Make a (shallow) copy of the set.
+
+ There is a 'clone protocol' that subclasses of this class
+ should use. To make a copy, first call your super's _clone()
+ method, and use the object returned as the new instance. Then
+ make shallow copies of the attributes defined in the subclass.
+
+ This protocol allows us to write the set algorithms that
+ return new instances (e.g. union) once, and keep using them in
+ subclasses.
+ """
+
+ if hasattr(self, "_clone_class"):
+ cls = self._clone_class # type: ignore
+ else:
+ cls = self.__class__
+ obj = cls.__new__(cls)
+ obj.items = dict()
+ obj.items.update(self.items)
+ return obj
+
+ def __copy__(self):
+ """Make a (shallow) copy of the set."""
+
+ return self._clone()
+
+ def copy(self):
+ """Make a (shallow) copy of the set."""
+
+ return self._clone()
+
+ def union_update(self, other):
+ """Update the set, adding any elements from other which are not
+ already in the set.
+ """
+
+ if not isinstance(other, Set):
+ raise ValueError("other must be a Set instance")
+ if self is other: # lgtm[py/comparison-using-is]
+ return
+ for item in other.items:
+ self.add(item)
+
+ def intersection_update(self, other):
+ """Update the set, removing any elements from other which are not
+ in both sets.
+ """
+
+ if not isinstance(other, Set):
+ raise ValueError("other must be a Set instance")
+ if self is other: # lgtm[py/comparison-using-is]
+ return
+ # we make a copy of the list so that we can remove items from
+ # the list without breaking the iterator.
+ for item in list(self.items):
+ if item not in other.items:
+ del self.items[item]
+
+ def difference_update(self, other):
+ """Update the set, removing any elements from other which are in
+ the set.
+ """
+
+ if not isinstance(other, Set):
+ raise ValueError("other must be a Set instance")
+ if self is other: # lgtm[py/comparison-using-is]
+ self.items.clear()
+ else:
+ for item in other.items:
+ self.discard(item)
+
+ def symmetric_difference_update(self, other):
+ """Update the set, retaining only elements unique to both sets."""
+
+ if not isinstance(other, Set):
+ raise ValueError("other must be a Set instance")
+ if self is other: # lgtm[py/comparison-using-is]
+ self.items.clear()
+ else:
+ overlap = self.intersection(other)
+ self.union_update(other)
+ self.difference_update(overlap)
+
+ def union(self, other):
+ """Return a new set which is the union of ``self`` and ``other``.
+
+ Returns the same Set type as this set.
+ """
+
+ obj = self._clone()
+ obj.union_update(other)
+ return obj
+
+ def intersection(self, other):
+ """Return a new set which is the intersection of ``self`` and
+ ``other``.
+
+ Returns the same Set type as this set.
+ """
+
+ obj = self._clone()
+ obj.intersection_update(other)
+ return obj
+
+ def difference(self, other):
+ """Return a new set which ``self`` - ``other``, i.e. the items
+ in ``self`` which are not also in ``other``.
+
+ Returns the same Set type as this set.
+ """
+
+ obj = self._clone()
+ obj.difference_update(other)
+ return obj
+
+ def symmetric_difference(self, other):
+ """Return a new set which (``self`` - ``other``) | (``other``
+ - ``self), ie: the items in either ``self`` or ``other`` which
+ are not contained in their intersection.
+
+ Returns the same Set type as this set.
+ """
+
+ obj = self._clone()
+ obj.symmetric_difference_update(other)
+ return obj
+
+ def __or__(self, other):
+ return self.union(other)
+
+ def __and__(self, other):
+ return self.intersection(other)
+
+ def __add__(self, other):
+ return self.union(other)
+
+ def __sub__(self, other):
+ return self.difference(other)
+
+ def __xor__(self, other):
+ return self.symmetric_difference(other)
+
+ def __ior__(self, other):
+ self.union_update(other)
+ return self
+
+ def __iand__(self, other):
+ self.intersection_update(other)
+ return self
+
+ def __iadd__(self, other):
+ self.union_update(other)
+ return self
+
+ def __isub__(self, other):
+ self.difference_update(other)
+ return self
+
+ def __ixor__(self, other):
+ self.symmetric_difference_update(other)
+ return self
+
+ def update(self, other):
+ """Update the set, adding any elements from other which are not
+ already in the set.
+
+ *other*, the collection of items with which to update the set, which
+ may be any iterable type.
+ """
+
+ for item in other:
+ self.add(item)
+
+ def clear(self):
+ """Make the set empty."""
+ self.items.clear()
+
+ def __eq__(self, other):
+ return self.items == other.items
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __len__(self):
+ return len(self.items)
+
+ def __iter__(self):
+ return iter(self.items)
+
+ def __getitem__(self, i):
+ if isinstance(i, slice):
+ return list(itertools.islice(self.items, i.start, i.stop, i.step))
+ else:
+ return next(itertools.islice(self.items, i, i + 1))
+
+ def __delitem__(self, i):
+ if isinstance(i, slice):
+ for elt in list(self[i]):
+ del self.items[elt]
+ else:
+ del self.items[self[i]]
+
+ def issubset(self, other):
+ """Is this set a subset of *other*?
+
+ Returns a ``bool``.
+ """
+
+ if not isinstance(other, Set):
+ raise ValueError("other must be a Set instance")
+ for item in self.items:
+ if item not in other.items:
+ return False
+ return True
+
+ def issuperset(self, other):
+ """Is this set a superset of *other*?
+
+ Returns a ``bool``.
+ """
+
+ if not isinstance(other, Set):
+ raise ValueError("other must be a Set instance")
+ for item in other.items:
+ if item not in self.items:
+ return False
+ return True
+
+ def isdisjoint(self, other):
+ if not isinstance(other, Set):
+ raise ValueError("other must be a Set instance")
+ for item in other.items:
+ if item in self.items:
+ return False
+ return True
diff --git a/.venv/lib/python3.12/site-packages/dns/tokenizer.py b/.venv/lib/python3.12/site-packages/dns/tokenizer.py
new file mode 100644
index 00000000..ab205bc3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/tokenizer.py
@@ -0,0 +1,708 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Tokenize DNS zone file format"""
+
+import io
+import sys
+from typing import Any, List, Optional, Tuple
+
+import dns.exception
+import dns.name
+import dns.ttl
+
+_DELIMITERS = {" ", "\t", "\n", ";", "(", ")", '"'}
+_QUOTING_DELIMITERS = {'"'}
+
+EOF = 0
+EOL = 1
+WHITESPACE = 2
+IDENTIFIER = 3
+QUOTED_STRING = 4
+COMMENT = 5
+DELIMITER = 6
+
+
+class UngetBufferFull(dns.exception.DNSException):
+ """An attempt was made to unget a token when the unget buffer was full."""
+
+
+class Token:
+ """A DNS zone file format token.
+
+ ttype: The token type
+ value: The token value
+ has_escape: Does the token value contain escapes?
+ """
+
+ def __init__(
+ self,
+ ttype: int,
+ value: Any = "",
+ has_escape: bool = False,
+ comment: Optional[str] = None,
+ ):
+ """Initialize a token instance."""
+
+ self.ttype = ttype
+ self.value = value
+ self.has_escape = has_escape
+ self.comment = comment
+
+ def is_eof(self) -> bool:
+ return self.ttype == EOF
+
+ def is_eol(self) -> bool:
+ return self.ttype == EOL
+
+ def is_whitespace(self) -> bool:
+ return self.ttype == WHITESPACE
+
+ def is_identifier(self) -> bool:
+ return self.ttype == IDENTIFIER
+
+ def is_quoted_string(self) -> bool:
+ return self.ttype == QUOTED_STRING
+
+ def is_comment(self) -> bool:
+ return self.ttype == COMMENT
+
+ def is_delimiter(self) -> bool: # pragma: no cover (we don't return delimiters yet)
+ return self.ttype == DELIMITER
+
+ def is_eol_or_eof(self) -> bool:
+ return self.ttype == EOL or self.ttype == EOF
+
+ def __eq__(self, other):
+ if not isinstance(other, Token):
+ return False
+ return self.ttype == other.ttype and self.value == other.value
+
+ def __ne__(self, other):
+ if not isinstance(other, Token):
+ return True
+ return self.ttype != other.ttype or self.value != other.value
+
+ def __str__(self):
+ return '%d "%s"' % (self.ttype, self.value)
+
+ def unescape(self) -> "Token":
+ if not self.has_escape:
+ return self
+ unescaped = ""
+ l = len(self.value)
+ i = 0
+ while i < l:
+ c = self.value[i]
+ i += 1
+ if c == "\\":
+ if i >= l: # pragma: no cover (can't happen via get())
+ raise dns.exception.UnexpectedEnd
+ c = self.value[i]
+ i += 1
+ if c.isdigit():
+ if i >= l:
+ raise dns.exception.UnexpectedEnd
+ c2 = self.value[i]
+ i += 1
+ if i >= l:
+ raise dns.exception.UnexpectedEnd
+ c3 = self.value[i]
+ i += 1
+ if not (c2.isdigit() and c3.isdigit()):
+ raise dns.exception.SyntaxError
+ codepoint = int(c) * 100 + int(c2) * 10 + int(c3)
+ if codepoint > 255:
+ raise dns.exception.SyntaxError
+ c = chr(codepoint)
+ unescaped += c
+ return Token(self.ttype, unescaped)
+
+ def unescape_to_bytes(self) -> "Token":
+ # We used to use unescape() for TXT-like records, but this
+ # caused problems as we'd process DNS escapes into Unicode code
+ # points instead of byte values, and then a to_text() of the
+ # processed data would not equal the original input. For
+ # example, \226 in the TXT record would have a to_text() of
+ # \195\162 because we applied UTF-8 encoding to Unicode code
+ # point 226.
+ #
+ # We now apply escapes while converting directly to bytes,
+ # avoiding this double encoding.
+ #
+ # This code also handles cases where the unicode input has
+ # non-ASCII code-points in it by converting it to UTF-8. TXT
+ # records aren't defined for Unicode, but this is the best we
+ # can do to preserve meaning. For example,
+ #
+ # foo\u200bbar
+ #
+ # (where \u200b is Unicode code point 0x200b) will be treated
+ # as if the input had been the UTF-8 encoding of that string,
+ # namely:
+ #
+ # foo\226\128\139bar
+ #
+ unescaped = b""
+ l = len(self.value)
+ i = 0
+ while i < l:
+ c = self.value[i]
+ i += 1
+ if c == "\\":
+ if i >= l: # pragma: no cover (can't happen via get())
+ raise dns.exception.UnexpectedEnd
+ c = self.value[i]
+ i += 1
+ if c.isdigit():
+ if i >= l:
+ raise dns.exception.UnexpectedEnd
+ c2 = self.value[i]
+ i += 1
+ if i >= l:
+ raise dns.exception.UnexpectedEnd
+ c3 = self.value[i]
+ i += 1
+ if not (c2.isdigit() and c3.isdigit()):
+ raise dns.exception.SyntaxError
+ codepoint = int(c) * 100 + int(c2) * 10 + int(c3)
+ if codepoint > 255:
+ raise dns.exception.SyntaxError
+ unescaped += b"%c" % (codepoint)
+ else:
+ # Note that as mentioned above, if c is a Unicode
+ # code point outside of the ASCII range, then this
+ # += is converting that code point to its UTF-8
+ # encoding and appending multiple bytes to
+ # unescaped.
+ unescaped += c.encode()
+ else:
+ unescaped += c.encode()
+ return Token(self.ttype, bytes(unescaped))
+
+
+class Tokenizer:
+ """A DNS zone file format tokenizer.
+
+ A token object is basically a (type, value) tuple. The valid
+ types are EOF, EOL, WHITESPACE, IDENTIFIER, QUOTED_STRING,
+ COMMENT, and DELIMITER.
+
+ file: The file to tokenize
+
+ ungotten_char: The most recently ungotten character, or None.
+
+ ungotten_token: The most recently ungotten token, or None.
+
+ multiline: The current multiline level. This value is increased
+ by one every time a '(' delimiter is read, and decreased by one every time
+ a ')' delimiter is read.
+
+ quoting: This variable is true if the tokenizer is currently
+ reading a quoted string.
+
+ eof: This variable is true if the tokenizer has encountered EOF.
+
+ delimiters: The current delimiter dictionary.
+
+ line_number: The current line number
+
+ filename: A filename that will be returned by the where() method.
+
+ idna_codec: A dns.name.IDNACodec, specifies the IDNA
+ encoder/decoder. If None, the default IDNA 2003
+ encoder/decoder is used.
+ """
+
+ def __init__(
+ self,
+ f: Any = sys.stdin,
+ filename: Optional[str] = None,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ ):
+ """Initialize a tokenizer instance.
+
+ f: The file to tokenize. The default is sys.stdin.
+ This parameter may also be a string, in which case the tokenizer
+ will take its input from the contents of the string.
+
+ filename: the name of the filename that the where() method
+ will return.
+
+ idna_codec: A dns.name.IDNACodec, specifies the IDNA
+ encoder/decoder. If None, the default IDNA 2003
+ encoder/decoder is used.
+ """
+
+ if isinstance(f, str):
+ f = io.StringIO(f)
+ if filename is None:
+ filename = "<string>"
+ elif isinstance(f, bytes):
+ f = io.StringIO(f.decode())
+ if filename is None:
+ filename = "<string>"
+ else:
+ if filename is None:
+ if f is sys.stdin:
+ filename = "<stdin>"
+ else:
+ filename = "<file>"
+ self.file = f
+ self.ungotten_char: Optional[str] = None
+ self.ungotten_token: Optional[Token] = None
+ self.multiline = 0
+ self.quoting = False
+ self.eof = False
+ self.delimiters = _DELIMITERS
+ self.line_number = 1
+ assert filename is not None
+ self.filename = filename
+ if idna_codec is None:
+ self.idna_codec: dns.name.IDNACodec = dns.name.IDNA_2003
+ else:
+ self.idna_codec = idna_codec
+
+ def _get_char(self) -> str:
+ """Read a character from input."""
+
+ if self.ungotten_char is None:
+ if self.eof:
+ c = ""
+ else:
+ c = self.file.read(1)
+ if c == "":
+ self.eof = True
+ elif c == "\n":
+ self.line_number += 1
+ else:
+ c = self.ungotten_char
+ self.ungotten_char = None
+ return c
+
+ def where(self) -> Tuple[str, int]:
+ """Return the current location in the input.
+
+ Returns a (string, int) tuple. The first item is the filename of
+ the input, the second is the current line number.
+ """
+
+ return (self.filename, self.line_number)
+
+ def _unget_char(self, c: str) -> None:
+ """Unget a character.
+
+ The unget buffer for characters is only one character large; it is
+ an error to try to unget a character when the unget buffer is not
+ empty.
+
+ c: the character to unget
+ raises UngetBufferFull: there is already an ungotten char
+ """
+
+ if self.ungotten_char is not None:
+ # this should never happen!
+ raise UngetBufferFull # pragma: no cover
+ self.ungotten_char = c
+
+ def skip_whitespace(self) -> int:
+ """Consume input until a non-whitespace character is encountered.
+
+ The non-whitespace character is then ungotten, and the number of
+ whitespace characters consumed is returned.
+
+ If the tokenizer is in multiline mode, then newlines are whitespace.
+
+ Returns the number of characters skipped.
+ """
+
+ skipped = 0
+ while True:
+ c = self._get_char()
+ if c != " " and c != "\t":
+ if (c != "\n") or not self.multiline:
+ self._unget_char(c)
+ return skipped
+ skipped += 1
+
+ def get(self, want_leading: bool = False, want_comment: bool = False) -> Token:
+ """Get the next token.
+
+ want_leading: If True, return a WHITESPACE token if the
+ first character read is whitespace. The default is False.
+
+ want_comment: If True, return a COMMENT token if the
+ first token read is a comment. The default is False.
+
+ Raises dns.exception.UnexpectedEnd: input ended prematurely
+
+ Raises dns.exception.SyntaxError: input was badly formed
+
+ Returns a Token.
+ """
+
+ if self.ungotten_token is not None:
+ utoken = self.ungotten_token
+ self.ungotten_token = None
+ if utoken.is_whitespace():
+ if want_leading:
+ return utoken
+ elif utoken.is_comment():
+ if want_comment:
+ return utoken
+ else:
+ return utoken
+ skipped = self.skip_whitespace()
+ if want_leading and skipped > 0:
+ return Token(WHITESPACE, " ")
+ token = ""
+ ttype = IDENTIFIER
+ has_escape = False
+ while True:
+ c = self._get_char()
+ if c == "" or c in self.delimiters:
+ if c == "" and self.quoting:
+ raise dns.exception.UnexpectedEnd
+ if token == "" and ttype != QUOTED_STRING:
+ if c == "(":
+ self.multiline += 1
+ self.skip_whitespace()
+ continue
+ elif c == ")":
+ if self.multiline <= 0:
+ raise dns.exception.SyntaxError
+ self.multiline -= 1
+ self.skip_whitespace()
+ continue
+ elif c == '"':
+ if not self.quoting:
+ self.quoting = True
+ self.delimiters = _QUOTING_DELIMITERS
+ ttype = QUOTED_STRING
+ continue
+ else:
+ self.quoting = False
+ self.delimiters = _DELIMITERS
+ self.skip_whitespace()
+ continue
+ elif c == "\n":
+ return Token(EOL, "\n")
+ elif c == ";":
+ while 1:
+ c = self._get_char()
+ if c == "\n" or c == "":
+ break
+ token += c
+ if want_comment:
+ self._unget_char(c)
+ return Token(COMMENT, token)
+ elif c == "":
+ if self.multiline:
+ raise dns.exception.SyntaxError(
+ "unbalanced parentheses"
+ )
+ return Token(EOF, comment=token)
+ elif self.multiline:
+ self.skip_whitespace()
+ token = ""
+ continue
+ else:
+ return Token(EOL, "\n", comment=token)
+ else:
+ # This code exists in case we ever want a
+ # delimiter to be returned. It never produces
+ # a token currently.
+ token = c
+ ttype = DELIMITER
+ else:
+ self._unget_char(c)
+ break
+ elif self.quoting and c == "\n":
+ raise dns.exception.SyntaxError("newline in quoted string")
+ elif c == "\\":
+ #
+ # It's an escape. Put it and the next character into
+ # the token; it will be checked later for goodness.
+ #
+ token += c
+ has_escape = True
+ c = self._get_char()
+ if c == "" or (c == "\n" and not self.quoting):
+ raise dns.exception.UnexpectedEnd
+ token += c
+ if token == "" and ttype != QUOTED_STRING:
+ if self.multiline:
+ raise dns.exception.SyntaxError("unbalanced parentheses")
+ ttype = EOF
+ return Token(ttype, token, has_escape)
+
+ def unget(self, token: Token) -> None:
+ """Unget a token.
+
+ The unget buffer for tokens is only one token large; it is
+ an error to try to unget a token when the unget buffer is not
+ empty.
+
+ token: the token to unget
+
+ Raises UngetBufferFull: there is already an ungotten token
+ """
+
+ if self.ungotten_token is not None:
+ raise UngetBufferFull
+ self.ungotten_token = token
+
+ def next(self):
+ """Return the next item in an iteration.
+
+ Returns a Token.
+ """
+
+ token = self.get()
+ if token.is_eof():
+ raise StopIteration
+ return token
+
+ __next__ = next
+
+ def __iter__(self):
+ return self
+
+ # Helpers
+
+ def get_int(self, base: int = 10) -> int:
+ """Read the next token and interpret it as an unsigned integer.
+
+ Raises dns.exception.SyntaxError if not an unsigned integer.
+
+ Returns an int.
+ """
+
+ token = self.get().unescape()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError("expecting an identifier")
+ if not token.value.isdigit():
+ raise dns.exception.SyntaxError("expecting an integer")
+ return int(token.value, base)
+
+ def get_uint8(self) -> int:
+ """Read the next token and interpret it as an 8-bit unsigned
+ integer.
+
+ Raises dns.exception.SyntaxError if not an 8-bit unsigned integer.
+
+ Returns an int.
+ """
+
+ value = self.get_int()
+ if value < 0 or value > 255:
+ raise dns.exception.SyntaxError(
+ "%d is not an unsigned 8-bit integer" % value
+ )
+ return value
+
+ def get_uint16(self, base: int = 10) -> int:
+ """Read the next token and interpret it as a 16-bit unsigned
+ integer.
+
+ Raises dns.exception.SyntaxError if not a 16-bit unsigned integer.
+
+ Returns an int.
+ """
+
+ value = self.get_int(base=base)
+ if value < 0 or value > 65535:
+ if base == 8:
+ raise dns.exception.SyntaxError(
+ f"{value:o} is not an octal unsigned 16-bit integer"
+ )
+ else:
+ raise dns.exception.SyntaxError(
+ "%d is not an unsigned 16-bit integer" % value
+ )
+ return value
+
+ def get_uint32(self, base: int = 10) -> int:
+ """Read the next token and interpret it as a 32-bit unsigned
+ integer.
+
+ Raises dns.exception.SyntaxError if not a 32-bit unsigned integer.
+
+ Returns an int.
+ """
+
+ value = self.get_int(base=base)
+ if value < 0 or value > 4294967295:
+ raise dns.exception.SyntaxError(
+ "%d is not an unsigned 32-bit integer" % value
+ )
+ return value
+
+ def get_uint48(self, base: int = 10) -> int:
+ """Read the next token and interpret it as a 48-bit unsigned
+ integer.
+
+ Raises dns.exception.SyntaxError if not a 48-bit unsigned integer.
+
+ Returns an int.
+ """
+
+ value = self.get_int(base=base)
+ if value < 0 or value > 281474976710655:
+ raise dns.exception.SyntaxError(
+ "%d is not an unsigned 48-bit integer" % value
+ )
+ return value
+
+ def get_string(self, max_length: Optional[int] = None) -> str:
+ """Read the next token and interpret it as a string.
+
+ Raises dns.exception.SyntaxError if not a string.
+ Raises dns.exception.SyntaxError if token value length
+ exceeds max_length (if specified).
+
+ Returns a string.
+ """
+
+ token = self.get().unescape()
+ if not (token.is_identifier() or token.is_quoted_string()):
+ raise dns.exception.SyntaxError("expecting a string")
+ if max_length and len(token.value) > max_length:
+ raise dns.exception.SyntaxError("string too long")
+ return token.value
+
+ def get_identifier(self) -> str:
+ """Read the next token, which should be an identifier.
+
+ Raises dns.exception.SyntaxError if not an identifier.
+
+ Returns a string.
+ """
+
+ token = self.get().unescape()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError("expecting an identifier")
+ return token.value
+
+ def get_remaining(self, max_tokens: Optional[int] = None) -> List[Token]:
+ """Return the remaining tokens on the line, until an EOL or EOF is seen.
+
+ max_tokens: If not None, stop after this number of tokens.
+
+ Returns a list of tokens.
+ """
+
+ tokens = []
+ while True:
+ token = self.get()
+ if token.is_eol_or_eof():
+ self.unget(token)
+ break
+ tokens.append(token)
+ if len(tokens) == max_tokens:
+ break
+ return tokens
+
+ def concatenate_remaining_identifiers(self, allow_empty: bool = False) -> str:
+ """Read the remaining tokens on the line, which should be identifiers.
+
+ Raises dns.exception.SyntaxError if there are no remaining tokens,
+ unless `allow_empty=True` is given.
+
+ Raises dns.exception.SyntaxError if a token is seen that is not an
+ identifier.
+
+ Returns a string containing a concatenation of the remaining
+ identifiers.
+ """
+ s = ""
+ while True:
+ token = self.get().unescape()
+ if token.is_eol_or_eof():
+ self.unget(token)
+ break
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ s += token.value
+ if not (allow_empty or s):
+ raise dns.exception.SyntaxError("expecting another identifier")
+ return s
+
+ def as_name(
+ self,
+ token: Token,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = False,
+ relativize_to: Optional[dns.name.Name] = None,
+ ) -> dns.name.Name:
+ """Try to interpret the token as a DNS name.
+
+ Raises dns.exception.SyntaxError if not a name.
+
+ Returns a dns.name.Name.
+ """
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError("expecting an identifier")
+ name = dns.name.from_text(token.value, origin, self.idna_codec)
+ return name.choose_relativity(relativize_to or origin, relativize)
+
+ def get_name(
+ self,
+ origin: Optional[dns.name.Name] = None,
+ relativize: bool = False,
+ relativize_to: Optional[dns.name.Name] = None,
+ ) -> dns.name.Name:
+ """Read the next token and interpret it as a DNS name.
+
+ Raises dns.exception.SyntaxError if not a name.
+
+ Returns a dns.name.Name.
+ """
+
+ token = self.get()
+ return self.as_name(token, origin, relativize, relativize_to)
+
+ def get_eol_as_token(self) -> Token:
+ """Read the next token and raise an exception if it isn't EOL or
+ EOF.
+
+ Returns a string.
+ """
+
+ token = self.get()
+ if not token.is_eol_or_eof():
+ raise dns.exception.SyntaxError(
+ 'expected EOL or EOF, got %d "%s"' % (token.ttype, token.value)
+ )
+ return token
+
+ def get_eol(self) -> str:
+ return self.get_eol_as_token().value
+
+ def get_ttl(self) -> int:
+ """Read the next token and interpret it as a DNS TTL.
+
+ Raises dns.exception.SyntaxError or dns.ttl.BadTTL if not an
+ identifier or badly formed.
+
+ Returns an int.
+ """
+
+ token = self.get().unescape()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError("expecting an identifier")
+ return dns.ttl.from_text(token.value)
diff --git a/.venv/lib/python3.12/site-packages/dns/transaction.py b/.venv/lib/python3.12/site-packages/dns/transaction.py
new file mode 100644
index 00000000..aa2e1160
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/transaction.py
@@ -0,0 +1,649 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import collections
+from typing import Any, Callable, Iterator, List, Optional, Tuple, Union
+
+import dns.exception
+import dns.name
+import dns.node
+import dns.rdataclass
+import dns.rdataset
+import dns.rdatatype
+import dns.rrset
+import dns.serial
+import dns.ttl
+
+
+class TransactionManager:
+ def reader(self) -> "Transaction":
+ """Begin a read-only transaction."""
+ raise NotImplementedError # pragma: no cover
+
+ def writer(self, replacement: bool = False) -> "Transaction":
+ """Begin a writable transaction.
+
+ *replacement*, a ``bool``. If `True`, the content of the
+ transaction completely replaces any prior content. If False,
+ the default, then the content of the transaction updates the
+ existing content.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def origin_information(
+ self,
+ ) -> Tuple[Optional[dns.name.Name], bool, Optional[dns.name.Name]]:
+ """Returns a tuple
+
+ (absolute_origin, relativize, effective_origin)
+
+ giving the absolute name of the default origin for any
+ relative domain names, the "effective origin", and whether
+ names should be relativized. The "effective origin" is the
+ absolute origin if relativize is False, and the empty name if
+ relativize is true. (The effective origin is provided even
+ though it can be computed from the absolute_origin and
+ relativize setting because it avoids a lot of code
+ duplication.)
+
+ If the returned names are `None`, then no origin information is
+ available.
+
+ This information is used by code working with transactions to
+ allow it to coordinate relativization. The transaction code
+ itself takes what it gets (i.e. does not change name
+ relativity).
+
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def get_class(self) -> dns.rdataclass.RdataClass:
+ """The class of the transaction manager."""
+ raise NotImplementedError # pragma: no cover
+
+ def from_wire_origin(self) -> Optional[dns.name.Name]:
+ """Origin to use in from_wire() calls."""
+ (absolute_origin, relativize, _) = self.origin_information()
+ if relativize:
+ return absolute_origin
+ else:
+ return None
+
+
+class DeleteNotExact(dns.exception.DNSException):
+ """Existing data did not match data specified by an exact delete."""
+
+
+class ReadOnly(dns.exception.DNSException):
+ """Tried to write to a read-only transaction."""
+
+
+class AlreadyEnded(dns.exception.DNSException):
+ """Tried to use an already-ended transaction."""
+
+
+def _ensure_immutable_rdataset(rdataset):
+ if rdataset is None or isinstance(rdataset, dns.rdataset.ImmutableRdataset):
+ return rdataset
+ return dns.rdataset.ImmutableRdataset(rdataset)
+
+
+def _ensure_immutable_node(node):
+ if node is None or node.is_immutable():
+ return node
+ return dns.node.ImmutableNode(node)
+
+
+CheckPutRdatasetType = Callable[
+ ["Transaction", dns.name.Name, dns.rdataset.Rdataset], None
+]
+CheckDeleteRdatasetType = Callable[
+ ["Transaction", dns.name.Name, dns.rdatatype.RdataType, dns.rdatatype.RdataType],
+ None,
+]
+CheckDeleteNameType = Callable[["Transaction", dns.name.Name], None]
+
+
+class Transaction:
+ def __init__(
+ self,
+ manager: TransactionManager,
+ replacement: bool = False,
+ read_only: bool = False,
+ ):
+ self.manager = manager
+ self.replacement = replacement
+ self.read_only = read_only
+ self._ended = False
+ self._check_put_rdataset: List[CheckPutRdatasetType] = []
+ self._check_delete_rdataset: List[CheckDeleteRdatasetType] = []
+ self._check_delete_name: List[CheckDeleteNameType] = []
+
+ #
+ # This is the high level API
+ #
+ # Note that we currently use non-immutable types in the return type signature to
+ # avoid covariance problems, e.g. if the caller has a List[Rdataset], mypy will be
+ # unhappy if we return an ImmutableRdataset.
+
+ def get(
+ self,
+ name: Optional[Union[dns.name.Name, str]],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ ) -> dns.rdataset.Rdataset:
+ """Return the rdataset associated with *name*, *rdtype*, and *covers*,
+ or `None` if not found.
+
+ Note that the returned rdataset is immutable.
+ """
+ self._check_ended()
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ covers = dns.rdatatype.RdataType.make(covers)
+ rdataset = self._get_rdataset(name, rdtype, covers)
+ return _ensure_immutable_rdataset(rdataset)
+
+ def get_node(self, name: dns.name.Name) -> Optional[dns.node.Node]:
+ """Return the node at *name*, if any.
+
+ Returns an immutable node or ``None``.
+ """
+ return _ensure_immutable_node(self._get_node(name))
+
+ def _check_read_only(self) -> None:
+ if self.read_only:
+ raise ReadOnly
+
+ def add(self, *args: Any) -> None:
+ """Add records.
+
+ The arguments may be:
+
+ - rrset
+
+ - name, rdataset...
+
+ - name, ttl, rdata...
+ """
+ self._check_ended()
+ self._check_read_only()
+ self._add(False, args)
+
+ def replace(self, *args: Any) -> None:
+ """Replace the existing rdataset at the name with the specified
+ rdataset, or add the specified rdataset if there was no existing
+ rdataset.
+
+ The arguments may be:
+
+ - rrset
+
+ - name, rdataset...
+
+ - name, ttl, rdata...
+
+ Note that if you want to replace the entire node, you should do
+ a delete of the name followed by one or more calls to add() or
+ replace().
+ """
+ self._check_ended()
+ self._check_read_only()
+ self._add(True, args)
+
+ def delete(self, *args: Any) -> None:
+ """Delete records.
+
+ It is not an error if some of the records are not in the existing
+ set.
+
+ The arguments may be:
+
+ - rrset
+
+ - name
+
+ - name, rdatatype, [covers]
+
+ - name, rdataset...
+
+ - name, rdata...
+ """
+ self._check_ended()
+ self._check_read_only()
+ self._delete(False, args)
+
+ def delete_exact(self, *args: Any) -> None:
+ """Delete records.
+
+ The arguments may be:
+
+ - rrset
+
+ - name
+
+ - name, rdatatype, [covers]
+
+ - name, rdataset...
+
+ - name, rdata...
+
+ Raises dns.transaction.DeleteNotExact if some of the records
+ are not in the existing set.
+
+ """
+ self._check_ended()
+ self._check_read_only()
+ self._delete(True, args)
+
+ def name_exists(self, name: Union[dns.name.Name, str]) -> bool:
+ """Does the specified name exist?"""
+ self._check_ended()
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None)
+ return self._name_exists(name)
+
+ def update_serial(
+ self,
+ value: int = 1,
+ relative: bool = True,
+ name: dns.name.Name = dns.name.empty,
+ ) -> None:
+ """Update the serial number.
+
+ *value*, an `int`, is an increment if *relative* is `True`, or the
+ actual value to set if *relative* is `False`.
+
+ Raises `KeyError` if there is no SOA rdataset at *name*.
+
+ Raises `ValueError` if *value* is negative or if the increment is
+ so large that it would cause the new serial to be less than the
+ prior value.
+ """
+ self._check_ended()
+ if value < 0:
+ raise ValueError("negative update_serial() value")
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None)
+ rdataset = self._get_rdataset(name, dns.rdatatype.SOA, dns.rdatatype.NONE)
+ if rdataset is None or len(rdataset) == 0:
+ raise KeyError
+ if relative:
+ serial = dns.serial.Serial(rdataset[0].serial) + value
+ else:
+ serial = dns.serial.Serial(value)
+ serial = serial.value # convert back to int
+ if serial == 0:
+ serial = 1
+ rdata = rdataset[0].replace(serial=serial)
+ new_rdataset = dns.rdataset.from_rdata(rdataset.ttl, rdata)
+ self.replace(name, new_rdataset)
+
+ def __iter__(self):
+ self._check_ended()
+ return self._iterate_rdatasets()
+
+ def changed(self) -> bool:
+ """Has this transaction changed anything?
+
+ For read-only transactions, the result is always `False`.
+
+ For writable transactions, the result is `True` if at some time
+ during the life of the transaction, the content was changed.
+ """
+ self._check_ended()
+ return self._changed()
+
+ def commit(self) -> None:
+ """Commit the transaction.
+
+ Normally transactions are used as context managers and commit
+ or rollback automatically, but it may be done explicitly if needed.
+ A ``dns.transaction.Ended`` exception will be raised if you try
+ to use a transaction after it has been committed or rolled back.
+
+ Raises an exception if the commit fails (in which case the transaction
+ is also rolled back.
+ """
+ self._end(True)
+
+ def rollback(self) -> None:
+ """Rollback the transaction.
+
+ Normally transactions are used as context managers and commit
+ or rollback automatically, but it may be done explicitly if needed.
+ A ``dns.transaction.AlreadyEnded`` exception will be raised if you try
+ to use a transaction after it has been committed or rolled back.
+
+ Rollback cannot otherwise fail.
+ """
+ self._end(False)
+
+ def check_put_rdataset(self, check: CheckPutRdatasetType) -> None:
+ """Call *check* before putting (storing) an rdataset.
+
+ The function is called with the transaction, the name, and the rdataset.
+
+ The check function may safely make non-mutating transaction method
+ calls, but behavior is undefined if mutating transaction methods are
+ called. The check function should raise an exception if it objects to
+ the put, and otherwise should return ``None``.
+ """
+ self._check_put_rdataset.append(check)
+
+ def check_delete_rdataset(self, check: CheckDeleteRdatasetType) -> None:
+ """Call *check* before deleting an rdataset.
+
+ The function is called with the transaction, the name, the rdatatype,
+ and the covered rdatatype.
+
+ The check function may safely make non-mutating transaction method
+ calls, but behavior is undefined if mutating transaction methods are
+ called. The check function should raise an exception if it objects to
+ the put, and otherwise should return ``None``.
+ """
+ self._check_delete_rdataset.append(check)
+
+ def check_delete_name(self, check: CheckDeleteNameType) -> None:
+ """Call *check* before putting (storing) an rdataset.
+
+ The function is called with the transaction and the name.
+
+ The check function may safely make non-mutating transaction method
+ calls, but behavior is undefined if mutating transaction methods are
+ called. The check function should raise an exception if it objects to
+ the put, and otherwise should return ``None``.
+ """
+ self._check_delete_name.append(check)
+
+ def iterate_rdatasets(
+ self,
+ ) -> Iterator[Tuple[dns.name.Name, dns.rdataset.Rdataset]]:
+ """Iterate all the rdatasets in the transaction, returning
+ (`dns.name.Name`, `dns.rdataset.Rdataset`) tuples.
+
+ Note that as is usual with python iterators, adding or removing items
+ while iterating will invalidate the iterator and may raise `RuntimeError`
+ or fail to iterate over all entries."""
+ self._check_ended()
+ return self._iterate_rdatasets()
+
+ def iterate_names(self) -> Iterator[dns.name.Name]:
+ """Iterate all the names in the transaction.
+
+ Note that as is usual with python iterators, adding or removing names
+ while iterating will invalidate the iterator and may raise `RuntimeError`
+ or fail to iterate over all entries."""
+ self._check_ended()
+ return self._iterate_names()
+
+ #
+ # Helper methods
+ #
+
+ def _raise_if_not_empty(self, method, args):
+ if len(args) != 0:
+ raise TypeError(f"extra parameters to {method}")
+
+ def _rdataset_from_args(self, method, deleting, args):
+ try:
+ arg = args.popleft()
+ if isinstance(arg, dns.rrset.RRset):
+ rdataset = arg.to_rdataset()
+ elif isinstance(arg, dns.rdataset.Rdataset):
+ rdataset = arg
+ else:
+ if deleting:
+ ttl = 0
+ else:
+ if isinstance(arg, int):
+ ttl = arg
+ if ttl > dns.ttl.MAX_TTL:
+ raise ValueError(f"{method}: TTL value too big")
+ else:
+ raise TypeError(f"{method}: expected a TTL")
+ arg = args.popleft()
+ if isinstance(arg, dns.rdata.Rdata):
+ rdataset = dns.rdataset.from_rdata(ttl, arg)
+ else:
+ raise TypeError(f"{method}: expected an Rdata")
+ return rdataset
+ except IndexError:
+ if deleting:
+ return None
+ else:
+ # reraise
+ raise TypeError(f"{method}: expected more arguments")
+
+ def _add(self, replace, args):
+ try:
+ args = collections.deque(args)
+ if replace:
+ method = "replace()"
+ else:
+ method = "add()"
+ arg = args.popleft()
+ if isinstance(arg, str):
+ arg = dns.name.from_text(arg, None)
+ if isinstance(arg, dns.name.Name):
+ name = arg
+ rdataset = self._rdataset_from_args(method, False, args)
+ elif isinstance(arg, dns.rrset.RRset):
+ rrset = arg
+ name = rrset.name
+ # rrsets are also rdatasets, but they don't print the
+ # same and can't be stored in nodes, so convert.
+ rdataset = rrset.to_rdataset()
+ else:
+ raise TypeError(
+ f"{method} requires a name or RRset as the first argument"
+ )
+ if rdataset.rdclass != self.manager.get_class():
+ raise ValueError(f"{method} has objects of wrong RdataClass")
+ if rdataset.rdtype == dns.rdatatype.SOA:
+ (_, _, origin) = self._origin_information()
+ if name != origin:
+ raise ValueError(f"{method} has non-origin SOA")
+ self._raise_if_not_empty(method, args)
+ if not replace:
+ existing = self._get_rdataset(name, rdataset.rdtype, rdataset.covers)
+ if existing is not None:
+ if isinstance(existing, dns.rdataset.ImmutableRdataset):
+ trds = dns.rdataset.Rdataset(
+ existing.rdclass, existing.rdtype, existing.covers
+ )
+ trds.update(existing)
+ existing = trds
+ rdataset = existing.union(rdataset)
+ self._checked_put_rdataset(name, rdataset)
+ except IndexError:
+ raise TypeError(f"not enough parameters to {method}")
+
+ def _delete(self, exact, args):
+ try:
+ args = collections.deque(args)
+ if exact:
+ method = "delete_exact()"
+ else:
+ method = "delete()"
+ arg = args.popleft()
+ if isinstance(arg, str):
+ arg = dns.name.from_text(arg, None)
+ if isinstance(arg, dns.name.Name):
+ name = arg
+ if len(args) > 0 and (
+ isinstance(args[0], int) or isinstance(args[0], str)
+ ):
+ # deleting by type and (optionally) covers
+ rdtype = dns.rdatatype.RdataType.make(args.popleft())
+ if len(args) > 0:
+ covers = dns.rdatatype.RdataType.make(args.popleft())
+ else:
+ covers = dns.rdatatype.NONE
+ self._raise_if_not_empty(method, args)
+ existing = self._get_rdataset(name, rdtype, covers)
+ if existing is None:
+ if exact:
+ raise DeleteNotExact(f"{method}: missing rdataset")
+ else:
+ self._checked_delete_rdataset(name, rdtype, covers)
+ return
+ else:
+ rdataset = self._rdataset_from_args(method, True, args)
+ elif isinstance(arg, dns.rrset.RRset):
+ rdataset = arg # rrsets are also rdatasets
+ name = rdataset.name
+ else:
+ raise TypeError(
+ f"{method} requires a name or RRset as the first argument"
+ )
+ self._raise_if_not_empty(method, args)
+ if rdataset:
+ if rdataset.rdclass != self.manager.get_class():
+ raise ValueError(f"{method} has objects of wrong RdataClass")
+ existing = self._get_rdataset(name, rdataset.rdtype, rdataset.covers)
+ if existing is not None:
+ if exact:
+ intersection = existing.intersection(rdataset)
+ if intersection != rdataset:
+ raise DeleteNotExact(f"{method}: missing rdatas")
+ rdataset = existing.difference(rdataset)
+ if len(rdataset) == 0:
+ self._checked_delete_rdataset(
+ name, rdataset.rdtype, rdataset.covers
+ )
+ else:
+ self._checked_put_rdataset(name, rdataset)
+ elif exact:
+ raise DeleteNotExact(f"{method}: missing rdataset")
+ else:
+ if exact and not self._name_exists(name):
+ raise DeleteNotExact(f"{method}: name not known")
+ self._checked_delete_name(name)
+ except IndexError:
+ raise TypeError(f"not enough parameters to {method}")
+
+ def _check_ended(self):
+ if self._ended:
+ raise AlreadyEnded
+
+ def _end(self, commit):
+ self._check_ended()
+ try:
+ self._end_transaction(commit)
+ finally:
+ self._ended = True
+
+ def _checked_put_rdataset(self, name, rdataset):
+ for check in self._check_put_rdataset:
+ check(self, name, rdataset)
+ self._put_rdataset(name, rdataset)
+
+ def _checked_delete_rdataset(self, name, rdtype, covers):
+ for check in self._check_delete_rdataset:
+ check(self, name, rdtype, covers)
+ self._delete_rdataset(name, rdtype, covers)
+
+ def _checked_delete_name(self, name):
+ for check in self._check_delete_name:
+ check(self, name)
+ self._delete_name(name)
+
+ #
+ # Transactions are context managers.
+ #
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if not self._ended:
+ if exc_type is None:
+ self.commit()
+ else:
+ self.rollback()
+ return False
+
+ #
+ # This is the low level API, which must be implemented by subclasses
+ # of Transaction.
+ #
+
+ def _get_rdataset(self, name, rdtype, covers):
+ """Return the rdataset associated with *name*, *rdtype*, and *covers*,
+ or `None` if not found.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def _put_rdataset(self, name, rdataset):
+ """Store the rdataset."""
+ raise NotImplementedError # pragma: no cover
+
+ def _delete_name(self, name):
+ """Delete all data associated with *name*.
+
+ It is not an error if the name does not exist.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def _delete_rdataset(self, name, rdtype, covers):
+ """Delete all data associated with *name*, *rdtype*, and *covers*.
+
+ It is not an error if the rdataset does not exist.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def _name_exists(self, name):
+ """Does name exist?
+
+ Returns a bool.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def _changed(self):
+ """Has this transaction changed anything?"""
+ raise NotImplementedError # pragma: no cover
+
+ def _end_transaction(self, commit):
+ """End the transaction.
+
+ *commit*, a bool. If ``True``, commit the transaction, otherwise
+ roll it back.
+
+ If committing and the commit fails, then roll back and raise an
+ exception.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def _set_origin(self, origin):
+ """Set the origin.
+
+ This method is called when reading a possibly relativized
+ source, and an origin setting operation occurs (e.g. $ORIGIN
+ in a zone file).
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def _iterate_rdatasets(self):
+ """Return an iterator that yields (name, rdataset) tuples."""
+ raise NotImplementedError # pragma: no cover
+
+ def _iterate_names(self):
+ """Return an iterator that yields a name."""
+ raise NotImplementedError # pragma: no cover
+
+ def _get_node(self, name):
+ """Return the node at *name*, if any.
+
+ Returns a node or ``None``.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ #
+ # Low-level API with a default implementation, in case a subclass needs
+ # to override.
+ #
+
+ def _origin_information(self):
+ # This is only used by _add()
+ return self.manager.origin_information()
diff --git a/.venv/lib/python3.12/site-packages/dns/tsig.py b/.venv/lib/python3.12/site-packages/dns/tsig.py
new file mode 100644
index 00000000..780852e8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/tsig.py
@@ -0,0 +1,352 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS TSIG support."""
+
+import base64
+import hashlib
+import hmac
+import struct
+
+import dns.exception
+import dns.name
+import dns.rcode
+import dns.rdataclass
+
+
+class BadTime(dns.exception.DNSException):
+ """The current time is not within the TSIG's validity time."""
+
+
+class BadSignature(dns.exception.DNSException):
+ """The TSIG signature fails to verify."""
+
+
+class BadKey(dns.exception.DNSException):
+ """The TSIG record owner name does not match the key."""
+
+
+class BadAlgorithm(dns.exception.DNSException):
+ """The TSIG algorithm does not match the key."""
+
+
+class PeerError(dns.exception.DNSException):
+ """Base class for all TSIG errors generated by the remote peer"""
+
+
+class PeerBadKey(PeerError):
+ """The peer didn't know the key we used"""
+
+
+class PeerBadSignature(PeerError):
+ """The peer didn't like the signature we sent"""
+
+
+class PeerBadTime(PeerError):
+ """The peer didn't like the time we sent"""
+
+
+class PeerBadTruncation(PeerError):
+ """The peer didn't like amount of truncation in the TSIG we sent"""
+
+
+# TSIG Algorithms
+
+HMAC_MD5 = dns.name.from_text("HMAC-MD5.SIG-ALG.REG.INT")
+HMAC_SHA1 = dns.name.from_text("hmac-sha1")
+HMAC_SHA224 = dns.name.from_text("hmac-sha224")
+HMAC_SHA256 = dns.name.from_text("hmac-sha256")
+HMAC_SHA256_128 = dns.name.from_text("hmac-sha256-128")
+HMAC_SHA384 = dns.name.from_text("hmac-sha384")
+HMAC_SHA384_192 = dns.name.from_text("hmac-sha384-192")
+HMAC_SHA512 = dns.name.from_text("hmac-sha512")
+HMAC_SHA512_256 = dns.name.from_text("hmac-sha512-256")
+GSS_TSIG = dns.name.from_text("gss-tsig")
+
+default_algorithm = HMAC_SHA256
+
+mac_sizes = {
+ HMAC_SHA1: 20,
+ HMAC_SHA224: 28,
+ HMAC_SHA256: 32,
+ HMAC_SHA256_128: 16,
+ HMAC_SHA384: 48,
+ HMAC_SHA384_192: 24,
+ HMAC_SHA512: 64,
+ HMAC_SHA512_256: 32,
+ HMAC_MD5: 16,
+ GSS_TSIG: 128, # This is what we assume to be the worst case!
+}
+
+
+class GSSTSig:
+ """
+ GSS-TSIG TSIG implementation. This uses the GSS-API context established
+ in the TKEY message handshake to sign messages using GSS-API message
+ integrity codes, per the RFC.
+
+ In order to avoid a direct GSSAPI dependency, the keyring holds a ref
+ to the GSSAPI object required, rather than the key itself.
+ """
+
+ def __init__(self, gssapi_context):
+ self.gssapi_context = gssapi_context
+ self.data = b""
+ self.name = "gss-tsig"
+
+ def update(self, data):
+ self.data += data
+
+ def sign(self):
+ # defer to the GSSAPI function to sign
+ return self.gssapi_context.get_signature(self.data)
+
+ def verify(self, expected):
+ try:
+ # defer to the GSSAPI function to verify
+ return self.gssapi_context.verify_signature(self.data, expected)
+ except Exception:
+ # note the usage of a bare exception
+ raise BadSignature
+
+
+class GSSTSigAdapter:
+ def __init__(self, keyring):
+ self.keyring = keyring
+
+ def __call__(self, message, keyname):
+ if keyname in self.keyring:
+ key = self.keyring[keyname]
+ if isinstance(key, Key) and key.algorithm == GSS_TSIG:
+ if message:
+ GSSTSigAdapter.parse_tkey_and_step(key, message, keyname)
+ return key
+ else:
+ return None
+
+ @classmethod
+ def parse_tkey_and_step(cls, key, message, keyname):
+ # if the message is a TKEY type, absorb the key material
+ # into the context using step(); this is used to allow the
+ # client to complete the GSSAPI negotiation before attempting
+ # to verify the signed response to a TKEY message exchange
+ try:
+ rrset = message.find_rrset(
+ message.answer, keyname, dns.rdataclass.ANY, dns.rdatatype.TKEY
+ )
+ if rrset:
+ token = rrset[0].key
+ gssapi_context = key.secret
+ return gssapi_context.step(token)
+ except KeyError:
+ pass
+
+
+class HMACTSig:
+ """
+ HMAC TSIG implementation. This uses the HMAC python module to handle the
+ sign/verify operations.
+ """
+
+ _hashes = {
+ HMAC_SHA1: hashlib.sha1,
+ HMAC_SHA224: hashlib.sha224,
+ HMAC_SHA256: hashlib.sha256,
+ HMAC_SHA256_128: (hashlib.sha256, 128),
+ HMAC_SHA384: hashlib.sha384,
+ HMAC_SHA384_192: (hashlib.sha384, 192),
+ HMAC_SHA512: hashlib.sha512,
+ HMAC_SHA512_256: (hashlib.sha512, 256),
+ HMAC_MD5: hashlib.md5,
+ }
+
+ def __init__(self, key, algorithm):
+ try:
+ hashinfo = self._hashes[algorithm]
+ except KeyError:
+ raise NotImplementedError(f"TSIG algorithm {algorithm} is not supported")
+
+ # create the HMAC context
+ if isinstance(hashinfo, tuple):
+ self.hmac_context = hmac.new(key, digestmod=hashinfo[0])
+ self.size = hashinfo[1]
+ else:
+ self.hmac_context = hmac.new(key, digestmod=hashinfo)
+ self.size = None
+ self.name = self.hmac_context.name
+ if self.size:
+ self.name += f"-{self.size}"
+
+ def update(self, data):
+ return self.hmac_context.update(data)
+
+ def sign(self):
+ # defer to the HMAC digest() function for that digestmod
+ digest = self.hmac_context.digest()
+ if self.size:
+ digest = digest[: (self.size // 8)]
+ return digest
+
+ def verify(self, expected):
+ # re-digest and compare the results
+ mac = self.sign()
+ if not hmac.compare_digest(mac, expected):
+ raise BadSignature
+
+
+def _digest(wire, key, rdata, time=None, request_mac=None, ctx=None, multi=None):
+ """Return a context containing the TSIG rdata for the input parameters
+ @rtype: dns.tsig.HMACTSig or dns.tsig.GSSTSig object
+ @raises ValueError: I{other_data} is too long
+ @raises NotImplementedError: I{algorithm} is not supported
+ """
+
+ first = not (ctx and multi)
+ if first:
+ ctx = get_context(key)
+ if request_mac:
+ ctx.update(struct.pack("!H", len(request_mac)))
+ ctx.update(request_mac)
+ ctx.update(struct.pack("!H", rdata.original_id))
+ ctx.update(wire[2:])
+ if first:
+ ctx.update(key.name.to_digestable())
+ ctx.update(struct.pack("!H", dns.rdataclass.ANY))
+ ctx.update(struct.pack("!I", 0))
+ if time is None:
+ time = rdata.time_signed
+ upper_time = (time >> 32) & 0xFFFF
+ lower_time = time & 0xFFFFFFFF
+ time_encoded = struct.pack("!HIH", upper_time, lower_time, rdata.fudge)
+ other_len = len(rdata.other)
+ if other_len > 65535:
+ raise ValueError("TSIG Other Data is > 65535 bytes")
+ if first:
+ ctx.update(key.algorithm.to_digestable() + time_encoded)
+ ctx.update(struct.pack("!HH", rdata.error, other_len) + rdata.other)
+ else:
+ ctx.update(time_encoded)
+ return ctx
+
+
+def _maybe_start_digest(key, mac, multi):
+ """If this is the first message in a multi-message sequence,
+ start a new context.
+ @rtype: dns.tsig.HMACTSig or dns.tsig.GSSTSig object
+ """
+ if multi:
+ ctx = get_context(key)
+ ctx.update(struct.pack("!H", len(mac)))
+ ctx.update(mac)
+ return ctx
+ else:
+ return None
+
+
+def sign(wire, key, rdata, time=None, request_mac=None, ctx=None, multi=False):
+ """Return a (tsig_rdata, mac, ctx) tuple containing the HMAC TSIG rdata
+ for the input parameters, the HMAC MAC calculated by applying the
+ TSIG signature algorithm, and the TSIG digest context.
+ @rtype: (string, dns.tsig.HMACTSig or dns.tsig.GSSTSig object)
+ @raises ValueError: I{other_data} is too long
+ @raises NotImplementedError: I{algorithm} is not supported
+ """
+
+ ctx = _digest(wire, key, rdata, time, request_mac, ctx, multi)
+ mac = ctx.sign()
+ tsig = rdata.replace(time_signed=time, mac=mac)
+
+ return (tsig, _maybe_start_digest(key, mac, multi))
+
+
+def validate(
+ wire, key, owner, rdata, now, request_mac, tsig_start, ctx=None, multi=False
+):
+ """Validate the specified TSIG rdata against the other input parameters.
+
+ @raises FormError: The TSIG is badly formed.
+ @raises BadTime: There is too much time skew between the client and the
+ server.
+ @raises BadSignature: The TSIG signature did not validate
+ @rtype: dns.tsig.HMACTSig or dns.tsig.GSSTSig object"""
+
+ (adcount,) = struct.unpack("!H", wire[10:12])
+ if adcount == 0:
+ raise dns.exception.FormError
+ adcount -= 1
+ new_wire = wire[0:10] + struct.pack("!H", adcount) + wire[12:tsig_start]
+ if rdata.error != 0:
+ if rdata.error == dns.rcode.BADSIG:
+ raise PeerBadSignature
+ elif rdata.error == dns.rcode.BADKEY:
+ raise PeerBadKey
+ elif rdata.error == dns.rcode.BADTIME:
+ raise PeerBadTime
+ elif rdata.error == dns.rcode.BADTRUNC:
+ raise PeerBadTruncation
+ else:
+ raise PeerError("unknown TSIG error code %d" % rdata.error)
+ if abs(rdata.time_signed - now) > rdata.fudge:
+ raise BadTime
+ if key.name != owner:
+ raise BadKey
+ if key.algorithm != rdata.algorithm:
+ raise BadAlgorithm
+ ctx = _digest(new_wire, key, rdata, None, request_mac, ctx, multi)
+ ctx.verify(rdata.mac)
+ return _maybe_start_digest(key, rdata.mac, multi)
+
+
+def get_context(key):
+ """Returns an HMAC context for the specified key.
+
+ @rtype: HMAC context
+ @raises NotImplementedError: I{algorithm} is not supported
+ """
+
+ if key.algorithm == GSS_TSIG:
+ return GSSTSig(key.secret)
+ else:
+ return HMACTSig(key.secret, key.algorithm)
+
+
+class Key:
+ def __init__(self, name, secret, algorithm=default_algorithm):
+ if isinstance(name, str):
+ name = dns.name.from_text(name)
+ self.name = name
+ if isinstance(secret, str):
+ secret = base64.decodebytes(secret.encode())
+ self.secret = secret
+ if isinstance(algorithm, str):
+ algorithm = dns.name.from_text(algorithm)
+ self.algorithm = algorithm
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, Key)
+ and self.name == other.name
+ and self.secret == other.secret
+ and self.algorithm == other.algorithm
+ )
+
+ def __repr__(self):
+ r = f"<DNS key name='{self.name}', " + f"algorithm='{self.algorithm}'"
+ if self.algorithm != GSS_TSIG:
+ r += f", secret='{base64.b64encode(self.secret).decode()}'"
+ r += ">"
+ return r
diff --git a/.venv/lib/python3.12/site-packages/dns/tsigkeyring.py b/.venv/lib/python3.12/site-packages/dns/tsigkeyring.py
new file mode 100644
index 00000000..1010a79f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/tsigkeyring.py
@@ -0,0 +1,68 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""A place to store TSIG keys."""
+
+import base64
+from typing import Any, Dict
+
+import dns.name
+import dns.tsig
+
+
+def from_text(textring: Dict[str, Any]) -> Dict[dns.name.Name, dns.tsig.Key]:
+ """Convert a dictionary containing (textual DNS name, base64 secret)
+ pairs into a binary keyring which has (dns.name.Name, bytes) pairs, or
+ a dictionary containing (textual DNS name, (algorithm, base64 secret))
+ pairs into a binary keyring which has (dns.name.Name, dns.tsig.Key) pairs.
+ @rtype: dict"""
+
+ keyring = {}
+ for name, value in textring.items():
+ kname = dns.name.from_text(name)
+ if isinstance(value, str):
+ keyring[kname] = dns.tsig.Key(kname, value).secret
+ else:
+ (algorithm, secret) = value
+ keyring[kname] = dns.tsig.Key(kname, secret, algorithm)
+ return keyring
+
+
+def to_text(keyring: Dict[dns.name.Name, Any]) -> Dict[str, Any]:
+ """Convert a dictionary containing (dns.name.Name, dns.tsig.Key) pairs
+ into a text keyring which has (textual DNS name, (textual algorithm,
+ base64 secret)) pairs, or a dictionary containing (dns.name.Name, bytes)
+ pairs into a text keyring which has (textual DNS name, base64 secret) pairs.
+ @rtype: dict"""
+
+ textring = {}
+
+ def b64encode(secret):
+ return base64.encodebytes(secret).decode().rstrip()
+
+ for name, key in keyring.items():
+ tname = name.to_text()
+ if isinstance(key, bytes):
+ textring[tname] = b64encode(key)
+ else:
+ if isinstance(key.secret, bytes):
+ text_secret = b64encode(key.secret)
+ else:
+ text_secret = str(key.secret)
+
+ textring[tname] = (key.algorithm.to_text(), text_secret)
+ return textring
diff --git a/.venv/lib/python3.12/site-packages/dns/ttl.py b/.venv/lib/python3.12/site-packages/dns/ttl.py
new file mode 100644
index 00000000..b9a99fe3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/ttl.py
@@ -0,0 +1,92 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS TTL conversion."""
+
+from typing import Union
+
+import dns.exception
+
+# Technically TTLs are supposed to be between 0 and 2**31 - 1, with values
+# greater than that interpreted as 0, but we do not impose this policy here
+# as values > 2**31 - 1 occur in real world data.
+#
+# We leave it to applications to impose tighter bounds if desired.
+MAX_TTL = 2**32 - 1
+
+
+class BadTTL(dns.exception.SyntaxError):
+ """DNS TTL value is not well-formed."""
+
+
+def from_text(text: str) -> int:
+ """Convert the text form of a TTL to an integer.
+
+ The BIND 8 units syntax for TTLs (e.g. '1w6d4h3m10s') is supported.
+
+ *text*, a ``str``, the textual TTL.
+
+ Raises ``dns.ttl.BadTTL`` if the TTL is not well-formed.
+
+ Returns an ``int``.
+ """
+
+ if text.isdigit():
+ total = int(text)
+ elif len(text) == 0:
+ raise BadTTL
+ else:
+ total = 0
+ current = 0
+ need_digit = True
+ for c in text:
+ if c.isdigit():
+ current *= 10
+ current += int(c)
+ need_digit = False
+ else:
+ if need_digit:
+ raise BadTTL
+ c = c.lower()
+ if c == "w":
+ total += current * 604800
+ elif c == "d":
+ total += current * 86400
+ elif c == "h":
+ total += current * 3600
+ elif c == "m":
+ total += current * 60
+ elif c == "s":
+ total += current
+ else:
+ raise BadTTL(f"unknown unit '{c}'")
+ current = 0
+ need_digit = True
+ if not current == 0:
+ raise BadTTL("trailing integer")
+ if total < 0 or total > MAX_TTL:
+ raise BadTTL("TTL should be between 0 and 2**32 - 1 (inclusive)")
+ return total
+
+
+def make(value: Union[int, str]) -> int:
+ if isinstance(value, int):
+ return value
+ elif isinstance(value, str):
+ return dns.ttl.from_text(value)
+ else:
+ raise ValueError("cannot convert value to TTL")
diff --git a/.venv/lib/python3.12/site-packages/dns/update.py b/.venv/lib/python3.12/site-packages/dns/update.py
new file mode 100644
index 00000000..bf1157ac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/update.py
@@ -0,0 +1,386 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Dynamic Update Support"""
+
+from typing import Any, List, Optional, Union
+
+import dns.message
+import dns.name
+import dns.opcode
+import dns.rdata
+import dns.rdataclass
+import dns.rdataset
+import dns.rdatatype
+import dns.tsig
+
+
+class UpdateSection(dns.enum.IntEnum):
+ """Update sections"""
+
+ ZONE = 0
+ PREREQ = 1
+ UPDATE = 2
+ ADDITIONAL = 3
+
+ @classmethod
+ def _maximum(cls):
+ return 3
+
+
+class UpdateMessage(dns.message.Message): # lgtm[py/missing-equals]
+ # ignore the mypy error here as we mean to use a different enum
+ _section_enum = UpdateSection # type: ignore
+
+ def __init__(
+ self,
+ zone: Optional[Union[dns.name.Name, str]] = None,
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+ keyring: Optional[Any] = None,
+ keyname: Optional[dns.name.Name] = None,
+ keyalgorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm,
+ id: Optional[int] = None,
+ ):
+ """Initialize a new DNS Update object.
+
+ See the documentation of the Message class for a complete
+ description of the keyring dictionary.
+
+ *zone*, a ``dns.name.Name``, ``str``, or ``None``, the zone
+ which is being updated. ``None`` should only be used by dnspython's
+ message constructors, as a zone is required for the convenience
+ methods like ``add()``, ``replace()``, etc.
+
+ *rdclass*, an ``int`` or ``str``, the class of the zone.
+
+ The *keyring*, *keyname*, and *keyalgorithm* parameters are passed to
+ ``use_tsig()``; see its documentation for details.
+ """
+ super().__init__(id=id)
+ self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE)
+ if isinstance(zone, str):
+ zone = dns.name.from_text(zone)
+ self.origin = zone
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ self.zone_rdclass = rdclass
+ if self.origin:
+ self.find_rrset(
+ self.zone,
+ self.origin,
+ rdclass,
+ dns.rdatatype.SOA,
+ create=True,
+ force_unique=True,
+ )
+ if keyring is not None:
+ self.use_tsig(keyring, keyname, algorithm=keyalgorithm)
+
+ @property
+ def zone(self) -> List[dns.rrset.RRset]:
+ """The zone section."""
+ return self.sections[0]
+
+ @zone.setter
+ def zone(self, v):
+ self.sections[0] = v
+
+ @property
+ def prerequisite(self) -> List[dns.rrset.RRset]:
+ """The prerequisite section."""
+ return self.sections[1]
+
+ @prerequisite.setter
+ def prerequisite(self, v):
+ self.sections[1] = v
+
+ @property
+ def update(self) -> List[dns.rrset.RRset]:
+ """The update section."""
+ return self.sections[2]
+
+ @update.setter
+ def update(self, v):
+ self.sections[2] = v
+
+ def _add_rr(self, name, ttl, rd, deleting=None, section=None):
+ """Add a single RR to the update section."""
+
+ if section is None:
+ section = self.update
+ covers = rd.covers()
+ rrset = self.find_rrset(
+ section, name, self.zone_rdclass, rd.rdtype, covers, deleting, True, True
+ )
+ rrset.add(rd, ttl)
+
+ def _add(self, replace, section, name, *args):
+ """Add records.
+
+ *replace* is the replacement mode. If ``False``,
+ RRs are added to an existing RRset; if ``True``, the RRset
+ is replaced with the specified contents. The second
+ argument is the section to add to. The third argument
+ is always a name. The other arguments can be:
+
+ - rdataset...
+
+ - ttl, rdata...
+
+ - ttl, rdtype, string...
+ """
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None)
+ if isinstance(args[0], dns.rdataset.Rdataset):
+ for rds in args:
+ if replace:
+ self.delete(name, rds.rdtype)
+ for rd in rds:
+ self._add_rr(name, rds.ttl, rd, section=section)
+ else:
+ args = list(args)
+ ttl = int(args.pop(0))
+ if isinstance(args[0], dns.rdata.Rdata):
+ if replace:
+ self.delete(name, args[0].rdtype)
+ for rd in args:
+ self._add_rr(name, ttl, rd, section=section)
+ else:
+ rdtype = dns.rdatatype.RdataType.make(args.pop(0))
+ if replace:
+ self.delete(name, rdtype)
+ for s in args:
+ rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s, self.origin)
+ self._add_rr(name, ttl, rd, section=section)
+
+ def add(self, name: Union[dns.name.Name, str], *args: Any) -> None:
+ """Add records.
+
+ The first argument is always a name. The other
+ arguments can be:
+
+ - rdataset...
+
+ - ttl, rdata...
+
+ - ttl, rdtype, string...
+ """
+
+ self._add(False, self.update, name, *args)
+
+ def delete(self, name: Union[dns.name.Name, str], *args: Any) -> None:
+ """Delete records.
+
+ The first argument is always a name. The other
+ arguments can be:
+
+ - *empty*
+
+ - rdataset...
+
+ - rdata...
+
+ - rdtype, [string...]
+ """
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None)
+ if len(args) == 0:
+ self.find_rrset(
+ self.update,
+ name,
+ dns.rdataclass.ANY,
+ dns.rdatatype.ANY,
+ dns.rdatatype.NONE,
+ dns.rdataclass.ANY,
+ True,
+ True,
+ )
+ elif isinstance(args[0], dns.rdataset.Rdataset):
+ for rds in args:
+ for rd in rds:
+ self._add_rr(name, 0, rd, dns.rdataclass.NONE)
+ else:
+ largs = list(args)
+ if isinstance(largs[0], dns.rdata.Rdata):
+ for rd in largs:
+ self._add_rr(name, 0, rd, dns.rdataclass.NONE)
+ else:
+ rdtype = dns.rdatatype.RdataType.make(largs.pop(0))
+ if len(largs) == 0:
+ self.find_rrset(
+ self.update,
+ name,
+ self.zone_rdclass,
+ rdtype,
+ dns.rdatatype.NONE,
+ dns.rdataclass.ANY,
+ True,
+ True,
+ )
+ else:
+ for s in largs:
+ rd = dns.rdata.from_text(
+ self.zone_rdclass,
+ rdtype,
+ s, # type: ignore[arg-type]
+ self.origin,
+ )
+ self._add_rr(name, 0, rd, dns.rdataclass.NONE)
+
+ def replace(self, name: Union[dns.name.Name, str], *args: Any) -> None:
+ """Replace records.
+
+ The first argument is always a name. The other
+ arguments can be:
+
+ - rdataset...
+
+ - ttl, rdata...
+
+ - ttl, rdtype, string...
+
+ Note that if you want to replace the entire node, you should do
+ a delete of the name followed by one or more calls to add.
+ """
+
+ self._add(True, self.update, name, *args)
+
+ def present(self, name: Union[dns.name.Name, str], *args: Any) -> None:
+ """Require that an owner name (and optionally an rdata type,
+ or specific rdataset) exists as a prerequisite to the
+ execution of the update.
+
+ The first argument is always a name.
+ The other arguments can be:
+
+ - rdataset...
+
+ - rdata...
+
+ - rdtype, string...
+ """
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None)
+ if len(args) == 0:
+ self.find_rrset(
+ self.prerequisite,
+ name,
+ dns.rdataclass.ANY,
+ dns.rdatatype.ANY,
+ dns.rdatatype.NONE,
+ None,
+ True,
+ True,
+ )
+ elif (
+ isinstance(args[0], dns.rdataset.Rdataset)
+ or isinstance(args[0], dns.rdata.Rdata)
+ or len(args) > 1
+ ):
+ if not isinstance(args[0], dns.rdataset.Rdataset):
+ # Add a 0 TTL
+ largs = list(args)
+ largs.insert(0, 0) # type: ignore[arg-type]
+ self._add(False, self.prerequisite, name, *largs)
+ else:
+ self._add(False, self.prerequisite, name, *args)
+ else:
+ rdtype = dns.rdatatype.RdataType.make(args[0])
+ self.find_rrset(
+ self.prerequisite,
+ name,
+ dns.rdataclass.ANY,
+ rdtype,
+ dns.rdatatype.NONE,
+ None,
+ True,
+ True,
+ )
+
+ def absent(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Optional[Union[dns.rdatatype.RdataType, str]] = None,
+ ) -> None:
+ """Require that an owner name (and optionally an rdata type) does
+ not exist as a prerequisite to the execution of the update."""
+
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None)
+ if rdtype is None:
+ self.find_rrset(
+ self.prerequisite,
+ name,
+ dns.rdataclass.NONE,
+ dns.rdatatype.ANY,
+ dns.rdatatype.NONE,
+ None,
+ True,
+ True,
+ )
+ else:
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ self.find_rrset(
+ self.prerequisite,
+ name,
+ dns.rdataclass.NONE,
+ rdtype,
+ dns.rdatatype.NONE,
+ None,
+ True,
+ True,
+ )
+
+ def _get_one_rr_per_rrset(self, value):
+ # Updates are always one_rr_per_rrset
+ return True
+
+ def _parse_rr_header(self, section, name, rdclass, rdtype):
+ deleting = None
+ empty = False
+ if section == UpdateSection.ZONE:
+ if (
+ dns.rdataclass.is_metaclass(rdclass)
+ or rdtype != dns.rdatatype.SOA
+ or self.zone
+ ):
+ raise dns.exception.FormError
+ else:
+ if not self.zone:
+ raise dns.exception.FormError
+ if rdclass in (dns.rdataclass.ANY, dns.rdataclass.NONE):
+ deleting = rdclass
+ rdclass = self.zone[0].rdclass
+ empty = (
+ deleting == dns.rdataclass.ANY or section == UpdateSection.PREREQ
+ )
+ return (rdclass, rdtype, deleting, empty)
+
+
+# backwards compatibility
+Update = UpdateMessage
+
+### BEGIN generated UpdateSection constants
+
+ZONE = UpdateSection.ZONE
+PREREQ = UpdateSection.PREREQ
+UPDATE = UpdateSection.UPDATE
+ADDITIONAL = UpdateSection.ADDITIONAL
+
+### END generated UpdateSection constants
diff --git a/.venv/lib/python3.12/site-packages/dns/version.py b/.venv/lib/python3.12/site-packages/dns/version.py
new file mode 100644
index 00000000..9ed2ce19
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/version.py
@@ -0,0 +1,58 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""dnspython release version information."""
+
+#: MAJOR
+MAJOR = 2
+#: MINOR
+MINOR = 7
+#: MICRO
+MICRO = 0
+#: RELEASELEVEL
+RELEASELEVEL = 0x0F
+#: SERIAL
+SERIAL = 0
+
+if RELEASELEVEL == 0x0F: # pragma: no cover lgtm[py/unreachable-statement]
+ #: version
+ version = "%d.%d.%d" % (MAJOR, MINOR, MICRO) # lgtm[py/unreachable-statement]
+elif RELEASELEVEL == 0x00: # pragma: no cover lgtm[py/unreachable-statement]
+ version = "%d.%d.%ddev%d" % (
+ MAJOR,
+ MINOR,
+ MICRO,
+ SERIAL,
+ ) # lgtm[py/unreachable-statement]
+elif RELEASELEVEL == 0x0C: # pragma: no cover lgtm[py/unreachable-statement]
+ version = "%d.%d.%drc%d" % (
+ MAJOR,
+ MINOR,
+ MICRO,
+ SERIAL,
+ ) # lgtm[py/unreachable-statement]
+else: # pragma: no cover lgtm[py/unreachable-statement]
+ version = "%d.%d.%d%x%d" % (
+ MAJOR,
+ MINOR,
+ MICRO,
+ RELEASELEVEL,
+ SERIAL,
+ ) # lgtm[py/unreachable-statement]
+
+#: hexversion
+hexversion = MAJOR << 24 | MINOR << 16 | MICRO << 8 | RELEASELEVEL << 4 | SERIAL
diff --git a/.venv/lib/python3.12/site-packages/dns/versioned.py b/.venv/lib/python3.12/site-packages/dns/versioned.py
new file mode 100644
index 00000000..fd78e674
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/versioned.py
@@ -0,0 +1,318 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+"""DNS Versioned Zones."""
+
+import collections
+import threading
+from typing import Callable, Deque, Optional, Set, Union
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.node
+import dns.rdataclass
+import dns.rdataset
+import dns.rdatatype
+import dns.rdtypes.ANY.SOA
+import dns.zone
+
+
+class UseTransaction(dns.exception.DNSException):
+ """To alter a versioned zone, use a transaction."""
+
+
+# Backwards compatibility
+Node = dns.zone.VersionedNode
+ImmutableNode = dns.zone.ImmutableVersionedNode
+Version = dns.zone.Version
+WritableVersion = dns.zone.WritableVersion
+ImmutableVersion = dns.zone.ImmutableVersion
+Transaction = dns.zone.Transaction
+
+
+class Zone(dns.zone.Zone): # lgtm[py/missing-equals]
+ __slots__ = [
+ "_versions",
+ "_versions_lock",
+ "_write_txn",
+ "_write_waiters",
+ "_write_event",
+ "_pruning_policy",
+ "_readers",
+ ]
+
+ node_factory = Node
+
+ def __init__(
+ self,
+ origin: Optional[Union[dns.name.Name, str]],
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+ relativize: bool = True,
+ pruning_policy: Optional[Callable[["Zone", Version], Optional[bool]]] = None,
+ ):
+ """Initialize a versioned zone object.
+
+ *origin* is the origin of the zone. It may be a ``dns.name.Name``,
+ a ``str``, or ``None``. If ``None``, then the zone's origin will
+ be set by the first ``$ORIGIN`` line in a zone file.
+
+ *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
+
+ *relativize*, a ``bool``, determine's whether domain names are
+ relativized to the zone's origin. The default is ``True``.
+
+ *pruning policy*, a function taking a ``Zone`` and a ``Version`` and returning
+ a ``bool``, or ``None``. Should the version be pruned? If ``None``,
+ the default policy, which retains one version is used.
+ """
+ super().__init__(origin, rdclass, relativize)
+ self._versions: Deque[Version] = collections.deque()
+ self._version_lock = threading.Lock()
+ if pruning_policy is None:
+ self._pruning_policy = self._default_pruning_policy
+ else:
+ self._pruning_policy = pruning_policy
+ self._write_txn: Optional[Transaction] = None
+ self._write_event: Optional[threading.Event] = None
+ self._write_waiters: Deque[threading.Event] = collections.deque()
+ self._readers: Set[Transaction] = set()
+ self._commit_version_unlocked(
+ None, WritableVersion(self, replacement=True), origin
+ )
+
+ def reader(
+ self, id: Optional[int] = None, serial: Optional[int] = None
+ ) -> Transaction: # pylint: disable=arguments-differ
+ if id is not None and serial is not None:
+ raise ValueError("cannot specify both id and serial")
+ with self._version_lock:
+ if id is not None:
+ version = None
+ for v in reversed(self._versions):
+ if v.id == id:
+ version = v
+ break
+ if version is None:
+ raise KeyError("version not found")
+ elif serial is not None:
+ if self.relativize:
+ oname = dns.name.empty
+ else:
+ assert self.origin is not None
+ oname = self.origin
+ version = None
+ for v in reversed(self._versions):
+ n = v.nodes.get(oname)
+ if n:
+ rds = n.get_rdataset(self.rdclass, dns.rdatatype.SOA)
+ if rds and rds[0].serial == serial:
+ version = v
+ break
+ if version is None:
+ raise KeyError("serial not found")
+ else:
+ version = self._versions[-1]
+ txn = Transaction(self, False, version)
+ self._readers.add(txn)
+ return txn
+
+ def writer(self, replacement: bool = False) -> Transaction:
+ event = None
+ while True:
+ with self._version_lock:
+ # Checking event == self._write_event ensures that either
+ # no one was waiting before we got lucky and found no write
+ # txn, or we were the one who was waiting and got woken up.
+ # This prevents "taking cuts" when creating a write txn.
+ if self._write_txn is None and event == self._write_event:
+ # Creating the transaction defers version setup
+ # (i.e. copying the nodes dictionary) until we
+ # give up the lock, so that we hold the lock as
+ # short a time as possible. This is why we call
+ # _setup_version() below.
+ self._write_txn = Transaction(
+ self, replacement, make_immutable=True
+ )
+ # give up our exclusive right to make a Transaction
+ self._write_event = None
+ break
+ # Someone else is writing already, so we will have to
+ # wait, but we want to do the actual wait outside the
+ # lock.
+ event = threading.Event()
+ self._write_waiters.append(event)
+ # wait (note we gave up the lock!)
+ #
+ # We only wake one sleeper at a time, so it's important
+ # that no event waiter can exit this method (e.g. via
+ # cancellation) without returning a transaction or waking
+ # someone else up.
+ #
+ # This is not a problem with Threading module threads as
+ # they cannot be canceled, but could be an issue with trio
+ # tasks when we do the async version of writer().
+ # I.e. we'd need to do something like:
+ #
+ # try:
+ # event.wait()
+ # except trio.Cancelled:
+ # with self._version_lock:
+ # self._maybe_wakeup_one_waiter_unlocked()
+ # raise
+ #
+ event.wait()
+ # Do the deferred version setup.
+ self._write_txn._setup_version()
+ return self._write_txn
+
+ def _maybe_wakeup_one_waiter_unlocked(self):
+ if len(self._write_waiters) > 0:
+ self._write_event = self._write_waiters.popleft()
+ self._write_event.set()
+
+ # pylint: disable=unused-argument
+ def _default_pruning_policy(self, zone, version):
+ return True
+
+ # pylint: enable=unused-argument
+
+ def _prune_versions_unlocked(self):
+ assert len(self._versions) > 0
+ # Don't ever prune a version greater than or equal to one that
+ # a reader has open. This pins versions in memory while the
+ # reader is open, and importantly lets the reader open a txn on
+ # a successor version (e.g. if generating an IXFR).
+ #
+ # Note our definition of least_kept also ensures we do not try to
+ # delete the greatest version.
+ if len(self._readers) > 0:
+ least_kept = min(txn.version.id for txn in self._readers)
+ else:
+ least_kept = self._versions[-1].id
+ while self._versions[0].id < least_kept and self._pruning_policy(
+ self, self._versions[0]
+ ):
+ self._versions.popleft()
+
+ def set_max_versions(self, max_versions: Optional[int]) -> None:
+ """Set a pruning policy that retains up to the specified number
+ of versions
+ """
+ if max_versions is not None and max_versions < 1:
+ raise ValueError("max versions must be at least 1")
+ if max_versions is None:
+
+ def policy(zone, _): # pylint: disable=unused-argument
+ return False
+
+ else:
+
+ def policy(zone, _):
+ return len(zone._versions) > max_versions
+
+ self.set_pruning_policy(policy)
+
+ def set_pruning_policy(
+ self, policy: Optional[Callable[["Zone", Version], Optional[bool]]]
+ ) -> None:
+ """Set the pruning policy for the zone.
+
+ The *policy* function takes a `Version` and returns `True` if
+ the version should be pruned, and `False` otherwise. `None`
+ may also be specified for policy, in which case the default policy
+ is used.
+
+ Pruning checking proceeds from the least version and the first
+ time the function returns `False`, the checking stops. I.e. the
+ retained versions are always a consecutive sequence.
+ """
+ if policy is None:
+ policy = self._default_pruning_policy
+ with self._version_lock:
+ self._pruning_policy = policy
+ self._prune_versions_unlocked()
+
+ def _end_read(self, txn):
+ with self._version_lock:
+ self._readers.remove(txn)
+ self._prune_versions_unlocked()
+
+ def _end_write_unlocked(self, txn):
+ assert self._write_txn == txn
+ self._write_txn = None
+ self._maybe_wakeup_one_waiter_unlocked()
+
+ def _end_write(self, txn):
+ with self._version_lock:
+ self._end_write_unlocked(txn)
+
+ def _commit_version_unlocked(self, txn, version, origin):
+ self._versions.append(version)
+ self._prune_versions_unlocked()
+ self.nodes = version.nodes
+ if self.origin is None:
+ self.origin = origin
+ # txn can be None in __init__ when we make the empty version.
+ if txn is not None:
+ self._end_write_unlocked(txn)
+
+ def _commit_version(self, txn, version, origin):
+ with self._version_lock:
+ self._commit_version_unlocked(txn, version, origin)
+
+ def _get_next_version_id(self):
+ if len(self._versions) > 0:
+ id = self._versions[-1].id + 1
+ else:
+ id = 1
+ return id
+
+ def find_node(
+ self, name: Union[dns.name.Name, str], create: bool = False
+ ) -> dns.node.Node:
+ if create:
+ raise UseTransaction
+ return super().find_node(name)
+
+ def delete_node(self, name: Union[dns.name.Name, str]) -> None:
+ raise UseTransaction
+
+ def find_rdataset(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> dns.rdataset.Rdataset:
+ if create:
+ raise UseTransaction
+ rdataset = super().find_rdataset(name, rdtype, covers)
+ return dns.rdataset.ImmutableRdataset(rdataset)
+
+ def get_rdataset(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> Optional[dns.rdataset.Rdataset]:
+ if create:
+ raise UseTransaction
+ rdataset = super().get_rdataset(name, rdtype, covers)
+ if rdataset is not None:
+ return dns.rdataset.ImmutableRdataset(rdataset)
+ else:
+ return None
+
+ def delete_rdataset(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ ) -> None:
+ raise UseTransaction
+
+ def replace_rdataset(
+ self, name: Union[dns.name.Name, str], replacement: dns.rdataset.Rdataset
+ ) -> None:
+ raise UseTransaction
diff --git a/.venv/lib/python3.12/site-packages/dns/win32util.py b/.venv/lib/python3.12/site-packages/dns/win32util.py
new file mode 100644
index 00000000..9ed3f11b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/win32util.py
@@ -0,0 +1,242 @@
+import sys
+
+import dns._features
+
+if sys.platform == "win32":
+ from typing import Any
+
+ import dns.name
+
+ _prefer_wmi = True
+
+ import winreg # pylint: disable=import-error
+
+ # Keep pylint quiet on non-windows.
+ try:
+ _ = WindowsError # pylint: disable=used-before-assignment
+ except NameError:
+ WindowsError = Exception
+
+ if dns._features.have("wmi"):
+ import threading
+
+ import pythoncom # pylint: disable=import-error
+ import wmi # pylint: disable=import-error
+
+ _have_wmi = True
+ else:
+ _have_wmi = False
+
+ def _config_domain(domain):
+ # Sometimes DHCP servers add a '.' prefix to the default domain, and
+ # Windows just stores such values in the registry (see #687).
+ # Check for this and fix it.
+ if domain.startswith("."):
+ domain = domain[1:]
+ return dns.name.from_text(domain)
+
+ class DnsInfo:
+ def __init__(self):
+ self.domain = None
+ self.nameservers = []
+ self.search = []
+
+ if _have_wmi:
+
+ class _WMIGetter(threading.Thread):
+ # pylint: disable=possibly-used-before-assignment
+ def __init__(self):
+ super().__init__()
+ self.info = DnsInfo()
+
+ def run(self):
+ pythoncom.CoInitialize()
+ try:
+ system = wmi.WMI()
+ for interface in system.Win32_NetworkAdapterConfiguration():
+ if interface.IPEnabled and interface.DNSServerSearchOrder:
+ self.info.nameservers = list(interface.DNSServerSearchOrder)
+ if interface.DNSDomain:
+ self.info.domain = _config_domain(interface.DNSDomain)
+ if interface.DNSDomainSuffixSearchOrder:
+ self.info.search = [
+ _config_domain(x)
+ for x in interface.DNSDomainSuffixSearchOrder
+ ]
+ break
+ finally:
+ pythoncom.CoUninitialize()
+
+ def get(self):
+ # We always run in a separate thread to avoid any issues with
+ # the COM threading model.
+ self.start()
+ self.join()
+ return self.info
+
+ else:
+
+ class _WMIGetter: # type: ignore
+ pass
+
+ class _RegistryGetter:
+ def __init__(self):
+ self.info = DnsInfo()
+
+ def _split(self, text):
+ # The windows registry has used both " " and "," as a delimiter, and while
+ # it is currently using "," in Windows 10 and later, updates can seemingly
+ # leave a space in too, e.g. "a, b". So we just convert all commas to
+ # spaces, and use split() in its default configuration, which splits on
+ # all whitespace and ignores empty strings.
+ return text.replace(",", " ").split()
+
+ def _config_nameservers(self, nameservers):
+ for ns in self._split(nameservers):
+ if ns not in self.info.nameservers:
+ self.info.nameservers.append(ns)
+
+ def _config_search(self, search):
+ for s in self._split(search):
+ s = _config_domain(s)
+ if s not in self.info.search:
+ self.info.search.append(s)
+
+ def _config_fromkey(self, key, always_try_domain):
+ try:
+ servers, _ = winreg.QueryValueEx(key, "NameServer")
+ except WindowsError:
+ servers = None
+ if servers:
+ self._config_nameservers(servers)
+ if servers or always_try_domain:
+ try:
+ dom, _ = winreg.QueryValueEx(key, "Domain")
+ if dom:
+ self.info.domain = _config_domain(dom)
+ except WindowsError:
+ pass
+ else:
+ try:
+ servers, _ = winreg.QueryValueEx(key, "DhcpNameServer")
+ except WindowsError:
+ servers = None
+ if servers:
+ self._config_nameservers(servers)
+ try:
+ dom, _ = winreg.QueryValueEx(key, "DhcpDomain")
+ if dom:
+ self.info.domain = _config_domain(dom)
+ except WindowsError:
+ pass
+ try:
+ search, _ = winreg.QueryValueEx(key, "SearchList")
+ except WindowsError:
+ search = None
+ if search is None:
+ try:
+ search, _ = winreg.QueryValueEx(key, "DhcpSearchList")
+ except WindowsError:
+ search = None
+ if search:
+ self._config_search(search)
+
+ def _is_nic_enabled(self, lm, guid):
+ # Look in the Windows Registry to determine whether the network
+ # interface corresponding to the given guid is enabled.
+ #
+ # (Code contributed by Paul Marks, thanks!)
+ #
+ try:
+ # This hard-coded location seems to be consistent, at least
+ # from Windows 2000 through Vista.
+ connection_key = winreg.OpenKey(
+ lm,
+ r"SYSTEM\CurrentControlSet\Control\Network"
+ r"\{4D36E972-E325-11CE-BFC1-08002BE10318}"
+ rf"\{guid}\Connection",
+ )
+
+ try:
+ # The PnpInstanceID points to a key inside Enum
+ (pnp_id, ttype) = winreg.QueryValueEx(
+ connection_key, "PnpInstanceID"
+ )
+
+ if ttype != winreg.REG_SZ:
+ raise ValueError # pragma: no cover
+
+ device_key = winreg.OpenKey(
+ lm, rf"SYSTEM\CurrentControlSet\Enum\{pnp_id}"
+ )
+
+ try:
+ # Get ConfigFlags for this device
+ (flags, ttype) = winreg.QueryValueEx(device_key, "ConfigFlags")
+
+ if ttype != winreg.REG_DWORD:
+ raise ValueError # pragma: no cover
+
+ # Based on experimentation, bit 0x1 indicates that the
+ # device is disabled.
+ #
+ # XXXRTH I suspect we really want to & with 0x03 so
+ # that CONFIGFLAGS_REMOVED devices are also ignored,
+ # but we're shifting to WMI as ConfigFlags is not
+ # supposed to be used.
+ return not flags & 0x1
+
+ finally:
+ device_key.Close()
+ finally:
+ connection_key.Close()
+ except Exception: # pragma: no cover
+ return False
+
+ def get(self):
+ """Extract resolver configuration from the Windows registry."""
+
+ lm = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+ try:
+ tcp_params = winreg.OpenKey(
+ lm, r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters"
+ )
+ try:
+ self._config_fromkey(tcp_params, True)
+ finally:
+ tcp_params.Close()
+ interfaces = winreg.OpenKey(
+ lm,
+ r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces",
+ )
+ try:
+ i = 0
+ while True:
+ try:
+ guid = winreg.EnumKey(interfaces, i)
+ i += 1
+ key = winreg.OpenKey(interfaces, guid)
+ try:
+ if not self._is_nic_enabled(lm, guid):
+ continue
+ self._config_fromkey(key, False)
+ finally:
+ key.Close()
+ except OSError:
+ break
+ finally:
+ interfaces.Close()
+ finally:
+ lm.Close()
+ return self.info
+
+ _getter_class: Any
+ if _have_wmi and _prefer_wmi:
+ _getter_class = _WMIGetter
+ else:
+ _getter_class = _RegistryGetter
+
+ def get_dns_info():
+ """Extract resolver configuration."""
+ getter = _getter_class()
+ return getter.get()
diff --git a/.venv/lib/python3.12/site-packages/dns/wire.py b/.venv/lib/python3.12/site-packages/dns/wire.py
new file mode 100644
index 00000000..9f9b1573
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/wire.py
@@ -0,0 +1,89 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import contextlib
+import struct
+from typing import Iterator, Optional, Tuple
+
+import dns.exception
+import dns.name
+
+
+class Parser:
+ def __init__(self, wire: bytes, current: int = 0):
+ self.wire = wire
+ self.current = 0
+ self.end = len(self.wire)
+ if current:
+ self.seek(current)
+ self.furthest = current
+
+ def remaining(self) -> int:
+ return self.end - self.current
+
+ def get_bytes(self, size: int) -> bytes:
+ assert size >= 0
+ if size > self.remaining():
+ raise dns.exception.FormError
+ output = self.wire[self.current : self.current + size]
+ self.current += size
+ self.furthest = max(self.furthest, self.current)
+ return output
+
+ def get_counted_bytes(self, length_size: int = 1) -> bytes:
+ length = int.from_bytes(self.get_bytes(length_size), "big")
+ return self.get_bytes(length)
+
+ def get_remaining(self) -> bytes:
+ return self.get_bytes(self.remaining())
+
+ def get_uint8(self) -> int:
+ return struct.unpack("!B", self.get_bytes(1))[0]
+
+ def get_uint16(self) -> int:
+ return struct.unpack("!H", self.get_bytes(2))[0]
+
+ def get_uint32(self) -> int:
+ return struct.unpack("!I", self.get_bytes(4))[0]
+
+ def get_uint48(self) -> int:
+ return int.from_bytes(self.get_bytes(6), "big")
+
+ def get_struct(self, format: str) -> Tuple:
+ return struct.unpack(format, self.get_bytes(struct.calcsize(format)))
+
+ def get_name(self, origin: Optional["dns.name.Name"] = None) -> "dns.name.Name":
+ name = dns.name.from_wire_parser(self)
+ if origin:
+ name = name.relativize(origin)
+ return name
+
+ def seek(self, where: int) -> None:
+ # Note that seeking to the end is OK! (If you try to read
+ # after such a seek, you'll get an exception as expected.)
+ if where < 0 or where > self.end:
+ raise dns.exception.FormError
+ self.current = where
+
+ @contextlib.contextmanager
+ def restrict_to(self, size: int) -> Iterator:
+ assert size >= 0
+ if size > self.remaining():
+ raise dns.exception.FormError
+ saved_end = self.end
+ try:
+ self.end = self.current + size
+ yield
+ # We make this check here and not in the finally as we
+ # don't want to raise if we're already raising for some
+ # other reason.
+ if self.current != self.end:
+ raise dns.exception.FormError
+ finally:
+ self.end = saved_end
+
+ @contextlib.contextmanager
+ def restore_furthest(self) -> Iterator:
+ try:
+ yield None
+ finally:
+ self.current = self.furthest
diff --git a/.venv/lib/python3.12/site-packages/dns/xfr.py b/.venv/lib/python3.12/site-packages/dns/xfr.py
new file mode 100644
index 00000000..520aa32d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/xfr.py
@@ -0,0 +1,343 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from typing import Any, List, Optional, Tuple, Union
+
+import dns.exception
+import dns.message
+import dns.name
+import dns.rcode
+import dns.rdataset
+import dns.rdatatype
+import dns.serial
+import dns.transaction
+import dns.tsig
+import dns.zone
+
+
+class TransferError(dns.exception.DNSException):
+ """A zone transfer response got a non-zero rcode."""
+
+ def __init__(self, rcode):
+ message = f"Zone transfer error: {dns.rcode.to_text(rcode)}"
+ super().__init__(message)
+ self.rcode = rcode
+
+
+class SerialWentBackwards(dns.exception.FormError):
+ """The current serial number is less than the serial we know."""
+
+
+class UseTCP(dns.exception.DNSException):
+ """This IXFR cannot be completed with UDP."""
+
+
+class Inbound:
+ """
+ State machine for zone transfers.
+ """
+
+ def __init__(
+ self,
+ txn_manager: dns.transaction.TransactionManager,
+ rdtype: dns.rdatatype.RdataType = dns.rdatatype.AXFR,
+ serial: Optional[int] = None,
+ is_udp: bool = False,
+ ):
+ """Initialize an inbound zone transfer.
+
+ *txn_manager* is a :py:class:`dns.transaction.TransactionManager`.
+
+ *rdtype* can be `dns.rdatatype.AXFR` or `dns.rdatatype.IXFR`
+
+ *serial* is the base serial number for IXFRs, and is required in
+ that case.
+
+ *is_udp*, a ``bool`` indidicates if UDP is being used for this
+ XFR.
+ """
+ self.txn_manager = txn_manager
+ self.txn: Optional[dns.transaction.Transaction] = None
+ self.rdtype = rdtype
+ if rdtype == dns.rdatatype.IXFR:
+ if serial is None:
+ raise ValueError("a starting serial must be supplied for IXFRs")
+ elif is_udp:
+ raise ValueError("is_udp specified for AXFR")
+ self.serial = serial
+ self.is_udp = is_udp
+ (_, _, self.origin) = txn_manager.origin_information()
+ self.soa_rdataset: Optional[dns.rdataset.Rdataset] = None
+ self.done = False
+ self.expecting_SOA = False
+ self.delete_mode = False
+
+ def process_message(self, message: dns.message.Message) -> bool:
+ """Process one message in the transfer.
+
+ The message should have the same relativization as was specified when
+ the `dns.xfr.Inbound` was created. The message should also have been
+ created with `one_rr_per_rrset=True` because order matters.
+
+ Returns `True` if the transfer is complete, and `False` otherwise.
+ """
+ if self.txn is None:
+ replacement = self.rdtype == dns.rdatatype.AXFR
+ self.txn = self.txn_manager.writer(replacement)
+ rcode = message.rcode()
+ if rcode != dns.rcode.NOERROR:
+ raise TransferError(rcode)
+ #
+ # We don't require a question section, but if it is present is
+ # should be correct.
+ #
+ if len(message.question) > 0:
+ if message.question[0].name != self.origin:
+ raise dns.exception.FormError("wrong question name")
+ if message.question[0].rdtype != self.rdtype:
+ raise dns.exception.FormError("wrong question rdatatype")
+ answer_index = 0
+ if self.soa_rdataset is None:
+ #
+ # This is the first message. We're expecting an SOA at
+ # the origin.
+ #
+ if not message.answer or message.answer[0].name != self.origin:
+ raise dns.exception.FormError("No answer or RRset not for zone origin")
+ rrset = message.answer[0]
+ rdataset = rrset
+ if rdataset.rdtype != dns.rdatatype.SOA:
+ raise dns.exception.FormError("first RRset is not an SOA")
+ answer_index = 1
+ self.soa_rdataset = rdataset.copy()
+ if self.rdtype == dns.rdatatype.IXFR:
+ if self.soa_rdataset[0].serial == self.serial:
+ #
+ # We're already up-to-date.
+ #
+ self.done = True
+ elif dns.serial.Serial(self.soa_rdataset[0].serial) < self.serial:
+ # It went backwards!
+ raise SerialWentBackwards
+ else:
+ if self.is_udp and len(message.answer[answer_index:]) == 0:
+ #
+ # There are no more records, so this is the
+ # "truncated" response. Say to use TCP
+ #
+ raise UseTCP
+ #
+ # Note we're expecting another SOA so we can detect
+ # if this IXFR response is an AXFR-style response.
+ #
+ self.expecting_SOA = True
+ #
+ # Process the answer section (other than the initial SOA in
+ # the first message).
+ #
+ for rrset in message.answer[answer_index:]:
+ name = rrset.name
+ rdataset = rrset
+ if self.done:
+ raise dns.exception.FormError("answers after final SOA")
+ assert self.txn is not None # for mypy
+ if rdataset.rdtype == dns.rdatatype.SOA and name == self.origin:
+ #
+ # Every time we see an origin SOA delete_mode inverts
+ #
+ if self.rdtype == dns.rdatatype.IXFR:
+ self.delete_mode = not self.delete_mode
+ #
+ # If this SOA Rdataset is equal to the first we saw
+ # then we're finished. If this is an IXFR we also
+ # check that we're seeing the record in the expected
+ # part of the response.
+ #
+ if rdataset == self.soa_rdataset and (
+ self.rdtype == dns.rdatatype.AXFR
+ or (self.rdtype == dns.rdatatype.IXFR and self.delete_mode)
+ ):
+ #
+ # This is the final SOA
+ #
+ if self.expecting_SOA:
+ # We got an empty IXFR sequence!
+ raise dns.exception.FormError("empty IXFR sequence")
+ if (
+ self.rdtype == dns.rdatatype.IXFR
+ and self.serial != rdataset[0].serial
+ ):
+ raise dns.exception.FormError("unexpected end of IXFR sequence")
+ self.txn.replace(name, rdataset)
+ self.txn.commit()
+ self.txn = None
+ self.done = True
+ else:
+ #
+ # This is not the final SOA
+ #
+ self.expecting_SOA = False
+ if self.rdtype == dns.rdatatype.IXFR:
+ if self.delete_mode:
+ # This is the start of an IXFR deletion set
+ if rdataset[0].serial != self.serial:
+ raise dns.exception.FormError(
+ "IXFR base serial mismatch"
+ )
+ else:
+ # This is the start of an IXFR addition set
+ self.serial = rdataset[0].serial
+ self.txn.replace(name, rdataset)
+ else:
+ # We saw a non-final SOA for the origin in an AXFR.
+ raise dns.exception.FormError("unexpected origin SOA in AXFR")
+ continue
+ if self.expecting_SOA:
+ #
+ # We made an IXFR request and are expecting another
+ # SOA RR, but saw something else, so this must be an
+ # AXFR response.
+ #
+ self.rdtype = dns.rdatatype.AXFR
+ self.expecting_SOA = False
+ self.delete_mode = False
+ self.txn.rollback()
+ self.txn = self.txn_manager.writer(True)
+ #
+ # Note we are falling through into the code below
+ # so whatever rdataset this was gets written.
+ #
+ # Add or remove the data
+ if self.delete_mode:
+ self.txn.delete_exact(name, rdataset)
+ else:
+ self.txn.add(name, rdataset)
+ if self.is_udp and not self.done:
+ #
+ # This is a UDP IXFR and we didn't get to done, and we didn't
+ # get the proper "truncated" response
+ #
+ raise dns.exception.FormError("unexpected end of UDP IXFR")
+ return self.done
+
+ #
+ # Inbounds are context managers.
+ #
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if self.txn:
+ self.txn.rollback()
+ return False
+
+
+def make_query(
+ txn_manager: dns.transaction.TransactionManager,
+ serial: Optional[int] = 0,
+ use_edns: Optional[Union[int, bool]] = None,
+ ednsflags: Optional[int] = None,
+ payload: Optional[int] = None,
+ request_payload: Optional[int] = None,
+ options: Optional[List[dns.edns.Option]] = None,
+ keyring: Any = None,
+ keyname: Optional[dns.name.Name] = None,
+ keyalgorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm,
+) -> Tuple[dns.message.QueryMessage, Optional[int]]:
+ """Make an AXFR or IXFR query.
+
+ *txn_manager* is a ``dns.transaction.TransactionManager``, typically a
+ ``dns.zone.Zone``.
+
+ *serial* is an ``int`` or ``None``. If 0, then IXFR will be
+ attempted using the most recent serial number from the
+ *txn_manager*; it is the caller's responsibility to ensure there
+ are no write transactions active that could invalidate the
+ retrieved serial. If a serial cannot be determined, AXFR will be
+ forced. Other integer values are the starting serial to use.
+ ``None`` forces an AXFR.
+
+ Please see the documentation for :py:func:`dns.message.make_query` and
+ :py:func:`dns.message.Message.use_tsig` for details on the other parameters
+ to this function.
+
+ Returns a `(query, serial)` tuple.
+ """
+ (zone_origin, _, origin) = txn_manager.origin_information()
+ if zone_origin is None:
+ raise ValueError("no zone origin")
+ if serial is None:
+ rdtype = dns.rdatatype.AXFR
+ elif not isinstance(serial, int):
+ raise ValueError("serial is not an integer")
+ elif serial == 0:
+ with txn_manager.reader() as txn:
+ rdataset = txn.get(origin, "SOA")
+ if rdataset:
+ serial = rdataset[0].serial
+ rdtype = dns.rdatatype.IXFR
+ else:
+ serial = None
+ rdtype = dns.rdatatype.AXFR
+ elif serial > 0 and serial < 4294967296:
+ rdtype = dns.rdatatype.IXFR
+ else:
+ raise ValueError("serial out-of-range")
+ rdclass = txn_manager.get_class()
+ q = dns.message.make_query(
+ zone_origin,
+ rdtype,
+ rdclass,
+ use_edns,
+ False,
+ ednsflags,
+ payload,
+ request_payload,
+ options,
+ )
+ if serial is not None:
+ rdata = dns.rdata.from_text(rdclass, "SOA", f". . {serial} 0 0 0 0")
+ rrset = q.find_rrset(
+ q.authority, zone_origin, rdclass, dns.rdatatype.SOA, create=True
+ )
+ rrset.add(rdata, 0)
+ if keyring is not None:
+ q.use_tsig(keyring, keyname, algorithm=keyalgorithm)
+ return (q, serial)
+
+
+def extract_serial_from_query(query: dns.message.Message) -> Optional[int]:
+ """Extract the SOA serial number from query if it is an IXFR and return
+ it, otherwise return None.
+
+ *query* is a dns.message.QueryMessage that is an IXFR or AXFR request.
+
+ Raises if the query is not an IXFR or AXFR, or if an IXFR doesn't have
+ an appropriate SOA RRset in the authority section.
+ """
+ if not isinstance(query, dns.message.QueryMessage):
+ raise ValueError("query not a QueryMessage")
+ question = query.question[0]
+ if question.rdtype == dns.rdatatype.AXFR:
+ return None
+ elif question.rdtype != dns.rdatatype.IXFR:
+ raise ValueError("query is not an AXFR or IXFR")
+ soa = query.find_rrset(
+ query.authority, question.name, question.rdclass, dns.rdatatype.SOA
+ )
+ return soa[0].serial
diff --git a/.venv/lib/python3.12/site-packages/dns/zone.py b/.venv/lib/python3.12/site-packages/dns/zone.py
new file mode 100644
index 00000000..844919e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/zone.py
@@ -0,0 +1,1434 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Zones."""
+
+import contextlib
+import io
+import os
+import struct
+from typing import (
+ Any,
+ Callable,
+ Iterable,
+ Iterator,
+ List,
+ MutableMapping,
+ Optional,
+ Set,
+ Tuple,
+ Union,
+)
+
+import dns.exception
+import dns.grange
+import dns.immutable
+import dns.name
+import dns.node
+import dns.rdata
+import dns.rdataclass
+import dns.rdataset
+import dns.rdatatype
+import dns.rdtypes.ANY.SOA
+import dns.rdtypes.ANY.ZONEMD
+import dns.rrset
+import dns.tokenizer
+import dns.transaction
+import dns.ttl
+import dns.zonefile
+from dns.zonetypes import DigestHashAlgorithm, DigestScheme, _digest_hashers
+
+
+class BadZone(dns.exception.DNSException):
+ """The DNS zone is malformed."""
+
+
+class NoSOA(BadZone):
+ """The DNS zone has no SOA RR at its origin."""
+
+
+class NoNS(BadZone):
+ """The DNS zone has no NS RRset at its origin."""
+
+
+class UnknownOrigin(BadZone):
+ """The DNS zone's origin is unknown."""
+
+
+class UnsupportedDigestScheme(dns.exception.DNSException):
+ """The zone digest's scheme is unsupported."""
+
+
+class UnsupportedDigestHashAlgorithm(dns.exception.DNSException):
+ """The zone digest's origin is unsupported."""
+
+
+class NoDigest(dns.exception.DNSException):
+ """The DNS zone has no ZONEMD RRset at its origin."""
+
+
+class DigestVerificationFailure(dns.exception.DNSException):
+ """The ZONEMD digest failed to verify."""
+
+
+def _validate_name(
+ name: dns.name.Name,
+ origin: Optional[dns.name.Name],
+ relativize: bool,
+) -> dns.name.Name:
+ # This name validation code is shared by Zone and Version
+ if origin is None:
+ # This should probably never happen as other code (e.g.
+ # _rr_line) will notice the lack of an origin before us, but
+ # we check just in case!
+ raise KeyError("no zone origin is defined")
+ if name.is_absolute():
+ if not name.is_subdomain(origin):
+ raise KeyError("name parameter must be a subdomain of the zone origin")
+ if relativize:
+ name = name.relativize(origin)
+ else:
+ # We have a relative name. Make sure that the derelativized name is
+ # not too long.
+ try:
+ abs_name = name.derelativize(origin)
+ except dns.name.NameTooLong:
+ # We map dns.name.NameTooLong to KeyError to be consistent with
+ # the other exceptions above.
+ raise KeyError("relative name too long for zone")
+ if not relativize:
+ # We have a relative name in a non-relative zone, so use the
+ # derelativized name.
+ name = abs_name
+ return name
+
+
+class Zone(dns.transaction.TransactionManager):
+ """A DNS zone.
+
+ A ``Zone`` is a mapping from names to nodes. The zone object may be
+ treated like a Python dictionary, e.g. ``zone[name]`` will retrieve
+ the node associated with that name. The *name* may be a
+ ``dns.name.Name object``, or it may be a string. In either case,
+ if the name is relative it is treated as relative to the origin of
+ the zone.
+ """
+
+ node_factory: Callable[[], dns.node.Node] = dns.node.Node
+ map_factory: Callable[[], MutableMapping[dns.name.Name, dns.node.Node]] = dict
+ writable_version_factory: Optional[Callable[[], "WritableVersion"]] = None
+ immutable_version_factory: Optional[Callable[[], "ImmutableVersion"]] = None
+
+ __slots__ = ["rdclass", "origin", "nodes", "relativize"]
+
+ def __init__(
+ self,
+ origin: Optional[Union[dns.name.Name, str]],
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+ relativize: bool = True,
+ ):
+ """Initialize a zone object.
+
+ *origin* is the origin of the zone. It may be a ``dns.name.Name``,
+ a ``str``, or ``None``. If ``None``, then the zone's origin will
+ be set by the first ``$ORIGIN`` line in a zone file.
+
+ *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
+
+ *relativize*, a ``bool``, determine's whether domain names are
+ relativized to the zone's origin. The default is ``True``.
+ """
+
+ if origin is not None:
+ if isinstance(origin, str):
+ origin = dns.name.from_text(origin)
+ elif not isinstance(origin, dns.name.Name):
+ raise ValueError("origin parameter must be convertible to a DNS name")
+ if not origin.is_absolute():
+ raise ValueError("origin parameter must be an absolute name")
+ self.origin = origin
+ self.rdclass = rdclass
+ self.nodes: MutableMapping[dns.name.Name, dns.node.Node] = self.map_factory()
+ self.relativize = relativize
+
+ def __eq__(self, other):
+ """Two zones are equal if they have the same origin, class, and
+ nodes.
+
+ Returns a ``bool``.
+ """
+
+ if not isinstance(other, Zone):
+ return False
+ if (
+ self.rdclass != other.rdclass
+ or self.origin != other.origin
+ or self.nodes != other.nodes
+ ):
+ return False
+ return True
+
+ def __ne__(self, other):
+ """Are two zones not equal?
+
+ Returns a ``bool``.
+ """
+
+ return not self.__eq__(other)
+
+ def _validate_name(self, name: Union[dns.name.Name, str]) -> dns.name.Name:
+ # Note that any changes in this method should have corresponding changes
+ # made in the Version _validate_name() method.
+ if isinstance(name, str):
+ name = dns.name.from_text(name, None)
+ elif not isinstance(name, dns.name.Name):
+ raise KeyError("name parameter must be convertible to a DNS name")
+ return _validate_name(name, self.origin, self.relativize)
+
+ def __getitem__(self, key):
+ key = self._validate_name(key)
+ return self.nodes[key]
+
+ def __setitem__(self, key, value):
+ key = self._validate_name(key)
+ self.nodes[key] = value
+
+ def __delitem__(self, key):
+ key = self._validate_name(key)
+ del self.nodes[key]
+
+ def __iter__(self):
+ return self.nodes.__iter__()
+
+ def keys(self):
+ return self.nodes.keys()
+
+ def values(self):
+ return self.nodes.values()
+
+ def items(self):
+ return self.nodes.items()
+
+ def get(self, key):
+ key = self._validate_name(key)
+ return self.nodes.get(key)
+
+ def __contains__(self, key):
+ key = self._validate_name(key)
+ return key in self.nodes
+
+ def find_node(
+ self, name: Union[dns.name.Name, str], create: bool = False
+ ) -> dns.node.Node:
+ """Find a node in the zone, possibly creating it.
+
+ *name*: the name of the node to find.
+ The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
+ name must be a subdomain of the zone's origin. If ``zone.relativize``
+ is ``True``, then the name will be relativized.
+
+ *create*, a ``bool``. If true, the node will be created if it does
+ not exist.
+
+ Raises ``KeyError`` if the name is not known and create was
+ not specified, or if the name was not a subdomain of the origin.
+
+ Returns a ``dns.node.Node``.
+ """
+
+ name = self._validate_name(name)
+ node = self.nodes.get(name)
+ if node is None:
+ if not create:
+ raise KeyError
+ node = self.node_factory()
+ self.nodes[name] = node
+ return node
+
+ def get_node(
+ self, name: Union[dns.name.Name, str], create: bool = False
+ ) -> Optional[dns.node.Node]:
+ """Get a node in the zone, possibly creating it.
+
+ This method is like ``find_node()``, except it returns None instead
+ of raising an exception if the node does not exist and creation
+ has not been requested.
+
+ *name*: the name of the node to find.
+ The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
+ name must be a subdomain of the zone's origin. If ``zone.relativize``
+ is ``True``, then the name will be relativized.
+
+ *create*, a ``bool``. If true, the node will be created if it does
+ not exist.
+
+ Returns a ``dns.node.Node`` or ``None``.
+ """
+
+ try:
+ node = self.find_node(name, create)
+ except KeyError:
+ node = None
+ return node
+
+ def delete_node(self, name: Union[dns.name.Name, str]) -> None:
+ """Delete the specified node if it exists.
+
+ *name*: the name of the node to find.
+ The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
+ name must be a subdomain of the zone's origin. If ``zone.relativize``
+ is ``True``, then the name will be relativized.
+
+ It is not an error if the node does not exist.
+ """
+
+ name = self._validate_name(name)
+ if name in self.nodes:
+ del self.nodes[name]
+
+ def find_rdataset(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> dns.rdataset.Rdataset:
+ """Look for an rdataset with the specified name and type in the zone,
+ and return an rdataset encapsulating it.
+
+ The rdataset returned is not a copy; changes to it will change
+ the zone.
+
+ KeyError is raised if the name or type are not found.
+
+ *name*: the name of the node to find.
+ The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
+ name must be a subdomain of the zone's origin. If ``zone.relativize``
+ is ``True``, then the name will be relativized.
+
+ *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired.
+
+ *covers*, a ``dns.rdatatype.RdataType`` or ``str`` the covered type.
+ Usually this value is ``dns.rdatatype.NONE``, but if the
+ rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
+ then the covers value will be the rdata type the SIG/RRSIG
+ covers. The library treats the SIG and RRSIG types as if they
+ were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
+ This makes RRSIGs much easier to work with than if RRSIGs
+ covering different rdata types were aggregated into a single
+ RRSIG rdataset.
+
+ *create*, a ``bool``. If true, the node will be created if it does
+ not exist.
+
+ Raises ``KeyError`` if the name is not known and create was
+ not specified, or if the name was not a subdomain of the origin.
+
+ Returns a ``dns.rdataset.Rdataset``.
+ """
+
+ name = self._validate_name(name)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ covers = dns.rdatatype.RdataType.make(covers)
+ node = self.find_node(name, create)
+ return node.find_rdataset(self.rdclass, rdtype, covers, create)
+
+ def get_rdataset(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> Optional[dns.rdataset.Rdataset]:
+ """Look for an rdataset with the specified name and type in the zone.
+
+ This method is like ``find_rdataset()``, except it returns None instead
+ of raising an exception if the rdataset does not exist and creation
+ has not been requested.
+
+ The rdataset returned is not a copy; changes to it will change
+ the zone.
+
+ *name*: the name of the node to find.
+ The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
+ name must be a subdomain of the zone's origin. If ``zone.relativize``
+ is ``True``, then the name will be relativized.
+
+ *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired.
+
+ *covers*, a ``dns.rdatatype.RdataType`` or ``str``, the covered type.
+ Usually this value is ``dns.rdatatype.NONE``, but if the
+ rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
+ then the covers value will be the rdata type the SIG/RRSIG
+ covers. The library treats the SIG and RRSIG types as if they
+ were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
+ This makes RRSIGs much easier to work with than if RRSIGs
+ covering different rdata types were aggregated into a single
+ RRSIG rdataset.
+
+ *create*, a ``bool``. If true, the node will be created if it does
+ not exist.
+
+ Raises ``KeyError`` if the name is not known and create was
+ not specified, or if the name was not a subdomain of the origin.
+
+ Returns a ``dns.rdataset.Rdataset`` or ``None``.
+ """
+
+ try:
+ rdataset = self.find_rdataset(name, rdtype, covers, create)
+ except KeyError:
+ rdataset = None
+ return rdataset
+
+ def delete_rdataset(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ ) -> None:
+ """Delete the rdataset matching *rdtype* and *covers*, if it
+ exists at the node specified by *name*.
+
+ It is not an error if the node does not exist, or if there is no matching
+ rdataset at the node.
+
+ If the node has no rdatasets after the deletion, it will itself be deleted.
+
+ *name*: the name of the node to find. The value may be a ``dns.name.Name`` or a
+ ``str``. If absolute, the name must be a subdomain of the zone's origin. If
+ ``zone.relativize`` is ``True``, then the name will be relativized.
+
+ *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired.
+
+ *covers*, a ``dns.rdatatype.RdataType`` or ``str`` or ``None``, the covered
+ type. Usually this value is ``dns.rdatatype.NONE``, but if the rdtype is
+ ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, then the covers value will be
+ the rdata type the SIG/RRSIG covers. The library treats the SIG and RRSIG types
+ as if they were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This
+ makes RRSIGs much easier to work with than if RRSIGs covering different rdata
+ types were aggregated into a single RRSIG rdataset.
+ """
+
+ name = self._validate_name(name)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ covers = dns.rdatatype.RdataType.make(covers)
+ node = self.get_node(name)
+ if node is not None:
+ node.delete_rdataset(self.rdclass, rdtype, covers)
+ if len(node) == 0:
+ self.delete_node(name)
+
+ def replace_rdataset(
+ self, name: Union[dns.name.Name, str], replacement: dns.rdataset.Rdataset
+ ) -> None:
+ """Replace an rdataset at name.
+
+ It is not an error if there is no rdataset matching I{replacement}.
+
+ Ownership of the *replacement* object is transferred to the zone;
+ in other words, this method does not store a copy of *replacement*
+ at the node, it stores *replacement* itself.
+
+ If the node does not exist, it is created.
+
+ *name*: the name of the node to find.
+ The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
+ name must be a subdomain of the zone's origin. If ``zone.relativize``
+ is ``True``, then the name will be relativized.
+
+ *replacement*, a ``dns.rdataset.Rdataset``, the replacement rdataset.
+ """
+
+ if replacement.rdclass != self.rdclass:
+ raise ValueError("replacement.rdclass != zone.rdclass")
+ node = self.find_node(name, True)
+ node.replace_rdataset(replacement)
+
+ def find_rrset(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ ) -> dns.rrset.RRset:
+ """Look for an rdataset with the specified name and type in the zone,
+ and return an RRset encapsulating it.
+
+ This method is less efficient than the similar
+ ``find_rdataset()`` because it creates an RRset instead of
+ returning the matching rdataset. It may be more convenient
+ for some uses since it returns an object which binds the owner
+ name to the rdataset.
+
+ This method may not be used to create new nodes or rdatasets;
+ use ``find_rdataset`` instead.
+
+ *name*: the name of the node to find.
+ The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
+ name must be a subdomain of the zone's origin. If ``zone.relativize``
+ is ``True``, then the name will be relativized.
+
+ *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired.
+
+ *covers*, a ``dns.rdatatype.RdataType`` or ``str``, the covered type.
+ Usually this value is ``dns.rdatatype.NONE``, but if the
+ rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
+ then the covers value will be the rdata type the SIG/RRSIG
+ covers. The library treats the SIG and RRSIG types as if they
+ were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
+ This makes RRSIGs much easier to work with than if RRSIGs
+ covering different rdata types were aggregated into a single
+ RRSIG rdataset.
+
+ *create*, a ``bool``. If true, the node will be created if it does
+ not exist.
+
+ Raises ``KeyError`` if the name is not known and create was
+ not specified, or if the name was not a subdomain of the origin.
+
+ Returns a ``dns.rrset.RRset`` or ``None``.
+ """
+
+ vname = self._validate_name(name)
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ covers = dns.rdatatype.RdataType.make(covers)
+ rdataset = self.nodes[vname].find_rdataset(self.rdclass, rdtype, covers)
+ rrset = dns.rrset.RRset(vname, self.rdclass, rdtype, covers)
+ rrset.update(rdataset)
+ return rrset
+
+ def get_rrset(
+ self,
+ name: Union[dns.name.Name, str],
+ rdtype: Union[dns.rdatatype.RdataType, str],
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ ) -> Optional[dns.rrset.RRset]:
+ """Look for an rdataset with the specified name and type in the zone,
+ and return an RRset encapsulating it.
+
+ This method is less efficient than the similar ``get_rdataset()``
+ because it creates an RRset instead of returning the matching
+ rdataset. It may be more convenient for some uses since it
+ returns an object which binds the owner name to the rdataset.
+
+ This method may not be used to create new nodes or rdatasets;
+ use ``get_rdataset()`` instead.
+
+ *name*: the name of the node to find.
+ The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
+ name must be a subdomain of the zone's origin. If ``zone.relativize``
+ is ``True``, then the name will be relativized.
+
+ *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired.
+
+ *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type.
+ Usually this value is ``dns.rdatatype.NONE``, but if the
+ rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
+ then the covers value will be the rdata type the SIG/RRSIG
+ covers. The library treats the SIG and RRSIG types as if they
+ were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
+ This makes RRSIGs much easier to work with than if RRSIGs
+ covering different rdata types were aggregated into a single
+ RRSIG rdataset.
+
+ *create*, a ``bool``. If true, the node will be created if it does
+ not exist.
+
+ Returns a ``dns.rrset.RRset`` or ``None``.
+ """
+
+ try:
+ rrset = self.find_rrset(name, rdtype, covers)
+ except KeyError:
+ rrset = None
+ return rrset
+
+ def iterate_rdatasets(
+ self,
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.ANY,
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ ) -> Iterator[Tuple[dns.name.Name, dns.rdataset.Rdataset]]:
+ """Return a generator which yields (name, rdataset) tuples for
+ all rdatasets in the zone which have the specified *rdtype*
+ and *covers*. If *rdtype* is ``dns.rdatatype.ANY``, the default,
+ then all rdatasets will be matched.
+
+ *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired.
+
+ *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type.
+ Usually this value is ``dns.rdatatype.NONE``, but if the
+ rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
+ then the covers value will be the rdata type the SIG/RRSIG
+ covers. The library treats the SIG and RRSIG types as if they
+ were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
+ This makes RRSIGs much easier to work with than if RRSIGs
+ covering different rdata types were aggregated into a single
+ RRSIG rdataset.
+ """
+
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ covers = dns.rdatatype.RdataType.make(covers)
+ for name, node in self.items():
+ for rds in node:
+ if rdtype == dns.rdatatype.ANY or (
+ rds.rdtype == rdtype and rds.covers == covers
+ ):
+ yield (name, rds)
+
+ def iterate_rdatas(
+ self,
+ rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.ANY,
+ covers: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.NONE,
+ ) -> Iterator[Tuple[dns.name.Name, int, dns.rdata.Rdata]]:
+ """Return a generator which yields (name, ttl, rdata) tuples for
+ all rdatas in the zone which have the specified *rdtype*
+ and *covers*. If *rdtype* is ``dns.rdatatype.ANY``, the default,
+ then all rdatas will be matched.
+
+ *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired.
+
+ *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type.
+ Usually this value is ``dns.rdatatype.NONE``, but if the
+ rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
+ then the covers value will be the rdata type the SIG/RRSIG
+ covers. The library treats the SIG and RRSIG types as if they
+ were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
+ This makes RRSIGs much easier to work with than if RRSIGs
+ covering different rdata types were aggregated into a single
+ RRSIG rdataset.
+ """
+
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ covers = dns.rdatatype.RdataType.make(covers)
+ for name, node in self.items():
+ for rds in node:
+ if rdtype == dns.rdatatype.ANY or (
+ rds.rdtype == rdtype and rds.covers == covers
+ ):
+ for rdata in rds:
+ yield (name, rds.ttl, rdata)
+
+ def to_file(
+ self,
+ f: Any,
+ sorted: bool = True,
+ relativize: bool = True,
+ nl: Optional[str] = None,
+ want_comments: bool = False,
+ want_origin: bool = False,
+ ) -> None:
+ """Write a zone to a file.
+
+ *f*, a file or `str`. If *f* is a string, it is treated
+ as the name of a file to open.
+
+ *sorted*, a ``bool``. If True, the default, then the file
+ will be written with the names sorted in DNSSEC order from
+ least to greatest. Otherwise the names will be written in
+ whatever order they happen to have in the zone's dictionary.
+
+ *relativize*, a ``bool``. If True, the default, then domain
+ names in the output will be relativized to the zone's origin
+ if possible.
+
+ *nl*, a ``str`` or None. The end of line string. If not
+ ``None``, the output will use the platform's native
+ end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
+
+ *want_comments*, a ``bool``. If ``True``, emit end-of-line comments
+ as part of writing the file. If ``False``, the default, do not
+ emit them.
+
+ *want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at
+ the start of the file. If ``False``, the default, do not emit
+ one.
+ """
+
+ if isinstance(f, str):
+ cm: contextlib.AbstractContextManager = open(f, "wb")
+ else:
+ cm = contextlib.nullcontext(f)
+ with cm as f:
+ # must be in this way, f.encoding may contain None, or even
+ # attribute may not be there
+ file_enc = getattr(f, "encoding", None)
+ if file_enc is None:
+ file_enc = "utf-8"
+
+ if nl is None:
+ # binary mode, '\n' is not enough
+ nl_b = os.linesep.encode(file_enc)
+ nl = "\n"
+ elif isinstance(nl, str):
+ nl_b = nl.encode(file_enc)
+ else:
+ nl_b = nl
+ nl = nl.decode()
+
+ if want_origin:
+ assert self.origin is not None
+ l = "$ORIGIN " + self.origin.to_text()
+ l_b = l.encode(file_enc)
+ try:
+ f.write(l_b)
+ f.write(nl_b)
+ except TypeError: # textual mode
+ f.write(l)
+ f.write(nl)
+
+ if sorted:
+ names = list(self.keys())
+ names.sort()
+ else:
+ names = self.keys()
+ for n in names:
+ l = self[n].to_text(
+ n,
+ origin=self.origin,
+ relativize=relativize,
+ want_comments=want_comments,
+ )
+ l_b = l.encode(file_enc)
+
+ try:
+ f.write(l_b)
+ f.write(nl_b)
+ except TypeError: # textual mode
+ f.write(l)
+ f.write(nl)
+
+ def to_text(
+ self,
+ sorted: bool = True,
+ relativize: bool = True,
+ nl: Optional[str] = None,
+ want_comments: bool = False,
+ want_origin: bool = False,
+ ) -> str:
+ """Return a zone's text as though it were written to a file.
+
+ *sorted*, a ``bool``. If True, the default, then the file
+ will be written with the names sorted in DNSSEC order from
+ least to greatest. Otherwise the names will be written in
+ whatever order they happen to have in the zone's dictionary.
+
+ *relativize*, a ``bool``. If True, the default, then domain
+ names in the output will be relativized to the zone's origin
+ if possible.
+
+ *nl*, a ``str`` or None. The end of line string. If not
+ ``None``, the output will use the platform's native
+ end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
+
+ *want_comments*, a ``bool``. If ``True``, emit end-of-line comments
+ as part of writing the file. If ``False``, the default, do not
+ emit them.
+
+ *want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at
+ the start of the output. If ``False``, the default, do not emit
+ one.
+
+ Returns a ``str``.
+ """
+ temp_buffer = io.StringIO()
+ self.to_file(temp_buffer, sorted, relativize, nl, want_comments, want_origin)
+ return_value = temp_buffer.getvalue()
+ temp_buffer.close()
+ return return_value
+
+ def check_origin(self) -> None:
+ """Do some simple checking of the zone's origin.
+
+ Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
+
+ Raises ``dns.zone.NoNS`` if there is no NS RRset.
+
+ Raises ``KeyError`` if there is no origin node.
+ """
+ if self.relativize:
+ name = dns.name.empty
+ else:
+ assert self.origin is not None
+ name = self.origin
+ if self.get_rdataset(name, dns.rdatatype.SOA) is None:
+ raise NoSOA
+ if self.get_rdataset(name, dns.rdatatype.NS) is None:
+ raise NoNS
+
+ def get_soa(
+ self, txn: Optional[dns.transaction.Transaction] = None
+ ) -> dns.rdtypes.ANY.SOA.SOA:
+ """Get the zone SOA rdata.
+
+ Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
+
+ Returns a ``dns.rdtypes.ANY.SOA.SOA`` Rdata.
+ """
+ if self.relativize:
+ origin_name = dns.name.empty
+ else:
+ if self.origin is None:
+ # get_soa() has been called very early, and there must not be
+ # an SOA if there is no origin.
+ raise NoSOA
+ origin_name = self.origin
+ soa: Optional[dns.rdataset.Rdataset]
+ if txn:
+ soa = txn.get(origin_name, dns.rdatatype.SOA)
+ else:
+ soa = self.get_rdataset(origin_name, dns.rdatatype.SOA)
+ if soa is None:
+ raise NoSOA
+ return soa[0]
+
+ def _compute_digest(
+ self,
+ hash_algorithm: DigestHashAlgorithm,
+ scheme: DigestScheme = DigestScheme.SIMPLE,
+ ) -> bytes:
+ hashinfo = _digest_hashers.get(hash_algorithm)
+ if not hashinfo:
+ raise UnsupportedDigestHashAlgorithm
+ if scheme != DigestScheme.SIMPLE:
+ raise UnsupportedDigestScheme
+
+ if self.relativize:
+ origin_name = dns.name.empty
+ else:
+ assert self.origin is not None
+ origin_name = self.origin
+ hasher = hashinfo()
+ for name, node in sorted(self.items()):
+ rrnamebuf = name.to_digestable(self.origin)
+ for rdataset in sorted(node, key=lambda rds: (rds.rdtype, rds.covers)):
+ if name == origin_name and dns.rdatatype.ZONEMD in (
+ rdataset.rdtype,
+ rdataset.covers,
+ ):
+ continue
+ rrfixed = struct.pack(
+ "!HHI", rdataset.rdtype, rdataset.rdclass, rdataset.ttl
+ )
+ rdatas = [rdata.to_digestable(self.origin) for rdata in rdataset]
+ for rdata in sorted(rdatas):
+ rrlen = struct.pack("!H", len(rdata))
+ hasher.update(rrnamebuf + rrfixed + rrlen + rdata)
+ return hasher.digest()
+
+ def compute_digest(
+ self,
+ hash_algorithm: DigestHashAlgorithm,
+ scheme: DigestScheme = DigestScheme.SIMPLE,
+ ) -> dns.rdtypes.ANY.ZONEMD.ZONEMD:
+ serial = self.get_soa().serial
+ digest = self._compute_digest(hash_algorithm, scheme)
+ return dns.rdtypes.ANY.ZONEMD.ZONEMD(
+ self.rdclass, dns.rdatatype.ZONEMD, serial, scheme, hash_algorithm, digest
+ )
+
+ def verify_digest(
+ self, zonemd: Optional[dns.rdtypes.ANY.ZONEMD.ZONEMD] = None
+ ) -> None:
+ digests: Union[dns.rdataset.Rdataset, List[dns.rdtypes.ANY.ZONEMD.ZONEMD]]
+ if zonemd:
+ digests = [zonemd]
+ else:
+ assert self.origin is not None
+ rds = self.get_rdataset(self.origin, dns.rdatatype.ZONEMD)
+ if rds is None:
+ raise NoDigest
+ digests = rds
+ for digest in digests:
+ try:
+ computed = self._compute_digest(digest.hash_algorithm, digest.scheme)
+ if computed == digest.digest:
+ return
+ except Exception:
+ pass
+ raise DigestVerificationFailure
+
+ # TransactionManager methods
+
+ def reader(self) -> "Transaction":
+ return Transaction(self, False, Version(self, 1, self.nodes, self.origin))
+
+ def writer(self, replacement: bool = False) -> "Transaction":
+ txn = Transaction(self, replacement)
+ txn._setup_version()
+ return txn
+
+ def origin_information(
+ self,
+ ) -> Tuple[Optional[dns.name.Name], bool, Optional[dns.name.Name]]:
+ effective: Optional[dns.name.Name]
+ if self.relativize:
+ effective = dns.name.empty
+ else:
+ effective = self.origin
+ return (self.origin, self.relativize, effective)
+
+ def get_class(self):
+ return self.rdclass
+
+ # Transaction methods
+
+ def _end_read(self, txn):
+ pass
+
+ def _end_write(self, txn):
+ pass
+
+ def _commit_version(self, _, version, origin):
+ self.nodes = version.nodes
+ if self.origin is None:
+ self.origin = origin
+
+ def _get_next_version_id(self):
+ # Versions are ephemeral and all have id 1
+ return 1
+
+
+# These classes used to be in dns.versioned, but have moved here so we can use
+# the copy-on-write transaction mechanism for both kinds of zones. In a
+# regular zone, the version only exists during the transaction, and the nodes
+# are regular dns.node.Nodes.
+
+# A node with a version id.
+
+
+class VersionedNode(dns.node.Node): # lgtm[py/missing-equals]
+ __slots__ = ["id"]
+
+ def __init__(self):
+ super().__init__()
+ # A proper id will get set by the Version
+ self.id = 0
+
+
+@dns.immutable.immutable
+class ImmutableVersionedNode(VersionedNode):
+ def __init__(self, node):
+ super().__init__()
+ self.id = node.id
+ self.rdatasets = tuple(
+ [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets]
+ )
+
+ def find_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> dns.rdataset.Rdataset:
+ if create:
+ raise TypeError("immutable")
+ return super().find_rdataset(rdclass, rdtype, covers, False)
+
+ def get_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ create: bool = False,
+ ) -> Optional[dns.rdataset.Rdataset]:
+ if create:
+ raise TypeError("immutable")
+ return super().get_rdataset(rdclass, rdtype, covers, False)
+
+ def delete_rdataset(
+ self,
+ rdclass: dns.rdataclass.RdataClass,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
+ ) -> None:
+ raise TypeError("immutable")
+
+ def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None:
+ raise TypeError("immutable")
+
+ def is_immutable(self) -> bool:
+ return True
+
+
+class Version:
+ def __init__(
+ self,
+ zone: Zone,
+ id: int,
+ nodes: Optional[MutableMapping[dns.name.Name, dns.node.Node]] = None,
+ origin: Optional[dns.name.Name] = None,
+ ):
+ self.zone = zone
+ self.id = id
+ if nodes is not None:
+ self.nodes = nodes
+ else:
+ self.nodes = zone.map_factory()
+ self.origin = origin
+
+ def _validate_name(self, name: dns.name.Name) -> dns.name.Name:
+ return _validate_name(name, self.origin, self.zone.relativize)
+
+ def get_node(self, name: dns.name.Name) -> Optional[dns.node.Node]:
+ name = self._validate_name(name)
+ return self.nodes.get(name)
+
+ def get_rdataset(
+ self,
+ name: dns.name.Name,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType,
+ ) -> Optional[dns.rdataset.Rdataset]:
+ node = self.get_node(name)
+ if node is None:
+ return None
+ return node.get_rdataset(self.zone.rdclass, rdtype, covers)
+
+ def keys(self):
+ return self.nodes.keys()
+
+ def items(self):
+ return self.nodes.items()
+
+
+class WritableVersion(Version):
+ def __init__(self, zone: Zone, replacement: bool = False):
+ # The zone._versions_lock must be held by our caller in a versioned
+ # zone.
+ id = zone._get_next_version_id()
+ super().__init__(zone, id)
+ if not replacement:
+ # We copy the map, because that gives us a simple and thread-safe
+ # way of doing versions, and we have a garbage collector to help
+ # us. We only make new node objects if we actually change the
+ # node.
+ self.nodes.update(zone.nodes)
+ # We have to copy the zone origin as it may be None in the first
+ # version, and we don't want to mutate the zone until we commit.
+ self.origin = zone.origin
+ self.changed: Set[dns.name.Name] = set()
+
+ def _maybe_cow(self, name: dns.name.Name) -> dns.node.Node:
+ name = self._validate_name(name)
+ node = self.nodes.get(name)
+ if node is None or name not in self.changed:
+ new_node = self.zone.node_factory()
+ if hasattr(new_node, "id"):
+ # We keep doing this for backwards compatibility, as earlier
+ # code used new_node.id != self.id for the "do we need to CoW?"
+ # test. Now we use the changed set as this works with both
+ # regular zones and versioned zones.
+ #
+ # We ignore the mypy error as this is safe but it doesn't see it.
+ new_node.id = self.id # type: ignore
+ if node is not None:
+ # moo! copy on write!
+ new_node.rdatasets.extend(node.rdatasets)
+ self.nodes[name] = new_node
+ self.changed.add(name)
+ return new_node
+ else:
+ return node
+
+ def delete_node(self, name: dns.name.Name) -> None:
+ name = self._validate_name(name)
+ if name in self.nodes:
+ del self.nodes[name]
+ self.changed.add(name)
+
+ def put_rdataset(
+ self, name: dns.name.Name, rdataset: dns.rdataset.Rdataset
+ ) -> None:
+ node = self._maybe_cow(name)
+ node.replace_rdataset(rdataset)
+
+ def delete_rdataset(
+ self,
+ name: dns.name.Name,
+ rdtype: dns.rdatatype.RdataType,
+ covers: dns.rdatatype.RdataType,
+ ) -> None:
+ node = self._maybe_cow(name)
+ node.delete_rdataset(self.zone.rdclass, rdtype, covers)
+ if len(node) == 0:
+ del self.nodes[name]
+
+
+@dns.immutable.immutable
+class ImmutableVersion(Version):
+ def __init__(self, version: WritableVersion):
+ # We tell super() that it's a replacement as we don't want it
+ # to copy the nodes, as we're about to do that with an
+ # immutable Dict.
+ super().__init__(version.zone, True)
+ # set the right id!
+ self.id = version.id
+ # keep the origin
+ self.origin = version.origin
+ # Make changed nodes immutable
+ for name in version.changed:
+ node = version.nodes.get(name)
+ # it might not exist if we deleted it in the version
+ if node:
+ version.nodes[name] = ImmutableVersionedNode(node)
+ # We're changing the type of the nodes dictionary here on purpose, so
+ # we ignore the mypy error.
+ self.nodes = dns.immutable.Dict(
+ version.nodes, True, self.zone.map_factory
+ ) # type: ignore
+
+
+class Transaction(dns.transaction.Transaction):
+ def __init__(self, zone, replacement, version=None, make_immutable=False):
+ read_only = version is not None
+ super().__init__(zone, replacement, read_only)
+ self.version = version
+ self.make_immutable = make_immutable
+
+ @property
+ def zone(self):
+ return self.manager
+
+ def _setup_version(self):
+ assert self.version is None
+ factory = self.manager.writable_version_factory
+ if factory is None:
+ factory = WritableVersion
+ self.version = factory(self.zone, self.replacement)
+
+ def _get_rdataset(self, name, rdtype, covers):
+ return self.version.get_rdataset(name, rdtype, covers)
+
+ def _put_rdataset(self, name, rdataset):
+ assert not self.read_only
+ self.version.put_rdataset(name, rdataset)
+
+ def _delete_name(self, name):
+ assert not self.read_only
+ self.version.delete_node(name)
+
+ def _delete_rdataset(self, name, rdtype, covers):
+ assert not self.read_only
+ self.version.delete_rdataset(name, rdtype, covers)
+
+ def _name_exists(self, name):
+ return self.version.get_node(name) is not None
+
+ def _changed(self):
+ if self.read_only:
+ return False
+ else:
+ return len(self.version.changed) > 0
+
+ def _end_transaction(self, commit):
+ if self.read_only:
+ self.zone._end_read(self)
+ elif commit and len(self.version.changed) > 0:
+ if self.make_immutable:
+ factory = self.manager.immutable_version_factory
+ if factory is None:
+ factory = ImmutableVersion
+ version = factory(self.version)
+ else:
+ version = self.version
+ self.zone._commit_version(self, version, self.version.origin)
+ else:
+ # rollback
+ self.zone._end_write(self)
+
+ def _set_origin(self, origin):
+ if self.version.origin is None:
+ self.version.origin = origin
+
+ def _iterate_rdatasets(self):
+ for name, node in self.version.items():
+ for rdataset in node:
+ yield (name, rdataset)
+
+ def _iterate_names(self):
+ return self.version.keys()
+
+ def _get_node(self, name):
+ return self.version.get_node(name)
+
+ def _origin_information(self):
+ (absolute, relativize, effective) = self.manager.origin_information()
+ if absolute is None and self.version.origin is not None:
+ # No origin has been committed yet, but we've learned one as part of
+ # this txn. Use it.
+ absolute = self.version.origin
+ if relativize:
+ effective = dns.name.empty
+ else:
+ effective = absolute
+ return (absolute, relativize, effective)
+
+
+def _from_text(
+ text: Any,
+ origin: Optional[Union[dns.name.Name, str]] = None,
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+ relativize: bool = True,
+ zone_factory: Any = Zone,
+ filename: Optional[str] = None,
+ allow_include: bool = False,
+ check_origin: bool = True,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ allow_directives: Union[bool, Iterable[str]] = True,
+) -> Zone:
+ # See the comments for the public APIs from_text() and from_file() for
+ # details.
+
+ # 'text' can also be a file, but we don't publish that fact
+ # since it's an implementation detail. The official file
+ # interface is from_file().
+
+ if filename is None:
+ filename = "<string>"
+ zone = zone_factory(origin, rdclass, relativize=relativize)
+ with zone.writer(True) as txn:
+ tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec)
+ reader = dns.zonefile.Reader(
+ tok,
+ rdclass,
+ txn,
+ allow_include=allow_include,
+ allow_directives=allow_directives,
+ )
+ try:
+ reader.read()
+ except dns.zonefile.UnknownOrigin:
+ # for backwards compatibility
+ raise dns.zone.UnknownOrigin
+ # Now that we're done reading, do some basic checking of the zone.
+ if check_origin:
+ zone.check_origin()
+ return zone
+
+
+def from_text(
+ text: str,
+ origin: Optional[Union[dns.name.Name, str]] = None,
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+ relativize: bool = True,
+ zone_factory: Any = Zone,
+ filename: Optional[str] = None,
+ allow_include: bool = False,
+ check_origin: bool = True,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ allow_directives: Union[bool, Iterable[str]] = True,
+) -> Zone:
+ """Build a zone object from a zone file format string.
+
+ *text*, a ``str``, the zone file format input.
+
+ *origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin
+ of the zone; if not specified, the first ``$ORIGIN`` statement in the
+ zone file will determine the origin of the zone.
+
+ *rdclass*, a ``dns.rdataclass.RdataClass``, the zone's rdata class; the default is
+ class IN.
+
+ *relativize*, a ``bool``, determine's whether domain names are
+ relativized to the zone's origin. The default is ``True``.
+
+ *zone_factory*, the zone factory to use or ``None``. If ``None``, then
+ ``dns.zone.Zone`` will be used. The value may be any class or callable
+ that returns a subclass of ``dns.zone.Zone``.
+
+ *filename*, a ``str`` or ``None``, the filename to emit when
+ describing where an error occurred; the default is ``'<string>'``.
+
+ *allow_include*, a ``bool``. If ``True``, the default, then ``$INCLUDE``
+ directives are permitted. If ``False``, then encoutering a ``$INCLUDE``
+ will raise a ``SyntaxError`` exception.
+
+ *check_origin*, a ``bool``. If ``True``, the default, then sanity
+ checks of the origin node will be made by calling the zone's
+ ``check_origin()`` method.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ *allow_directives*, a ``bool`` or an iterable of `str`. If ``True``, the default,
+ then directives are permitted, and the *allow_include* parameter controls whether
+ ``$INCLUDE`` is permitted. If ``False`` or an empty iterable, then no directive
+ processing is done and any directive-like text will be treated as a regular owner
+ name. If a non-empty iterable, then only the listed directives (including the
+ ``$``) are allowed.
+
+ Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
+
+ Raises ``dns.zone.NoNS`` if there is no NS RRset.
+
+ Raises ``KeyError`` if there is no origin node.
+
+ Returns a subclass of ``dns.zone.Zone``.
+ """
+ return _from_text(
+ text,
+ origin,
+ rdclass,
+ relativize,
+ zone_factory,
+ filename,
+ allow_include,
+ check_origin,
+ idna_codec,
+ allow_directives,
+ )
+
+
+def from_file(
+ f: Any,
+ origin: Optional[Union[dns.name.Name, str]] = None,
+ rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
+ relativize: bool = True,
+ zone_factory: Any = Zone,
+ filename: Optional[str] = None,
+ allow_include: bool = True,
+ check_origin: bool = True,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ allow_directives: Union[bool, Iterable[str]] = True,
+) -> Zone:
+ """Read a zone file and build a zone object.
+
+ *f*, a file or ``str``. If *f* is a string, it is treated
+ as the name of a file to open.
+
+ *origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin
+ of the zone; if not specified, the first ``$ORIGIN`` statement in the
+ zone file will determine the origin of the zone.
+
+ *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
+
+ *relativize*, a ``bool``, determine's whether domain names are
+ relativized to the zone's origin. The default is ``True``.
+
+ *zone_factory*, the zone factory to use or ``None``. If ``None``, then
+ ``dns.zone.Zone`` will be used. The value may be any class or callable
+ that returns a subclass of ``dns.zone.Zone``.
+
+ *filename*, a ``str`` or ``None``, the filename to emit when
+ describing where an error occurred; the default is ``'<string>'``.
+
+ *allow_include*, a ``bool``. If ``True``, the default, then ``$INCLUDE``
+ directives are permitted. If ``False``, then encoutering a ``$INCLUDE``
+ will raise a ``SyntaxError`` exception.
+
+ *check_origin*, a ``bool``. If ``True``, the default, then sanity
+ checks of the origin node will be made by calling the zone's
+ ``check_origin()`` method.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used.
+
+ *allow_directives*, a ``bool`` or an iterable of `str`. If ``True``, the default,
+ then directives are permitted, and the *allow_include* parameter controls whether
+ ``$INCLUDE`` is permitted. If ``False`` or an empty iterable, then no directive
+ processing is done and any directive-like text will be treated as a regular owner
+ name. If a non-empty iterable, then only the listed directives (including the
+ ``$``) are allowed.
+
+ Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
+
+ Raises ``dns.zone.NoNS`` if there is no NS RRset.
+
+ Raises ``KeyError`` if there is no origin node.
+
+ Returns a subclass of ``dns.zone.Zone``.
+ """
+
+ if isinstance(f, str):
+ if filename is None:
+ filename = f
+ cm: contextlib.AbstractContextManager = open(f)
+ else:
+ cm = contextlib.nullcontext(f)
+ with cm as f:
+ return _from_text(
+ f,
+ origin,
+ rdclass,
+ relativize,
+ zone_factory,
+ filename,
+ allow_include,
+ check_origin,
+ idna_codec,
+ allow_directives,
+ )
+ assert False # make mypy happy lgtm[py/unreachable-statement]
+
+
+def from_xfr(
+ xfr: Any,
+ zone_factory: Any = Zone,
+ relativize: bool = True,
+ check_origin: bool = True,
+) -> Zone:
+ """Convert the output of a zone transfer generator into a zone object.
+
+ *xfr*, a generator of ``dns.message.Message`` objects, typically
+ ``dns.query.xfr()``.
+
+ *relativize*, a ``bool``, determine's whether domain names are
+ relativized to the zone's origin. The default is ``True``.
+ It is essential that the relativize setting matches the one specified
+ to the generator.
+
+ *check_origin*, a ``bool``. If ``True``, the default, then sanity
+ checks of the origin node will be made by calling the zone's
+ ``check_origin()`` method.
+
+ Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
+
+ Raises ``dns.zone.NoNS`` if there is no NS RRset.
+
+ Raises ``KeyError`` if there is no origin node.
+
+ Raises ``ValueError`` if no messages are yielded by the generator.
+
+ Returns a subclass of ``dns.zone.Zone``.
+ """
+
+ z = None
+ for r in xfr:
+ if z is None:
+ if relativize:
+ origin = r.origin
+ else:
+ origin = r.answer[0].name
+ rdclass = r.answer[0].rdclass
+ z = zone_factory(origin, rdclass, relativize=relativize)
+ for rrset in r.answer:
+ znode = z.nodes.get(rrset.name)
+ if not znode:
+ znode = z.node_factory()
+ z.nodes[rrset.name] = znode
+ zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype, rrset.covers, True)
+ zrds.update_ttl(rrset.ttl)
+ for rd in rrset:
+ zrds.add(rd)
+ if z is None:
+ raise ValueError("empty transfer")
+ if check_origin:
+ z.check_origin()
+ return z
diff --git a/.venv/lib/python3.12/site-packages/dns/zonefile.py b/.venv/lib/python3.12/site-packages/dns/zonefile.py
new file mode 100644
index 00000000..d74510b2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/zonefile.py
@@ -0,0 +1,744 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Zones."""
+
+import re
+import sys
+from typing import Any, Iterable, List, Optional, Set, Tuple, Union
+
+import dns.exception
+import dns.grange
+import dns.name
+import dns.node
+import dns.rdata
+import dns.rdataclass
+import dns.rdatatype
+import dns.rdtypes.ANY.SOA
+import dns.rrset
+import dns.tokenizer
+import dns.transaction
+import dns.ttl
+
+
+class UnknownOrigin(dns.exception.DNSException):
+ """Unknown origin"""
+
+
+class CNAMEAndOtherData(dns.exception.DNSException):
+ """A node has a CNAME and other data"""
+
+
+def _check_cname_and_other_data(txn, name, rdataset):
+ rdataset_kind = dns.node.NodeKind.classify_rdataset(rdataset)
+ node = txn.get_node(name)
+ if node is None:
+ # empty nodes are neutral.
+ return
+ node_kind = node.classify()
+ if (
+ node_kind == dns.node.NodeKind.CNAME
+ and rdataset_kind == dns.node.NodeKind.REGULAR
+ ):
+ raise CNAMEAndOtherData("rdataset type is not compatible with a CNAME node")
+ elif (
+ node_kind == dns.node.NodeKind.REGULAR
+ and rdataset_kind == dns.node.NodeKind.CNAME
+ ):
+ raise CNAMEAndOtherData(
+ "CNAME rdataset is not compatible with a regular data node"
+ )
+ # Otherwise at least one of the node and the rdataset is neutral, so
+ # adding the rdataset is ok
+
+
+SavedStateType = Tuple[
+ dns.tokenizer.Tokenizer,
+ Optional[dns.name.Name], # current_origin
+ Optional[dns.name.Name], # last_name
+ Optional[Any], # current_file
+ int, # last_ttl
+ bool, # last_ttl_known
+ int, # default_ttl
+ bool,
+] # default_ttl_known
+
+
+def _upper_dollarize(s):
+ s = s.upper()
+ if not s.startswith("$"):
+ s = "$" + s
+ return s
+
+
+class Reader:
+ """Read a DNS zone file into a transaction."""
+
+ def __init__(
+ self,
+ tok: dns.tokenizer.Tokenizer,
+ rdclass: dns.rdataclass.RdataClass,
+ txn: dns.transaction.Transaction,
+ allow_include: bool = False,
+ allow_directives: Union[bool, Iterable[str]] = True,
+ force_name: Optional[dns.name.Name] = None,
+ force_ttl: Optional[int] = None,
+ force_rdclass: Optional[dns.rdataclass.RdataClass] = None,
+ force_rdtype: Optional[dns.rdatatype.RdataType] = None,
+ default_ttl: Optional[int] = None,
+ ):
+ self.tok = tok
+ (self.zone_origin, self.relativize, _) = txn.manager.origin_information()
+ self.current_origin = self.zone_origin
+ self.last_ttl = 0
+ self.last_ttl_known = False
+ if force_ttl is not None:
+ default_ttl = force_ttl
+ if default_ttl is None:
+ self.default_ttl = 0
+ self.default_ttl_known = False
+ else:
+ self.default_ttl = default_ttl
+ self.default_ttl_known = True
+ self.last_name = self.current_origin
+ self.zone_rdclass = rdclass
+ self.txn = txn
+ self.saved_state: List[SavedStateType] = []
+ self.current_file: Optional[Any] = None
+ self.allowed_directives: Set[str]
+ if allow_directives is True:
+ self.allowed_directives = {"$GENERATE", "$ORIGIN", "$TTL"}
+ if allow_include:
+ self.allowed_directives.add("$INCLUDE")
+ elif allow_directives is False:
+ # allow_include was ignored in earlier releases if allow_directives was
+ # False, so we continue that.
+ self.allowed_directives = set()
+ else:
+ # Note that if directives are explicitly specified, then allow_include
+ # is ignored.
+ self.allowed_directives = set(_upper_dollarize(d) for d in allow_directives)
+ self.force_name = force_name
+ self.force_ttl = force_ttl
+ self.force_rdclass = force_rdclass
+ self.force_rdtype = force_rdtype
+ self.txn.check_put_rdataset(_check_cname_and_other_data)
+
+ def _eat_line(self):
+ while 1:
+ token = self.tok.get()
+ if token.is_eol_or_eof():
+ break
+
+ def _get_identifier(self):
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ return token
+
+ def _rr_line(self):
+ """Process one line from a DNS zone file."""
+ token = None
+ # Name
+ if self.force_name is not None:
+ name = self.force_name
+ else:
+ if self.current_origin is None:
+ raise UnknownOrigin
+ token = self.tok.get(want_leading=True)
+ if not token.is_whitespace():
+ self.last_name = self.tok.as_name(token, self.current_origin)
+ else:
+ token = self.tok.get()
+ if token.is_eol_or_eof():
+ # treat leading WS followed by EOL/EOF as if they were EOL/EOF.
+ return
+ self.tok.unget(token)
+ name = self.last_name
+ if not name.is_subdomain(self.zone_origin):
+ self._eat_line()
+ return
+ if self.relativize:
+ name = name.relativize(self.zone_origin)
+
+ # TTL
+ if self.force_ttl is not None:
+ ttl = self.force_ttl
+ self.last_ttl = ttl
+ self.last_ttl_known = True
+ else:
+ token = self._get_identifier()
+ ttl = None
+ try:
+ ttl = dns.ttl.from_text(token.value)
+ self.last_ttl = ttl
+ self.last_ttl_known = True
+ token = None
+ except dns.ttl.BadTTL:
+ self.tok.unget(token)
+
+ # Class
+ if self.force_rdclass is not None:
+ rdclass = self.force_rdclass
+ else:
+ token = self._get_identifier()
+ try:
+ rdclass = dns.rdataclass.from_text(token.value)
+ except dns.exception.SyntaxError:
+ raise
+ except Exception:
+ rdclass = self.zone_rdclass
+ self.tok.unget(token)
+ if rdclass != self.zone_rdclass:
+ raise dns.exception.SyntaxError("RR class is not zone's class")
+
+ if ttl is None:
+ # support for <class> <ttl> <type> syntax
+ token = self._get_identifier()
+ ttl = None
+ try:
+ ttl = dns.ttl.from_text(token.value)
+ self.last_ttl = ttl
+ self.last_ttl_known = True
+ token = None
+ except dns.ttl.BadTTL:
+ if self.default_ttl_known:
+ ttl = self.default_ttl
+ elif self.last_ttl_known:
+ ttl = self.last_ttl
+ self.tok.unget(token)
+
+ # Type
+ if self.force_rdtype is not None:
+ rdtype = self.force_rdtype
+ else:
+ token = self._get_identifier()
+ try:
+ rdtype = dns.rdatatype.from_text(token.value)
+ except Exception:
+ raise dns.exception.SyntaxError(f"unknown rdatatype '{token.value}'")
+
+ try:
+ rd = dns.rdata.from_text(
+ rdclass,
+ rdtype,
+ self.tok,
+ self.current_origin,
+ self.relativize,
+ self.zone_origin,
+ )
+ except dns.exception.SyntaxError:
+ # Catch and reraise.
+ raise
+ except Exception:
+ # All exceptions that occur in the processing of rdata
+ # are treated as syntax errors. This is not strictly
+ # correct, but it is correct almost all of the time.
+ # We convert them to syntax errors so that we can emit
+ # helpful filename:line info.
+ (ty, va) = sys.exc_info()[:2]
+ raise dns.exception.SyntaxError(f"caught exception {str(ty)}: {str(va)}")
+
+ if not self.default_ttl_known and rdtype == dns.rdatatype.SOA:
+ # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default
+ # TTL from the SOA minttl if no $TTL statement is present before the
+ # SOA is parsed.
+ self.default_ttl = rd.minimum
+ self.default_ttl_known = True
+ if ttl is None:
+ # if we didn't have a TTL on the SOA, set it!
+ ttl = rd.minimum
+
+ # TTL check. We had to wait until now to do this as the SOA RR's
+ # own TTL can be inferred from its minimum.
+ if ttl is None:
+ raise dns.exception.SyntaxError("Missing default TTL value")
+
+ self.txn.add(name, ttl, rd)
+
+ def _parse_modify(self, side: str) -> Tuple[str, str, int, int, str]:
+ # Here we catch everything in '{' '}' in a group so we can replace it
+ # with ''.
+ is_generate1 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")
+ is_generate2 = re.compile(r"^.*\$({(\+|-?)(\d+)}).*$")
+ is_generate3 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+)}).*$")
+ # Sometimes there are modifiers in the hostname. These come after
+ # the dollar sign. They are in the form: ${offset[,width[,base]]}.
+ # Make names
+ mod = ""
+ sign = "+"
+ offset = "0"
+ width = "0"
+ base = "d"
+ g1 = is_generate1.match(side)
+ if g1:
+ mod, sign, offset, width, base = g1.groups()
+ if sign == "":
+ sign = "+"
+ else:
+ g2 = is_generate2.match(side)
+ if g2:
+ mod, sign, offset = g2.groups()
+ if sign == "":
+ sign = "+"
+ width = "0"
+ base = "d"
+ else:
+ g3 = is_generate3.match(side)
+ if g3:
+ mod, sign, offset, width = g3.groups()
+ if sign == "":
+ sign = "+"
+ base = "d"
+
+ ioffset = int(offset)
+ iwidth = int(width)
+
+ if sign not in ["+", "-"]:
+ raise dns.exception.SyntaxError(f"invalid offset sign {sign}")
+ if base not in ["d", "o", "x", "X", "n", "N"]:
+ raise dns.exception.SyntaxError(f"invalid type {base}")
+
+ return mod, sign, ioffset, iwidth, base
+
+ def _generate_line(self):
+ # range lhs [ttl] [class] type rhs [ comment ]
+ """Process one line containing the GENERATE statement from a DNS
+ zone file."""
+ if self.current_origin is None:
+ raise UnknownOrigin
+
+ token = self.tok.get()
+ # Range (required)
+ try:
+ start, stop, step = dns.grange.from_text(token.value)
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ except Exception:
+ raise dns.exception.SyntaxError
+
+ # lhs (required)
+ try:
+ lhs = token.value
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ except Exception:
+ raise dns.exception.SyntaxError
+
+ # TTL
+ try:
+ ttl = dns.ttl.from_text(token.value)
+ self.last_ttl = ttl
+ self.last_ttl_known = True
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ except dns.ttl.BadTTL:
+ if not (self.last_ttl_known or self.default_ttl_known):
+ raise dns.exception.SyntaxError("Missing default TTL value")
+ if self.default_ttl_known:
+ ttl = self.default_ttl
+ elif self.last_ttl_known:
+ ttl = self.last_ttl
+ # Class
+ try:
+ rdclass = dns.rdataclass.from_text(token.value)
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ except dns.exception.SyntaxError:
+ raise dns.exception.SyntaxError
+ except Exception:
+ rdclass = self.zone_rdclass
+ if rdclass != self.zone_rdclass:
+ raise dns.exception.SyntaxError("RR class is not zone's class")
+ # Type
+ try:
+ rdtype = dns.rdatatype.from_text(token.value)
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError
+ except Exception:
+ raise dns.exception.SyntaxError(f"unknown rdatatype '{token.value}'")
+
+ # rhs (required)
+ rhs = token.value
+
+ def _calculate_index(counter: int, offset_sign: str, offset: int) -> int:
+ """Calculate the index from the counter and offset."""
+ if offset_sign == "-":
+ offset *= -1
+ return counter + offset
+
+ def _format_index(index: int, base: str, width: int) -> str:
+ """Format the index with the given base, and zero-fill it
+ to the given width."""
+ if base in ["d", "o", "x", "X"]:
+ return format(index, base).zfill(width)
+
+ # base can only be n or N here
+ hexa = _format_index(index, "x", width)
+ nibbles = ".".join(hexa[::-1])[:width]
+ if base == "N":
+ nibbles = nibbles.upper()
+ return nibbles
+
+ lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs)
+ rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs)
+ for i in range(start, stop + 1, step):
+ # +1 because bind is inclusive and python is exclusive
+
+ lindex = _calculate_index(i, lsign, loffset)
+ rindex = _calculate_index(i, rsign, roffset)
+
+ lzfindex = _format_index(lindex, lbase, lwidth)
+ rzfindex = _format_index(rindex, rbase, rwidth)
+
+ name = lhs.replace(f"${lmod}", lzfindex)
+ rdata = rhs.replace(f"${rmod}", rzfindex)
+
+ self.last_name = dns.name.from_text(
+ name, self.current_origin, self.tok.idna_codec
+ )
+ name = self.last_name
+ if not name.is_subdomain(self.zone_origin):
+ self._eat_line()
+ return
+ if self.relativize:
+ name = name.relativize(self.zone_origin)
+
+ try:
+ rd = dns.rdata.from_text(
+ rdclass,
+ rdtype,
+ rdata,
+ self.current_origin,
+ self.relativize,
+ self.zone_origin,
+ )
+ except dns.exception.SyntaxError:
+ # Catch and reraise.
+ raise
+ except Exception:
+ # All exceptions that occur in the processing of rdata
+ # are treated as syntax errors. This is not strictly
+ # correct, but it is correct almost all of the time.
+ # We convert them to syntax errors so that we can emit
+ # helpful filename:line info.
+ (ty, va) = sys.exc_info()[:2]
+ raise dns.exception.SyntaxError(
+ f"caught exception {str(ty)}: {str(va)}"
+ )
+
+ self.txn.add(name, ttl, rd)
+
+ def read(self) -> None:
+ """Read a DNS zone file and build a zone object.
+
+ @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
+ @raises dns.zone.NoNS: No NS RRset was found at the zone origin
+ """
+
+ try:
+ while 1:
+ token = self.tok.get(True, True)
+ if token.is_eof():
+ if self.current_file is not None:
+ self.current_file.close()
+ if len(self.saved_state) > 0:
+ (
+ self.tok,
+ self.current_origin,
+ self.last_name,
+ self.current_file,
+ self.last_ttl,
+ self.last_ttl_known,
+ self.default_ttl,
+ self.default_ttl_known,
+ ) = self.saved_state.pop(-1)
+ continue
+ break
+ elif token.is_eol():
+ continue
+ elif token.is_comment():
+ self.tok.get_eol()
+ continue
+ elif token.value[0] == "$" and len(self.allowed_directives) > 0:
+ # Note that we only run directive processing code if at least
+ # one directive is allowed in order to be backwards compatible
+ c = token.value.upper()
+ if c not in self.allowed_directives:
+ raise dns.exception.SyntaxError(
+ f"zone file directive '{c}' is not allowed"
+ )
+ if c == "$TTL":
+ token = self.tok.get()
+ if not token.is_identifier():
+ raise dns.exception.SyntaxError("bad $TTL")
+ self.default_ttl = dns.ttl.from_text(token.value)
+ self.default_ttl_known = True
+ self.tok.get_eol()
+ elif c == "$ORIGIN":
+ self.current_origin = self.tok.get_name()
+ self.tok.get_eol()
+ if self.zone_origin is None:
+ self.zone_origin = self.current_origin
+ self.txn._set_origin(self.current_origin)
+ elif c == "$INCLUDE":
+ token = self.tok.get()
+ filename = token.value
+ token = self.tok.get()
+ new_origin: Optional[dns.name.Name]
+ if token.is_identifier():
+ new_origin = dns.name.from_text(
+ token.value, self.current_origin, self.tok.idna_codec
+ )
+ self.tok.get_eol()
+ elif not token.is_eol_or_eof():
+ raise dns.exception.SyntaxError("bad origin in $INCLUDE")
+ else:
+ new_origin = self.current_origin
+ self.saved_state.append(
+ (
+ self.tok,
+ self.current_origin,
+ self.last_name,
+ self.current_file,
+ self.last_ttl,
+ self.last_ttl_known,
+ self.default_ttl,
+ self.default_ttl_known,
+ )
+ )
+ self.current_file = open(filename)
+ self.tok = dns.tokenizer.Tokenizer(self.current_file, filename)
+ self.current_origin = new_origin
+ elif c == "$GENERATE":
+ self._generate_line()
+ else:
+ raise dns.exception.SyntaxError(
+ f"Unknown zone file directive '{c}'"
+ )
+ continue
+ self.tok.unget(token)
+ self._rr_line()
+ except dns.exception.SyntaxError as detail:
+ (filename, line_number) = self.tok.where()
+ if detail is None:
+ detail = "syntax error"
+ ex = dns.exception.SyntaxError(
+ "%s:%d: %s" % (filename, line_number, detail)
+ )
+ tb = sys.exc_info()[2]
+ raise ex.with_traceback(tb) from None
+
+
+class RRsetsReaderTransaction(dns.transaction.Transaction):
+ def __init__(self, manager, replacement, read_only):
+ assert not read_only
+ super().__init__(manager, replacement, read_only)
+ self.rdatasets = {}
+
+ def _get_rdataset(self, name, rdtype, covers):
+ return self.rdatasets.get((name, rdtype, covers))
+
+ def _get_node(self, name):
+ rdatasets = []
+ for (rdataset_name, _, _), rdataset in self.rdatasets.items():
+ if name == rdataset_name:
+ rdatasets.append(rdataset)
+ if len(rdatasets) == 0:
+ return None
+ node = dns.node.Node()
+ node.rdatasets = rdatasets
+ return node
+
+ def _put_rdataset(self, name, rdataset):
+ self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = rdataset
+
+ def _delete_name(self, name):
+ # First remove any changes involving the name
+ remove = []
+ for key in self.rdatasets:
+ if key[0] == name:
+ remove.append(key)
+ if len(remove) > 0:
+ for key in remove:
+ del self.rdatasets[key]
+
+ def _delete_rdataset(self, name, rdtype, covers):
+ try:
+ del self.rdatasets[(name, rdtype, covers)]
+ except KeyError:
+ pass
+
+ def _name_exists(self, name):
+ for n, _, _ in self.rdatasets:
+ if n == name:
+ return True
+ return False
+
+ def _changed(self):
+ return len(self.rdatasets) > 0
+
+ def _end_transaction(self, commit):
+ if commit and self._changed():
+ rrsets = []
+ for (name, _, _), rdataset in self.rdatasets.items():
+ rrset = dns.rrset.RRset(
+ name, rdataset.rdclass, rdataset.rdtype, rdataset.covers
+ )
+ rrset.update(rdataset)
+ rrsets.append(rrset)
+ self.manager.set_rrsets(rrsets)
+
+ def _set_origin(self, origin):
+ pass
+
+ def _iterate_rdatasets(self):
+ raise NotImplementedError # pragma: no cover
+
+ def _iterate_names(self):
+ raise NotImplementedError # pragma: no cover
+
+
+class RRSetsReaderManager(dns.transaction.TransactionManager):
+ def __init__(
+ self, origin=dns.name.root, relativize=False, rdclass=dns.rdataclass.IN
+ ):
+ self.origin = origin
+ self.relativize = relativize
+ self.rdclass = rdclass
+ self.rrsets = []
+
+ def reader(self): # pragma: no cover
+ raise NotImplementedError
+
+ def writer(self, replacement=False):
+ assert replacement is True
+ return RRsetsReaderTransaction(self, True, False)
+
+ def get_class(self):
+ return self.rdclass
+
+ def origin_information(self):
+ if self.relativize:
+ effective = dns.name.empty
+ else:
+ effective = self.origin
+ return (self.origin, self.relativize, effective)
+
+ def set_rrsets(self, rrsets):
+ self.rrsets = rrsets
+
+
+def read_rrsets(
+ text: Any,
+ name: Optional[Union[dns.name.Name, str]] = None,
+ ttl: Optional[int] = None,
+ rdclass: Optional[Union[dns.rdataclass.RdataClass, str]] = dns.rdataclass.IN,
+ default_rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
+ rdtype: Optional[Union[dns.rdatatype.RdataType, str]] = None,
+ default_ttl: Optional[Union[int, str]] = None,
+ idna_codec: Optional[dns.name.IDNACodec] = None,
+ origin: Optional[Union[dns.name.Name, str]] = dns.name.root,
+ relativize: bool = False,
+) -> List[dns.rrset.RRset]:
+ """Read one or more rrsets from the specified text, possibly subject
+ to restrictions.
+
+ *text*, a file object or a string, is the input to process.
+
+ *name*, a string, ``dns.name.Name``, or ``None``, is the owner name of
+ the rrset. If not ``None``, then the owner name is "forced", and the
+ input must not specify an owner name. If ``None``, then any owner names
+ are allowed and must be present in the input.
+
+ *ttl*, an ``int``, string, or None. If not ``None``, the the TTL is
+ forced to be the specified value and the input must not specify a TTL.
+ If ``None``, then a TTL may be specified in the input. If it is not
+ specified, then the *default_ttl* will be used.
+
+ *rdclass*, a ``dns.rdataclass.RdataClass``, string, or ``None``. If
+ not ``None``, then the class is forced to the specified value, and the
+ input must not specify a class. If ``None``, then the input may specify
+ a class that matches *default_rdclass*. Note that it is not possible to
+ return rrsets with differing classes; specifying ``None`` for the class
+ simply allows the user to optionally type a class as that may be convenient
+ when cutting and pasting.
+
+ *default_rdclass*, a ``dns.rdataclass.RdataClass`` or string. The class
+ of the returned rrsets.
+
+ *rdtype*, a ``dns.rdatatype.RdataType``, string, or ``None``. If not
+ ``None``, then the type is forced to the specified value, and the
+ input must not specify a type. If ``None``, then a type must be present
+ for each RR.
+
+ *default_ttl*, an ``int``, string, or ``None``. If not ``None``, then if
+ the TTL is not forced and is not specified, then this value will be used.
+ if ``None``, then if the TTL is not forced an error will occur if the TTL
+ is not specified.
+
+ *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+ encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
+ is used. Note that codecs only apply to the owner name; dnspython does
+ not do IDNA for names in rdata, as there is no IDNA zonefile format.
+
+ *origin*, a string, ``dns.name.Name``, or ``None``, is the origin for any
+ relative names in the input, and also the origin to relativize to if
+ *relativize* is ``True``.
+
+ *relativize*, a bool. If ``True``, names are relativized to the *origin*;
+ if ``False`` then any relative names in the input are made absolute by
+ appending the *origin*.
+ """
+ if isinstance(origin, str):
+ origin = dns.name.from_text(origin, dns.name.root, idna_codec)
+ if isinstance(name, str):
+ name = dns.name.from_text(name, origin, idna_codec)
+ if isinstance(ttl, str):
+ ttl = dns.ttl.from_text(ttl)
+ if isinstance(default_ttl, str):
+ default_ttl = dns.ttl.from_text(default_ttl)
+ if rdclass is not None:
+ rdclass = dns.rdataclass.RdataClass.make(rdclass)
+ else:
+ rdclass = None
+ default_rdclass = dns.rdataclass.RdataClass.make(default_rdclass)
+ if rdtype is not None:
+ rdtype = dns.rdatatype.RdataType.make(rdtype)
+ else:
+ rdtype = None
+ manager = RRSetsReaderManager(origin, relativize, default_rdclass)
+ with manager.writer(True) as txn:
+ tok = dns.tokenizer.Tokenizer(text, "<input>", idna_codec=idna_codec)
+ reader = Reader(
+ tok,
+ default_rdclass,
+ txn,
+ allow_directives=False,
+ force_name=name,
+ force_ttl=ttl,
+ force_rdclass=rdclass,
+ force_rdtype=rdtype,
+ default_ttl=default_ttl,
+ )
+ reader.read()
+ return manager.rrsets
diff --git a/.venv/lib/python3.12/site-packages/dns/zonetypes.py b/.venv/lib/python3.12/site-packages/dns/zonetypes.py
new file mode 100644
index 00000000..195ee2ec
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/dns/zonetypes.py
@@ -0,0 +1,37 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+"""Common zone-related types."""
+
+# This is a separate file to avoid import circularity between dns.zone and
+# the implementation of the ZONEMD type.
+
+import hashlib
+
+import dns.enum
+
+
+class DigestScheme(dns.enum.IntEnum):
+ """ZONEMD Scheme"""
+
+ SIMPLE = 1
+
+ @classmethod
+ def _maximum(cls):
+ return 255
+
+
+class DigestHashAlgorithm(dns.enum.IntEnum):
+ """ZONEMD Hash Algorithm"""
+
+ SHA384 = 1
+ SHA512 = 2
+
+ @classmethod
+ def _maximum(cls):
+ return 255
+
+
+_digest_hashers = {
+ DigestHashAlgorithm.SHA384: hashlib.sha384,
+ DigestHashAlgorithm.SHA512: hashlib.sha512,
+}