Загрузить файлы в «venv/Lib/site-packages/dns»
This commit is contained in:
953
venv/Lib/site-packages/dns/asyncquery.py
Normal file
953
venv/Lib/site-packages/dns/asyncquery.py
Normal file
@@ -0,0 +1,953 @@
|
||||
# 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, cast
|
||||
|
||||
import dns.asyncbackend
|
||||
import dns.exception
|
||||
import dns.inet
|
||||
import dns.message
|
||||
import dns.name
|
||||
import dns.quic
|
||||
import dns.rdatatype
|
||||
import dns.transaction
|
||||
import dns.tsig
|
||||
import dns.xfr
|
||||
from dns._asyncbackend import NullContext
|
||||
from dns.query import (
|
||||
BadResponse,
|
||||
HTTPVersion,
|
||||
NoDOH,
|
||||
NoDOQ,
|
||||
UDPMode,
|
||||
_check_status,
|
||||
_compute_times,
|
||||
_matches_destination,
|
||||
_remaining,
|
||||
have_doh,
|
||||
make_ssl_context,
|
||||
)
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
import dns._no_ssl as ssl # type: ignore
|
||||
|
||||
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: dns.message.Message | bytes,
|
||||
destination: Any,
|
||||
expiration: float | None = 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: Any | None = None,
|
||||
expiration: float | None = None,
|
||||
ignore_unexpected: bool = False,
|
||||
one_rr_per_rrset: bool = False,
|
||||
keyring: Dict[dns.name.Name, dns.tsig.Key] | None = None,
|
||||
request_mac: bytes | None = b"",
|
||||
ignore_trailing: bool = False,
|
||||
raise_on_truncation: bool = False,
|
||||
ignore_errors: bool = False,
|
||||
query: dns.message.Message | None = 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: float | None = None,
|
||||
port: int = 53,
|
||||
source: str | None = 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: dns.asyncbackend.DatagramSocket | None = None,
|
||||
backend: dns.asyncbackend.Backend | None = 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) # pyright: ignore
|
||||
(r, received_time, _) = await receive_udp(
|
||||
s, # pyright: ignore
|
||||
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: float | None = None,
|
||||
port: int = 53,
|
||||
source: str | None = None,
|
||||
source_port: int = 0,
|
||||
ignore_unexpected: bool = False,
|
||||
one_rr_per_rrset: bool = False,
|
||||
ignore_trailing: bool = False,
|
||||
udp_sock: dns.asyncbackend.DatagramSocket | None = None,
|
||||
tcp_sock: dns.asyncbackend.StreamSocket | None = None,
|
||||
backend: dns.asyncbackend.Backend | None = 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: dns.message.Message | bytes,
|
||||
expiration: float | None = 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: float | None = None,
|
||||
one_rr_per_rrset: bool = False,
|
||||
keyring: Dict[dns.name.Name, dns.tsig.Key] | None = None,
|
||||
request_mac: bytes | None = 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: float | None = None,
|
||||
port: int = 53,
|
||||
source: str | None = None,
|
||||
source_port: int = 0,
|
||||
one_rr_per_rrset: bool = False,
|
||||
ignore_trailing: bool = False,
|
||||
sock: dns.asyncbackend.StreamSocket | None = None,
|
||||
backend: dns.asyncbackend.Backend | None = 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) # pyright: ignore
|
||||
(r, received_time) = await receive_tcp(
|
||||
s, # pyright: ignore
|
||||
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: float | None = None,
|
||||
port: int = 853,
|
||||
source: str | None = None,
|
||||
source_port: int = 0,
|
||||
one_rr_per_rrset: bool = False,
|
||||
ignore_trailing: bool = False,
|
||||
sock: dns.asyncbackend.StreamSocket | None = None,
|
||||
backend: dns.asyncbackend.Backend | None = None,
|
||||
ssl_context: ssl.SSLContext | None = None,
|
||||
server_hostname: str | None = None,
|
||||
verify: 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_ssl_context(verify, server_hostname is not None, ["dot"])
|
||||
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"], # pyright: ignore
|
||||
) -> "dns.asyncresolver.Resolver": # pyright: ignore
|
||||
# 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: float | None = None,
|
||||
port: int = 443,
|
||||
source: str | None = None,
|
||||
source_port: int = 0, # pylint: disable=W0613
|
||||
one_rr_per_rrset: bool = False,
|
||||
ignore_trailing: bool = False,
|
||||
client: Optional["httpx.AsyncClient|dns.quic.AsyncQuicConnection"] = None,
|
||||
path: str = "/dns-query",
|
||||
post: bool = True,
|
||||
verify: bool | str | ssl.SSLContext = True,
|
||||
bootstrap_address: str | None = None,
|
||||
resolver: Optional["dns.asyncresolver.Resolver"] = None, # pyright: ignore
|
||||
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
|
||||
# we bind url and then override as pyright can't figure out all paths bind.
|
||||
url = where
|
||||
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}"
|
||||
|
||||
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 # pyright: ignore
|
||||
answers = await resolver.resolve_name( # pyright: ignore
|
||||
parsed.hostname, family # pyright: ignore
|
||||
)
|
||||
bootstrap_address = random.choice(list(answers.addresses()))
|
||||
if client and not isinstance(
|
||||
client, dns.quic.AsyncQuicConnection
|
||||
): # pyright: ignore
|
||||
raise ValueError("client parameter must be a dns.quic.AsyncQuicConnection.")
|
||||
assert client is None or isinstance(client, dns.quic.AsyncQuicConnection)
|
||||
return await _http3(
|
||||
q,
|
||||
bootstrap_address,
|
||||
url,
|
||||
timeout,
|
||||
port,
|
||||
source,
|
||||
source_port,
|
||||
one_rr_per_rrset,
|
||||
ignore_trailing,
|
||||
verify=verify,
|
||||
post=post,
|
||||
connection=client,
|
||||
)
|
||||
|
||||
if not have_doh:
|
||||
raise NoDOH # pragma: no cover
|
||||
# pylint: disable=possibly-used-before-assignment
|
||||
if client and not isinstance(client, httpx.AsyncClient): # pyright: ignore
|
||||
raise ValueError("client 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( # pyright: ignore
|
||||
http1=h1, http2=h2, verify=verify, transport=transport # type: ignore
|
||||
)
|
||||
|
||||
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( # pyright: ignore
|
||||
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( # pyright: ignore
|
||||
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: float | None = None,
|
||||
port: int = 443,
|
||||
source: str | None = None,
|
||||
source_port: int = 0,
|
||||
one_rr_per_rrset: bool = False,
|
||||
ignore_trailing: bool = False,
|
||||
verify: bool | str | ssl.SSLContext = True,
|
||||
backend: dns.asyncbackend.Backend | None = None,
|
||||
post: bool = True,
|
||||
connection: dns.quic.AsyncQuicConnection | None = None,
|
||||
) -> 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
|
||||
assert hostname is not None
|
||||
if url_parts.port is not None:
|
||||
port = url_parts.port
|
||||
|
||||
q.id = 0
|
||||
wire = q.to_wire()
|
||||
the_connection: dns.quic.AsyncQuicConnection
|
||||
if connection:
|
||||
cfactory = dns.quic.null_factory
|
||||
mfactory = dns.quic.null_factory
|
||||
else:
|
||||
(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:
|
||||
if connection:
|
||||
the_connection = connection
|
||||
else:
|
||||
the_connection = the_manager.connect( # pyright: ignore
|
||||
where, port, source, source_port
|
||||
)
|
||||
(start, expiration) = _compute_times(timeout)
|
||||
stream = await the_connection.make_stream(timeout) # pyright: ignore
|
||||
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: float | None = None,
|
||||
port: int = 853,
|
||||
source: str | None = None,
|
||||
source_port: int = 0,
|
||||
one_rr_per_rrset: bool = False,
|
||||
ignore_trailing: bool = False,
|
||||
connection: dns.quic.AsyncQuicConnection | None = None,
|
||||
verify: bool | str = True,
|
||||
backend: dns.asyncbackend.Backend | None = None,
|
||||
hostname: str | None = None,
|
||||
server_hostname: str | None = 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( # pyright: ignore
|
||||
where, port, source, source_port
|
||||
)
|
||||
(start, expiration) = _compute_times(timeout)
|
||||
stream = await the_connection.make_stream(timeout) # pyright: ignore
|
||||
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: int | None,
|
||||
timeout: float | None,
|
||||
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
|
||||
r: dns.message.Message | None = 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) # pyright: ignore
|
||||
else:
|
||||
ldata = await _read_exactly(tcp_sock, 2, mexpiration) # pyright: ignore
|
||||
(l,) = struct.unpack("!H", ldata)
|
||||
rwire = await _read_exactly(tcp_sock, l, mexpiration) # pyright: ignore
|
||||
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 r is not None and not r.had_tsig:
|
||||
raise dns.exception.FormError("missing TSIG")
|
||||
|
||||
|
||||
async def inbound_xfr(
|
||||
where: str,
|
||||
txn_manager: dns.transaction.TransactionManager,
|
||||
query: dns.message.Message | None = None,
|
||||
port: int = 53,
|
||||
timeout: float | None = None,
|
||||
lifetime: float | None = None,
|
||||
source: str | None = None,
|
||||
source_port: int = 0,
|
||||
udp_mode: UDPMode = UDPMode.NEVER,
|
||||
backend: dns.asyncbackend.Backend | None = 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( # pyright: ignore
|
||||
txn_manager,
|
||||
s,
|
||||
query,
|
||||
serial,
|
||||
timeout,
|
||||
expiration, # pyright: ignore
|
||||
):
|
||||
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( # pyright: ignore
|
||||
txn_manager, s, query, serial, timeout, expiration # pyright: ignore
|
||||
):
|
||||
pass
|
||||
478
venv/Lib/site-packages/dns/asyncresolver.py
Normal file
478
venv/Lib/site-packages/dns/asyncresolver.py
Normal file
@@ -0,0 +1,478 @@
|
||||
# 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
|
||||
|
||||
import dns._ddr
|
||||
import dns.asyncbackend
|
||||
import dns.asyncquery
|
||||
import dns.exception
|
||||
import dns.inet
|
||||
import dns.name
|
||||
import dns.nameserver
|
||||
import dns.query
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
import dns.resolver # lgtm[py/import-and-import-from]
|
||||
import dns.reversename
|
||||
|
||||
# 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: dns.name.Name | str,
|
||||
rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A,
|
||||
rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN,
|
||||
tcp: bool = False,
|
||||
source: str | None = None,
|
||||
raise_on_no_answer: bool = True,
|
||||
source_port: int = 0,
|
||||
lifetime: float | None = None,
|
||||
search: bool | None = None,
|
||||
backend: dns.asyncbackend.Backend | None = 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: 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: 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: dns.name.Name | str,
|
||||
rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A,
|
||||
rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN,
|
||||
tcp: bool = False,
|
||||
source: str | None = None,
|
||||
raise_on_no_answer: bool = True,
|
||||
source_port: int = 0,
|
||||
lifetime: float | None = None,
|
||||
search: bool | None = None,
|
||||
backend: dns.asyncbackend.Backend | None = 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: 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: 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: dns.name.Name | str,
|
||||
rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
|
||||
tcp: bool = False,
|
||||
resolver: Resolver | None = None,
|
||||
backend: dns.asyncbackend.Backend | None = 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: dns.name.Name | str,
|
||||
port: int = 53,
|
||||
family: int = socket.AF_UNSPEC,
|
||||
resolver: Resolver | None = 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[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 = Resolver(configure=False)
|
||||
res.nameservers = nameservers
|
||||
return res
|
||||
|
||||
|
||||
async def resolve_at(
|
||||
where: dns.name.Name | str,
|
||||
qname: dns.name.Name | str,
|
||||
rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A,
|
||||
rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN,
|
||||
tcp: bool = False,
|
||||
source: str | None = None,
|
||||
raise_on_no_answer: bool = True,
|
||||
source_port: int = 0,
|
||||
lifetime: float | None = None,
|
||||
search: bool | None = None,
|
||||
backend: dns.asyncbackend.Backend | None = None,
|
||||
port: int = 53,
|
||||
family: int = socket.AF_UNSPEC,
|
||||
resolver: Resolver | None = 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,
|
||||
)
|
||||
850
venv/Lib/site-packages/dns/btree.py
Normal file
850
venv/Lib/site-packages/dns/btree.py
Normal file
@@ -0,0 +1,850 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
"""
|
||||
A BTree in the style of Cormen, Leiserson, and Rivest's "Algorithms" book, with
|
||||
copy-on-write node updates, cursors, and optional space optimization for mostly-in-order
|
||||
insertion.
|
||||
"""
|
||||
|
||||
from collections.abc import MutableMapping, MutableSet
|
||||
from typing import Any, Callable, Generic, Optional, Tuple, TypeVar, cast
|
||||
|
||||
DEFAULT_T = 127
|
||||
|
||||
KT = TypeVar("KT") # the type of a key in Element
|
||||
|
||||
|
||||
class Element(Generic[KT]):
|
||||
"""All items stored in the BTree are Elements."""
|
||||
|
||||
def key(self) -> KT:
|
||||
"""The key for this element; the returned type must implement comparison."""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
|
||||
ET = TypeVar("ET", bound=Element) # the type of a value in a _KV
|
||||
|
||||
|
||||
def _MIN(t: int) -> int:
|
||||
"""The minimum number of keys in a non-root node for a BTree with the specified
|
||||
``t``
|
||||
"""
|
||||
return t - 1
|
||||
|
||||
|
||||
def _MAX(t: int) -> int:
|
||||
"""The maximum number of keys in node for a BTree with the specified ``t``"""
|
||||
return 2 * t - 1
|
||||
|
||||
|
||||
class _Creator:
|
||||
"""A _Creator class instance is used as a unique id for the BTree which created
|
||||
a node.
|
||||
|
||||
We use a dedicated creator rather than just a BTree reference to avoid circularity
|
||||
that would complicate GC.
|
||||
"""
|
||||
|
||||
def __str__(self): # pragma: no cover
|
||||
return f"{id(self):x}"
|
||||
|
||||
|
||||
class _Node(Generic[KT, ET]):
|
||||
"""A Node in the BTree.
|
||||
|
||||
A Node (leaf or internal) of the BTree.
|
||||
"""
|
||||
|
||||
__slots__ = ["t", "creator", "is_leaf", "elts", "children"]
|
||||
|
||||
def __init__(self, t: int, creator: _Creator, is_leaf: bool):
|
||||
assert t >= 3
|
||||
self.t = t
|
||||
self.creator = creator
|
||||
self.is_leaf = is_leaf
|
||||
self.elts: list[ET] = []
|
||||
self.children: list[_Node[KT, ET]] = []
|
||||
|
||||
def is_maximal(self) -> bool:
|
||||
"""Does this node have the maximal number of keys?"""
|
||||
assert len(self.elts) <= _MAX(self.t)
|
||||
return len(self.elts) == _MAX(self.t)
|
||||
|
||||
def is_minimal(self) -> bool:
|
||||
"""Does this node have the minimal number of keys?"""
|
||||
assert len(self.elts) >= _MIN(self.t)
|
||||
return len(self.elts) == _MIN(self.t)
|
||||
|
||||
def search_in_node(self, key: KT) -> tuple[int, bool]:
|
||||
"""Get the index of the ``Element`` matching ``key`` or the index of its
|
||||
least successor.
|
||||
|
||||
Returns a tuple of the index and an ``equal`` boolean that is ``True`` iff.
|
||||
the key was found.
|
||||
"""
|
||||
l = len(self.elts)
|
||||
if l > 0 and key > self.elts[l - 1].key():
|
||||
# This is optimizing near in-order insertion.
|
||||
return l, False
|
||||
l = 0
|
||||
i = len(self.elts)
|
||||
r = i - 1
|
||||
equal = False
|
||||
while l <= r:
|
||||
m = (l + r) // 2
|
||||
k = self.elts[m].key()
|
||||
if key == k:
|
||||
i = m
|
||||
equal = True
|
||||
break
|
||||
elif key < k:
|
||||
i = m
|
||||
r = m - 1
|
||||
else:
|
||||
l = m + 1
|
||||
return i, equal
|
||||
|
||||
def maybe_cow_child(self, index: int) -> "_Node[KT, ET]":
|
||||
assert not self.is_leaf
|
||||
child = self.children[index]
|
||||
cloned = child.maybe_cow(self.creator)
|
||||
if cloned:
|
||||
self.children[index] = cloned
|
||||
return cloned
|
||||
else:
|
||||
return child
|
||||
|
||||
def _get_node(self, key: KT) -> Tuple[Optional["_Node[KT, ET]"], int]:
|
||||
"""Get the node associated with key and its index, doing
|
||||
copy-on-write if we have to descend.
|
||||
|
||||
Returns a tuple of the node and the index, or the tuple ``(None, 0)``
|
||||
if the key was not found.
|
||||
"""
|
||||
i, equal = self.search_in_node(key)
|
||||
if equal:
|
||||
return (self, i)
|
||||
elif self.is_leaf:
|
||||
return (None, 0)
|
||||
else:
|
||||
child = self.maybe_cow_child(i)
|
||||
return child._get_node(key)
|
||||
|
||||
def get(self, key: KT) -> ET | None:
|
||||
"""Get the element associated with *key* or return ``None``"""
|
||||
i, equal = self.search_in_node(key)
|
||||
if equal:
|
||||
return self.elts[i]
|
||||
elif self.is_leaf:
|
||||
return None
|
||||
else:
|
||||
return self.children[i].get(key)
|
||||
|
||||
def optimize_in_order_insertion(self, index: int) -> None:
|
||||
"""Try to minimize the number of Nodes in a BTree where the insertion
|
||||
is done in-order or close to it, by stealing as much as we can from our
|
||||
right sibling.
|
||||
|
||||
If we don't do this, then an in-order insertion will produce a BTree
|
||||
where most of the nodes are minimal.
|
||||
"""
|
||||
if index == 0:
|
||||
return
|
||||
left = self.children[index - 1]
|
||||
if len(left.elts) == _MAX(self.t):
|
||||
return
|
||||
left = self.maybe_cow_child(index - 1)
|
||||
while len(left.elts) < _MAX(self.t):
|
||||
if not left.try_right_steal(self, index - 1):
|
||||
break
|
||||
|
||||
def insert_nonfull(self, element: ET, in_order: bool) -> ET | None:
|
||||
assert not self.is_maximal()
|
||||
while True:
|
||||
key = element.key()
|
||||
i, equal = self.search_in_node(key)
|
||||
if equal:
|
||||
# replace
|
||||
old = self.elts[i]
|
||||
self.elts[i] = element
|
||||
return old
|
||||
elif self.is_leaf:
|
||||
self.elts.insert(i, element)
|
||||
return None
|
||||
else:
|
||||
child = self.maybe_cow_child(i)
|
||||
if child.is_maximal():
|
||||
self.adopt(*child.split())
|
||||
# Splitting might result in our target moving to us, so
|
||||
# search again.
|
||||
continue
|
||||
oelt = child.insert_nonfull(element, in_order)
|
||||
if in_order:
|
||||
self.optimize_in_order_insertion(i)
|
||||
return oelt
|
||||
|
||||
def split(self) -> tuple["_Node[KT, ET]", ET, "_Node[KT, ET]"]:
|
||||
"""Split a maximal node into two minimal ones and a central element."""
|
||||
assert self.is_maximal()
|
||||
right = self.__class__(self.t, self.creator, self.is_leaf)
|
||||
right.elts = list(self.elts[_MIN(self.t) + 1 :])
|
||||
middle = self.elts[_MIN(self.t)]
|
||||
self.elts = list(self.elts[: _MIN(self.t)])
|
||||
if not self.is_leaf:
|
||||
right.children = list(self.children[_MIN(self.t) + 1 :])
|
||||
self.children = list(self.children[: _MIN(self.t) + 1])
|
||||
return self, middle, right
|
||||
|
||||
def try_left_steal(self, parent: "_Node[KT, ET]", index: int) -> bool:
|
||||
"""Try to steal from this Node's left sibling for balancing purposes.
|
||||
|
||||
Returns ``True`` if the theft was successful, or ``False`` if not.
|
||||
"""
|
||||
if index != 0:
|
||||
left = parent.children[index - 1]
|
||||
if not left.is_minimal():
|
||||
left = parent.maybe_cow_child(index - 1)
|
||||
elt = parent.elts[index - 1]
|
||||
parent.elts[index - 1] = left.elts.pop()
|
||||
self.elts.insert(0, elt)
|
||||
if not left.is_leaf:
|
||||
assert not self.is_leaf
|
||||
child = left.children.pop()
|
||||
self.children.insert(0, child)
|
||||
return True
|
||||
return False
|
||||
|
||||
def try_right_steal(self, parent: "_Node[KT, ET]", index: int) -> bool:
|
||||
"""Try to steal from this Node's right sibling for balancing purposes.
|
||||
|
||||
Returns ``True`` if the theft was successful, or ``False`` if not.
|
||||
"""
|
||||
if index + 1 < len(parent.children):
|
||||
right = parent.children[index + 1]
|
||||
if not right.is_minimal():
|
||||
right = parent.maybe_cow_child(index + 1)
|
||||
elt = parent.elts[index]
|
||||
parent.elts[index] = right.elts.pop(0)
|
||||
self.elts.append(elt)
|
||||
if not right.is_leaf:
|
||||
assert not self.is_leaf
|
||||
child = right.children.pop(0)
|
||||
self.children.append(child)
|
||||
return True
|
||||
return False
|
||||
|
||||
def adopt(self, left: "_Node[KT, ET]", middle: ET, right: "_Node[KT, ET]") -> None:
|
||||
"""Adopt left, middle, and right into our Node (which must not be maximal,
|
||||
and which must not be a leaf). In the case were we are not the new root,
|
||||
then the left child must already be in the Node."""
|
||||
assert not self.is_maximal()
|
||||
assert not self.is_leaf
|
||||
key = middle.key()
|
||||
i, equal = self.search_in_node(key)
|
||||
assert not equal
|
||||
self.elts.insert(i, middle)
|
||||
if len(self.children) == 0:
|
||||
# We are the new root
|
||||
self.children = [left, right]
|
||||
else:
|
||||
assert self.children[i] == left
|
||||
self.children.insert(i + 1, right)
|
||||
|
||||
def merge(self, parent: "_Node[KT, ET]", index: int) -> None:
|
||||
"""Merge this node's parent and its right sibling into this node."""
|
||||
right = parent.children.pop(index + 1)
|
||||
self.elts.append(parent.elts.pop(index))
|
||||
self.elts.extend(right.elts)
|
||||
if not self.is_leaf:
|
||||
self.children.extend(right.children)
|
||||
|
||||
def minimum(self) -> ET:
|
||||
"""The least element in this subtree."""
|
||||
if self.is_leaf:
|
||||
return self.elts[0]
|
||||
else:
|
||||
return self.children[0].minimum()
|
||||
|
||||
def maximum(self) -> ET:
|
||||
"""The greatest element in this subtree."""
|
||||
if self.is_leaf:
|
||||
return self.elts[-1]
|
||||
else:
|
||||
return self.children[-1].maximum()
|
||||
|
||||
def balance(self, parent: "_Node[KT, ET]", index: int) -> None:
|
||||
"""This Node is minimal, and we want to make it non-minimal so we can delete.
|
||||
We try to steal from our siblings, and if that doesn't work we will merge
|
||||
with one of them."""
|
||||
assert not parent.is_leaf
|
||||
if self.try_left_steal(parent, index):
|
||||
return
|
||||
if self.try_right_steal(parent, index):
|
||||
return
|
||||
# Stealing didn't work, so both siblings must be minimal.
|
||||
if index == 0:
|
||||
# We are the left-most node so merge with our right sibling.
|
||||
self.merge(parent, index)
|
||||
else:
|
||||
# Have our left sibling merge with us. This lets us only have "merge right"
|
||||
# code.
|
||||
left = parent.maybe_cow_child(index - 1)
|
||||
left.merge(parent, index - 1)
|
||||
|
||||
def delete(
|
||||
self, key: KT, parent: Optional["_Node[KT, ET]"], exact: ET | None
|
||||
) -> ET | None:
|
||||
"""Delete an element matching *key* if it exists. If *exact* is not ``None``
|
||||
then it must be an exact match with that element. The Node must not be
|
||||
minimal unless it is the root."""
|
||||
assert parent is None or not self.is_minimal()
|
||||
i, equal = self.search_in_node(key)
|
||||
original_key = None
|
||||
if equal:
|
||||
# Note we use "is" here as we meant "exactly this object".
|
||||
if exact is not None and self.elts[i] is not exact:
|
||||
raise ValueError("exact delete did not match existing elt")
|
||||
if self.is_leaf:
|
||||
return self.elts.pop(i)
|
||||
# Note we need to ensure exact is None going forward as we've
|
||||
# already checked exactness and are about to change our target key
|
||||
# to the least successor.
|
||||
exact = None
|
||||
original_key = key
|
||||
least_successor = self.children[i + 1].minimum()
|
||||
key = least_successor.key()
|
||||
i = i + 1
|
||||
if self.is_leaf:
|
||||
# No match
|
||||
if exact is not None:
|
||||
raise ValueError("exact delete had no match")
|
||||
return None
|
||||
# recursively delete in the appropriate child
|
||||
child = self.maybe_cow_child(i)
|
||||
if child.is_minimal():
|
||||
child.balance(self, i)
|
||||
# Things may have moved.
|
||||
i, equal = self.search_in_node(key)
|
||||
assert not equal
|
||||
child = self.children[i]
|
||||
assert not child.is_minimal()
|
||||
elt = child.delete(key, self, exact)
|
||||
if original_key is not None:
|
||||
node, i = self._get_node(original_key)
|
||||
assert node is not None
|
||||
assert elt is not None
|
||||
oelt = node.elts[i]
|
||||
node.elts[i] = elt
|
||||
elt = oelt
|
||||
return elt
|
||||
|
||||
def visit_in_order(self, visit: Callable[[ET], None]) -> None:
|
||||
"""Call *visit* on all of the elements in order."""
|
||||
for i, elt in enumerate(self.elts):
|
||||
if not self.is_leaf:
|
||||
self.children[i].visit_in_order(visit)
|
||||
visit(elt)
|
||||
if not self.is_leaf:
|
||||
self.children[-1].visit_in_order(visit)
|
||||
|
||||
def _visit_preorder_by_node(self, visit: Callable[["_Node[KT, ET]"], None]) -> None:
|
||||
"""Visit nodes in preorder. This method is only used for testing."""
|
||||
visit(self)
|
||||
if not self.is_leaf:
|
||||
for child in self.children:
|
||||
child._visit_preorder_by_node(visit)
|
||||
|
||||
def maybe_cow(self, creator: _Creator) -> Optional["_Node[KT, ET]"]:
|
||||
"""Return a clone of this Node if it was not created by *creator*, or ``None``
|
||||
otherwise (i.e. copy for copy-on-write if we haven't already copied it)."""
|
||||
if self.creator is not creator:
|
||||
return self.clone(creator)
|
||||
else:
|
||||
return None
|
||||
|
||||
def clone(self, creator: _Creator) -> "_Node[KT, ET]":
|
||||
"""Make a shallow-copy duplicate of this node."""
|
||||
cloned = self.__class__(self.t, creator, self.is_leaf)
|
||||
cloned.elts.extend(self.elts)
|
||||
if not self.is_leaf:
|
||||
cloned.children.extend(self.children)
|
||||
return cloned
|
||||
|
||||
def __str__(self): # pragma: no cover
|
||||
if not self.is_leaf:
|
||||
children = " " + " ".join([f"{id(c):x}" for c in self.children])
|
||||
else:
|
||||
children = ""
|
||||
return f"{id(self):x} {self.creator} {self.elts}{children}"
|
||||
|
||||
|
||||
class Cursor(Generic[KT, ET]):
|
||||
"""A seekable cursor for a BTree.
|
||||
|
||||
If you are going to use a cursor on a mutable BTree, you should use it
|
||||
in a ``with`` block so that any mutations of the BTree automatically park
|
||||
the cursor.
|
||||
"""
|
||||
|
||||
def __init__(self, btree: "BTree[KT, ET]"):
|
||||
self.btree = btree
|
||||
self.current_node: _Node | None = None
|
||||
# The current index is the element index within the current node, or
|
||||
# if there is no current node then it is 0 on the left boundary and 1
|
||||
# on the right boundary.
|
||||
self.current_index: int = 0
|
||||
self.recurse = False
|
||||
self.increasing = True
|
||||
self.parents: list[tuple[_Node, int]] = []
|
||||
self.parked = False
|
||||
self.parking_key: KT | None = None
|
||||
self.parking_key_read = False
|
||||
|
||||
def _seek_least(self) -> None:
|
||||
# seek to the least value in the subtree beneath the current index of the
|
||||
# current node
|
||||
assert self.current_node is not None
|
||||
while not self.current_node.is_leaf:
|
||||
self.parents.append((self.current_node, self.current_index))
|
||||
self.current_node = self.current_node.children[self.current_index]
|
||||
assert self.current_node is not None
|
||||
self.current_index = 0
|
||||
|
||||
def _seek_greatest(self) -> None:
|
||||
# seek to the greatest value in the subtree beneath the current index of the
|
||||
# current node
|
||||
assert self.current_node is not None
|
||||
while not self.current_node.is_leaf:
|
||||
self.parents.append((self.current_node, self.current_index))
|
||||
self.current_node = self.current_node.children[self.current_index]
|
||||
assert self.current_node is not None
|
||||
self.current_index = len(self.current_node.elts)
|
||||
|
||||
def park(self):
|
||||
"""Park the cursor.
|
||||
|
||||
A cursor must be "parked" before mutating the BTree to avoid undefined behavior.
|
||||
Cursors created in a ``with`` block register with their BTree and will park
|
||||
automatically. Note that a parked cursor may not observe some changes made when
|
||||
it is parked; for example a cursor being iterated with next() will not see items
|
||||
inserted before its current position.
|
||||
"""
|
||||
if not self.parked:
|
||||
self.parked = True
|
||||
|
||||
def _maybe_unpark(self):
|
||||
if self.parked:
|
||||
if self.parking_key is not None:
|
||||
# remember our increasing hint, as seeking might change it
|
||||
increasing = self.increasing
|
||||
if self.parking_key_read:
|
||||
# We've already returned the parking key, so we want to be before it
|
||||
# if decreasing and after it if increasing.
|
||||
before = not self.increasing
|
||||
else:
|
||||
# We haven't returned the parking key, so we've parked right
|
||||
# after seeking or are on a boundary. Either way, the before
|
||||
# hint we want is the value of self.increasing.
|
||||
before = self.increasing
|
||||
self.seek(self.parking_key, before)
|
||||
self.increasing = increasing # might have been altered by seek()
|
||||
self.parked = False
|
||||
self.parking_key = None
|
||||
|
||||
def prev(self) -> ET | None:
|
||||
"""Get the previous element, or return None if on the left boundary."""
|
||||
self._maybe_unpark()
|
||||
self.parking_key = None
|
||||
if self.current_node is None:
|
||||
# on a boundary
|
||||
if self.current_index == 0:
|
||||
# left boundary, there is no prev
|
||||
return None
|
||||
else:
|
||||
assert self.current_index == 1
|
||||
# right boundary; seek to the actual boundary
|
||||
# so we can do a prev()
|
||||
self.current_node = self.btree.root
|
||||
self.current_index = len(self.btree.root.elts)
|
||||
self._seek_greatest()
|
||||
while True:
|
||||
if self.recurse:
|
||||
if not self.increasing:
|
||||
# We only want to recurse if we are continuing in the decreasing
|
||||
# direction.
|
||||
self._seek_greatest()
|
||||
self.recurse = False
|
||||
self.increasing = False
|
||||
self.current_index -= 1
|
||||
if self.current_index >= 0:
|
||||
elt = self.current_node.elts[self.current_index]
|
||||
if not self.current_node.is_leaf:
|
||||
self.recurse = True
|
||||
self.parking_key = elt.key()
|
||||
self.parking_key_read = True
|
||||
return elt
|
||||
else:
|
||||
if len(self.parents) > 0:
|
||||
self.current_node, self.current_index = self.parents.pop()
|
||||
else:
|
||||
self.current_node = None
|
||||
self.current_index = 0
|
||||
return None
|
||||
|
||||
def next(self) -> ET | None:
|
||||
"""Get the next element, or return None if on the right boundary."""
|
||||
self._maybe_unpark()
|
||||
self.parking_key = None
|
||||
if self.current_node is None:
|
||||
# on a boundary
|
||||
if self.current_index == 1:
|
||||
# right boundary, there is no next
|
||||
return None
|
||||
else:
|
||||
assert self.current_index == 0
|
||||
# left boundary; seek to the actual boundary
|
||||
# so we can do a next()
|
||||
self.current_node = self.btree.root
|
||||
self.current_index = 0
|
||||
self._seek_least()
|
||||
while True:
|
||||
if self.recurse:
|
||||
if self.increasing:
|
||||
# We only want to recurse if we are continuing in the increasing
|
||||
# direction.
|
||||
self._seek_least()
|
||||
self.recurse = False
|
||||
self.increasing = True
|
||||
if self.current_index < len(self.current_node.elts):
|
||||
elt = self.current_node.elts[self.current_index]
|
||||
self.current_index += 1
|
||||
if not self.current_node.is_leaf:
|
||||
self.recurse = True
|
||||
self.parking_key = elt.key()
|
||||
self.parking_key_read = True
|
||||
return elt
|
||||
else:
|
||||
if len(self.parents) > 0:
|
||||
self.current_node, self.current_index = self.parents.pop()
|
||||
else:
|
||||
self.current_node = None
|
||||
self.current_index = 1
|
||||
return None
|
||||
|
||||
def _adjust_for_before(self, before: bool, i: int) -> None:
|
||||
if before:
|
||||
self.current_index = i
|
||||
else:
|
||||
self.current_index = i + 1
|
||||
|
||||
def seek(self, key: KT, before: bool = True) -> None:
|
||||
"""Seek to the specified key.
|
||||
|
||||
If *before* is ``True`` (the default) then the cursor is positioned just
|
||||
before *key* if it exists, or before its least successor if it doesn't. A
|
||||
subsequent next() will retrieve this value. If *before* is ``False``, then
|
||||
the cursor is positioned just after *key* if it exists, or its greatest
|
||||
precessessor if it doesn't. A subsequent prev() will return this value.
|
||||
"""
|
||||
self.current_node = self.btree.root
|
||||
assert self.current_node is not None
|
||||
self.recurse = False
|
||||
self.parents = []
|
||||
self.increasing = before
|
||||
self.parked = False
|
||||
self.parking_key = key
|
||||
self.parking_key_read = False
|
||||
while not self.current_node.is_leaf:
|
||||
i, equal = self.current_node.search_in_node(key)
|
||||
if equal:
|
||||
self._adjust_for_before(before, i)
|
||||
if before:
|
||||
self._seek_greatest()
|
||||
else:
|
||||
self._seek_least()
|
||||
return
|
||||
self.parents.append((self.current_node, i))
|
||||
self.current_node = self.current_node.children[i]
|
||||
assert self.current_node is not None
|
||||
i, equal = self.current_node.search_in_node(key)
|
||||
if equal:
|
||||
self._adjust_for_before(before, i)
|
||||
else:
|
||||
self.current_index = i
|
||||
|
||||
def seek_first(self) -> None:
|
||||
"""Seek to the left boundary (i.e. just before the least element).
|
||||
|
||||
A subsequent next() will return the least element if the BTree isn't empty."""
|
||||
self.current_node = None
|
||||
self.current_index = 0
|
||||
self.recurse = False
|
||||
self.increasing = True
|
||||
self.parents = []
|
||||
self.parked = False
|
||||
self.parking_key = None
|
||||
|
||||
def seek_last(self) -> None:
|
||||
"""Seek to the right boundary (i.e. just after the greatest element).
|
||||
|
||||
A subsequent prev() will return the greatest element if the BTree isn't empty.
|
||||
"""
|
||||
self.current_node = None
|
||||
self.current_index = 1
|
||||
self.recurse = False
|
||||
self.increasing = False
|
||||
self.parents = []
|
||||
self.parked = False
|
||||
self.parking_key = None
|
||||
|
||||
def __enter__(self):
|
||||
self.btree.register_cursor(self)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.btree.deregister_cursor(self)
|
||||
return False
|
||||
|
||||
|
||||
class Immutable(Exception):
|
||||
"""The BTree is immutable."""
|
||||
|
||||
|
||||
class BTree(Generic[KT, ET]):
|
||||
"""An in-memory BTree with copy-on-write and cursors."""
|
||||
|
||||
def __init__(self, *, t: int = DEFAULT_T, original: Optional["BTree"] = None):
|
||||
"""Create a BTree.
|
||||
|
||||
If *original* is not ``None``, then the BTree is shallow-cloned from
|
||||
*original* using copy-on-write. Otherwise a new BTree with the specified
|
||||
*t* value is created.
|
||||
|
||||
The BTree is not thread-safe.
|
||||
"""
|
||||
# We don't use a reference to ourselves as a creator as we don't want
|
||||
# to prevent GC of old btrees.
|
||||
self.creator = _Creator()
|
||||
self._immutable = False
|
||||
self.t: int
|
||||
self.root: _Node
|
||||
self.size: int
|
||||
self.cursors: set[Cursor] = set()
|
||||
if original is not None:
|
||||
if not original._immutable:
|
||||
raise ValueError("original BTree is not immutable")
|
||||
self.t = original.t
|
||||
self.root = original.root
|
||||
self.size = original.size
|
||||
else:
|
||||
if t < 3:
|
||||
raise ValueError("t must be >= 3")
|
||||
self.t = t
|
||||
self.root = _Node(self.t, self.creator, True)
|
||||
self.size = 0
|
||||
|
||||
def make_immutable(self):
|
||||
"""Make the BTree immutable.
|
||||
|
||||
Attempts to alter the BTree after making it immutable will raise an
|
||||
Immutable exception. This operation cannot be undone.
|
||||
"""
|
||||
if not self._immutable:
|
||||
self._immutable = True
|
||||
|
||||
def _check_mutable_and_park(self) -> None:
|
||||
if self._immutable:
|
||||
raise Immutable
|
||||
for cursor in self.cursors:
|
||||
cursor.park()
|
||||
|
||||
# Note that we don't use insert() and delete() but rather insert_element() and
|
||||
# delete_key() so that BTreeDict can be a proper MutableMapping and supply the
|
||||
# rest of the standard mapping API.
|
||||
|
||||
def insert_element(self, elt: ET, in_order: bool = False) -> ET | None:
|
||||
"""Insert the element into the BTree.
|
||||
|
||||
If *in_order* is ``True``, then extra work will be done to make left siblings
|
||||
full, which optimizes storage space when the the elements are inserted in-order
|
||||
or close to it.
|
||||
|
||||
Returns the previously existing element at the element's key or ``None``.
|
||||
"""
|
||||
self._check_mutable_and_park()
|
||||
cloned = self.root.maybe_cow(self.creator)
|
||||
if cloned:
|
||||
self.root = cloned
|
||||
if self.root.is_maximal():
|
||||
old_root = self.root
|
||||
self.root = _Node(self.t, self.creator, False)
|
||||
self.root.adopt(*old_root.split())
|
||||
oelt = self.root.insert_nonfull(elt, in_order)
|
||||
if oelt is None:
|
||||
# We did not replace, so something was added.
|
||||
self.size += 1
|
||||
return oelt
|
||||
|
||||
def get_element(self, key: KT) -> ET | None:
|
||||
"""Get the element matching *key* from the BTree, or return ``None`` if it
|
||||
does not exist.
|
||||
"""
|
||||
return self.root.get(key)
|
||||
|
||||
def _delete(self, key: KT, exact: ET | None) -> ET | None:
|
||||
self._check_mutable_and_park()
|
||||
cloned = self.root.maybe_cow(self.creator)
|
||||
if cloned:
|
||||
self.root = cloned
|
||||
elt = self.root.delete(key, None, exact)
|
||||
if elt is not None:
|
||||
# We deleted something
|
||||
self.size -= 1
|
||||
if len(self.root.elts) == 0:
|
||||
# The root is now empty. If there is a child, then collapse this root
|
||||
# level and make the child the new root.
|
||||
if not self.root.is_leaf:
|
||||
assert len(self.root.children) == 1
|
||||
self.root = self.root.children[0]
|
||||
return elt
|
||||
|
||||
def delete_key(self, key: KT) -> ET | None:
|
||||
"""Delete the element matching *key* from the BTree.
|
||||
|
||||
Returns the matching element or ``None`` if it does not exist.
|
||||
"""
|
||||
return self._delete(key, None)
|
||||
|
||||
def delete_exact(self, element: ET) -> ET | None:
|
||||
"""Delete *element* from the BTree.
|
||||
|
||||
Returns the matching element or ``None`` if it was not in the BTree.
|
||||
"""
|
||||
delt = self._delete(element.key(), element)
|
||||
assert delt is element
|
||||
return delt
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
def visit_in_order(self, visit: Callable[[ET], None]) -> None:
|
||||
"""Call *visit*(element) on all elements in the tree in sorted order."""
|
||||
self.root.visit_in_order(visit)
|
||||
|
||||
def _visit_preorder_by_node(self, visit: Callable[[_Node], None]) -> None:
|
||||
self.root._visit_preorder_by_node(visit)
|
||||
|
||||
def cursor(self) -> Cursor[KT, ET]:
|
||||
"""Create a cursor."""
|
||||
return Cursor(self)
|
||||
|
||||
def register_cursor(self, cursor: Cursor) -> None:
|
||||
"""Register a cursor for the automatic parking service."""
|
||||
self.cursors.add(cursor)
|
||||
|
||||
def deregister_cursor(self, cursor: Cursor) -> None:
|
||||
"""Deregister a cursor from the automatic parking service."""
|
||||
self.cursors.discard(cursor)
|
||||
|
||||
def __copy__(self):
|
||||
return self.__class__(original=self)
|
||||
|
||||
def __iter__(self):
|
||||
with self.cursor() as cursor:
|
||||
while True:
|
||||
elt = cursor.next()
|
||||
if elt is None:
|
||||
break
|
||||
yield elt.key()
|
||||
|
||||
|
||||
VT = TypeVar("VT") # the type of a value in a BTreeDict
|
||||
|
||||
|
||||
class KV(Element, Generic[KT, VT]):
|
||||
"""The BTree element type used in a ``BTreeDict``."""
|
||||
|
||||
def __init__(self, key: KT, value: VT):
|
||||
self._key = key
|
||||
self._value = value
|
||||
|
||||
def key(self) -> KT:
|
||||
return self._key
|
||||
|
||||
def value(self) -> VT:
|
||||
return self._value
|
||||
|
||||
def __str__(self): # pragma: no cover
|
||||
return f"KV({self._key}, {self._value})"
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
return f"KV({self._key}, {self._value})"
|
||||
|
||||
|
||||
class BTreeDict(Generic[KT, VT], BTree[KT, KV[KT, VT]], MutableMapping[KT, VT]):
|
||||
"""A MutableMapping implemented with a BTree.
|
||||
|
||||
Unlike a normal Python dict, the BTreeDict may be mutated while iterating.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
t: int = DEFAULT_T,
|
||||
original: BTree | None = None,
|
||||
in_order: bool = False,
|
||||
):
|
||||
super().__init__(t=t, original=original)
|
||||
self.in_order = in_order
|
||||
|
||||
def __getitem__(self, key: KT) -> VT:
|
||||
elt = self.get_element(key)
|
||||
if elt is None:
|
||||
raise KeyError
|
||||
else:
|
||||
return cast(KV, elt).value()
|
||||
|
||||
def __setitem__(self, key: KT, value: VT) -> None:
|
||||
elt = KV(key, value)
|
||||
self.insert_element(elt, self.in_order)
|
||||
|
||||
def __delitem__(self, key: KT) -> None:
|
||||
if self.delete_key(key) is None:
|
||||
raise KeyError
|
||||
|
||||
|
||||
class Member(Element, Generic[KT]):
|
||||
"""The BTree element type used in a ``BTreeSet``."""
|
||||
|
||||
def __init__(self, key: KT):
|
||||
self._key = key
|
||||
|
||||
def key(self) -> KT:
|
||||
return self._key
|
||||
|
||||
|
||||
class BTreeSet(BTree, Generic[KT], MutableSet[KT]):
|
||||
"""A MutableSet implemented with a BTree.
|
||||
|
||||
Unlike a normal Python set, the BTreeSet may be mutated while iterating.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
t: int = DEFAULT_T,
|
||||
original: BTree | None = None,
|
||||
in_order: bool = False,
|
||||
):
|
||||
super().__init__(t=t, original=original)
|
||||
self.in_order = in_order
|
||||
|
||||
def __contains__(self, key: Any) -> bool:
|
||||
return self.get_element(key) is not None
|
||||
|
||||
def add(self, value: KT) -> None:
|
||||
elt = Member(value)
|
||||
self.insert_element(elt, self.in_order)
|
||||
|
||||
def discard(self, value: KT) -> None:
|
||||
self.delete_key(value)
|
||||
367
venv/Lib/site-packages/dns/btreezone.py
Normal file
367
venv/Lib/site-packages/dns/btreezone.py
Normal file
@@ -0,0 +1,367 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# A derivative of a dnspython VersionedZone and related classes, using a BTreeDict and
|
||||
# a separate per-version delegation index. These additions let us
|
||||
#
|
||||
# 1) Do efficient CoW versioning (useful for future online updates).
|
||||
# 2) Maintain sort order
|
||||
# 3) Allow delegations to be found easily
|
||||
# 4) Handle glue
|
||||
# 5) Add Node flags ORIGIN, DELEGATION, and GLUE whenever relevant. The ORIGIN
|
||||
# flag is set at the origin node, the DELEGATION FLAG is set at delegation
|
||||
# points, and the GLUE flag is set on nodes beneath delegation points.
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, MutableMapping, Tuple, cast
|
||||
|
||||
import dns.btree
|
||||
import dns.immutable
|
||||
import dns.name
|
||||
import dns.node
|
||||
import dns.rdataclass
|
||||
import dns.rdataset
|
||||
import dns.rdatatype
|
||||
import dns.versioned
|
||||
import dns.zone
|
||||
|
||||
|
||||
class NodeFlags(enum.IntFlag):
|
||||
ORIGIN = 0x01
|
||||
DELEGATION = 0x02
|
||||
GLUE = 0x04
|
||||
|
||||
|
||||
class Node(dns.node.Node):
|
||||
__slots__ = ["flags", "id"]
|
||||
|
||||
def __init__(self, flags: NodeFlags | None = None):
|
||||
super().__init__()
|
||||
if flags is None:
|
||||
# We allow optional flags rather than a default
|
||||
# as pyright doesn't like assigning a literal 0
|
||||
# to flags.
|
||||
flags = NodeFlags(0)
|
||||
self.flags = flags
|
||||
self.id = 0
|
||||
|
||||
def is_delegation(self):
|
||||
return (self.flags & NodeFlags.DELEGATION) != 0
|
||||
|
||||
def is_glue(self):
|
||||
return (self.flags & NodeFlags.GLUE) != 0
|
||||
|
||||
def is_origin(self):
|
||||
return (self.flags & NodeFlags.ORIGIN) != 0
|
||||
|
||||
def is_origin_or_glue(self):
|
||||
return (self.flags & (NodeFlags.ORIGIN | NodeFlags.GLUE)) != 0
|
||||
|
||||
|
||||
@dns.immutable.immutable
|
||||
class ImmutableNode(Node):
|
||||
def __init__(self, node: Node):
|
||||
super().__init__()
|
||||
self.id = node.id
|
||||
self.rdatasets = tuple( # type: ignore
|
||||
[dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets]
|
||||
)
|
||||
self.flags = node.flags
|
||||
|
||||
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,
|
||||
) -> dns.rdataset.Rdataset | None:
|
||||
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 Delegations(dns.btree.BTreeSet[dns.name.Name]):
|
||||
def get_delegation(self, name: dns.name.Name) -> Tuple[dns.name.Name | None, bool]:
|
||||
"""Get the delegation applicable to *name*, if it exists.
|
||||
|
||||
If there delegation, then return a tuple consisting of the name of
|
||||
the delegation point, and a boolean which is `True` if the name is a proper
|
||||
subdomain of the delegation point, and `False` if it is equal to the delegation
|
||||
point.
|
||||
"""
|
||||
cursor = self.cursor()
|
||||
cursor.seek(name, before=False)
|
||||
prev = cursor.prev()
|
||||
if prev is None:
|
||||
return None, False
|
||||
cut = prev.key()
|
||||
reln, _, _ = name.fullcompare(cut)
|
||||
is_subdomain = reln == dns.name.NameRelation.SUBDOMAIN
|
||||
if is_subdomain or reln == dns.name.NameRelation.EQUAL:
|
||||
return cut, is_subdomain
|
||||
else:
|
||||
return None, False
|
||||
|
||||
def is_glue(self, name: dns.name.Name) -> bool:
|
||||
"""Is *name* glue, i.e. is it beneath a delegation?"""
|
||||
cursor = self.cursor()
|
||||
cursor.seek(name, before=False)
|
||||
cut, is_subdomain = self.get_delegation(name)
|
||||
if cut is None:
|
||||
return False
|
||||
return is_subdomain
|
||||
|
||||
|
||||
class WritableVersion(dns.zone.WritableVersion):
|
||||
def __init__(self, zone: dns.zone.Zone, replacement: bool = False):
|
||||
super().__init__(zone, True)
|
||||
if not replacement:
|
||||
assert isinstance(zone, dns.versioned.Zone)
|
||||
version = zone._versions[-1]
|
||||
self.nodes: dns.btree.BTreeDict[dns.name.Name, Node] = dns.btree.BTreeDict(
|
||||
original=version.nodes # type: ignore
|
||||
)
|
||||
self.delegations = Delegations(original=version.delegations) # type: ignore
|
||||
else:
|
||||
self.delegations = Delegations()
|
||||
|
||||
def _is_origin(self, name: dns.name.Name) -> bool:
|
||||
# Assumes name has already been validated (and thus adjusted to the right
|
||||
# relativity too)
|
||||
if self.zone.relativize:
|
||||
return name == dns.name.empty
|
||||
else:
|
||||
return name == self.zone.origin
|
||||
|
||||
def _maybe_cow_with_name(
|
||||
self, name: dns.name.Name
|
||||
) -> Tuple[dns.node.Node, dns.name.Name]:
|
||||
(node, name) = super()._maybe_cow_with_name(name)
|
||||
node = cast(Node, node)
|
||||
if self._is_origin(name):
|
||||
node.flags |= NodeFlags.ORIGIN
|
||||
elif self.delegations.is_glue(name):
|
||||
node.flags |= NodeFlags.GLUE
|
||||
return (node, name)
|
||||
|
||||
def update_glue_flag(self, name: dns.name.Name, is_glue: bool) -> None:
|
||||
cursor = self.nodes.cursor() # type: ignore
|
||||
cursor.seek(name, False)
|
||||
updates = []
|
||||
while True:
|
||||
elt = cursor.next()
|
||||
if elt is None:
|
||||
break
|
||||
ename = elt.key()
|
||||
if not ename.is_subdomain(name):
|
||||
break
|
||||
node = cast(dns.node.Node, elt.value())
|
||||
if ename not in self.changed:
|
||||
new_node = self.zone.node_factory()
|
||||
new_node.id = self.id # type: ignore
|
||||
new_node.rdatasets.extend(node.rdatasets)
|
||||
self.changed.add(ename)
|
||||
node = new_node
|
||||
assert isinstance(node, Node)
|
||||
if is_glue:
|
||||
node.flags |= NodeFlags.GLUE
|
||||
else:
|
||||
node.flags &= ~NodeFlags.GLUE
|
||||
# We don't update node here as any insertion could disturb the
|
||||
# btree and invalidate our cursor. We could use the cursor in a
|
||||
# with block and avoid this, but it would do a lot of parking and
|
||||
# unparking so the deferred update mode may still be better.
|
||||
updates.append((ename, node))
|
||||
for ename, node in updates:
|
||||
self.nodes[ename] = node
|
||||
|
||||
def delete_node(self, name: dns.name.Name) -> None:
|
||||
name = self._validate_name(name)
|
||||
node = self.nodes.get(name)
|
||||
if node is not None:
|
||||
if node.is_delegation(): # type: ignore
|
||||
self.delegations.discard(name)
|
||||
self.update_glue_flag(name, False)
|
||||
del self.nodes[name]
|
||||
self.changed.add(name)
|
||||
|
||||
def put_rdataset(
|
||||
self, name: dns.name.Name, rdataset: dns.rdataset.Rdataset
|
||||
) -> None:
|
||||
(node, name) = self._maybe_cow_with_name(name)
|
||||
if (
|
||||
rdataset.rdtype == dns.rdatatype.NS and not node.is_origin_or_glue() # type: ignore
|
||||
):
|
||||
node.flags |= NodeFlags.DELEGATION # type: ignore
|
||||
if name not in self.delegations:
|
||||
self.delegations.add(name)
|
||||
self.update_glue_flag(name, True)
|
||||
node.replace_rdataset(rdataset)
|
||||
|
||||
def delete_rdataset(
|
||||
self,
|
||||
name: dns.name.Name,
|
||||
rdtype: dns.rdatatype.RdataType,
|
||||
covers: dns.rdatatype.RdataType,
|
||||
) -> None:
|
||||
(node, name) = self._maybe_cow_with_name(name)
|
||||
if rdtype == dns.rdatatype.NS and name in self.delegations: # type: ignore
|
||||
node.flags &= ~NodeFlags.DELEGATION # type: ignore
|
||||
self.delegations.discard(name) # type: ignore
|
||||
self.update_glue_flag(name, False)
|
||||
node.delete_rdataset(self.zone.rdclass, rdtype, covers)
|
||||
if len(node) == 0:
|
||||
del self.nodes[name]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Bounds:
|
||||
name: dns.name.Name
|
||||
left: dns.name.Name
|
||||
right: dns.name.Name | None
|
||||
closest_encloser: dns.name.Name
|
||||
is_equal: bool
|
||||
is_delegation: bool
|
||||
|
||||
def __str__(self):
|
||||
if self.is_equal:
|
||||
op = "="
|
||||
else:
|
||||
op = "<"
|
||||
if self.is_delegation:
|
||||
zonecut = " zonecut"
|
||||
else:
|
||||
zonecut = ""
|
||||
return (
|
||||
f"{self.left} {op} {self.name} < {self.right}{zonecut}; "
|
||||
f"{self.closest_encloser}"
|
||||
)
|
||||
|
||||
|
||||
@dns.immutable.immutable
|
||||
class ImmutableVersion(dns.zone.Version):
|
||||
def __init__(self, version: dns.zone.Version):
|
||||
if not isinstance(version, WritableVersion):
|
||||
raise ValueError(
|
||||
"a dns.btreezone.ImmutableVersion requires a "
|
||||
"dns.btreezone.WritableVersion"
|
||||
)
|
||||
super().__init__(version.zone, True)
|
||||
self.id = version.id
|
||||
self.origin = version.origin
|
||||
for name in version.changed:
|
||||
node = version.nodes.get(name)
|
||||
if node:
|
||||
version.nodes[name] = ImmutableNode(node)
|
||||
# the cast below is for mypy
|
||||
self.nodes = cast(MutableMapping[dns.name.Name, dns.node.Node], version.nodes)
|
||||
self.nodes.make_immutable() # type: ignore
|
||||
self.delegations = version.delegations
|
||||
self.delegations.make_immutable()
|
||||
|
||||
def bounds(self, name: dns.name.Name | str) -> Bounds:
|
||||
"""Return the 'bounds' of *name* in its zone.
|
||||
|
||||
The bounds information is useful when making an authoritative response, as
|
||||
it can be used to determine whether the query name is at or beneath a delegation
|
||||
point. The other data in the ``Bounds`` object is useful for making on-the-fly
|
||||
DNSSEC signatures.
|
||||
|
||||
The left bound of *name* is *name* itself if it is in the zone, or the greatest
|
||||
predecessor which is in the zone.
|
||||
|
||||
The right bound of *name* is the least successor of *name*, or ``None`` if
|
||||
no name in the zone is greater than *name*.
|
||||
|
||||
The closest encloser of *name* is *name* itself, if *name* is in the zone;
|
||||
otherwise it is the name with the largest number of labels in common with
|
||||
*name* that is in the zone, either explicitly or by the implied existence
|
||||
of empty non-terminals.
|
||||
|
||||
The bounds *is_equal* field is ``True`` if and only if *name* is equal to
|
||||
its left bound.
|
||||
|
||||
The bounds *is_delegation* field is ``True`` if and only if the left bound is a
|
||||
delegation point.
|
||||
"""
|
||||
assert self.origin is not None
|
||||
# validate the origin because we may need to relativize
|
||||
origin = self.zone._validate_name(self.origin)
|
||||
name = self.zone._validate_name(name)
|
||||
cut, _ = self.delegations.get_delegation(name)
|
||||
if cut is not None:
|
||||
target = cut
|
||||
is_delegation = True
|
||||
else:
|
||||
target = name
|
||||
is_delegation = False
|
||||
c = cast(dns.btree.BTreeDict, self.nodes).cursor()
|
||||
c.seek(target, False)
|
||||
left = c.prev()
|
||||
assert left is not None
|
||||
c.next() # skip over left
|
||||
while True:
|
||||
right = c.next()
|
||||
if right is None or not right.value().is_glue():
|
||||
break
|
||||
left_comparison = left.key().fullcompare(name)
|
||||
if right is not None:
|
||||
right_key = right.key()
|
||||
right_comparison = right_key.fullcompare(name)
|
||||
else:
|
||||
right_comparison = (
|
||||
dns.name.NAMERELN_COMMONANCESTOR,
|
||||
-1,
|
||||
len(origin),
|
||||
)
|
||||
right_key = None
|
||||
closest_encloser = dns.name.Name(
|
||||
name[-max(left_comparison[2], right_comparison[2]) :]
|
||||
)
|
||||
return Bounds(
|
||||
name,
|
||||
left.key(),
|
||||
right_key,
|
||||
closest_encloser,
|
||||
left_comparison[0] == dns.name.NameRelation.EQUAL,
|
||||
is_delegation,
|
||||
)
|
||||
|
||||
|
||||
class Zone(dns.versioned.Zone):
|
||||
node_factory: Callable[[], dns.node.Node] = Node
|
||||
map_factory: Callable[[], MutableMapping[dns.name.Name, dns.node.Node]] = cast(
|
||||
Callable[[], MutableMapping[dns.name.Name, dns.node.Node]],
|
||||
dns.btree.BTreeDict[dns.name.Name, Node],
|
||||
)
|
||||
writable_version_factory: (
|
||||
Callable[[dns.zone.Zone, bool], dns.zone.Version] | None
|
||||
) = WritableVersion
|
||||
immutable_version_factory: Callable[[dns.zone.Version], dns.zone.Version] | None = (
|
||||
ImmutableVersion
|
||||
)
|
||||
1242
venv/Lib/site-packages/dns/dnssec.py
Normal file
1242
venv/Lib/site-packages/dns/dnssec.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user