123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
- # Copyright (C) 2003-2017 Nominum, Inc.
- #
- # Permission to use, copy, modify, and distribute this software and its
- # documentation for any purpose with or without fee is hereby granted,
- # provided that the above copyright notice and this permission notice
- # appear in all copies.
- #
- # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
- # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
- # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
- # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- """Asynchronous DNS stub resolver."""
- import socket
- import time
- from typing import Any, Dict, List, Optional, Union
- import dns._ddr
- import dns.asyncbackend
- import dns.asyncquery
- import dns.exception
- import dns.name
- import dns.query
- import dns.rdataclass
- import dns.rdatatype
- import dns.resolver # lgtm[py/import-and-import-from]
- # import some resolver symbols for brevity
- from dns.resolver import NXDOMAIN, NoAnswer, NoRootSOA, NotAbsolute
- # for indentation purposes below
- _udp = dns.asyncquery.udp
- _tcp = dns.asyncquery.tcp
- class Resolver(dns.resolver.BaseResolver):
- """Asynchronous DNS stub resolver."""
- async def resolve(
- self,
- qname: Union[dns.name.Name, str],
- rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
- rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
- tcp: bool = False,
- source: Optional[str] = None,
- raise_on_no_answer: bool = True,
- source_port: int = 0,
- lifetime: Optional[float] = None,
- search: Optional[bool] = None,
- backend: Optional[dns.asyncbackend.Backend] = None,
- ) -> dns.resolver.Answer:
- """Query nameservers asynchronously to find the answer to the question.
- *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,
- the default, then dnspython will use the default backend.
- See :py:func:`dns.resolver.Resolver.resolve()` for the
- documentation of the other parameters, exceptions, and return
- type of this method.
- """
- resolution = dns.resolver._Resolution(
- self, qname, rdtype, rdclass, tcp, raise_on_no_answer, search
- )
- if not backend:
- backend = dns.asyncbackend.get_default_backend()
- start = time.time()
- while True:
- (request, answer) = resolution.next_request()
- # Note we need to say "if answer is not None" and not just
- # "if answer" because answer implements __len__, and python
- # will call that. We want to return if we have an answer
- # object, including in cases where its length is 0.
- if answer is not None:
- # cache hit!
- return answer
- assert request is not None # needed for type checking
- done = False
- while not done:
- (nameserver, tcp, backoff) = resolution.next_nameserver()
- if backoff:
- await backend.sleep(backoff)
- timeout = self._compute_timeout(start, lifetime, resolution.errors)
- try:
- response = await nameserver.async_query(
- request,
- timeout=timeout,
- source=source,
- source_port=source_port,
- max_size=tcp,
- backend=backend,
- )
- except Exception as ex:
- (_, done) = resolution.query_result(None, ex)
- continue
- (answer, done) = resolution.query_result(response, None)
- # Note we need to say "if answer is not None" and not just
- # "if answer" because answer implements __len__, and python
- # will call that. We want to return if we have an answer
- # object, including in cases where its length is 0.
- if answer is not None:
- return answer
- async def resolve_address(
- self, ipaddr: str, *args: Any, **kwargs: Any
- ) -> dns.resolver.Answer:
- """Use an asynchronous resolver to run a reverse query for PTR
- records.
- This utilizes the resolve() method to perform a PTR lookup on the
- specified IP address.
- *ipaddr*, a ``str``, the IPv4 or IPv6 address you want to get
- the PTR record for.
- All other arguments that can be passed to the resolve() function
- except for rdtype and rdclass are also supported by this
- function.
- """
- # We make a modified kwargs for type checking happiness, as otherwise
- # we get a legit warning about possibly having rdtype and rdclass
- # in the kwargs more than once.
- modified_kwargs: Dict[str, Any] = {}
- modified_kwargs.update(kwargs)
- modified_kwargs["rdtype"] = dns.rdatatype.PTR
- modified_kwargs["rdclass"] = dns.rdataclass.IN
- return await self.resolve(
- dns.reversename.from_address(ipaddr), *args, **modified_kwargs
- )
- async def resolve_name(
- self,
- name: Union[dns.name.Name, str],
- family: int = socket.AF_UNSPEC,
- **kwargs: Any,
- ) -> dns.resolver.HostAnswers:
- """Use an asynchronous resolver to query for address records.
- This utilizes the resolve() method to perform A and/or AAAA lookups on
- the specified name.
- *qname*, a ``dns.name.Name`` or ``str``, the name to resolve.
- *family*, an ``int``, the address family. If socket.AF_UNSPEC
- (the default), both A and AAAA records will be retrieved.
- All other arguments that can be passed to the resolve() function
- except for rdtype and rdclass are also supported by this
- function.
- """
- # We make a modified kwargs for type checking happiness, as otherwise
- # we get a legit warning about possibly having rdtype and rdclass
- # in the kwargs more than once.
- modified_kwargs: Dict[str, Any] = {}
- modified_kwargs.update(kwargs)
- modified_kwargs.pop("rdtype", None)
- modified_kwargs["rdclass"] = dns.rdataclass.IN
- if family == socket.AF_INET:
- v4 = await self.resolve(name, dns.rdatatype.A, **modified_kwargs)
- return dns.resolver.HostAnswers.make(v4=v4)
- elif family == socket.AF_INET6:
- v6 = await self.resolve(name, dns.rdatatype.AAAA, **modified_kwargs)
- return dns.resolver.HostAnswers.make(v6=v6)
- elif family != socket.AF_UNSPEC:
- raise NotImplementedError(f"unknown address family {family}")
- raise_on_no_answer = modified_kwargs.pop("raise_on_no_answer", True)
- lifetime = modified_kwargs.pop("lifetime", None)
- start = time.time()
- v6 = await self.resolve(
- name,
- dns.rdatatype.AAAA,
- raise_on_no_answer=False,
- lifetime=self._compute_timeout(start, lifetime),
- **modified_kwargs,
- )
- # Note that setting name ensures we query the same name
- # for A as we did for AAAA. (This is just in case search lists
- # are active by default in the resolver configuration and
- # we might be talking to a server that says NXDOMAIN when it
- # wants to say NOERROR no data.
- name = v6.qname
- v4 = await self.resolve(
- name,
- dns.rdatatype.A,
- raise_on_no_answer=False,
- lifetime=self._compute_timeout(start, lifetime),
- **modified_kwargs,
- )
- answers = dns.resolver.HostAnswers.make(
- v6=v6, v4=v4, add_empty=not raise_on_no_answer
- )
- if not answers:
- raise NoAnswer(response=v6.response)
- return answers
- # pylint: disable=redefined-outer-name
- async def canonical_name(self, name: Union[dns.name.Name, str]) -> dns.name.Name:
- """Determine the canonical name of *name*.
- The canonical name is the name the resolver uses for queries
- after all CNAME and DNAME renamings have been applied.
- *name*, a ``dns.name.Name`` or ``str``, the query name.
- This method can raise any exception that ``resolve()`` can
- raise, other than ``dns.resolver.NoAnswer`` and
- ``dns.resolver.NXDOMAIN``.
- Returns a ``dns.name.Name``.
- """
- try:
- answer = await self.resolve(name, raise_on_no_answer=False)
- canonical_name = answer.canonical_name
- except dns.resolver.NXDOMAIN as e:
- canonical_name = e.canonical_name
- return canonical_name
- async def try_ddr(self, lifetime: float = 5.0) -> None:
- """Try to update the resolver's nameservers using Discovery of Designated
- Resolvers (DDR). If successful, the resolver will subsequently use
- DNS-over-HTTPS or DNS-over-TLS for future queries.
- *lifetime*, a float, is the maximum time to spend attempting DDR. The default
- is 5 seconds.
- If the SVCB query is successful and results in a non-empty list of nameservers,
- then the resolver's nameservers are set to the returned servers in priority
- order.
- The current implementation does not use any address hints from the SVCB record,
- nor does it resolve addresses for the SCVB target name, rather it assumes that
- the bootstrap nameserver will always be one of the addresses and uses it.
- A future revision to the code may offer fuller support. The code verifies that
- the bootstrap nameserver is in the Subject Alternative Name field of the
- TLS certficate.
- """
- try:
- expiration = time.time() + lifetime
- answer = await self.resolve(
- dns._ddr._local_resolver_name, "svcb", lifetime=lifetime
- )
- timeout = dns.query._remaining(expiration)
- nameservers = await dns._ddr._get_nameservers_async(answer, timeout)
- if len(nameservers) > 0:
- self.nameservers = nameservers
- except Exception:
- pass
- default_resolver = None
- def get_default_resolver() -> Resolver:
- """Get the default asynchronous resolver, initializing it if necessary."""
- if default_resolver is None:
- reset_default_resolver()
- assert default_resolver is not None
- return default_resolver
- def reset_default_resolver() -> None:
- """Re-initialize default asynchronous resolver.
- Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX
- systems) will be re-read immediately.
- """
- global default_resolver
- default_resolver = Resolver()
- async def resolve(
- qname: Union[dns.name.Name, str],
- rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
- rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
- tcp: bool = False,
- source: Optional[str] = None,
- raise_on_no_answer: bool = True,
- source_port: int = 0,
- lifetime: Optional[float] = None,
- search: Optional[bool] = None,
- backend: Optional[dns.asyncbackend.Backend] = None,
- ) -> dns.resolver.Answer:
- """Query nameservers asynchronously to find the answer to the question.
- This is a convenience function that uses the default resolver
- object to make the query.
- See :py:func:`dns.asyncresolver.Resolver.resolve` for more
- information on the parameters.
- """
- return await get_default_resolver().resolve(
- qname,
- rdtype,
- rdclass,
- tcp,
- source,
- raise_on_no_answer,
- source_port,
- lifetime,
- search,
- backend,
- )
- async def resolve_address(
- ipaddr: str, *args: Any, **kwargs: Any
- ) -> dns.resolver.Answer:
- """Use a resolver to run a reverse query for PTR records.
- See :py:func:`dns.asyncresolver.Resolver.resolve_address` for more
- information on the parameters.
- """
- return await get_default_resolver().resolve_address(ipaddr, *args, **kwargs)
- async def resolve_name(
- name: Union[dns.name.Name, str], family: int = socket.AF_UNSPEC, **kwargs: Any
- ) -> dns.resolver.HostAnswers:
- """Use a resolver to asynchronously query for address records.
- See :py:func:`dns.asyncresolver.Resolver.resolve_name` for more
- information on the parameters.
- """
- return await get_default_resolver().resolve_name(name, family, **kwargs)
- async def canonical_name(name: Union[dns.name.Name, str]) -> dns.name.Name:
- """Determine the canonical name of *name*.
- See :py:func:`dns.resolver.Resolver.canonical_name` for more
- information on the parameters and possible exceptions.
- """
- return await get_default_resolver().canonical_name(name)
- async def try_ddr(timeout: float = 5.0) -> None:
- """Try to update the default resolver's nameservers using Discovery of Designated
- Resolvers (DDR). If successful, the resolver will subsequently use
- DNS-over-HTTPS or DNS-over-TLS for future queries.
- See :py:func:`dns.resolver.Resolver.try_ddr` for more information.
- """
- return await get_default_resolver().try_ddr(timeout)
- async def zone_for_name(
- name: Union[dns.name.Name, str],
- rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
- tcp: bool = False,
- resolver: Optional[Resolver] = None,
- backend: Optional[dns.asyncbackend.Backend] = None,
- ) -> dns.name.Name:
- """Find the name of the zone which contains the specified name.
- See :py:func:`dns.resolver.Resolver.zone_for_name` for more
- information on the parameters and possible exceptions.
- """
- if isinstance(name, str):
- name = dns.name.from_text(name, dns.name.root)
- if resolver is None:
- resolver = get_default_resolver()
- if not name.is_absolute():
- raise NotAbsolute(name)
- while True:
- try:
- answer = await resolver.resolve(
- name, dns.rdatatype.SOA, rdclass, tcp, backend=backend
- )
- assert answer.rrset is not None
- if answer.rrset.name == name:
- return name
- # otherwise we were CNAMEd or DNAMEd and need to look higher
- except (NXDOMAIN, NoAnswer):
- pass
- try:
- name = name.parent()
- except dns.name.NoParent: # pragma: no cover
- raise NoRootSOA
- async def make_resolver_at(
- where: Union[dns.name.Name, str],
- port: int = 53,
- family: int = socket.AF_UNSPEC,
- resolver: Optional[Resolver] = None,
- ) -> Resolver:
- """Make a stub resolver using the specified destination as the full resolver.
- *where*, a ``dns.name.Name`` or ``str`` the domain name or IP address of the
- full resolver.
- *port*, an ``int``, the port to use. If not specified, the default is 53.
- *family*, an ``int``, the address family to use. This parameter is used if
- *where* is not an address. The default is ``socket.AF_UNSPEC`` in which case
- the first address returned by ``resolve_name()`` will be used, otherwise the
- first address of the specified family will be used.
- *resolver*, a ``dns.asyncresolver.Resolver`` or ``None``, the resolver to use for
- resolution of hostnames. If not specified, the default resolver will be used.
- Returns a ``dns.resolver.Resolver`` or raises an exception.
- """
- if resolver is None:
- resolver = get_default_resolver()
- nameservers: List[Union[str, dns.nameserver.Nameserver]] = []
- if isinstance(where, str) and dns.inet.is_address(where):
- nameservers.append(dns.nameserver.Do53Nameserver(where, port))
- else:
- answers = await resolver.resolve_name(where, family)
- for address in answers.addresses():
- nameservers.append(dns.nameserver.Do53Nameserver(address, port))
- res = dns.asyncresolver.Resolver(configure=False)
- res.nameservers = nameservers
- return res
- async def resolve_at(
- where: Union[dns.name.Name, str],
- qname: Union[dns.name.Name, str],
- rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A,
- rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
- tcp: bool = False,
- source: Optional[str] = None,
- raise_on_no_answer: bool = True,
- source_port: int = 0,
- lifetime: Optional[float] = None,
- search: Optional[bool] = None,
- backend: Optional[dns.asyncbackend.Backend] = None,
- port: int = 53,
- family: int = socket.AF_UNSPEC,
- resolver: Optional[Resolver] = None,
- ) -> dns.resolver.Answer:
- """Query nameservers to find the answer to the question.
- This is a convenience function that calls ``dns.asyncresolver.make_resolver_at()``
- to make a resolver, and then uses it to resolve the query.
- See ``dns.asyncresolver.Resolver.resolve`` for more information on the resolution
- parameters, and ``dns.asyncresolver.make_resolver_at`` for information about the
- resolver parameters *where*, *port*, *family*, and *resolver*.
- If making more than one query, it is more efficient to call
- ``dns.asyncresolver.make_resolver_at()`` and then use that resolver for the queries
- instead of calling ``resolve_at()`` multiple times.
- """
- res = await make_resolver_at(where, port, family, resolver)
- return await res.resolve(
- qname,
- rdtype,
- rdclass,
- tcp,
- source,
- raise_on_no_answer,
- source_port,
- lifetime,
- search,
- backend,
- )
|