codekingpro/portable-devtools
114k
1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license2 3# Copyright (C) 2003-2017 Nominum, Inc.4#5# Permission to use, copy, modify, and distribute this software and its6# documentation for any purpose with or without fee is hereby granted,7# provided that the above copyright notice and this permission notice8# appear in all copies.9#10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.17 18"""DNS stub resolver."""19 20import contextlib21import random22import socket23import sys24import threading25import time26import warnings27from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union28from urllib.parse import urlparse29 30import dns._ddr31import dns.edns32import dns.exception33import dns.flags34import dns.inet35import dns.ipv436import dns.ipv637import dns.message38import dns.name39import dns.nameserver40import dns.query41import dns.rcode42import dns.rdataclass43import dns.rdatatype44import dns.rdtypes.svcbbase45import dns.reversename46import dns.tsig47 48if sys.platform == "win32":49 import dns.win32util50 51 52class NXDOMAIN(dns.exception.DNSException):53 """The DNS query name does not exist."""54 55 supp_kwargs = {"qnames", "responses"}56 fmt = None # we have our own __str__ implementation57 58 # pylint: disable=arguments-differ59 60 # We do this as otherwise mypy complains about unexpected keyword argument61 # idna_exception62 def __init__(self, *args, **kwargs):63 super().__init__(*args, **kwargs)64 65 def _check_kwargs(self, qnames, responses=None):66 if not isinstance(qnames, (list, tuple, set)):67 raise AttributeError("qnames must be a list, tuple or set")68 if len(qnames) == 0:69 raise AttributeError("qnames must contain at least one element")70 if responses is None:71 responses = {}72 elif not isinstance(responses, dict):73 raise AttributeError("responses must be a dict(qname=response)")74 kwargs = dict(qnames=qnames, responses=responses)75 return kwargs76 77 def __str__(self) -> str:78 if "qnames" not in self.kwargs:79 return super().__str__()80 qnames = self.kwargs["qnames"]81 if len(qnames) > 1:82 msg = "None of DNS query names exist"83 else:84 msg = "The DNS query name does not exist"85 qnames = ", ".join(map(str, qnames))86 return "{}: {}".format(msg, qnames)87 88 @property89 def canonical_name(self):90 """Return the unresolved canonical name."""91 if "qnames" not in self.kwargs:92 raise TypeError("parametrized exception required")93 for qname in self.kwargs["qnames"]:94 response = self.kwargs["responses"][qname]95 try:96 cname = response.canonical_name()97 if cname != qname:98 return cname99 except Exception:100 # We can just eat this exception as it means there was101 # something wrong with the response.102 pass103 return self.kwargs["qnames"][0]104 105 def __add__(self, e_nx):106 """Augment by results from another NXDOMAIN exception."""107 qnames0 = list(self.kwargs.get("qnames", []))108 responses0 = dict(self.kwargs.get("responses", {}))109 responses1 = e_nx.kwargs.get("responses", {})110 for qname1 in e_nx.kwargs.get("qnames", []):111 if qname1 not in qnames0:112 qnames0.append(qname1)113 if qname1 in responses1:114 responses0[qname1] = responses1[qname1]115 return NXDOMAIN(qnames=qnames0, responses=responses0)116 117 def qnames(self):118 """All of the names that were tried.119 120 Returns a list of ``dns.name.Name``.121 """122 return self.kwargs["qnames"]123 124 def responses(self):125 """A map from queried names to their NXDOMAIN responses.126 127 Returns a dict mapping a ``dns.name.Name`` to a128 ``dns.message.Message``.129 """130 return self.kwargs["responses"]131 132 def response(self, qname):133 """The response for query *qname*.134 135 Returns a ``dns.message.Message``.136 """137 return self.kwargs["responses"][qname]138 139 140class YXDOMAIN(dns.exception.DNSException):141 """The DNS query name is too long after DNAME substitution."""142 143 144ErrorTuple = Tuple[145 Optional[str],146 bool,147 int,148 Union[Exception, str],149 Optional[dns.message.Message],150]151 152 153def _errors_to_text(errors: List[ErrorTuple]) -> List[str]:154 """Turn a resolution errors trace into a list of text."""155 texts = []156 for err in errors:157 texts.append("Server {} answered {}".format(err[0], err[3]))158 return texts159 160 161class LifetimeTimeout(dns.exception.Timeout):162 """The resolution lifetime expired."""163 164 msg = "The resolution lifetime expired."165 fmt = "%s after {timeout:.3f} seconds: {errors}" % msg[:-1]166 supp_kwargs = {"timeout", "errors"}167 168 # We do this as otherwise mypy complains about unexpected keyword argument169 # idna_exception170 def __init__(self, *args, **kwargs):171 super().__init__(*args, **kwargs)172 173 def _fmt_kwargs(self, **kwargs):174 srv_msgs = _errors_to_text(kwargs["errors"])175 return super()._fmt_kwargs(176 timeout=kwargs["timeout"], errors="; ".join(srv_msgs)177 )178 179 180# We added more detail to resolution timeouts, but they are still181# subclasses of dns.exception.Timeout for backwards compatibility. We also182# keep dns.resolver.Timeout defined for backwards compatibility.183Timeout = LifetimeTimeout184 185 186class NoAnswer(dns.exception.DNSException):187 """The DNS response does not contain an answer to the question."""188 189 fmt = "The DNS response does not contain an answer to the question: {query}"190 supp_kwargs = {"response"}191 192 # We do this as otherwise mypy complains about unexpected keyword argument193 # idna_exception194 def __init__(self, *args, **kwargs):195 super().__init__(*args, **kwargs)196 197 def _fmt_kwargs(self, **kwargs):198 return super()._fmt_kwargs(query=kwargs["response"].question)199 200 def response(self):201 return self.kwargs["response"]202 203 204class NoNameservers(dns.exception.DNSException):205 """All nameservers failed to answer the query.206 207 errors: list of servers and respective errors208 The type of errors is209 [(server IP address, any object convertible to string)].210 Non-empty errors list will add explanatory message ()211 """212 213 msg = "All nameservers failed to answer the query."214 fmt = "%s {query}: {errors}" % msg[:-1]215 supp_kwargs = {"request", "errors"}216 217 # We do this as otherwise mypy complains about unexpected keyword argument218 # idna_exception219 def __init__(self, *args, **kwargs):220 super().__init__(*args, **kwargs)221 222 def _fmt_kwargs(self, **kwargs):223 srv_msgs = _errors_to_text(kwargs["errors"])224 return super()._fmt_kwargs(225 query=kwargs["request"].question, errors="; ".join(srv_msgs)226 )227 228 229class NotAbsolute(dns.exception.DNSException):230 """An absolute domain name is required but a relative name was provided."""231 232 233class NoRootSOA(dns.exception.DNSException):234 """There is no SOA RR at the DNS root name. This should never happen!"""235 236 237class NoMetaqueries(dns.exception.DNSException):238 """DNS metaqueries are not allowed."""239 240 241class NoResolverConfiguration(dns.exception.DNSException):242 """Resolver configuration could not be read or specified no nameservers."""243 244 245class Answer:246 """DNS stub resolver answer.247 248 Instances of this class bundle up the result of a successful DNS249 resolution.250 251 For convenience, the answer object implements much of the sequence252 protocol, forwarding to its ``rrset`` attribute. E.g.253 ``for a in answer`` is equivalent to ``for a in answer.rrset``.254 ``answer[i]`` is equivalent to ``answer.rrset[i]``, and255 ``answer[i:j]`` is equivalent to ``answer.rrset[i:j]``.256 257 Note that CNAMEs or DNAMEs in the response may mean that answer258 RRset's name might not be the query name.259 """260 261 def __init__(262 self,263 qname: dns.name.Name,264 rdtype: dns.rdatatype.RdataType,265 rdclass: dns.rdataclass.RdataClass,266 response: dns.message.QueryMessage,267 nameserver: Optional[str] = None,268 port: Optional[int] = None,269 ) -> None:270 self.qname = qname271 self.rdtype = rdtype272 self.rdclass = rdclass273 self.response = response274 self.nameserver = nameserver275 self.port = port276 self.chaining_result = response.resolve_chaining()277 # Copy some attributes out of chaining_result for backwards278 # compatibility and convenience.279 self.canonical_name = self.chaining_result.canonical_name280 self.rrset = self.chaining_result.answer281 self.expiration = time.time() + self.chaining_result.minimum_ttl282 283 def __getattr__(self, attr): # pragma: no cover284 if attr == "name":285 return self.rrset.name286 elif attr == "ttl":287 return self.rrset.ttl288 elif attr == "covers":289 return self.rrset.covers290 elif attr == "rdclass":291 return self.rrset.rdclass292 elif attr == "rdtype":293 return self.rrset.rdtype294 else:295 raise AttributeError(attr)296 297 def __len__(self) -> int:298 return self.rrset and len(self.rrset) or 0299 300 def __iter__(self):301 return self.rrset and iter(self.rrset) or iter(tuple())302 303 def __getitem__(self, i):304 if self.rrset is None:305 raise IndexError306 return self.rrset[i]307 308 def __delitem__(self, i):309 if self.rrset is None:310 raise IndexError311 del self.rrset[i]312 313 314class Answers(dict):315 """A dict of DNS stub resolver answers, indexed by type."""316 317 318class HostAnswers(Answers):319 """A dict of DNS stub resolver answers to a host name lookup, indexed by320 type.321 """322 323 @classmethod324 def make(325 cls,326 v6: Optional[Answer] = None,327 v4: Optional[Answer] = None,328 add_empty: bool = True,329 ) -> "HostAnswers":330 answers = HostAnswers()331 if v6 is not None and (add_empty or v6.rrset):332 answers[dns.rdatatype.AAAA] = v6333 if v4 is not None and (add_empty or v4.rrset):334 answers[dns.rdatatype.A] = v4335 return answers336 337 # Returns pairs of (address, family) from this result, potentiallys338 # filtering by address family.339 def addresses_and_families(340 self, family: int = socket.AF_UNSPEC341 ) -> Iterator[Tuple[str, int]]:342 if family == socket.AF_UNSPEC:343 yield from self.addresses_and_families(socket.AF_INET6)344 yield from self.addresses_and_families(socket.AF_INET)345 return346 elif family == socket.AF_INET6:347 answer = self.get(dns.rdatatype.AAAA)348 elif family == socket.AF_INET:349 answer = self.get(dns.rdatatype.A)350 else:351 raise NotImplementedError(f"unknown address family {family}")352 if answer:353 for rdata in answer:354 yield (rdata.address, family)355 356 # Returns addresses from this result, potentially filtering by357 # address family.358 def addresses(self, family: int = socket.AF_UNSPEC) -> Iterator[str]:359 return (pair[0] for pair in self.addresses_and_families(family))360 361 # Returns the canonical name from this result.362 def canonical_name(self) -> dns.name.Name:363 answer = self.get(dns.rdatatype.AAAA, self.get(dns.rdatatype.A))364 return answer.canonical_name365 366 367class CacheStatistics:368 """Cache Statistics"""369 370 def __init__(self, hits: int = 0, misses: int = 0) -> None:371 self.hits = hits372 self.misses = misses373 374 def reset(self) -> None:375 self.hits = 0376 self.misses = 0377 378 def clone(self) -> "CacheStatistics":379 return CacheStatistics(self.hits, self.misses)380 381 382class CacheBase:383 def __init__(self) -> None:384 self.lock = threading.Lock()385 self.statistics = CacheStatistics()386 387 def reset_statistics(self) -> None:388 """Reset all statistics to zero."""389 with self.lock:390 self.statistics.reset()391 392 def hits(self) -> int:393 """How many hits has the cache had?"""394 with self.lock:395 return self.statistics.hits396 397 def misses(self) -> int:398 """How many misses has the cache had?"""399 with self.lock:400 return self.statistics.misses401 402 def get_statistics_snapshot(self) -> CacheStatistics:403 """Return a consistent snapshot of all the statistics.404 405 If running with multiple threads, it's better to take a406 snapshot than to call statistics methods such as hits() and407 misses() individually.408 """409 with self.lock:410 return self.statistics.clone()411 412 413CacheKey = Tuple[dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass]414 415 416class Cache(CacheBase):417 """Simple thread-safe DNS answer cache."""418 419 def __init__(self, cleaning_interval: float = 300.0) -> None:420 """*cleaning_interval*, a ``float`` is the number of seconds between421 periodic cleanings.422 """423 424 super().__init__()425 self.data: Dict[CacheKey, Answer] = {}426 self.cleaning_interval = cleaning_interval427 self.next_cleaning: float = time.time() + self.cleaning_interval428 429 def _maybe_clean(self) -> None:430 """Clean the cache if it's time to do so."""431 432 now = time.time()433 if self.next_cleaning <= now:434 keys_to_delete = []435 for k, v in self.data.items():436 if v.expiration <= now:437 keys_to_delete.append(k)438 for k in keys_to_delete:439 del self.data[k]440 now = time.time()441 self.next_cleaning = now + self.cleaning_interval442 443 def get(self, key: CacheKey) -> Optional[Answer]:444 """Get the answer associated with *key*.445 446 Returns None if no answer is cached for the key.447 448 *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``449 tuple whose values are the query name, rdtype, and rdclass respectively.450 451 Returns a ``dns.resolver.Answer`` or ``None``.452 """453 454 with self.lock:455 self._maybe_clean()456 v = self.data.get(key)457 if v is None or v.expiration <= time.time():458 self.statistics.misses += 1459 return None460 self.statistics.hits += 1461 return v462 463 def put(self, key: CacheKey, value: Answer) -> None:464 """Associate key and value in the cache.465 466 *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``467 tuple whose values are the query name, rdtype, and rdclass respectively.468 469 *value*, a ``dns.resolver.Answer``, the answer.470 """471 472 with self.lock:473 self._maybe_clean()474 self.data[key] = value475 476 def flush(self, key: Optional[CacheKey] = None) -> None:477 """Flush the cache.478 479 If *key* is not ``None``, only that item is flushed. Otherwise the entire cache480 is flushed.481 482 *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``483 tuple whose values are the query name, rdtype, and rdclass respectively.484 """485 486 with self.lock:487 if key is not None:488 if key in self.data:489 del self.data[key]490 else:491 self.data = {}492 self.next_cleaning = time.time() + self.cleaning_interval493 494 495class LRUCacheNode:496 """LRUCache node."""497 498 def __init__(self, key, value):499 self.key = key500 self.value = value501 self.hits = 0502 self.prev = self503 self.next = self504 505 def link_after(self, node: "LRUCacheNode") -> None:506 self.prev = node507 self.next = node.next508 node.next.prev = self509 node.next = self510 511 def unlink(self) -> None:512 self.next.prev = self.prev513 self.prev.next = self.next514 515 516class LRUCache(CacheBase):517 """Thread-safe, bounded, least-recently-used DNS answer cache.518 519 This cache is better than the simple cache (above) if you're520 running a web crawler or other process that does a lot of521 resolutions. The LRUCache has a maximum number of nodes, and when522 it is full, the least-recently used node is removed to make space523 for a new one.524 """525 526 def __init__(self, max_size: int = 100000) -> None:527 """*max_size*, an ``int``, is the maximum number of nodes to cache;528 it must be greater than 0.529 """530 531 super().__init__()532 self.data: Dict[CacheKey, LRUCacheNode] = {}533 self.set_max_size(max_size)534 self.sentinel: LRUCacheNode = LRUCacheNode(None, None)535 self.sentinel.prev = self.sentinel536 self.sentinel.next = self.sentinel537 538 def set_max_size(self, max_size: int) -> None:539 if max_size < 1:540 max_size = 1541 self.max_size = max_size542 543 def get(self, key: CacheKey) -> Optional[Answer]:544 """Get the answer associated with *key*.545 546 Returns None if no answer is cached for the key.547 548 *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``549 tuple whose values are the query name, rdtype, and rdclass respectively.550 551 Returns a ``dns.resolver.Answer`` or ``None``.552 """553 554 with self.lock:555 node = self.data.get(key)556 if node is None:557 self.statistics.misses += 1558 return None559 # Unlink because we're either going to move the node to the front560 # of the LRU list or we're going to free it.561 node.unlink()562 if node.value.expiration <= time.time():563 del self.data[node.key]564 self.statistics.misses += 1565 return None566 node.link_after(self.sentinel)567 self.statistics.hits += 1568 node.hits += 1569 return node.value570 571 def get_hits_for_key(self, key: CacheKey) -> int:572 """Return the number of cache hits associated with the specified key."""573 with self.lock:574 node = self.data.get(key)575 if node is None or node.value.expiration <= time.time():576 return 0577 else:578 return node.hits579 580 def put(self, key: CacheKey, value: Answer) -> None:581 """Associate key and value in the cache.582 583 *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``584 tuple whose values are the query name, rdtype, and rdclass respectively.585 586 *value*, a ``dns.resolver.Answer``, the answer.587 """588 589 with self.lock:590 node = self.data.get(key)591 if node is not None:592 node.unlink()593 del self.data[node.key]594 while len(self.data) >= self.max_size:595 gnode = self.sentinel.prev596 gnode.unlink()597 del self.data[gnode.key]598 node = LRUCacheNode(key, value)599 node.link_after(self.sentinel)600 self.data[key] = node601 602 def flush(self, key: Optional[CacheKey] = None) -> None:603 """Flush the cache.604 605 If *key* is not ``None``, only that item is flushed. Otherwise the entire cache606 is flushed.607 608 *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)``609 tuple whose values are the query name, rdtype, and rdclass respectively.610 """611 612 with self.lock:613 if key is not None:614 node = self.data.get(key)615 if node is not None:616 node.unlink()617 del self.data[node.key]618 else:619 gnode = self.sentinel.next620 while gnode != self.sentinel:621 next = gnode.next622 gnode.unlink()623 gnode = next624 self.data = {}625 626 627class _Resolution:628 """Helper class for dns.resolver.Resolver.resolve().629 630 All of the "business logic" of resolution is encapsulated in this631 class, allowing us to have multiple resolve() implementations632 using different I/O schemes without copying all of the633 complicated logic.634 635 This class is a "friend" to dns.resolver.Resolver and manipulates636 resolver data structures directly.637 """638 639 def __init__(640 self,641 resolver: "BaseResolver",642 qname: Union[dns.name.Name, str],643 rdtype: Union[dns.rdatatype.RdataType, str],644 rdclass: Union[dns.rdataclass.RdataClass, str],645 tcp: bool,646 raise_on_no_answer: bool,647 search: Optional[bool],648 ) -> None:649 if isinstance(qname, str):650 qname = dns.name.from_text(qname, None)651 rdtype = dns.rdatatype.RdataType.make(rdtype)652 if dns.rdatatype.is_metatype(rdtype):653 raise NoMetaqueries654 rdclass = dns.rdataclass.RdataClass.make(rdclass)655 if dns.rdataclass.is_metaclass(rdclass):656 raise NoMetaqueries657 self.resolver = resolver658 self.qnames_to_try = resolver._get_qnames_to_try(qname, search)659 self.qnames = self.qnames_to_try[:]660 self.rdtype = rdtype661 self.rdclass = rdclass662 self.tcp = tcp663 self.raise_on_no_answer = raise_on_no_answer664 self.nxdomain_responses: Dict[dns.name.Name, dns.message.QueryMessage] = {}665 # Initialize other things to help analysis tools666 self.qname = dns.name.empty667 self.nameservers: List[dns.nameserver.Nameserver] = []668 self.current_nameservers: List[dns.nameserver.Nameserver] = []669 self.errors: List[ErrorTuple] = []670 self.nameserver: Optional[dns.nameserver.Nameserver] = None671 self.tcp_attempt = False672 self.retry_with_tcp = False673 self.request: Optional[dns.message.QueryMessage] = None674 self.backoff = 0.0675 676 def next_request(677 self,678 ) -> Tuple[Optional[dns.message.QueryMessage], Optional[Answer]]:679 """Get the next request to send, and check the cache.680 681 Returns a (request, answer) tuple. At most one of request or682 answer will not be None.683 """684 685 # We return a tuple instead of Union[Message,Answer] as it lets686 # the caller avoid isinstance().687 688 while len(self.qnames) > 0:689 self.qname = self.qnames.pop(0)690 691 # Do we know the answer?692 if self.resolver.cache:693 answer = self.resolver.cache.get(694 (self.qname, self.rdtype, self.rdclass)695 )696 if answer is not None:697 if answer.rrset is None and self.raise_on_no_answer:698 raise NoAnswer(response=answer.response)699 else:700 return (None, answer)701 answer = self.resolver.cache.get(702 (self.qname, dns.rdatatype.ANY, self.rdclass)703 )704 if answer is not None and answer.response.rcode() == dns.rcode.NXDOMAIN:705 # cached NXDOMAIN; record it and continue to next706 # name.707 self.nxdomain_responses[self.qname] = answer.response708 continue709 710 # Build the request711 request = dns.message.make_query(self.qname, self.rdtype, self.rdclass)712 if self.resolver.keyname is not None:713 request.use_tsig(714 self.resolver.keyring,715 self.resolver.keyname,716 algorithm=self.resolver.keyalgorithm,717 )718 request.use_edns(719 self.resolver.edns,720 self.resolver.ednsflags,721 self.resolver.payload,722 options=self.resolver.ednsoptions,723 )724 if self.resolver.flags is not None:725 request.flags = self.resolver.flags726 727 self.nameservers = self.resolver._enrich_nameservers(728 self.resolver._nameservers,729 self.resolver.nameserver_ports,730 self.resolver.port,731 )732 if self.resolver.rotate:733 random.shuffle(self.nameservers)734 self.current_nameservers = self.nameservers[:]735 self.errors = []736 self.nameserver = None737 self.tcp_attempt = False738 self.retry_with_tcp = False739 self.request = request740 self.backoff = 0.10741 742 return (request, None)743 744 #745 # We've tried everything and only gotten NXDOMAINs. (We know746 # it's only NXDOMAINs as anything else would have returned747 # before now.)748 #749 raise NXDOMAIN(qnames=self.qnames_to_try, responses=self.nxdomain_responses)750 751 def next_nameserver(self) -> Tuple[dns.nameserver.Nameserver, bool, float]:752 if self.retry_with_tcp:753 assert self.nameserver is not None754 assert not self.nameserver.is_always_max_size()755 self.tcp_attempt = True756 self.retry_with_tcp = False757 return (self.nameserver, True, 0)758 759 backoff = 0.0760 if not self.current_nameservers:761 if len(self.nameservers) == 0:762 # Out of things to try!763 raise NoNameservers(request=self.request, errors=self.errors)764 self.current_nameservers = self.nameservers[:]765 backoff = self.backoff766 self.backoff = min(self.backoff * 2, 2)767 768 self.nameserver = self.current_nameservers.pop(0)769 self.tcp_attempt = self.tcp or self.nameserver.is_always_max_size()770 return (self.nameserver, self.tcp_attempt, backoff)771 772 def query_result(773 self, response: Optional[dns.message.Message], ex: Optional[Exception]774 ) -> Tuple[Optional[Answer], bool]:775 #776 # returns an (answer: Answer, end_loop: bool) tuple.777 #778 assert self.nameserver is not None779 if ex:780 # Exception during I/O or from_wire()781 assert response is None782 self.errors.append(783 (784 str(self.nameserver),785 self.tcp_attempt,786 self.nameserver.answer_port(),787 ex,788 response,789 )790 )791 if (792 isinstance(ex, dns.exception.FormError)793 or isinstance(ex, EOFError)794 or isinstance(ex, OSError)795 or isinstance(ex, NotImplementedError)796 ):797 # This nameserver is no good, take it out of the mix.798 self.nameservers.remove(self.nameserver)799 elif isinstance(ex, dns.message.Truncated):800 if self.tcp_attempt:801 # Truncation with TCP is no good!802 self.nameservers.remove(self.nameserver)803 else:804 self.retry_with_tcp = True805 return (None, False)806 # We got an answer!807 assert response is not None808 assert isinstance(response, dns.message.QueryMessage)809 rcode = response.rcode()810 if rcode == dns.rcode.NOERROR:811 try:812 answer = Answer(813 self.qname,814 self.rdtype,815 self.rdclass,816 response,817 self.nameserver.answer_nameserver(),818 self.nameserver.answer_port(),819 )820 except Exception as e:821 self.errors.append(822 (823 str(self.nameserver),824 self.tcp_attempt,825 self.nameserver.answer_port(),826 e,827 response,828 )829 )830 # The nameserver is no good, take it out of the mix.831 self.nameservers.remove(self.nameserver)832 return (None, False)833 if self.resolver.cache:834 self.resolver.cache.put((self.qname, self.rdtype, self.rdclass), answer)835 if answer.rrset is None and self.raise_on_no_answer:836 raise NoAnswer(response=answer.response)837 return (answer, True)838 elif rcode == dns.rcode.NXDOMAIN:839 # Further validate the response by making an Answer, even840 # if we aren't going to cache it.841 try:842 answer = Answer(843 self.qname, dns.rdatatype.ANY, dns.rdataclass.IN, response844 )845 except Exception as e:846 self.errors.append(847 (848 str(self.nameserver),849 self.tcp_attempt,850 self.nameserver.answer_port(),851 e,852 response,853 )854 )855 # The nameserver is no good, take it out of the mix.856 self.nameservers.remove(self.nameserver)857 return (None, False)858 self.nxdomain_responses[self.qname] = response859 if self.resolver.cache:860 self.resolver.cache.put(861 (self.qname, dns.rdatatype.ANY, self.rdclass), answer862 )863 # Make next_nameserver() return None, so caller breaks its864 # inner loop and calls next_request().865 return (None, True)866 elif rcode == dns.rcode.YXDOMAIN:867 yex = YXDOMAIN()868 self.errors.append(869 (870 str(self.nameserver),871 self.tcp_attempt,872 self.nameserver.answer_port(),873 yex,874 response,875 )876 )877 raise yex878 else:879 #880 # We got a response, but we're not happy with the881 # rcode in it.882 #883 if rcode != dns.rcode.SERVFAIL or not self.resolver.retry_servfail:884 self.nameservers.remove(self.nameserver)885 self.errors.append(886 (887 str(self.nameserver),888 self.tcp_attempt,889 self.nameserver.answer_port(),890 dns.rcode.to_text(rcode),891 response,892 )893 )894 return (None, False)895 896 897class BaseResolver:898 """DNS stub resolver."""899 900 # We initialize in reset()901 #902 # pylint: disable=attribute-defined-outside-init903 904 domain: dns.name.Name905 nameserver_ports: Dict[str, int]906 port: int907 search: List[dns.name.Name]908 use_search_by_default: bool909 timeout: float910 lifetime: float911 keyring: Optional[Any]912 keyname: Optional[Union[dns.name.Name, str]]913 keyalgorithm: Union[dns.name.Name, str]914 edns: int915 ednsflags: int916 ednsoptions: Optional[List[dns.edns.Option]]917 payload: int918 cache: Any919 flags: Optional[int]920 retry_servfail: bool921 rotate: bool922 ndots: Optional[int]923 _nameservers: Sequence[Union[str, dns.nameserver.Nameserver]]924 925 def __init__(926 self, filename: str = "/etc/resolv.conf", configure: bool = True927 ) -> None:928 """*filename*, a ``str`` or file object, specifying a file929 in standard /etc/resolv.conf format. This parameter is meaningful930 only when *configure* is true and the platform is POSIX.931 932 *configure*, a ``bool``. If True (the default), the resolver933 instance is configured in the normal fashion for the operating934 system the resolver is running on. (I.e. by reading a935 /etc/resolv.conf file on POSIX systems and from the registry936 on Windows systems.)937 """938 939 self.reset()940 if configure:941 if sys.platform == "win32":942 self.read_registry()943 elif filename:944 self.read_resolv_conf(filename)945 946 def reset(self) -> None:947 """Reset all resolver configuration to the defaults."""948 949 self.domain = dns.name.Name(dns.name.from_text(socket.gethostname())[1:])950 if len(self.domain) == 0:951 self.domain = dns.name.root952 self._nameservers = []953 self.nameserver_ports = {}954 self.port = 53955 self.search = []956 self.use_search_by_default = False957 self.timeout = 2.0958 self.lifetime = 5.0959 self.keyring = None960 self.keyname = None961 self.keyalgorithm = dns.tsig.default_algorithm962 self.edns = -1963 self.ednsflags = 0964 self.ednsoptions = None965 self.payload = 0966 self.cache = None967 self.flags = None968 self.retry_servfail = False969 self.rotate = False970 self.ndots = None971 972 def read_resolv_conf(self, f: Any) -> None:973 """Process *f* as a file in the /etc/resolv.conf format. If f is974 a ``str``, it is used as the name of the file to open; otherwise it975 is treated as the file itself.976 977 Interprets the following items:978 979 - nameserver - name server IP address980 981 - domain - local domain name982 983 - search - search list for host-name lookup984 985 - options - supported options are rotate, timeout, edns0, and ndots986 987 """988 989 nameservers = []990 if isinstance(f, str):991 try:992 cm: contextlib.AbstractContextManager = open(f)993 except OSError:994 # /etc/resolv.conf doesn't exist, can't be read, etc.995 raise NoResolverConfiguration(f"cannot open {f}")996 else:997 cm = contextlib.nullcontext(f)998 with cm as f:999 for l in f:1000 if len(l) == 0 or l[0] == "#" or l[0] == ";":1001 continue1002 tokens = l.split()1003 1004 # Any line containing less than 2 tokens is malformed1005 if len(tokens) < 2:1006 continue1007 1008 if tokens[0] == "nameserver":1009 nameservers.append(tokens[1])1010 elif tokens[0] == "domain":1011 self.domain = dns.name.from_text(tokens[1])1012 # domain and search are exclusive1013 self.search = []1014 elif tokens[0] == "search":1015 # the last search wins1016 self.search = []1017 for suffix in tokens[1:]:1018 self.search.append(dns.name.from_text(suffix))1019 # We don't set domain as it is not used if1020 # len(self.search) > 01021 elif tokens[0] == "options":1022 for opt in tokens[1:]:1023 if opt == "rotate":1024 self.rotate = True1025 elif opt == "edns0":1026 self.use_edns()1027 elif "timeout" in opt:1028 try:1029 self.timeout = int(opt.split(":")[1])1030 except (ValueError, IndexError):1031 pass1032 elif "ndots" in opt:1033 try:1034 self.ndots = int(opt.split(":")[1])1035 except (ValueError, IndexError):1036 pass1037 if len(nameservers) == 0:1038 raise NoResolverConfiguration("no nameservers")1039 # Assigning directly instead of appending means we invoke the1040 # setter logic, with additonal checking and enrichment.1041 self.nameservers = nameservers1042 1043 def read_registry(self) -> None:1044 """Extract resolver configuration from the Windows registry."""1045 try:1046 info = dns.win32util.get_dns_info() # type: ignore1047 if info.domain is not None:1048 self.domain = info.domain1049 self.nameservers = info.nameservers1050 self.search = info.search1051 except AttributeError:1052 raise NotImplementedError1053 1054 def _compute_timeout(1055 self,1056 start: float,1057 lifetime: Optional[float] = None,1058 errors: Optional[List[ErrorTuple]] = None,1059 ) -> float:1060 lifetime = self.lifetime if lifetime is None else lifetime1061 now = time.time()1062 duration = now - start1063 if errors is None:1064 errors = []1065 if duration < 0:1066 if duration < -1:1067 # Time going backwards is bad. Just give up.1068 raise LifetimeTimeout(timeout=duration, errors=errors)1069 else:1070 # Time went backwards, but only a little. This can1071 # happen, e.g. under vmware with older linux kernels.1072 # Pretend it didn't happen.1073 duration = 01074 if duration >= lifetime:1075 raise LifetimeTimeout(timeout=duration, errors=errors)1076 return min(lifetime - duration, self.timeout)1077 1078 def _get_qnames_to_try(1079 self, qname: dns.name.Name, search: Optional[bool]1080 ) -> List[dns.name.Name]:1081 # This is a separate method so we can unit test the search1082 # rules without requiring the Internet.1083 if search is None:1084 search = self.use_search_by_default1085 qnames_to_try = []1086 if qname.is_absolute():1087 qnames_to_try.append(qname)1088 else:1089 abs_qname = qname.concatenate(dns.name.root)1090 if search:1091 if len(self.search) > 0:1092 # There is a search list, so use it exclusively1093 search_list = self.search[:]1094 elif self.domain != dns.name.root and self.domain is not None:1095 # We have some notion of a domain that isn't the root, so1096 # use it as the search list.1097 search_list = [self.domain]1098 else:1099 search_list = []1100 # Figure out the effective ndots (default is 1)1101 if self.ndots is None:1102 ndots = 11103 else:1104 ndots = self.ndots1105 for suffix in search_list:1106 qnames_to_try.append(qname + suffix)1107 if len(qname) > ndots:1108 # The name has at least ndots dots, so we should try an1109 # absolute query first.1110 qnames_to_try.insert(0, abs_qname)1111 else:1112 # The name has less than ndots dots, so we should search1113 # first, then try the absolute name.1114 qnames_to_try.append(abs_qname)1115 else:1116 qnames_to_try.append(abs_qname)1117 return qnames_to_try1118 1119 def use_tsig(1120 self,1121 keyring: Any,1122 keyname: Optional[Union[dns.name.Name, str]] = None,1123 algorithm: Union[dns.name.Name, str] = dns.tsig.default_algorithm,1124 ) -> None:1125 """Add a TSIG signature to each query.1126 1127 The parameters are passed to ``dns.message.Message.use_tsig()``;1128 see its documentation for details.1129 """1130 1131 self.keyring = keyring1132 self.keyname = keyname1133 self.keyalgorithm = algorithm1134 1135 def use_edns(1136 self,1137 edns: Optional[Union[int, bool]] = 0,1138 ednsflags: int = 0,1139 payload: int = dns.message.DEFAULT_EDNS_PAYLOAD,1140 options: Optional[List[dns.edns.Option]] = None,1141 ) -> None:1142 """Configure EDNS behavior.1143 1144 *edns*, an ``int``, is the EDNS level to use. Specifying1145 ``None``, ``False``, or ``-1`` means "do not use EDNS", and in this case1146 the other parameters are ignored. Specifying ``True`` is1147 equivalent to specifying 0, i.e. "use EDNS0".1148 1149 *ednsflags*, an ``int``, the EDNS flag values.1150 1151 *payload*, an ``int``, is the EDNS sender's payload field, which is the1152 maximum size of UDP datagram the sender can handle. I.e. how big1153 a response to this message can be.1154 1155 *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS1156 options.1157 """1158 1159 if edns is None or edns is False:1160 edns = -11161 elif edns is True:1162 edns = 01163 self.edns = edns1164 self.ednsflags = ednsflags1165 self.payload = payload1166 self.ednsoptions = options1167 1168 def set_flags(self, flags: int) -> None:1169 """Overrides the default flags with your own.1170 1171 *flags*, an ``int``, the message flags to use.1172 """1173 1174 self.flags = flags1175 1176 @classmethod1177 def _enrich_nameservers(1178 cls,1179 nameservers: Sequence[Union[str, dns.nameserver.Nameserver]],1180 nameserver_ports: Dict[str, int],1181 default_port: int,1182 ) -> List[dns.nameserver.Nameserver]:1183 enriched_nameservers = []1184 if isinstance(nameservers, list):1185 for nameserver in nameservers:1186 enriched_nameserver: dns.nameserver.Nameserver1187 if isinstance(nameserver, dns.nameserver.Nameserver):1188 enriched_nameserver = nameserver1189 elif dns.inet.is_address(nameserver):1190 port = nameserver_ports.get(nameserver, default_port)1191 enriched_nameserver = dns.nameserver.Do53Nameserver(1192 nameserver, port1193 )1194 else:1195 try:1196 if urlparse(nameserver).scheme != "https":1197 raise NotImplementedError1198 except Exception:1199 raise ValueError(1200 f"nameserver {nameserver} is not a "