diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py3/twisted/names | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py3/twisted/names')
16 files changed, 7161 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py3/twisted/names/__init__.py b/contrib/python/Twisted/py3/twisted/names/__init__.py new file mode 100644 index 0000000000..ccdf8ba331 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Twisted Names: DNS server and client implementations. +""" diff --git a/contrib/python/Twisted/py3/twisted/names/_rfc1982.py b/contrib/python/Twisted/py3/twisted/names/_rfc1982.py new file mode 100644 index 0000000000..61d43c009b --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/_rfc1982.py @@ -0,0 +1,261 @@ +# -*- test-case-name: twisted.names.test.test_rfc1982 -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Utilities for handling RFC1982 Serial Number Arithmetic. + +@see: U{http://tools.ietf.org/html/rfc1982} + +@var RFC4034_TIME_FORMAT: RRSIG Time field presentation format. The Signature + Expiration Time and Inception Time field values MUST be represented either + as an unsigned decimal integer indicating seconds since 1 January 1970 + 00:00:00 UTC, or in the form YYYYMMDDHHmmSS in UTC. See U{RRSIG Presentation + Format<https://tools.ietf.org/html/rfc4034#section-3.2>} +""" + + +import calendar +from datetime import datetime, timedelta + +from twisted.python.compat import nativeString +from twisted.python.util import FancyStrMixin + +RFC4034_TIME_FORMAT = "%Y%m%d%H%M%S" + + +class SerialNumber(FancyStrMixin): + """ + An RFC1982 Serial Number. + + This class implements RFC1982 DNS Serial Number Arithmetic. + + SNA is used in DNS and specifically in DNSSEC as defined in RFC4034 in the + DNSSEC Signature Expiration and Inception Fields. + + @see: U{https://tools.ietf.org/html/rfc1982} + @see: U{https://tools.ietf.org/html/rfc4034} + + @ivar _serialBits: See C{serialBits} of L{__init__}. + @ivar _number: See C{number} of L{__init__}. + @ivar _modulo: The value at which wrapping will occur. + @ivar _halfRing: Half C{_modulo}. If another L{SerialNumber} value is larger + than this, it would lead to a wrapped value which is larger than the + first and comparisons are therefore ambiguous. + @ivar _maxAdd: Half C{_modulo} plus 1. If another L{SerialNumber} value is + larger than this, it would lead to a wrapped value which is larger than + the first. Comparisons with the original value would therefore be + ambiguous. + """ + + showAttributes = ( + ("_number", "number", "%d"), + ("_serialBits", "serialBits", "%d"), + ) + + def __init__(self, number: int, serialBits: int = 32): + """ + Construct an L{SerialNumber} instance. + + @param number: An L{int} which will be stored as the modulo + C{number % 2 ^ serialBits} + @type number: L{int} + + @param serialBits: The size of the serial number space. The power of two + which results in one larger than the largest integer corresponding + to a serial number value. + @type serialBits: L{int} + """ + self._serialBits = serialBits + self._modulo = 2**serialBits + self._halfRing: int = 2 ** (serialBits - 1) + self._maxAdd = 2 ** (serialBits - 1) - 1 + self._number: int = int(number) % self._modulo + + def _convertOther(self, other: object) -> "SerialNumber": + """ + Check that a foreign object is suitable for use in the comparison or + arithmetic magic methods of this L{SerialNumber} instance. Raise + L{TypeError} if not. + + @param other: The foreign L{object} to be checked. + @return: C{other} after compatibility checks and possible coercion. + @raise TypeError: If C{other} is not compatible. + """ + if not isinstance(other, SerialNumber): + raise TypeError(f"cannot compare or combine {self!r} and {other!r}") + + if self._serialBits != other._serialBits: + raise TypeError( + "cannot compare or combine SerialNumber instances with " + "different serialBits. %r and %r" % (self, other) + ) + + return other + + def __str__(self) -> str: + """ + Return a string representation of this L{SerialNumber} instance. + + @rtype: L{nativeString} + """ + return nativeString("%d" % (self._number,)) + + def __int__(self): + """ + @return: The integer value of this L{SerialNumber} instance. + @rtype: L{int} + """ + return self._number + + def __eq__(self, other: object) -> bool: + """ + Allow rich equality comparison with another L{SerialNumber} instance. + """ + try: + other = self._convertOther(other) + except TypeError: + return NotImplemented + return other._number == self._number + + def __lt__(self, other: object) -> bool: + """ + Allow I{less than} comparison with another L{SerialNumber} instance. + """ + try: + other = self._convertOther(other) + except TypeError: + return NotImplemented + return ( + self._number < other._number + and (other._number - self._number) < self._halfRing + ) or ( + self._number > other._number + and (self._number - other._number) > self._halfRing + ) + + def __gt__(self, other: object) -> bool: + """ + Allow I{greater than} comparison with another L{SerialNumber} instance. + """ + try: + other_sn = self._convertOther(other) + except TypeError: + return NotImplemented + return ( + self._number < other_sn._number + and (other_sn._number - self._number) > self._halfRing + ) or ( + self._number > other_sn._number + and (self._number - other_sn._number) < self._halfRing + ) + + def __le__(self, other: object) -> bool: + """ + Allow I{less than or equal} comparison with another L{SerialNumber} + instance. + """ + try: + other = self._convertOther(other) + except TypeError: + return NotImplemented + return self == other or self < other + + def __ge__(self, other: object) -> bool: + """ + Allow I{greater than or equal} comparison with another L{SerialNumber} + instance. + """ + try: + other = self._convertOther(other) + except TypeError: + return NotImplemented + return self == other or self > other + + def __add__(self, other: object) -> "SerialNumber": + """ + Allow I{addition} with another L{SerialNumber} instance. + + Serial numbers may be incremented by the addition of a positive + integer n, where n is taken from the range of integers + [0 .. (2^(SERIAL_BITS - 1) - 1)]. For a sequence number s, the + result of such an addition, s', is defined as + + s' = (s + n) modulo (2 ^ SERIAL_BITS) + + where the addition and modulus operations here act upon values that are + non-negative values of unbounded size in the usual ways of integer + arithmetic. + + Addition of a value outside the range + [0 .. (2^(SERIAL_BITS - 1) - 1)] is undefined. + + @see: U{http://tools.ietf.org/html/rfc1982#section-3.1} + + @raise ArithmeticError: If C{other} is more than C{_maxAdd} + ie more than half the maximum value of this serial number. + """ + try: + other = self._convertOther(other) + except TypeError: + return NotImplemented + if other._number <= self._maxAdd: + return SerialNumber( + (self._number + other._number) % self._modulo, + serialBits=self._serialBits, + ) + else: + raise ArithmeticError( + "value %r outside the range 0 .. %r" + % ( + other._number, + self._maxAdd, + ) + ) + + def __hash__(self): + """ + Allow L{SerialNumber} instances to be hashed for use as L{dict} keys. + + @rtype: L{int} + """ + return hash(self._number) + + @classmethod + def fromRFC4034DateString(cls, utcDateString): + """ + Create an L{SerialNumber} instance from a date string in format + 'YYYYMMDDHHMMSS' described in U{RFC4034 + 3.2<https://tools.ietf.org/html/rfc4034#section-3.2>}. + + The L{SerialNumber} instance stores the date as a 32bit UNIX timestamp. + + @see: U{https://tools.ietf.org/html/rfc4034#section-3.1.5} + + @param utcDateString: A UTC date/time string of format I{YYMMDDhhmmss} + which will be converted to seconds since the UNIX epoch. + @type utcDateString: L{unicode} + + @return: An L{SerialNumber} instance containing the supplied date as a + 32bit UNIX timestamp. + """ + parsedDate = datetime.strptime(utcDateString, RFC4034_TIME_FORMAT) + secondsSinceEpoch = calendar.timegm(parsedDate.utctimetuple()) + return cls(secondsSinceEpoch, serialBits=32) + + def toRFC4034DateString(self): + """ + Calculate a date by treating the current L{SerialNumber} value as a UNIX + timestamp and return a date string in the format described in + U{RFC4034 3.2<https://tools.ietf.org/html/rfc4034#section-3.2>}. + + @return: The date string. + """ + # Can't use datetime.utcfromtimestamp, because it seems to overflow the + # signed 32bit int used in the underlying C library. SNA is unsigned + # and capable of handling all timestamps up to 2**32. + d = datetime(1970, 1, 1) + timedelta(seconds=self._number) + return nativeString(d.strftime(RFC4034_TIME_FORMAT)) + + +__all__ = ["SerialNumber"] diff --git a/contrib/python/Twisted/py3/twisted/names/authority.py b/contrib/python/Twisted/py3/twisted/names/authority.py new file mode 100644 index 0000000000..33df6c0068 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/authority.py @@ -0,0 +1,503 @@ +# -*- test-case-name: twisted.names.test.test_names -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Authoritative resolvers. +""" + + +import os +import time + +from twisted.internet import defer +from twisted.names import common, dns, error +from twisted.python import failure +from twisted.python.compat import execfile, nativeString +from twisted.python.filepath import FilePath + + +def getSerial(filename="/tmp/twisted-names.serial"): + """ + Return a monotonically increasing (across program runs) integer. + + State is stored in the given file. If it does not exist, it is + created with rw-/---/--- permissions. + + This manipulates process-global state by calling C{os.umask()}, so it isn't + thread-safe. + + @param filename: Path to a file that is used to store the state across + program runs. + @type filename: L{str} + + @return: a monotonically increasing number + @rtype: L{str} + """ + serial = time.strftime("%Y%m%d") + + o = os.umask(0o177) + try: + if not os.path.exists(filename): + with open(filename, "w") as f: + f.write(serial + " 0") + finally: + os.umask(o) + + with open(filename) as serialFile: + lastSerial, zoneID = serialFile.readline().split() + + zoneID = (lastSerial == serial) and (int(zoneID) + 1) or 0 + + with open(filename, "w") as serialFile: + serialFile.write("%s %d" % (serial, zoneID)) + + serial = serial + ("%02d" % (zoneID,)) + return serial + + +class FileAuthority(common.ResolverBase): + """ + An Authority that is loaded from a file. + + This is an abstract class that implements record search logic. To create + a functional resolver, subclass it and override the L{loadFile} method. + + @ivar _ADDITIONAL_PROCESSING_TYPES: Record types for which additional + processing will be done. + + @ivar _ADDRESS_TYPES: Record types which are useful for inclusion in the + additional section generated during additional processing. + + @ivar soa: A 2-tuple containing the SOA domain name as a L{bytes} and a + L{dns.Record_SOA}. + + @ivar records: A mapping of domains (as lowercased L{bytes}) to records. + @type records: L{dict} with L{bytes} keys + """ + + # See https://twistedmatrix.com/trac/ticket/6650 + _ADDITIONAL_PROCESSING_TYPES = (dns.CNAME, dns.MX, dns.NS) + _ADDRESS_TYPES = (dns.A, dns.AAAA) + + soa = None + records = None + + def __init__(self, filename): + common.ResolverBase.__init__(self) + self.loadFile(filename) + self._cache = {} + + def __setstate__(self, state): + self.__dict__ = state + + def loadFile(self, filename): + """ + Load DNS records from a file. + + This method populates the I{soa} and I{records} attributes. It must be + overridden in a subclass. It is called once from the initializer. + + @param filename: The I{filename} parameter that was passed to the + initilizer. + + @returns: L{None} -- the return value is ignored + """ + + def _additionalRecords(self, answer, authority, ttl): + """ + Find locally known information that could be useful to the consumer of + the response and construct appropriate records to include in the + I{additional} section of that response. + + Essentially, implement RFC 1034 section 4.3.2 step 6. + + @param answer: A L{list} of the records which will be included in the + I{answer} section of the response. + + @param authority: A L{list} of the records which will be included in + the I{authority} section of the response. + + @param ttl: The default TTL for records for which this is not otherwise + specified. + + @return: A generator of L{dns.RRHeader} instances for inclusion in the + I{additional} section. These instances represent extra information + about the records in C{answer} and C{authority}. + """ + for record in answer + authority: + if record.type in self._ADDITIONAL_PROCESSING_TYPES: + name = record.payload.name.name + for rec in self.records.get(name.lower(), ()): + if rec.TYPE in self._ADDRESS_TYPES: + yield dns.RRHeader( + name, rec.TYPE, dns.IN, rec.ttl or ttl, rec, auth=True + ) + + def _lookup(self, name, cls, type, timeout=None): + """ + Determine a response to a particular DNS query. + + @param name: The name which is being queried and for which to lookup a + response. + @type name: L{bytes} + + @param cls: The class which is being queried. Only I{IN} is + implemented here and this value is presently disregarded. + @type cls: L{int} + + @param type: The type of records being queried. See the types defined + in L{twisted.names.dns}. + @type type: L{int} + + @param timeout: All processing is done locally and a result is + available immediately, so the timeout value is ignored. + + @return: A L{Deferred} that fires with a L{tuple} of three sets of + response records (to comprise the I{answer}, I{authority}, and + I{additional} sections of a DNS response) or with a L{Failure} if + there is a problem processing the query. + """ + cnames = [] + results = [] + authority = [] + additional = [] + default_ttl = max(self.soa[1].minimum, self.soa[1].expire) + + domain_records = self.records.get(name.lower()) + + if domain_records: + for record in domain_records: + if record.ttl is not None: + ttl = record.ttl + else: + ttl = default_ttl + + if record.TYPE == dns.NS and name.lower() != self.soa[0].lower(): + # NS record belong to a child zone: this is a referral. As + # NS records are authoritative in the child zone, ours here + # are not. RFC 2181, section 6.1. + authority.append( + dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=False) + ) + elif record.TYPE == type or type == dns.ALL_RECORDS: + results.append( + dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=True) + ) + if record.TYPE == dns.CNAME: + cnames.append( + dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=True) + ) + if not results: + results = cnames + + # Sort of https://tools.ietf.org/html/rfc1034#section-4.3.2 . + # See https://twistedmatrix.com/trac/ticket/6732 + additionalInformation = self._additionalRecords( + results, authority, default_ttl + ) + if cnames: + results.extend(additionalInformation) + else: + additional.extend(additionalInformation) + + if not results and not authority: + # Empty response. Include SOA record to allow clients to cache + # this response. RFC 1034, sections 3.7 and 4.3.4, and RFC 2181 + # section 7.1. + authority.append( + dns.RRHeader( + self.soa[0], dns.SOA, dns.IN, ttl, self.soa[1], auth=True + ) + ) + return defer.succeed((results, authority, additional)) + else: + if dns._isSubdomainOf(name, self.soa[0]): + # We may be the authority and we didn't find it. + # XXX: The QNAME may also be in a delegated child zone. See + # #6581 and #6580 + return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name))) + else: + # The QNAME is not a descendant of this zone. Fail with + # DomainError so that the next chained authority or + # resolver will be queried. + return defer.fail(failure.Failure(error.DomainError(name))) + + def lookupZone(self, name, timeout=10): + name = dns.domainString(name) + if self.soa[0].lower() == name.lower(): + # Wee hee hee hooo yea + default_ttl = max(self.soa[1].minimum, self.soa[1].expire) + if self.soa[1].ttl is not None: + soa_ttl = self.soa[1].ttl + else: + soa_ttl = default_ttl + results = [ + dns.RRHeader( + self.soa[0], dns.SOA, dns.IN, soa_ttl, self.soa[1], auth=True + ) + ] + for k, r in self.records.items(): + for rec in r: + if rec.ttl is not None: + ttl = rec.ttl + else: + ttl = default_ttl + if rec.TYPE != dns.SOA: + results.append( + dns.RRHeader(k, rec.TYPE, dns.IN, ttl, rec, auth=True) + ) + results.append(results[0]) + return defer.succeed((results, (), ())) + return defer.fail(failure.Failure(dns.DomainError(name))) + + def _cbAllRecords(self, results): + ans, auth, add = [], [], [] + for res in results: + if res[0]: + ans.extend(res[1][0]) + auth.extend(res[1][1]) + add.extend(res[1][2]) + return ans, auth, add + + +class PySourceAuthority(FileAuthority): + """ + A FileAuthority that is built up from Python source code. + """ + + def loadFile(self, filename): + g, l = self.setupConfigNamespace(), {} + execfile(filename, g, l) + if "zone" not in l: + raise ValueError("No zone defined in " + filename) + + self.records = {} + for rr in l["zone"]: + if isinstance(rr[1], dns.Record_SOA): + self.soa = rr + self.records.setdefault(rr[0].lower(), []).append(rr[1]) + + def wrapRecord(self, type): + def wrapRecordFunc(name, *arg, **kw): + return (dns.domainString(name), type(*arg, **kw)) + + return wrapRecordFunc + + def setupConfigNamespace(self): + r = {} + items = dns.__dict__.keys() + for record in [x for x in items if x.startswith("Record_")]: + type = getattr(dns, record) + f = self.wrapRecord(type) + r[record[len("Record_") :]] = f + return r + + +class BindAuthority(FileAuthority): + """ + An Authority that loads U{BIND zone files + <https://en.wikipedia.org/wiki/Zone_file>}. + + Supports only C{$ORIGIN} and C{$TTL} directives. + """ + + def loadFile(self, filename): + """ + Load records from C{filename}. + + @param filename: file to read from + @type filename: L{bytes} + """ + fp = FilePath(filename) + # Not the best way to set an origin. It can be set using $ORIGIN + # though. + self.origin = nativeString(fp.basename() + b".") + + lines = fp.getContent().splitlines(True) + lines = self.stripComments(lines) + lines = self.collapseContinuations(lines) + self.parseLines(lines) + + def stripComments(self, lines): + """ + Strip comments from C{lines}. + + @param lines: lines to work on + @type lines: iterable of L{bytes} + + @return: C{lines} sans comments. + """ + return ( + a.find(b";") == -1 and a or a[: a.find(b";")] + for a in [b.strip() for b in lines] + ) + + def collapseContinuations(self, lines): + """ + Transform multiline statements into single lines. + + @param lines: lines to work on + @type lines: iterable of L{bytes} + + @return: iterable of continuous lines + """ + l = [] + state = 0 + for line in lines: + if state == 0: + if line.find(b"(") == -1: + l.append(line) + else: + l.append(line[: line.find(b"(")]) + state = 1 + else: + if line.find(b")") != -1: + l[-1] += b" " + line[: line.find(b")")] + state = 0 + else: + l[-1] += b" " + line + return filter(None, (line.split() for line in l)) + + def parseLines(self, lines): + """ + Parse C{lines}. + + @param lines: lines to work on + @type lines: iterable of L{bytes} + """ + ttl = 60 * 60 * 3 + origin = self.origin + + self.records = {} + + for line in lines: + if line[0] == b"$TTL": + ttl = dns.str2time(line[1]) + elif line[0] == b"$ORIGIN": + origin = line[1] + elif line[0] == b"$INCLUDE": + raise NotImplementedError("$INCLUDE directive not implemented") + elif line[0] == b"$GENERATE": + raise NotImplementedError("$GENERATE directive not implemented") + else: + self.parseRecordLine(origin, ttl, line) + + # If the origin changed, reflect that within the instance. + self.origin = origin + + def addRecord(self, owner, ttl, type, domain, cls, rdata): + """ + Add a record to our authority. Expand domain with origin if necessary. + + @param owner: origin? + @type owner: L{bytes} + + @param ttl: time to live for the record + @type ttl: L{int} + + @param domain: the domain for which the record is to be added + @type domain: L{bytes} + + @param type: record type + @type type: L{str} + + @param cls: record class + @type cls: L{str} + + @param rdata: record data + @type rdata: L{list} of L{bytes} + """ + if not domain.endswith(b"."): + domain = domain + b"." + owner[:-1] + else: + domain = domain[:-1] + f = getattr(self, f"class_{cls}", None) + if f: + f(ttl, type, domain, rdata) + else: + raise NotImplementedError(f"Record class {cls!r} not supported") + + def class_IN(self, ttl, type, domain, rdata): + """ + Simulate a class IN and recurse into the actual class. + + @param ttl: time to live for the record + @type ttl: L{int} + + @param type: record type + @type type: str + + @param domain: the domain + @type domain: bytes + + @param rdata: + @type rdata: bytes + """ + record = getattr(dns, f"Record_{nativeString(type)}", None) + if record: + r = record(*rdata) + r.ttl = ttl + self.records.setdefault(domain.lower(), []).append(r) + + if type == "SOA": + self.soa = (domain, r) + else: + raise NotImplementedError( + f"Record type {nativeString(type)!r} not supported" + ) + + def parseRecordLine(self, origin, ttl, line): + """ + Parse a C{line} from a zone file respecting C{origin} and C{ttl}. + + Add resulting records to authority. + + @param origin: starting point for the zone + @type origin: L{bytes} + + @param ttl: time to live for the record + @type ttl: L{int} + + @param line: zone file line to parse; split by word + @type line: L{list} of L{bytes} + """ + queryClasses = {qc.encode("ascii") for qc in dns.QUERY_CLASSES.values()} + queryTypes = {qt.encode("ascii") for qt in dns.QUERY_TYPES.values()} + + markers = queryClasses | queryTypes + + cls = b"IN" + owner = origin + + if line[0] == b"@": + line = line[1:] + owner = origin + elif not line[0].isdigit() and line[0] not in markers: + owner = line[0] + line = line[1:] + + if line[0].isdigit() or line[0] in markers: + domain = owner + owner = origin + else: + domain = line[0] + line = line[1:] + + if line[0] in queryClasses: + cls = line[0] + line = line[1:] + if line[0].isdigit(): + ttl = int(line[0]) + line = line[1:] + elif line[0].isdigit(): + ttl = int(line[0]) + line = line[1:] + if line[0] in queryClasses: + cls = line[0] + line = line[1:] + + type = line[0] + rdata = line[1:] + + self.addRecord(owner, ttl, nativeString(type), domain, nativeString(cls), rdata) diff --git a/contrib/python/Twisted/py3/twisted/names/cache.py b/contrib/python/Twisted/py3/twisted/names/cache.py new file mode 100644 index 0000000000..a3833d7ab1 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/cache.py @@ -0,0 +1,131 @@ +# -*- test-case-name: twisted.names.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +An in-memory caching resolver. +""" + + +from twisted.internet import defer +from twisted.names import common, dns +from twisted.python import failure, log + + +class CacheResolver(common.ResolverBase): + """ + A resolver that serves records from a local, memory cache. + + @ivar _reactor: A provider of L{interfaces.IReactorTime}. + """ + + cache = None + + def __init__(self, cache=None, verbose=0, reactor=None): + common.ResolverBase.__init__(self) + + self.cache = {} + self.verbose = verbose + self.cancel = {} + if reactor is None: + from twisted.internet import reactor + self._reactor = reactor + + if cache: + for query, (seconds, payload) in cache.items(): + self.cacheResult(query, payload, seconds) + + def __setstate__(self, state): + self.__dict__ = state + + now = self._reactor.seconds() + for k, (when, (ans, add, ns)) in self.cache.items(): + diff = now - when + for rec in ans + add + ns: + if rec.ttl < diff: + del self.cache[k] + break + + def __getstate__(self): + for c in self.cancel.values(): + c.cancel() + self.cancel.clear() + return self.__dict__ + + def _lookup(self, name, cls, type, timeout): + now = self._reactor.seconds() + q = dns.Query(name, type, cls) + try: + when, (ans, auth, add) = self.cache[q] + except KeyError: + if self.verbose > 1: + log.msg("Cache miss for " + repr(name)) + return defer.fail(failure.Failure(dns.DomainError(name))) + else: + if self.verbose: + log.msg("Cache hit for " + repr(name)) + diff = now - when + + try: + result = ( + [ + dns.RRHeader( + r.name.name, r.type, r.cls, r.ttl - diff, r.payload + ) + for r in ans + ], + [ + dns.RRHeader( + r.name.name, r.type, r.cls, r.ttl - diff, r.payload + ) + for r in auth + ], + [ + dns.RRHeader( + r.name.name, r.type, r.cls, r.ttl - diff, r.payload + ) + for r in add + ], + ) + except ValueError: + return defer.fail(failure.Failure(dns.DomainError(name))) + else: + return defer.succeed(result) + + def lookupAllRecords(self, name, timeout=None): + return defer.fail(failure.Failure(dns.DomainError(name))) + + def cacheResult(self, query, payload, cacheTime=None): + """ + Cache a DNS entry. + + @param query: a L{dns.Query} instance. + + @param payload: a 3-tuple of lists of L{dns.RRHeader} records, the + matching result of the query (answers, authority and additional). + + @param cacheTime: The time (seconds since epoch) at which the entry is + considered to have been added to the cache. If L{None} is given, + the current time is used. + """ + if self.verbose > 1: + log.msg("Adding %r to cache" % query) + + self.cache[query] = (cacheTime or self._reactor.seconds(), payload) + + if query in self.cancel: + self.cancel[query].cancel() + + s = list(payload[0]) + list(payload[1]) + list(payload[2]) + if s: + m = s[0].ttl + for r in s: + m = min(m, r.ttl) + else: + m = 0 + + self.cancel[query] = self._reactor.callLater(m, self.clearEntry, query) + + def clearEntry(self, query): + del self.cache[query] + del self.cancel[query] diff --git a/contrib/python/Twisted/py3/twisted/names/client.py b/contrib/python/Twisted/py3/twisted/names/client.py new file mode 100644 index 0000000000..4052936ab9 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/client.py @@ -0,0 +1,734 @@ +# -*- test-case-name: twisted.names.test.test_names -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Asynchronous client DNS + +The functions exposed in this module can be used for asynchronous name +resolution and dns queries. + +If you need to create a resolver with specific requirements, such as needing to +do queries against a particular host, the L{createResolver} function will +return an C{IResolver}. + +Future plans: Proper nameserver acquisition on Windows/MacOS, +better caching, respect timeouts +""" + +import errno +import os +import warnings + +from zope.interface import moduleProvides + +from twisted.internet import defer, error, interfaces, protocol +from twisted.internet.abstract import isIPv6Address +from twisted.names import cache, common, dns, hosts as hostsModule, resolve, root +from twisted.python import failure, log + +# Twisted imports +from twisted.python.compat import nativeString +from twisted.python.filepath import FilePath +from twisted.python.runtime import platform + +moduleProvides(interfaces.IResolver) + + +class Resolver(common.ResolverBase): + """ + @ivar _waiting: A C{dict} mapping tuple keys of query name/type/class to + Deferreds which will be called back with the result of those queries. + This is used to avoid issuing the same query more than once in + parallel. This is more efficient on the network and helps avoid a + "birthday paradox" attack by keeping the number of outstanding requests + for a particular query fixed at one instead of allowing the attacker to + raise it to an arbitrary number. + + @ivar _reactor: A provider of L{IReactorTCP}, L{IReactorUDP}, and + L{IReactorTime} which will be used to set up network resources and + track timeouts. + """ + + index = 0 + timeout = None + + factory = None + servers = None + dynServers = () + pending = None + connections = None + + resolv = None + _lastResolvTime = None + _resolvReadInterval = 60 + + def __init__(self, resolv=None, servers=None, timeout=(1, 3, 11, 45), reactor=None): + """ + Construct a resolver which will query domain name servers listed in + the C{resolv.conf(5)}-format file given by C{resolv} as well as + those in the given C{servers} list. Servers are queried in a + round-robin fashion. If given, C{resolv} is periodically checked + for modification and re-parsed if it is noticed to have changed. + + @type servers: C{list} of C{(str, int)} or L{None} + @param servers: If not None, interpreted as a list of (host, port) + pairs specifying addresses of domain name servers to attempt to use + for this lookup. Host addresses should be in IPv4 dotted-quad + form. If specified, overrides C{resolv}. + + @type resolv: C{str} + @param resolv: Filename to read and parse as a resolver(5) + configuration file. + + @type timeout: Sequence of C{int} + @param timeout: Default number of seconds after which to reissue the + query. When the last timeout expires, the query is considered + failed. + + @param reactor: A provider of L{IReactorTime}, L{IReactorUDP}, and + L{IReactorTCP} which will be used to establish connections, listen + for DNS datagrams, and enforce timeouts. If not provided, the + global reactor will be used. + + @raise ValueError: Raised if no nameserver addresses can be found. + """ + common.ResolverBase.__init__(self) + + if reactor is None: + from twisted.internet import reactor + self._reactor = reactor + + self.timeout = timeout + + if servers is None: + self.servers = [] + else: + self.servers = servers + + self.resolv = resolv + + if not len(self.servers) and not resolv: + raise ValueError("No nameservers specified") + + self.factory = DNSClientFactory(self, timeout) + self.factory.noisy = 0 # Be quiet by default + + self.connections = [] + self.pending = [] + + self._waiting = {} + + self.maybeParseConfig() + + def __getstate__(self): + d = self.__dict__.copy() + d["connections"] = [] + d["_parseCall"] = None + return d + + def __setstate__(self, state): + self.__dict__.update(state) + self.maybeParseConfig() + + def _openFile(self, path): + """ + Wrapper used for opening files in the class, exists primarily for unit + testing purposes. + """ + return FilePath(path).open() + + def maybeParseConfig(self): + if self.resolv is None: + # Don't try to parse it, don't set up a call loop + return + + try: + resolvConf = self._openFile(self.resolv) + except OSError as e: + if e.errno == errno.ENOENT: + # Missing resolv.conf is treated the same as an empty resolv.conf + self.parseConfig(()) + else: + raise + else: + with resolvConf: + mtime = os.fstat(resolvConf.fileno()).st_mtime + if mtime != self._lastResolvTime: + log.msg(f"{self.resolv} changed, reparsing") + self._lastResolvTime = mtime + self.parseConfig(resolvConf) + + # Check again in a little while + self._parseCall = self._reactor.callLater( + self._resolvReadInterval, self.maybeParseConfig + ) + + def parseConfig(self, resolvConf): + servers = [] + for L in resolvConf: + L = L.strip() + if L.startswith(b"nameserver"): + resolver = (nativeString(L.split()[1]), dns.PORT) + servers.append(resolver) + log.msg(f"Resolver added {resolver!r} to server list") + elif L.startswith(b"domain"): + try: + self.domain = L.split()[1] + except IndexError: + self.domain = b"" + self.search = None + elif L.startswith(b"search"): + self.search = L.split()[1:] + self.domain = None + if not servers: + servers.append(("127.0.0.1", dns.PORT)) + self.dynServers = servers + + def pickServer(self): + """ + Return the address of a nameserver. + + TODO: Weight servers for response time so faster ones can be + preferred. + """ + if not self.servers and not self.dynServers: + return None + serverL = len(self.servers) + dynL = len(self.dynServers) + + self.index += 1 + self.index %= serverL + dynL + if self.index < serverL: + return self.servers[self.index] + else: + return self.dynServers[self.index - serverL] + + def _connectedProtocol(self, interface=""): + """ + Return a new L{DNSDatagramProtocol} bound to a randomly selected port + number. + """ + failures = 0 + proto = dns.DNSDatagramProtocol(self, reactor=self._reactor) + + while True: + try: + self._reactor.listenUDP(dns.randomSource(), proto, interface=interface) + except error.CannotListenError as e: + failures += 1 + + if ( + hasattr(e.socketError, "errno") + and e.socketError.errno == errno.EMFILE + ): + # We've run out of file descriptors. Stop trying. + raise + + if failures >= 1000: + # We've tried a thousand times and haven't found a port. + # This is almost impossible, and likely means something + # else weird is going on. Raise, as to not infinite loop. + raise + else: + return proto + + def connectionMade(self, protocol): + """ + Called by associated L{dns.DNSProtocol} instances when they connect. + """ + self.connections.append(protocol) + for d, q, t in self.pending: + self.queryTCP(q, t).chainDeferred(d) + del self.pending[:] + + def connectionLost(self, protocol): + """ + Called by associated L{dns.DNSProtocol} instances when they disconnect. + """ + if protocol in self.connections: + self.connections.remove(protocol) + + def messageReceived(self, message, protocol, address=None): + log.msg("Unexpected message (%d) received from %r" % (message.id, address)) + + def _query(self, *args): + """ + Get a new L{DNSDatagramProtocol} instance from L{_connectedProtocol}, + issue a query to it using C{*args}, and arrange for it to be + disconnected from its transport after the query completes. + + @param args: Positional arguments to be passed to + L{DNSDatagramProtocol.query}. + + @return: A L{Deferred} which will be called back with the result of the + query. + """ + if isIPv6Address(args[0][0]): + protocol = self._connectedProtocol(interface="::") + else: + protocol = self._connectedProtocol() + d = protocol.query(*args) + + def cbQueried(result): + protocol.transport.stopListening() + return result + + d.addBoth(cbQueried) + return d + + def queryUDP(self, queries, timeout=None): + """ + Make a number of DNS queries via UDP. + + @type queries: A C{list} of C{dns.Query} instances + @param queries: The queries to make. + + @type timeout: Sequence of C{int} + @param timeout: Number of seconds after which to reissue the query. + When the last timeout expires, the query is considered failed. + + @rtype: C{Deferred} + @raise C{twisted.internet.defer.TimeoutError}: When the query times + out. + """ + if timeout is None: + timeout = self.timeout + + addresses = self.servers + list(self.dynServers) + if not addresses: + return defer.fail(IOError("No domain name servers available")) + + # Make sure we go through servers in the list in the order they were + # specified. + addresses.reverse() + + used = addresses.pop() + d = self._query(used, queries, timeout[0]) + d.addErrback(self._reissue, addresses, [used], queries, timeout) + return d + + def _reissue(self, reason, addressesLeft, addressesUsed, query, timeout): + reason.trap(dns.DNSQueryTimeoutError) + + # If there are no servers left to be tried, adjust the timeout + # to the next longest timeout period and move all the + # "used" addresses back to the list of addresses to try. + if not addressesLeft: + addressesLeft = addressesUsed + addressesLeft.reverse() + addressesUsed = [] + timeout = timeout[1:] + + # If all timeout values have been used this query has failed. Tell the + # protocol we're giving up on it and return a terminal timeout failure + # to our caller. + if not timeout: + return failure.Failure(defer.TimeoutError(query)) + + # Get an address to try. Take it out of the list of addresses + # to try and put it ino the list of already tried addresses. + address = addressesLeft.pop() + addressesUsed.append(address) + + # Issue a query to a server. Use the current timeout. Add this + # function as a timeout errback in case another retry is required. + d = self._query(address, query, timeout[0], reason.value.id) + d.addErrback(self._reissue, addressesLeft, addressesUsed, query, timeout) + return d + + def queryTCP(self, queries, timeout=10): + """ + Make a number of DNS queries via TCP. + + @type queries: Any non-zero number of C{dns.Query} instances + @param queries: The queries to make. + + @type timeout: C{int} + @param timeout: The number of seconds after which to fail. + + @rtype: C{Deferred} + """ + if not len(self.connections): + address = self.pickServer() + if address is None: + return defer.fail(IOError("No domain name servers available")) + host, port = address + self._reactor.connectTCP(host, port, self.factory) + self.pending.append((defer.Deferred(), queries, timeout)) + return self.pending[-1][0] + else: + return self.connections[0].query(queries, timeout) + + def filterAnswers(self, message): + """ + Extract results from the given message. + + If the message was truncated, re-attempt the query over TCP and return + a Deferred which will fire with the results of that query. + + If the message's result code is not C{twisted.names.dns.OK}, return a + Failure indicating the type of error which occurred. + + Otherwise, return a three-tuple of lists containing the results from + the answers section, the authority section, and the additional section. + """ + if message.trunc: + return self.queryTCP(message.queries).addCallback(self.filterAnswers) + if message.rCode != dns.OK: + return failure.Failure(self.exceptionForCode(message.rCode)(message)) + return (message.answers, message.authority, message.additional) + + def _lookup(self, name, cls, type, timeout): + """ + Build a L{dns.Query} for the given parameters and dispatch it via UDP. + + If this query is already outstanding, it will not be re-issued. + Instead, when the outstanding query receives a response, that response + will be re-used for this query as well. + + @type name: C{str} + @type type: C{int} + @type cls: C{int} + + @return: A L{Deferred} which fires with a three-tuple giving the + answer, authority, and additional sections of the response or with + a L{Failure} if the response code is anything other than C{dns.OK}. + """ + key = (name, type, cls) + waiting = self._waiting.get(key) + if waiting is None: + self._waiting[key] = [] + d = self.queryUDP([dns.Query(name, type, cls)], timeout) + + def cbResult(result): + for d in self._waiting.pop(key): + d.callback(result) + return result + + d.addCallback(self.filterAnswers) + d.addBoth(cbResult) + else: + d = defer.Deferred() + waiting.append(d) + return d + + # This one doesn't ever belong on UDP + def lookupZone(self, name, timeout=10): + address = self.pickServer() + if address is None: + return defer.fail(IOError("No domain name servers available")) + host, port = address + d = defer.Deferred() + controller = AXFRController(name, d) + factory = DNSClientFactory(controller, timeout) + factory.noisy = False # stfu + + connector = self._reactor.connectTCP(host, port, factory) + controller.timeoutCall = self._reactor.callLater( + timeout or 10, self._timeoutZone, d, controller, connector, timeout or 10 + ) + + def eliminateTimeout(failure): + controller.timeoutCall.cancel() + controller.timeoutCall = None + return failure + + return d.addCallbacks( + self._cbLookupZone, eliminateTimeout, callbackArgs=(connector,) + ) + + def _timeoutZone(self, d, controller, connector, seconds): + connector.disconnect() + controller.timeoutCall = None + controller.deferred = None + d.errback( + error.TimeoutError("Zone lookup timed out after %d seconds" % (seconds,)) + ) + + def _cbLookupZone(self, result, connector): + connector.disconnect() + return (result, [], []) + + +class AXFRController: + timeoutCall = None + + def __init__(self, name, deferred): + self.name = name + self.deferred = deferred + self.soa = None + self.records = [] + self.pending = [(deferred,)] + + def connectionMade(self, protocol): + # dig saids recursion-desired to 0, so I will too + message = dns.Message(protocol.pickID(), recDes=0) + message.queries = [dns.Query(self.name, dns.AXFR, dns.IN)] + protocol.writeMessage(message) + + def connectionLost(self, protocol): + # XXX Do something here - see #3428 + pass + + def messageReceived(self, message, protocol): + # Caveat: We have to handle two cases: All records are in 1 + # message, or all records are in N messages. + + # According to http://cr.yp.to/djbdns/axfr-notes.html, + # 'authority' and 'additional' are always empty, and only + # 'answers' is present. + self.records.extend(message.answers) + if not self.records: + return + if not self.soa: + if self.records[0].type == dns.SOA: + # print "first SOA!" + self.soa = self.records[0] + if len(self.records) > 1 and self.records[-1].type == dns.SOA: + # print "It's the second SOA! We're done." + if self.timeoutCall is not None: + self.timeoutCall.cancel() + self.timeoutCall = None + if self.deferred is not None: + self.deferred.callback(self.records) + self.deferred = None + + +from twisted.internet.base import ThreadedResolver as _ThreadedResolverImpl + + +class ThreadedResolver(_ThreadedResolverImpl): + def __init__(self, reactor=None): + if reactor is None: + from twisted.internet import reactor + _ThreadedResolverImpl.__init__(self, reactor) + warnings.warn( + "twisted.names.client.ThreadedResolver is deprecated since " + "Twisted 9.0, use twisted.internet.base.ThreadedResolver " + "instead.", + category=DeprecationWarning, + stacklevel=2, + ) + + +class DNSClientFactory(protocol.ClientFactory): + def __init__(self, controller, timeout=10): + self.controller = controller + self.timeout = timeout + + def clientConnectionLost(self, connector, reason): + pass + + def clientConnectionFailed(self, connector, reason): + """ + Fail all pending TCP DNS queries if the TCP connection attempt + fails. + + @see: L{twisted.internet.protocol.ClientFactory} + + @param connector: Not used. + @type connector: L{twisted.internet.interfaces.IConnector} + + @param reason: A C{Failure} containing information about the + cause of the connection failure. This will be passed as the + argument to C{errback} on every pending TCP query + C{deferred}. + @type reason: L{twisted.python.failure.Failure} + """ + # Copy the current pending deferreds then reset the master + # pending list. This prevents triggering new deferreds which + # may be added by callback or errback functions on the current + # deferreds. + pending = self.controller.pending[:] + del self.controller.pending[:] + for pendingState in pending: + d = pendingState[0] + d.errback(reason) + + def buildProtocol(self, addr): + p = dns.DNSProtocol(self.controller) + p.factory = self + return p + + +def createResolver(servers=None, resolvconf=None, hosts=None): + r""" + Create and return a Resolver. + + @type servers: C{list} of C{(str, int)} or L{None} + + @param servers: If not L{None}, interpreted as a list of domain name servers + to attempt to use. Each server is a tuple of address in C{str} dotted-quad + form and C{int} port number. + + @type resolvconf: C{str} or L{None} + @param resolvconf: If not L{None}, on posix systems will be interpreted as + an alternate resolv.conf to use. Will do nothing on windows systems. If + L{None}, /etc/resolv.conf will be used. + + @type hosts: C{str} or L{None} + @param hosts: If not L{None}, an alternate hosts file to use. If L{None} + on posix systems, /etc/hosts will be used. On windows, C:\windows\hosts + will be used. + + @rtype: C{IResolver} + """ + if platform.getType() == "posix": + if resolvconf is None: + resolvconf = b"/etc/resolv.conf" + if hosts is None: + hosts = b"/etc/hosts" + theResolver = Resolver(resolvconf, servers) + hostResolver = hostsModule.Resolver(hosts) + else: + if hosts is None: + hosts = r"c:\windows\hosts" + from twisted.internet import reactor + + bootstrap = _ThreadedResolverImpl(reactor) + hostResolver = hostsModule.Resolver(hosts) + theResolver = root.bootstrap(bootstrap, resolverFactory=Resolver) + + L = [hostResolver, cache.CacheResolver(), theResolver] + return resolve.ResolverChain(L) + + +theResolver = None + + +def getResolver(): + """ + Get a Resolver instance. + + Create twisted.names.client.theResolver if it is L{None}, and then return + that value. + + @rtype: C{IResolver} + """ + global theResolver + if theResolver is None: + try: + theResolver = createResolver() + except ValueError: + theResolver = createResolver(servers=[("127.0.0.1", 53)]) + return theResolver + + +def getHostByName(name, timeout=None, effort=10): + """ + Resolve a name to a valid ipv4 or ipv6 address. + + Will errback with C{DNSQueryTimeoutError} on a timeout, C{DomainError} or + C{AuthoritativeDomainError} (or subclasses) on other errors. + + @type name: C{str} + @param name: DNS name to resolve. + + @type timeout: Sequence of C{int} + @param timeout: Number of seconds after which to reissue the query. + When the last timeout expires, the query is considered failed. + + @type effort: C{int} + @param effort: How many times CNAME and NS records to follow while + resolving this name. + + @rtype: C{Deferred} + """ + return getResolver().getHostByName(name, timeout, effort) + + +def query(query, timeout=None): + return getResolver().query(query, timeout) + + +def lookupAddress(name, timeout=None): + return getResolver().lookupAddress(name, timeout) + + +def lookupIPV6Address(name, timeout=None): + return getResolver().lookupIPV6Address(name, timeout) + + +def lookupAddress6(name, timeout=None): + return getResolver().lookupAddress6(name, timeout) + + +def lookupMailExchange(name, timeout=None): + return getResolver().lookupMailExchange(name, timeout) + + +def lookupNameservers(name, timeout=None): + return getResolver().lookupNameservers(name, timeout) + + +def lookupCanonicalName(name, timeout=None): + return getResolver().lookupCanonicalName(name, timeout) + + +def lookupMailBox(name, timeout=None): + return getResolver().lookupMailBox(name, timeout) + + +def lookupMailGroup(name, timeout=None): + return getResolver().lookupMailGroup(name, timeout) + + +def lookupMailRename(name, timeout=None): + return getResolver().lookupMailRename(name, timeout) + + +def lookupPointer(name, timeout=None): + return getResolver().lookupPointer(name, timeout) + + +def lookupAuthority(name, timeout=None): + return getResolver().lookupAuthority(name, timeout) + + +def lookupNull(name, timeout=None): + return getResolver().lookupNull(name, timeout) + + +def lookupWellKnownServices(name, timeout=None): + return getResolver().lookupWellKnownServices(name, timeout) + + +def lookupService(name, timeout=None): + return getResolver().lookupService(name, timeout) + + +def lookupHostInfo(name, timeout=None): + return getResolver().lookupHostInfo(name, timeout) + + +def lookupMailboxInfo(name, timeout=None): + return getResolver().lookupMailboxInfo(name, timeout) + + +def lookupText(name, timeout=None): + return getResolver().lookupText(name, timeout) + + +def lookupSenderPolicy(name, timeout=None): + return getResolver().lookupSenderPolicy(name, timeout) + + +def lookupResponsibility(name, timeout=None): + return getResolver().lookupResponsibility(name, timeout) + + +def lookupAFSDatabase(name, timeout=None): + return getResolver().lookupAFSDatabase(name, timeout) + + +def lookupZone(name, timeout=None): + return getResolver().lookupZone(name, timeout) + + +def lookupAllRecords(name, timeout=None): + return getResolver().lookupAllRecords(name, timeout) + + +def lookupNamingAuthorityPointer(name, timeout=None): + return getResolver().lookupNamingAuthorityPointer(name, timeout) diff --git a/contrib/python/Twisted/py3/twisted/names/common.py b/contrib/python/Twisted/py3/twisted/names/common.py new file mode 100644 index 0000000000..ee64b451f7 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/common.py @@ -0,0 +1,263 @@ +# -*- test-case-name: twisted.names.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Base functionality useful to various parts of Twisted Names. +""" + + +import socket + +from zope.interface import implementer + +from twisted.internet import defer, error, interfaces +from twisted.logger import Logger +from twisted.names import dns +from twisted.names.error import ( + DNSFormatError, + DNSNameError, + DNSNotImplementedError, + DNSQueryRefusedError, + DNSServerError, + DNSUnknownError, +) + +# Helpers for indexing the three-tuples that get thrown around by this code a +# lot. +_ANS, _AUTH, _ADD = range(3) + +EMPTY_RESULT = (), (), () + + +@implementer(interfaces.IResolver) +class ResolverBase: + """ + L{ResolverBase} is a base class for implementations of + L{interfaces.IResolver} which deals with a lot + of the boilerplate of implementing all of the lookup methods. + + @cvar _errormap: A C{dict} mapping DNS protocol failure response codes + to exception classes which will be used to represent those failures. + """ + + _log = Logger() + _errormap = { + dns.EFORMAT: DNSFormatError, + dns.ESERVER: DNSServerError, + dns.ENAME: DNSNameError, + dns.ENOTIMP: DNSNotImplementedError, + dns.EREFUSED: DNSQueryRefusedError, + } + + typeToMethod = None + + def __init__(self): + self.typeToMethod = {} + for k, v in typeToMethod.items(): + self.typeToMethod[k] = getattr(self, v) + + def exceptionForCode(self, responseCode): + """ + Convert a response code (one of the possible values of + L{dns.Message.rCode} to an exception instance representing it. + + @since: 10.0 + """ + return self._errormap.get(responseCode, DNSUnknownError) + + def query(self, query, timeout=None): + try: + method = self.typeToMethod[query.type] + except KeyError: + self._log.debug( + "Query of unknown type {query.type} for {query.name.name!r}", + query=query, + ) + return defer.maybeDeferred( + self._lookup, query.name.name, dns.IN, query.type, timeout + ) + else: + return defer.maybeDeferred(method, query.name.name, timeout) + + def _lookup(self, name, cls, type, timeout): + return defer.fail(NotImplementedError("ResolverBase._lookup")) + + def lookupAddress(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.A, timeout) + + def lookupIPV6Address(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.AAAA, timeout) + + def lookupAddress6(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.A6, timeout) + + def lookupMailExchange(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.MX, timeout) + + def lookupNameservers(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.NS, timeout) + + def lookupCanonicalName(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.CNAME, timeout) + + def lookupMailBox(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.MB, timeout) + + def lookupMailGroup(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.MG, timeout) + + def lookupMailRename(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.MR, timeout) + + def lookupPointer(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.PTR, timeout) + + def lookupAuthority(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.SOA, timeout) + + def lookupNull(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.NULL, timeout) + + def lookupWellKnownServices(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.WKS, timeout) + + def lookupService(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.SRV, timeout) + + def lookupHostInfo(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.HINFO, timeout) + + def lookupMailboxInfo(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.MINFO, timeout) + + def lookupText(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.TXT, timeout) + + def lookupSenderPolicy(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.SPF, timeout) + + def lookupResponsibility(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.RP, timeout) + + def lookupAFSDatabase(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.AFSDB, timeout) + + def lookupZone(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.AXFR, timeout) + + def lookupNamingAuthorityPointer(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.NAPTR, timeout) + + def lookupAllRecords(self, name, timeout=None): + return self._lookup(dns.domainString(name), dns.IN, dns.ALL_RECORDS, timeout) + + # IResolverSimple + def getHostByName(self, name, timeout=None, effort=10): + name = dns.domainString(name) + # XXX - respect timeout + # XXX - this should do A and AAAA lookups, not ANY (see RFC 8482). + # https://twistedmatrix.com/trac/ticket/9691 + d = self.lookupAllRecords(name, timeout) + d.addCallback(self._cbRecords, name, effort) + return d + + def _cbRecords(self, records, name, effort): + (ans, auth, add) = records + result = extractRecord(self, dns.Name(name), ans + auth + add, effort) + if not result: + raise error.DNSLookupError(name) + return result + + +def extractRecord(resolver, name, answers, level=10): + """ + Resolve a name to an IP address, following I{CNAME} records and I{NS} + referrals recursively. + + This is an implementation detail of L{ResolverBase.getHostByName}. + + @param resolver: The resolver to use for the next query (unless handling + an I{NS} referral). + @type resolver: L{IResolver} + + @param name: The name being looked up. + @type name: L{dns.Name} + + @param answers: All of the records returned by the previous query (answers, + authority, and additional concatenated). + @type answers: L{list} of L{dns.RRHeader} + + @param level: Remaining recursion budget. This is decremented at each + recursion. The query returns L{None} when it reaches 0. + @type level: L{int} + + @returns: The first IPv4 or IPv6 address (as a dotted quad or colon + quibbles), or L{None} when no result is found. + @rtype: native L{str} or L{None} + """ + if not level: + return None + # FIXME: twisted.python.compat monkeypatches this if missing, so this + # condition is always true. https://twistedmatrix.com/trac/ticket/9753 + if hasattr(socket, "inet_ntop"): + for r in answers: + if r.name == name and r.type == dns.A6: + return socket.inet_ntop(socket.AF_INET6, r.payload.address) + for r in answers: + if r.name == name and r.type == dns.AAAA: + return socket.inet_ntop(socket.AF_INET6, r.payload.address) + for r in answers: + if r.name == name and r.type == dns.A: + return socket.inet_ntop(socket.AF_INET, r.payload.address) + for r in answers: + if r.name == name and r.type == dns.CNAME: + result = extractRecord(resolver, r.payload.name, answers, level - 1) + if not result: + return resolver.getHostByName(r.payload.name.name, effort=level - 1) + return result + # No answers, but maybe there's a hint at who we should be asking about + # this + for r in answers: + if r.type != dns.NS: + continue + from twisted.names import client + + nsResolver = client.Resolver( + servers=[ + (r.payload.name.name.decode("ascii"), dns.PORT), + ] + ) + + def queryAgain(records): + (ans, auth, add) = records + return extractRecord(nsResolver, name, ans + auth + add, level - 1) + + return nsResolver.lookupAddress(name.name).addCallback(queryAgain) + + +typeToMethod = { + dns.A: "lookupAddress", + dns.AAAA: "lookupIPV6Address", + dns.A6: "lookupAddress6", + dns.NS: "lookupNameservers", + dns.CNAME: "lookupCanonicalName", + dns.SOA: "lookupAuthority", + dns.MB: "lookupMailBox", + dns.MG: "lookupMailGroup", + dns.MR: "lookupMailRename", + dns.NULL: "lookupNull", + dns.WKS: "lookupWellKnownServices", + dns.PTR: "lookupPointer", + dns.HINFO: "lookupHostInfo", + dns.MINFO: "lookupMailboxInfo", + dns.MX: "lookupMailExchange", + dns.TXT: "lookupText", + dns.SPF: "lookupSenderPolicy", + dns.RP: "lookupResponsibility", + dns.AFSDB: "lookupAFSDatabase", + dns.SRV: "lookupService", + dns.NAPTR: "lookupNamingAuthorityPointer", + dns.AXFR: "lookupZone", + dns.ALL_RECORDS: "lookupAllRecords", +} diff --git a/contrib/python/Twisted/py3/twisted/names/dns.py b/contrib/python/Twisted/py3/twisted/names/dns.py new file mode 100644 index 0000000000..c7644ef50d --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/dns.py @@ -0,0 +1,3390 @@ +# -*- test-case-name: twisted.names.test.test_dns -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +DNS protocol implementation. + +Future Plans: + - Get rid of some toplevels, maybe. +""" +from __future__ import annotations + +# System imports +import inspect +import random +import socket +import struct +from io import BytesIO +from itertools import chain +from typing import Optional, Sequence, SupportsInt, Union, overload + +from zope.interface import Attribute, Interface, implementer + +# Twisted imports +from twisted.internet import defer, protocol +from twisted.internet.error import CannotListenError +from twisted.python import failure, log, randbytes, util as tputil +from twisted.python.compat import cmp, comparable, nativeString + +__all__ = [ + "IEncodable", + "IRecord", + "IEncodableRecord", + "A", + "A6", + "AAAA", + "AFSDB", + "CNAME", + "DNAME", + "HINFO", + "MAILA", + "MAILB", + "MB", + "MD", + "MF", + "MG", + "MINFO", + "MR", + "MX", + "NAPTR", + "NS", + "NULL", + "OPT", + "PTR", + "RP", + "SOA", + "SPF", + "SRV", + "TXT", + "SSHFP", + "TSIG", + "WKS", + "ANY", + "CH", + "CS", + "HS", + "IN", + "ALL_RECORDS", + "AXFR", + "IXFR", + "EFORMAT", + "ENAME", + "ENOTIMP", + "EREFUSED", + "ESERVER", + "EBADVERSION", + "EBADSIG", + "EBADKEY", + "EBADTIME", + "Record_A", + "Record_A6", + "Record_AAAA", + "Record_AFSDB", + "Record_CNAME", + "Record_DNAME", + "Record_HINFO", + "Record_MB", + "Record_MD", + "Record_MF", + "Record_MG", + "Record_MINFO", + "Record_MR", + "Record_MX", + "Record_NAPTR", + "Record_NS", + "Record_NULL", + "Record_PTR", + "Record_RP", + "Record_SOA", + "Record_SPF", + "Record_SRV", + "Record_SSHFP", + "Record_TSIG", + "Record_TXT", + "Record_WKS", + "UnknownRecord", + "QUERY_CLASSES", + "QUERY_TYPES", + "REV_CLASSES", + "REV_TYPES", + "EXT_QUERIES", + "Charstr", + "Message", + "Name", + "Query", + "RRHeader", + "SimpleRecord", + "DNSDatagramProtocol", + "DNSMixin", + "DNSProtocol", + "OK", + "OP_INVERSE", + "OP_NOTIFY", + "OP_QUERY", + "OP_STATUS", + "OP_UPDATE", + "PORT", + "AuthoritativeDomainError", + "DNSQueryTimeoutError", + "DomainError", +] + + +AF_INET6 = socket.AF_INET6 + + +def _ord2bytes(ordinal): + """ + Construct a bytes object representing a single byte with the given + ordinal value. + + @type ordinal: L{int} + @rtype: L{bytes} + """ + return bytes([ordinal]) + + +def _nicebytes(bytes): + """ + Represent a mostly textful bytes object in a way suitable for + presentation to an end user. + + @param bytes: The bytes to represent. + @rtype: L{str} + """ + return repr(bytes)[1:] + + +def _nicebyteslist(list): + """ + Represent a list of mostly textful bytes objects in a way suitable for + presentation to an end user. + + @param list: The list of bytes to represent. + @rtype: L{str} + """ + return "[{}]".format(", ".join([_nicebytes(b) for b in list])) + + +def randomSource(): + """ + Wrapper around L{twisted.python.randbytes.RandomFactory.secureRandom} to + return 2 random bytes. + + @rtype: L{bytes} + """ + return struct.unpack("H", randbytes.secureRandom(2, fallback=True))[0] + + +PORT = 53 + +( + A, + NS, + MD, + MF, + CNAME, + SOA, + MB, + MG, + MR, + NULL, + WKS, + PTR, + HINFO, + MINFO, + MX, + TXT, + RP, + AFSDB, +) = range(1, 19) +AAAA = 28 +SRV = 33 +NAPTR = 35 +A6 = 38 +DNAME = 39 +OPT = 41 +SSHFP = 44 +SPF = 99 + +# These record types do not exist in zones, but are transferred in +# messages the same way normal RRs are. +TKEY = 249 +TSIG = 250 + +QUERY_TYPES = { + A: "A", + NS: "NS", + MD: "MD", + MF: "MF", + CNAME: "CNAME", + SOA: "SOA", + MB: "MB", + MG: "MG", + MR: "MR", + NULL: "NULL", + WKS: "WKS", + PTR: "PTR", + HINFO: "HINFO", + MINFO: "MINFO", + MX: "MX", + TXT: "TXT", + RP: "RP", + AFSDB: "AFSDB", + # 19 through 27? Eh, I'll get to 'em. + AAAA: "AAAA", + SRV: "SRV", + NAPTR: "NAPTR", + A6: "A6", + DNAME: "DNAME", + OPT: "OPT", + SSHFP: "SSHFP", + SPF: "SPF", + TKEY: "TKEY", + TSIG: "TSIG", +} + +IXFR, AXFR, MAILB, MAILA, ALL_RECORDS = range(251, 256) + +# "Extended" queries (Hey, half of these are deprecated, good job) +EXT_QUERIES = { + IXFR: "IXFR", + AXFR: "AXFR", + MAILB: "MAILB", + MAILA: "MAILA", + ALL_RECORDS: "ALL_RECORDS", +} + +REV_TYPES = {v: k for (k, v) in chain(QUERY_TYPES.items(), EXT_QUERIES.items())} + +IN, CS, CH, HS = range(1, 5) +ANY = 255 + +QUERY_CLASSES = {IN: "IN", CS: "CS", CH: "CH", HS: "HS", ANY: "ANY"} +REV_CLASSES = {v: k for (k, v) in QUERY_CLASSES.items()} + + +# Opcodes +OP_QUERY, OP_INVERSE, OP_STATUS = range(3) +OP_NOTIFY = 4 # RFC 1996 +OP_UPDATE = 5 # RFC 2136 + + +# Response Codes +OK, EFORMAT, ESERVER, ENAME, ENOTIMP, EREFUSED = range(6) +# https://tools.ietf.org/html/rfc6891#section-9 +EBADVERSION = 16 +# RFC 2845 +EBADSIG, EBADKEY, EBADTIME = range(16, 19) + + +class IRecord(Interface): + """ + A single entry in a zone of authority. + """ + + TYPE = Attribute("An indicator of what kind of record this is.") + + +# Backwards compatibility aliases - these should be deprecated or something I +# suppose. -exarkun +from twisted.names.error import ( + AuthoritativeDomainError, + DNSQueryTimeoutError, + DomainError, +) + + +def _nameToLabels(name): + """ + Split a domain name into its constituent labels. + + @type name: L{bytes} + @param name: A fully qualified domain name (with or without a + trailing dot). + + @return: A L{list} of labels ending with an empty label + representing the DNS root zone. + @rtype: L{list} of L{bytes} + """ + if name in (b"", b"."): + return [b""] + labels = name.split(b".") + if labels[-1] != b"": + labels.append(b"") + return labels + + +def domainString(domain: str | bytes) -> bytes: + """ + Coerce a domain name string to bytes. + + L{twisted.names} represents domain names as L{bytes}, but many interfaces + accept L{bytes} or a text string (L{unicode} on Python 2, L{str} on Python + 3). This function coerces text strings using IDNA encoding --- see + L{encodings.idna}. + + Note that DNS is I{case insensitive} but I{case preserving}. This function + doesn't normalize case, so you'll still need to do that whenever comparing + the strings it returns. + + @param domain: A domain name. If passed as a text string it will be + C{idna} encoded. + @type domain: L{bytes} or L{str} + + @returns: L{bytes} suitable for network transmission. + @rtype: L{bytes} + + @since: Twisted 20.3.0 + """ + if isinstance(domain, str): + domain = domain.encode("idna") + if not isinstance(domain, bytes): + raise TypeError( + "Expected {} or {} but found {!r} of type {}".format( + bytes.__name__, str.__name__, domain, type(domain) + ) + ) + return domain + + +def _isSubdomainOf(descendantName, ancestorName): + """ + Test whether C{descendantName} is equal to or is a I{subdomain} of + C{ancestorName}. + + The names are compared case-insensitively. + + The names are treated as byte strings containing one or more + DNS labels separated by B{.}. + + C{descendantName} is considered equal if its sequence of labels + exactly matches the labels of C{ancestorName}. + + C{descendantName} is considered a I{subdomain} if its sequence of + labels ends with the labels of C{ancestorName}. + + @type descendantName: L{bytes} + @param descendantName: The DNS subdomain name. + + @type ancestorName: L{bytes} + @param ancestorName: The DNS parent or ancestor domain name. + + @return: C{True} if C{descendantName} is equal to or if it is a + subdomain of C{ancestorName}. Otherwise returns C{False}. + """ + descendantLabels = _nameToLabels(descendantName.lower()) + ancestorLabels = _nameToLabels(ancestorName.lower()) + return descendantLabels[-len(ancestorLabels) :] == ancestorLabels + + +def _str2time(s: str) -> int: + """ + mypy doesn't like type-punning str | bytes | int | None into a str so we have this helper function. + """ + suffixes = ( + ("S", 1), + ("M", 60), + ("H", 60 * 60), + ("D", 60 * 60 * 24), + ("W", 60 * 60 * 24 * 7), + ("Y", 60 * 60 * 24 * 365), + ) + s = s.upper().strip() + for suff, mult in suffixes: + if s.endswith(suff): + return int(float(s[:-1]) * mult) + try: + return int(s) + except ValueError: + raise ValueError("Invalid time interval specifier: " + s) + + +@overload +def str2time(s: Union[str, bytes, int]) -> int: + ... + + +@overload +def str2time(s: None) -> None: + ... + + +def str2time(s: Union[str, bytes, int, None]) -> Union[int, None]: + """ + Parse a string description of an interval into an integer number of seconds. + + @param s: An interval definition constructed as an interval duration + followed by an interval unit. An interval duration is a base ten + representation of an integer. An interval unit is one of the following + letters: S (seconds), M (minutes), H (hours), D (days), W (weeks), or Y + (years). For example: C{"3S"} indicates an interval of three seconds; + C{"5D"} indicates an interval of five days. Alternatively, C{s} may be + any non-string and it will be returned unmodified. + @type s: text string (L{bytes} or L{str}) for parsing; anything else + for passthrough. + + @return: an L{int} giving the interval represented by the string C{s}, or + whatever C{s} is if it is not a string. + """ + if isinstance(s, bytes): + return _str2time(s.decode("ascii")) + + if isinstance(s, str): + return _str2time(s) + + return s + + +def readPrecisely(file, l): + buff = file.read(l) + if len(buff) < l: + raise EOFError + return buff + + +class IEncodable(Interface): + """ + Interface for something which can be encoded to and decoded + to the DNS wire format. + + A binary-mode file object (such as L{io.BytesIO}) is used as a buffer when + encoding or decoding. + """ + + def encode(strio, compDict=None): + """ + Write a representation of this object to the given + file object. + + @type strio: File-like object + @param strio: The buffer to write to. It must have a C{tell()} method. + + @type compDict: L{dict} of L{bytes} to L{int} r L{None} + @param compDict: A mapping of names to byte offsets that have already + been written to the buffer, which may be used for compression (see RFC + 1035 section 4.1.4). When L{None}, encode without compression. + """ + + def decode(strio, length=None): + """ + Reconstruct an object from data read from the given + file object. + + @type strio: File-like object + @param strio: A seekable buffer from which bytes may be read. + + @type length: L{int} or L{None} + @param length: The number of bytes in this RDATA field. Most + implementations can ignore this value. Only in the case of + records similar to TXT where the total length is in no way + encoded in the data is it necessary. + """ + + +class IEncodableRecord(IEncodable, IRecord): + """ + Interface for DNS records that can be encoded and decoded. + + @since: Twisted 21.2.0 + """ + + +@implementer(IEncodable) +class Charstr: + def __init__(self, string: bytes = b""): + if not isinstance(string, bytes): + raise ValueError(f"{string!r} is not a byte string") + self.string = string + + def encode(self, strio, compDict=None): + """ + Encode this Character string into the appropriate byte format. + + @type strio: file + @param strio: The byte representation of this Charstr will be written + to this file. + """ + string = self.string + ind = len(string) + strio.write(_ord2bytes(ind)) + strio.write(string) + + def decode(self, strio, length=None): + """ + Decode a byte string into this Charstr. + + @type strio: file + @param strio: Bytes will be read from this file until the full string + is decoded. + + @raise EOFError: Raised when there are not enough bytes available from + C{strio}. + """ + self.string = b"" + l = ord(readPrecisely(strio, 1)) + self.string = readPrecisely(strio, l) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Charstr): + return self.string == other.string + return NotImplemented + + def __hash__(self): + return hash(self.string) + + def __str__(self) -> str: + """ + Represent this L{Charstr} instance by its string value. + """ + return nativeString(self.string) + + +@implementer(IEncodable) +class Name: + """ + A name in the domain name system, made up of multiple labels. For example, + I{twistedmatrix.com}. + + @ivar name: A byte string giving the name. + @type name: L{bytes} + """ + + def __init__(self, name: bytes | str = b""): + """ + @param name: A name. + @type name: L{bytes} or L{str} + """ + self.name = domainString(name) + + def encode(self, strio, compDict=None): + """ + Encode this Name into the appropriate byte format. + + @type strio: file + @param strio: The byte representation of this Name will be written to + this file. + + @type compDict: dict + @param compDict: dictionary of Names that have already been encoded + and whose addresses may be backreferenced by this Name (for the purpose + of reducing the message size). + """ + name = self.name + while name: + if compDict is not None: + if name in compDict: + strio.write(struct.pack("!H", 0xC000 | compDict[name])) + return + else: + compDict[name] = strio.tell() + Message.headerSize + ind = name.find(b".") + if ind > 0: + label, name = name[:ind], name[ind + 1 :] + else: + # This is the last label, end the loop after handling it. + label = name + name = None + ind = len(label) + strio.write(_ord2bytes(ind)) + strio.write(label) + strio.write(b"\x00") + + def decode(self, strio, length=None): + """ + Decode a byte string into this Name. + + @type strio: file + @param strio: Bytes will be read from this file until the full Name + is decoded. + + @raise EOFError: Raised when there are not enough bytes available + from C{strio}. + + @raise ValueError: Raised when the name cannot be decoded (for example, + because it contains a loop). + """ + visited = set() + self.name = b"" + off = 0 + while 1: + l = ord(readPrecisely(strio, 1)) + if l == 0: + if off > 0: + strio.seek(off) + return + if (l >> 6) == 3: + new_off = (l & 63) << 8 | ord(readPrecisely(strio, 1)) + if new_off in visited: + raise ValueError("Compression loop in encoded name") + visited.add(new_off) + if off == 0: + off = strio.tell() + strio.seek(new_off) + continue + label = readPrecisely(strio, l) + if self.name == b"": + self.name = label + else: + self.name = self.name + b"." + label + + def __eq__(self, other: object) -> bool: + if isinstance(other, Name): + return self.name.lower() == other.name.lower() + return NotImplemented + + def __hash__(self): + return hash(self.name) + + def __str__(self) -> str: + """ + Represent this L{Name} instance by its string name. + """ + return nativeString(self.name) + + +@comparable +@implementer(IEncodable) +class Query: + """ + Represent a single DNS query. + + @ivar name: The name about which this query is requesting information. + @type name: L{Name} + + @ivar type: The query type. + @type type: L{int} + + @ivar cls: The query class. + @type cls: L{int} + """ + + def __init__(self, name: Union[bytes, str] = b"", type: int = A, cls: int = IN): + """ + @type name: L{bytes} or L{str} + @param name: See L{Query.name} + + @type type: L{int} + @param type: The query type. + + @type cls: L{int} + @param cls: The query class. + """ + self.name = Name(name) + self.type = type + self.cls = cls + + def encode(self, strio, compDict=None): + self.name.encode(strio, compDict) + strio.write(struct.pack("!HH", self.type, self.cls)) + + def decode(self, strio, length=None): + self.name.decode(strio) + buff = readPrecisely(strio, 4) + self.type, self.cls = struct.unpack("!HH", buff) + + def __hash__(self): + return hash((self.name.name.lower(), self.type, self.cls)) + + def __cmp__(self, other): + if isinstance(other, Query): + return cmp( + (self.name.name.lower(), self.type, self.cls), + (other.name.name.lower(), other.type, other.cls), + ) + return NotImplemented + + def __str__(self) -> str: + t = QUERY_TYPES.get( + self.type, EXT_QUERIES.get(self.type, "UNKNOWN (%d)" % self.type) + ) + c = QUERY_CLASSES.get(self.cls, "UNKNOWN (%d)" % self.cls) + return f"<Query {self.name} {t} {c}>" + + def __repr__(self) -> str: + return f"Query({self.name.name!r}, {self.type!r}, {self.cls!r})" + + +@implementer(IEncodable) +class _OPTHeader(tputil.FancyStrMixin, tputil.FancyEqMixin): + """ + An OPT record header. + + @ivar name: The DNS name associated with this record. Since this + is a pseudo record, the name is always an L{Name} instance + with value b'', which represents the DNS root zone. This + attribute is a readonly property. + + @ivar type: The DNS record type. This is a fixed value of 41 + C{dns.OPT} for OPT Record. This attribute is a readonly + property. + + @see: L{_OPTHeader.__init__} for documentation of other public + instance attributes. + + @see: U{https://tools.ietf.org/html/rfc6891#section-6.1.2} + + @since: 13.2 + """ + + showAttributes = ( + ("name", lambda n: nativeString(n.name)), + "type", + "udpPayloadSize", + "extendedRCODE", + "version", + "dnssecOK", + "options", + ) + + compareAttributes = ( + "name", + "type", + "udpPayloadSize", + "extendedRCODE", + "version", + "dnssecOK", + "options", + ) + + def __init__( + self, + udpPayloadSize=4096, + extendedRCODE=0, + version=0, + dnssecOK=False, + options=None, + ): + """ + @type udpPayloadSize: L{int} + @param udpPayloadSize: The number of octets of the largest UDP + payload that can be reassembled and delivered in the + requestor's network stack. + + @type extendedRCODE: L{int} + @param extendedRCODE: Forms the upper 8 bits of extended + 12-bit RCODE (together with the 4 bits defined in + [RFC1035]. Note that EXTENDED-RCODE value 0 indicates + that an unextended RCODE is in use (values 0 through 15). + + @type version: L{int} + @param version: Indicates the implementation level of the + setter. Full conformance with this specification is + indicated by version C{0}. + + @type dnssecOK: L{bool} + @param dnssecOK: DNSSEC OK bit as defined by [RFC3225]. + + @type options: L{list} + @param options: A L{list} of 0 or more L{_OPTVariableOption} + instances. + """ + self.udpPayloadSize = udpPayloadSize + self.extendedRCODE = extendedRCODE + self.version = version + self.dnssecOK = dnssecOK + + if options is None: + options = [] + self.options = options + + @property + def name(self): + """ + A readonly property for accessing the C{name} attribute of + this record. + + @return: The DNS name associated with this record. Since this + is a pseudo record, the name is always an L{Name} instance + with value b'', which represents the DNS root zone. + """ + return Name(b"") + + @property + def type(self): + """ + A readonly property for accessing the C{type} attribute of + this record. + + @return: The DNS record type. This is a fixed value of 41 + (C{dns.OPT} for OPT Record. + """ + return OPT + + def encode(self, strio, compDict=None): + """ + Encode this L{_OPTHeader} instance to bytes. + + @type strio: file + @param strio: the byte representation of this L{_OPTHeader} + will be written to this file. + + @type compDict: L{dict} or L{None} + @param compDict: A dictionary of backreference addresses that + have already been written to this stream and that may + be used for DNS name compression. + """ + b = BytesIO() + for o in self.options: + o.encode(b) + optionBytes = b.getvalue() + + RRHeader( + name=self.name.name, + type=self.type, + cls=self.udpPayloadSize, + ttl=(self.extendedRCODE << 24 | self.version << 16 | self.dnssecOK << 15), + payload=UnknownRecord(optionBytes), + ).encode(strio, compDict) + + def decode(self, strio, length=None): + """ + Decode bytes into an L{_OPTHeader} instance. + + @type strio: file + @param strio: Bytes will be read from this file until the full + L{_OPTHeader} is decoded. + + @type length: L{int} or L{None} + @param length: Not used. + """ + + h = RRHeader() + h.decode(strio, length) + h.payload = UnknownRecord(readPrecisely(strio, h.rdlength)) + + newOptHeader = self.fromRRHeader(h) + + for attrName in self.compareAttributes: + if attrName not in ("name", "type"): + setattr(self, attrName, getattr(newOptHeader, attrName)) + + @classmethod + def fromRRHeader(cls, rrHeader): + """ + A classmethod for constructing a new L{_OPTHeader} from the + attributes and payload of an existing L{RRHeader} instance. + + @type rrHeader: L{RRHeader} + @param rrHeader: An L{RRHeader} instance containing an + L{UnknownRecord} payload. + + @return: An instance of L{_OPTHeader}. + @rtype: L{_OPTHeader} + """ + options = None + if rrHeader.payload is not None: + options = [] + optionsBytes = BytesIO(rrHeader.payload.data) + optionsBytesLength = len(rrHeader.payload.data) + while optionsBytes.tell() < optionsBytesLength: + o = _OPTVariableOption() + o.decode(optionsBytes) + options.append(o) + + # Decode variable options if present + return cls( + udpPayloadSize=rrHeader.cls, + extendedRCODE=rrHeader.ttl >> 24, + version=rrHeader.ttl >> 16 & 0xFF, + dnssecOK=(rrHeader.ttl & 0xFFFF) >> 15, + options=options, + ) + + +@implementer(IEncodable) +class _OPTVariableOption(tputil.FancyStrMixin, tputil.FancyEqMixin): + """ + A class to represent OPT record variable options. + + @see: L{_OPTVariableOption.__init__} for documentation of public + instance attributes. + + @see: U{https://tools.ietf.org/html/rfc6891#section-6.1.2} + + @since: 13.2 + """ + + showAttributes = ("code", ("data", nativeString)) + compareAttributes = ("code", "data") + + _fmt = "!HH" + + def __init__(self, code=0, data=b""): + """ + @type code: L{int} + @param code: The option code + + @type data: L{bytes} + @param data: The option data + """ + self.code = code + self.data = data + + def encode(self, strio, compDict=None): + """ + Encode this L{_OPTVariableOption} to bytes. + + @type strio: file + @param strio: the byte representation of this + L{_OPTVariableOption} will be written to this file. + + @type compDict: L{dict} or L{None} + @param compDict: A dictionary of backreference addresses that + have already been written to this stream and that may + be used for DNS name compression. + """ + strio.write(struct.pack(self._fmt, self.code, len(self.data)) + self.data) + + def decode(self, strio, length=None): + """ + Decode bytes into an L{_OPTVariableOption} instance. + + @type strio: file + @param strio: Bytes will be read from this file until the full + L{_OPTVariableOption} is decoded. + + @type length: L{int} or L{None} + @param length: Not used. + """ + l = struct.calcsize(self._fmt) + buff = readPrecisely(strio, l) + self.code, length = struct.unpack(self._fmt, buff) + self.data = readPrecisely(strio, length) + + +@implementer(IEncodable) +class RRHeader(tputil.FancyEqMixin): + """ + A resource record header. + + @cvar fmt: L{str} specifying the byte format of an RR. + + @ivar name: The name about which this reply contains information. + @type name: L{Name} + + @ivar type: The query type of the original request. + @type type: L{int} + + @ivar cls: The query class of the original request. + + @ivar ttl: The time-to-live for this record. + @type ttl: L{int} + + @ivar payload: The record described by this header. + @type payload: L{IEncodableRecord} or L{None} + + @ivar auth: A L{bool} indicating whether this C{RRHeader} was parsed from + an authoritative message. + """ + + compareAttributes = ("name", "type", "cls", "ttl", "payload", "auth") + + fmt = "!HHIH" + + rdlength = None + + cachedResponse = None + + def __init__( + self, + name: Union[bytes, str] = b"", + type: int = A, + cls: int = IN, + ttl: SupportsInt = 0, + payload: Optional[IEncodableRecord] = None, + auth: bool = False, + ): + """ + @type name: L{bytes} or L{str} + @param name: See L{RRHeader.name} + + @type type: L{int} + @param type: The query type. + + @type cls: L{int} + @param cls: The query class. + + @type ttl: L{int} + @param ttl: Time to live for this record. This will be + converted to an L{int}. + + @type payload: L{IEncodableRecord} or L{None} + @param payload: An optional Query Type specific data object. + + @raises TypeError: if the ttl cannot be converted to an L{int}. + @raises ValueError: if the ttl is negative. + @raises ValueError: if the payload type is not equal to the C{type} + argument. + """ + payloadType = None if payload is None else payload.TYPE + if payloadType is not None and payloadType != type: + raise ValueError( + "Payload type (%s) does not match given type (%s)" + % ( + QUERY_TYPES.get(payloadType, payloadType), + QUERY_TYPES.get(type, type), + ) + ) + + integralTTL = int(ttl) + + if integralTTL < 0: + raise ValueError("TTL cannot be negative") + + self.name = Name(name) + self.type = type + self.cls = cls + self.ttl = integralTTL + self.payload = payload + self.auth = auth + + def encode(self, strio, compDict=None): + self.name.encode(strio, compDict) + strio.write(struct.pack(self.fmt, self.type, self.cls, self.ttl, 0)) + if self.payload: + prefix = strio.tell() + self.payload.encode(strio, compDict) + aft = strio.tell() + strio.seek(prefix - 2, 0) + strio.write(struct.pack("!H", aft - prefix)) + strio.seek(aft, 0) + + def decode(self, strio, length=None): + self.name.decode(strio) + l = struct.calcsize(self.fmt) + buff = readPrecisely(strio, l) + r = struct.unpack(self.fmt, buff) + self.type, self.cls, self.ttl, self.rdlength = r + + def isAuthoritative(self): + return self.auth + + def __str__(self) -> str: + t = QUERY_TYPES.get( + self.type, EXT_QUERIES.get(self.type, "UNKNOWN (%d)" % self.type) + ) + c = QUERY_CLASSES.get(self.cls, "UNKNOWN (%d)" % self.cls) + return "<RR name=%s type=%s class=%s ttl=%ds auth=%s>" % ( + self.name, + t, + c, + self.ttl, + self.auth and "True" or "False", + ) + + __repr__ = __str__ + + +@implementer(IEncodableRecord) +class SimpleRecord(tputil.FancyStrMixin, tputil.FancyEqMixin): + """ + A Resource Record which consists of a single RFC 1035 domain-name. + + @type name: L{Name} + @ivar name: The name associated with this record. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + """ + + showAttributes = (("name", "name", "%s"), "ttl") + compareAttributes = ("name", "ttl") + + TYPE: Optional[int] = None + name = None + + def __init__(self, name=b"", ttl=None): + """ + @param name: See L{SimpleRecord.name} + @type name: L{bytes} or L{str} + """ + self.name = Name(name) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + self.name.encode(strio, compDict) + + def decode(self, strio, length=None): + self.name = Name() + self.name.decode(strio) + + def __hash__(self): + return hash(self.name) + + +# Kinds of RRs - oh my! +class Record_NS(SimpleRecord): + """ + An authoritative nameserver. + """ + + TYPE = NS + fancybasename = "NS" + + +class Record_MD(SimpleRecord): + """ + A mail destination. + + This record type is obsolete. + + @see: L{Record_MX} + """ + + TYPE = MD + fancybasename = "MD" + + +class Record_MF(SimpleRecord): + """ + A mail forwarder. + + This record type is obsolete. + + @see: L{Record_MX} + """ + + TYPE = MF + fancybasename = "MF" + + +class Record_CNAME(SimpleRecord): + """ + The canonical name for an alias. + """ + + TYPE = CNAME + fancybasename = "CNAME" + + +class Record_MB(SimpleRecord): + """ + A mailbox domain name. + + This is an experimental record type. + """ + + TYPE = MB + fancybasename = "MB" + + +class Record_MG(SimpleRecord): + """ + A mail group member. + + This is an experimental record type. + """ + + TYPE = MG + fancybasename = "MG" + + +class Record_MR(SimpleRecord): + """ + A mail rename domain name. + + This is an experimental record type. + """ + + TYPE = MR + fancybasename = "MR" + + +class Record_PTR(SimpleRecord): + """ + A domain name pointer. + """ + + TYPE = PTR + fancybasename = "PTR" + + +class Record_DNAME(SimpleRecord): + """ + A non-terminal DNS name redirection. + + This record type provides the capability to map an entire subtree of the + DNS name space to another domain. It differs from the CNAME record which + maps a single node of the name space. + + @see: U{http://www.faqs.org/rfcs/rfc2672.html} + @see: U{http://www.faqs.org/rfcs/rfc3363.html} + """ + + TYPE = DNAME + fancybasename = "DNAME" + + +@implementer(IEncodableRecord) +class Record_A(tputil.FancyEqMixin): + """ + An IPv4 host address. + + @type address: L{bytes} + @ivar address: The packed network-order representation of the IPv4 address + associated with this record. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + """ + + compareAttributes = ("address", "ttl") + + TYPE = A + address = None + + def __init__(self, address="0.0.0.0", ttl=None): + """ + @type address: L{bytes} or L{str} + @param address: The IPv4 address associated with this record, in + quad-dotted notation. + """ + if isinstance(address, bytes): + address = address.decode("ascii") + + address = socket.inet_aton(address) + self.address = address + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(self.address) + + def decode(self, strio, length=None): + self.address = readPrecisely(strio, 4) + + def __hash__(self): + return hash(self.address) + + def __str__(self) -> str: + return f"<A address={self.dottedQuad()} ttl={self.ttl}>" + + __repr__ = __str__ + + def dottedQuad(self): + return socket.inet_ntoa(self.address) + + +@implementer(IEncodableRecord) +class Record_SOA(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + Marks the start of a zone of authority. + + This record describes parameters which are shared by all records within a + particular zone. + + @type mname: L{Name} + @ivar mname: The domain-name of the name server that was the original or + primary source of data for this zone. + + @type rname: L{Name} + @ivar rname: A domain-name which specifies the mailbox of the person + responsible for this zone. + + @type serial: L{int} + @ivar serial: The unsigned 32 bit version number of the original copy of + the zone. Zone transfers preserve this value. This value wraps and + should be compared using sequence space arithmetic. + + @type refresh: L{int} + @ivar refresh: A 32 bit time interval before the zone should be refreshed. + + @type minimum: L{int} + @ivar minimum: The unsigned 32 bit minimum TTL field that should be + exported with any RR from this zone. + + @type expire: L{int} + @ivar expire: A 32 bit time value that specifies the upper limit on the + time interval that can elapse before the zone is no longer + authoritative. + + @type retry: L{int} + @ivar retry: A 32 bit time interval that should elapse before a failed + refresh should be retried. + + @type ttl: L{int} + @ivar ttl: The default TTL to use for records served from this zone. + """ + + fancybasename = "SOA" + compareAttributes = ( + "serial", + "mname", + "rname", + "refresh", + "expire", + "retry", + "minimum", + "ttl", + ) + showAttributes = ( + ("mname", "mname", "%s"), + ("rname", "rname", "%s"), + "serial", + "refresh", + "retry", + "expire", + "minimum", + "ttl", + ) + + TYPE = SOA + + def __init__( + self, + mname=b"", + rname=b"", + serial=0, + refresh=0, + retry=0, + expire=0, + minimum=0, + ttl=None, + ): + """ + @param mname: See L{Record_SOA.mname} + @type mname: L{bytes} or L{str} + + @param rname: See L{Record_SOA.rname} + @type rname: L{bytes} or L{str} + """ + self.mname, self.rname = Name(mname), Name(rname) + self.serial, self.refresh = str2time(serial), str2time(refresh) + self.minimum, self.expire = str2time(minimum), str2time(expire) + self.retry = str2time(retry) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + self.mname.encode(strio, compDict) + self.rname.encode(strio, compDict) + strio.write( + struct.pack( + "!LlllL", + self.serial, + self.refresh, + self.retry, + self.expire, + self.minimum, + ) + ) + + def decode(self, strio, length=None): + self.mname, self.rname = Name(), Name() + self.mname.decode(strio) + self.rname.decode(strio) + r = struct.unpack("!LlllL", readPrecisely(strio, 20)) + self.serial, self.refresh, self.retry, self.expire, self.minimum = r + + def __hash__(self): + return hash( + (self.serial, self.mname, self.rname, self.refresh, self.expire, self.retry) + ) + + +@implementer(IEncodableRecord) +class Record_NULL(tputil.FancyStrMixin, tputil.FancyEqMixin): + """ + A null record. + + This is an experimental record type. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + """ + + fancybasename = "NULL" + showAttributes = (("payload", _nicebytes), "ttl") + compareAttributes = ("payload", "ttl") + + TYPE = NULL + + def __init__(self, payload=None, ttl=None): + self.payload = payload + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(self.payload) + + def decode(self, strio, length=None): + self.payload = readPrecisely(strio, length) + + def __hash__(self): + return hash(self.payload) + + +@implementer(IEncodableRecord) +class Record_WKS(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + A well known service description. + + This record type is obsolete. See L{Record_SRV}. + + @type address: L{bytes} + @ivar address: The packed network-order representation of the IPv4 address + associated with this record. + + @type protocol: L{int} + @ivar protocol: The 8 bit IP protocol number for which this service map is + relevant. + + @type map: L{bytes} + @ivar map: A bitvector indicating the services available at the specified + address. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + """ + + fancybasename = "WKS" + compareAttributes = ("address", "protocol", "map", "ttl") + showAttributes = [("_address", "address", "%s"), "protocol", "ttl"] + + TYPE = WKS + + @property + def _address(self): + return socket.inet_ntoa(self.address) + + def __init__(self, address="0.0.0.0", protocol=0, map=b"", ttl=None): + """ + @type address: L{bytes} or L{str} + @param address: The IPv4 address associated with this record, in + quad-dotted notation. + """ + if isinstance(address, bytes): + address = address.decode("idna") + + self.address = socket.inet_aton(address) + self.protocol, self.map = protocol, map + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(self.address) + strio.write(struct.pack("!B", self.protocol)) + strio.write(self.map) + + def decode(self, strio, length=None): + self.address = readPrecisely(strio, 4) + self.protocol = struct.unpack("!B", readPrecisely(strio, 1))[0] + self.map = readPrecisely(strio, length - 5) + + def __hash__(self): + return hash((self.address, self.protocol, self.map)) + + +@implementer(IEncodableRecord) +class Record_AAAA(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + An IPv6 host address. + + @type address: L{bytes} + @ivar address: The packed network-order representation of the IPv6 address + associated with this record. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + + @see: U{http://www.faqs.org/rfcs/rfc1886.html} + """ + + TYPE = AAAA + + fancybasename = "AAAA" + showAttributes = (("_address", "address", "%s"), "ttl") + compareAttributes = ("address", "ttl") + + @property + def _address(self): + return socket.inet_ntop(AF_INET6, self.address) + + def __init__(self, address="::", ttl=None): + """ + @type address: L{bytes} or L{str} + @param address: The IPv6 address for this host, in RFC 2373 format. + """ + if isinstance(address, bytes): + address = address.decode("idna") + + self.address = socket.inet_pton(AF_INET6, address) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(self.address) + + def decode(self, strio, length=None): + self.address = readPrecisely(strio, 16) + + def __hash__(self): + return hash(self.address) + + +@implementer(IEncodableRecord) +class Record_A6(tputil.FancyStrMixin, tputil.FancyEqMixin): + """ + An IPv6 address. + + This is an experimental record type. + + @type prefixLen: L{int} + @ivar prefixLen: The length of the suffix. + + @type suffix: L{bytes} + @ivar suffix: An IPv6 address suffix in network order. + + @type prefix: L{Name} + @ivar prefix: If specified, a name which will be used as a prefix for other + A6 records. + + @type bytes: L{int} + @ivar bytes: The length of the prefix. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + + @see: U{http://www.faqs.org/rfcs/rfc2874.html} + @see: U{http://www.faqs.org/rfcs/rfc3363.html} + @see: U{http://www.faqs.org/rfcs/rfc3364.html} + """ + + TYPE = A6 + + fancybasename = "A6" + showAttributes = (("_suffix", "suffix", "%s"), ("prefix", "prefix", "%s"), "ttl") + compareAttributes = ("prefixLen", "prefix", "suffix", "ttl") + + @property + def _suffix(self): + return socket.inet_ntop(AF_INET6, self.suffix) + + def __init__( + self, + prefixLen: int = 0, + suffix: bytes | str = "::", + prefix: bytes | str = b"", + ttl: Union[str, bytes, int, None] = None, + ): + """ + @param suffix: An IPv6 address suffix in in RFC 2373 format. + @type suffix: L{bytes} or L{str} + + @param prefix: An IPv6 address prefix for other A6 records. + @type prefix: L{bytes} or L{str} + """ + if isinstance(suffix, bytes): + suffix = suffix.decode("idna") + + self.prefixLen = prefixLen + self.suffix = socket.inet_pton(AF_INET6, suffix) + self.prefix = Name(prefix) + self.bytes = int((128 - self.prefixLen) / 8.0) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(struct.pack("!B", self.prefixLen)) + if self.bytes: + strio.write(self.suffix[-self.bytes :]) + if self.prefixLen: + # This may not be compressed + self.prefix.encode(strio, None) + + def decode(self, strio, length=None): + self.prefixLen = struct.unpack("!B", readPrecisely(strio, 1))[0] + self.bytes = int((128 - self.prefixLen) / 8.0) + if self.bytes: + self.suffix = b"\x00" * (16 - self.bytes) + readPrecisely(strio, self.bytes) + if self.prefixLen: + self.prefix.decode(strio) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Record_A6): + return ( + self.prefixLen == other.prefixLen + and self.suffix[-self.bytes :] == other.suffix[-self.bytes :] + and self.prefix == other.prefix + and self.ttl == other.ttl + ) + return NotImplemented + + def __hash__(self): + return hash((self.prefixLen, self.suffix[-self.bytes :], self.prefix)) + + def __str__(self) -> str: + return "<A6 %s %s (%d) ttl=%s>" % ( + self.prefix, + socket.inet_ntop(AF_INET6, self.suffix), + self.prefixLen, + self.ttl, + ) + + +@implementer(IEncodableRecord) +class Record_SRV(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + The location of the server(s) for a specific protocol and domain. + + This is an experimental record type. + + @type priority: L{int} + @ivar priority: The priority of this target host. A client MUST attempt to + contact the target host with the lowest-numbered priority it can reach; + target hosts with the same priority SHOULD be tried in an order defined + by the weight field. + + @type weight: L{int} + @ivar weight: Specifies a relative weight for entries with the same + priority. Larger weights SHOULD be given a proportionately higher + probability of being selected. + + @type port: L{int} + @ivar port: The port on this target host of this service. + + @type target: L{Name} + @ivar target: The domain name of the target host. There MUST be one or + more address records for this name, the name MUST NOT be an alias (in + the sense of RFC 1034 or RFC 2181). Implementors are urged, but not + required, to return the address record(s) in the Additional Data + section. Unless and until permitted by future standards action, name + compression is not to be used for this field. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + + @see: U{http://www.faqs.org/rfcs/rfc2782.html} + """ + + TYPE = SRV + + fancybasename = "SRV" + compareAttributes = ("priority", "weight", "target", "port", "ttl") + showAttributes = ("priority", "weight", ("target", "target", "%s"), "port", "ttl") + + def __init__(self, priority=0, weight=0, port=0, target=b"", ttl=None): + """ + @param target: See L{Record_SRV.target} + @type target: L{bytes} or L{str} + """ + self.priority = int(priority) + self.weight = int(weight) + self.port = int(port) + self.target = Name(target) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(struct.pack("!HHH", self.priority, self.weight, self.port)) + # This can't be compressed + self.target.encode(strio, None) + + def decode(self, strio, length=None): + r = struct.unpack("!HHH", readPrecisely(strio, struct.calcsize("!HHH"))) + self.priority, self.weight, self.port = r + self.target = Name() + self.target.decode(strio) + + def __hash__(self): + return hash((self.priority, self.weight, self.port, self.target)) + + +@implementer(IEncodableRecord) +class Record_NAPTR(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + The location of the server(s) for a specific protocol and domain. + + @type order: L{int} + @ivar order: An integer specifying the order in which the NAPTR records + MUST be processed to ensure the correct ordering of rules. Low numbers + are processed before high numbers. + + @type preference: L{int} + @ivar preference: An integer that specifies the order in which NAPTR + records with equal "order" values SHOULD be processed, low numbers + being processed before high numbers. + + @type flag: L{Charstr} + @ivar flag: A <character-string> containing flags to control aspects of the + rewriting and interpretation of the fields in the record. Flags + are single characters from the set [A-Z0-9]. The case of the alphabetic + characters is not significant. + + At this time only four flags, "S", "A", "U", and "P", are defined. + + @type service: L{Charstr} + @ivar service: Specifies the service(s) available down this rewrite path. + It may also specify the particular protocol that is used to talk with a + service. A protocol MUST be specified if the flags field states that + the NAPTR is terminal. + + @type regexp: L{Charstr} + @ivar regexp: A STRING containing a substitution expression that is applied + to the original string held by the client in order to construct the + next domain name to lookup. + + @type replacement: L{Name} + @ivar replacement: The next NAME to query for NAPTR, SRV, or address + records depending on the value of the flags field. This MUST be a + fully qualified domain-name. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + + @see: U{http://www.faqs.org/rfcs/rfc2915.html} + """ + + TYPE = NAPTR + + compareAttributes = ( + "order", + "preference", + "flags", + "service", + "regexp", + "replacement", + ) + fancybasename = "NAPTR" + + showAttributes = ( + "order", + "preference", + ("flags", "flags", "%s"), + ("service", "service", "%s"), + ("regexp", "regexp", "%s"), + ("replacement", "replacement", "%s"), + "ttl", + ) + + def __init__( + self, + order=0, + preference=0, + flags=b"", + service=b"", + regexp=b"", + replacement=b"", + ttl=None, + ): + """ + @param replacement: See L{Record_NAPTR.replacement} + @type replacement: L{bytes} or L{str} + """ + self.order = int(order) + self.preference = int(preference) + self.flags = Charstr(flags) + self.service = Charstr(service) + self.regexp = Charstr(regexp) + self.replacement = Name(replacement) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(struct.pack("!HH", self.order, self.preference)) + # This can't be compressed + self.flags.encode(strio, None) + self.service.encode(strio, None) + self.regexp.encode(strio, None) + self.replacement.encode(strio, None) + + def decode(self, strio, length=None): + r = struct.unpack("!HH", readPrecisely(strio, struct.calcsize("!HH"))) + self.order, self.preference = r + self.flags = Charstr() + self.service = Charstr() + self.regexp = Charstr() + self.replacement = Name() + self.flags.decode(strio) + self.service.decode(strio) + self.regexp.decode(strio) + self.replacement.decode(strio) + + def __hash__(self): + return hash( + ( + self.order, + self.preference, + self.flags, + self.service, + self.regexp, + self.replacement, + ) + ) + + +@implementer(IEncodableRecord) +class Record_AFSDB(tputil.FancyStrMixin, tputil.FancyEqMixin): + """ + Map from a domain name to the name of an AFS cell database server. + + @type subtype: L{int} + @ivar subtype: In the case of subtype 1, the host has an AFS version 3.0 + Volume Location Server for the named AFS cell. In the case of subtype + 2, the host has an authenticated name server holding the cell-root + directory node for the named DCE/NCA cell. + + @type hostname: L{Name} + @ivar hostname: The domain name of a host that has a server for the cell + named by this record. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + + @see: U{http://www.faqs.org/rfcs/rfc1183.html} + """ + + TYPE = AFSDB + + fancybasename = "AFSDB" + compareAttributes = ("subtype", "hostname", "ttl") + showAttributes = ("subtype", ("hostname", "hostname", "%s"), "ttl") + + def __init__(self, subtype=0, hostname=b"", ttl=None): + """ + @param hostname: See L{Record_AFSDB.hostname} + @type hostname: L{bytes} or L{str} + """ + self.subtype = int(subtype) + self.hostname = Name(hostname) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(struct.pack("!H", self.subtype)) + self.hostname.encode(strio, compDict) + + def decode(self, strio, length=None): + r = struct.unpack("!H", readPrecisely(strio, struct.calcsize("!H"))) + (self.subtype,) = r + self.hostname.decode(strio) + + def __hash__(self): + return hash((self.subtype, self.hostname)) + + +@implementer(IEncodableRecord) +class Record_RP(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + The responsible person for a domain. + + @type mbox: L{Name} + @ivar mbox: A domain name that specifies the mailbox for the responsible + person. + + @type txt: L{Name} + @ivar txt: A domain name for which TXT RR's exist (indirection through + which allows information sharing about the contents of this RP record). + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + + @see: U{http://www.faqs.org/rfcs/rfc1183.html} + """ + + TYPE = RP + + fancybasename = "RP" + compareAttributes = ("mbox", "txt", "ttl") + showAttributes = (("mbox", "mbox", "%s"), ("txt", "txt", "%s"), "ttl") + + def __init__(self, mbox=b"", txt=b"", ttl=None): + """ + @param mbox: See L{Record_RP.mbox}. + @type mbox: L{bytes} or L{str} + + @param txt: See L{Record_RP.txt} + @type txt: L{bytes} or L{str} + """ + self.mbox = Name(mbox) + self.txt = Name(txt) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + self.mbox.encode(strio, compDict) + self.txt.encode(strio, compDict) + + def decode(self, strio, length=None): + self.mbox = Name() + self.txt = Name() + self.mbox.decode(strio) + self.txt.decode(strio) + + def __hash__(self): + return hash((self.mbox, self.txt)) + + +@implementer(IEncodableRecord) +class Record_HINFO(tputil.FancyStrMixin, tputil.FancyEqMixin): + """ + Host information. + + @type cpu: L{bytes} + @ivar cpu: Specifies the CPU type. + + @type os: L{bytes} + @ivar os: Specifies the OS. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + """ + + TYPE = HINFO + + fancybasename = "HINFO" + showAttributes = (("cpu", _nicebytes), ("os", _nicebytes), "ttl") + compareAttributes = ("cpu", "os", "ttl") + + def __init__( + self, + cpu: bytes = b"", + os: bytes = b"", + ttl: Union[str, bytes, int, None] = None, + ): + self.cpu, self.os = cpu, os + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(struct.pack("!B", len(self.cpu)) + self.cpu) + strio.write(struct.pack("!B", len(self.os)) + self.os) + + def decode(self, strio, length=None): + cpu = struct.unpack("!B", readPrecisely(strio, 1))[0] + self.cpu = readPrecisely(strio, cpu) + os = struct.unpack("!B", readPrecisely(strio, 1))[0] + self.os = readPrecisely(strio, os) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Record_HINFO): + return ( + self.os.lower() == other.os.lower() + and self.cpu.lower() == other.cpu.lower() + and self.ttl == other.ttl + ) + return NotImplemented + + def __hash__(self): + return hash((self.os.lower(), self.cpu.lower())) + + +@implementer(IEncodableRecord) +class Record_MINFO(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + Mailbox or mail list information. + + This is an experimental record type. + + @type rmailbx: L{Name} + @ivar rmailbx: A domain-name which specifies a mailbox which is responsible + for the mailing list or mailbox. If this domain name names the root, + the owner of the MINFO RR is responsible for itself. + + @type emailbx: L{Name} + @ivar emailbx: A domain-name which specifies a mailbox which is to receive + error messages related to the mailing list or mailbox specified by the + owner of the MINFO record. If this domain name names the root, errors + should be returned to the sender of the message. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + """ + + TYPE = MINFO + + rmailbx = None + emailbx = None + + fancybasename = "MINFO" + compareAttributes = ("rmailbx", "emailbx", "ttl") + showAttributes = ( + ("rmailbx", "responsibility", "%s"), + ("emailbx", "errors", "%s"), + "ttl", + ) + + def __init__(self, rmailbx=b"", emailbx=b"", ttl=None): + """ + @param rmailbx: See L{Record_MINFO.rmailbx}. + @type rmailbx: L{bytes} or L{str} + + @param emailbx: See L{Record_MINFO.rmailbx}. + @type emailbx: L{bytes} or L{str} + """ + self.rmailbx, self.emailbx = Name(rmailbx), Name(emailbx) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + self.rmailbx.encode(strio, compDict) + self.emailbx.encode(strio, compDict) + + def decode(self, strio, length=None): + self.rmailbx, self.emailbx = Name(), Name() + self.rmailbx.decode(strio) + self.emailbx.decode(strio) + + def __hash__(self): + return hash((self.rmailbx, self.emailbx)) + + +@implementer(IEncodableRecord) +class Record_MX(tputil.FancyStrMixin, tputil.FancyEqMixin): + """ + Mail exchange. + + @type preference: L{int} + @ivar preference: Specifies the preference given to this RR among others at + the same owner. Lower values are preferred. + + @type name: L{Name} + @ivar name: A domain-name which specifies a host willing to act as a mail + exchange. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be + cached. + """ + + TYPE = MX + + fancybasename = "MX" + compareAttributes = ("preference", "name", "ttl") + showAttributes = ("preference", ("name", "name", "%s"), "ttl") + + def __init__(self, preference=0, name=b"", ttl=None, **kwargs): + """ + @param name: See L{Record_MX.name}. + @type name: L{bytes} or L{str} + """ + self.preference = int(preference) + self.name = Name(kwargs.get("exchange", name)) + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + strio.write(struct.pack("!H", self.preference)) + self.name.encode(strio, compDict) + + def decode(self, strio, length=None): + self.preference = struct.unpack("!H", readPrecisely(strio, 2))[0] + self.name = Name() + self.name.decode(strio) + + def __hash__(self): + return hash((self.preference, self.name)) + + +@implementer(IEncodableRecord) +class Record_SSHFP(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + A record containing the fingerprint of an SSH key. + + @type algorithm: L{int} + @ivar algorithm: The SSH key's algorithm, such as L{ALGORITHM_RSA}. + Note that the numbering used for SSH key algorithms is specific + to the SSHFP record, and is not the same as the numbering + used for KEY or SIG records. + + @type fingerprintType: L{int} + @ivar fingerprintType: The fingerprint type, + such as L{FINGERPRINT_TYPE_SHA256}. + + @type fingerprint: L{bytes} + @ivar fingerprint: The key's fingerprint, e.g. a 32-byte SHA-256 digest. + + @cvar ALGORITHM_RSA: The algorithm value for C{ssh-rsa} keys. + @cvar ALGORITHM_DSS: The algorithm value for C{ssh-dss} keys. + @cvar ALGORITHM_ECDSA: The algorithm value for C{ecdsa-sha2-*} keys. + @cvar ALGORITHM_Ed25519: The algorithm value for C{ed25519} keys. + + @cvar FINGERPRINT_TYPE_SHA1: The type for SHA-1 fingerprints. + @cvar FINGERPRINT_TYPE_SHA256: The type for SHA-256 fingerprints. + + @see: U{RFC 4255 <https://tools.ietf.org/html/rfc4255>} + and + U{RFC 6594 <https://tools.ietf.org/html/rfc6594>} + """ + + fancybasename = "SSHFP" + compareAttributes = ("algorithm", "fingerprintType", "fingerprint", "ttl") + showAttributes = ("algorithm", "fingerprintType", "fingerprint") + + TYPE = SSHFP + + ALGORITHM_RSA = 1 + ALGORITHM_DSS = 2 + ALGORITHM_ECDSA = 3 + ALGORITHM_Ed25519 = 4 + + FINGERPRINT_TYPE_SHA1 = 1 + FINGERPRINT_TYPE_SHA256 = 2 + + def __init__(self, algorithm=0, fingerprintType=0, fingerprint=b"", ttl=0): + self.algorithm = algorithm + self.fingerprintType = fingerprintType + self.fingerprint = fingerprint + self.ttl = ttl + + def encode(self, strio, compDict=None): + strio.write(struct.pack("!BB", self.algorithm, self.fingerprintType)) + strio.write(self.fingerprint) + + def decode(self, strio, length=None): + r = struct.unpack("!BB", readPrecisely(strio, 2)) + (self.algorithm, self.fingerprintType) = r + self.fingerprint = readPrecisely(strio, length - 2) + + def __hash__(self): + return hash((self.algorithm, self.fingerprintType, self.fingerprint)) + + +@implementer(IEncodableRecord) +class Record_TXT(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + Freeform text. + + @type data: L{list} of L{bytes} + @ivar data: Freeform text which makes up this record. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be cached. + """ + + TYPE = TXT + + fancybasename = "TXT" + showAttributes = (("data", _nicebyteslist), "ttl") + compareAttributes = ("data", "ttl") + + def __init__(self, *data, **kw): + self.data = list(data) + # arg man python sucks so bad + self.ttl = str2time(kw.get("ttl", None)) + + def encode(self, strio, compDict=None): + for d in self.data: + strio.write(struct.pack("!B", len(d)) + d) + + def decode(self, strio, length=None): + soFar = 0 + self.data = [] + while soFar < length: + L = struct.unpack("!B", readPrecisely(strio, 1))[0] + self.data.append(readPrecisely(strio, L)) + soFar += L + 1 + if soFar != length: + log.msg( + "Decoded %d bytes in %s record, but rdlength is %d" + % (soFar, self.fancybasename, length) + ) + + def __hash__(self): + return hash(tuple(self.data)) + + +@implementer(IEncodableRecord) +class UnknownRecord(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + Encapsulate the wire data for unknown record types so that they can + pass through the system unchanged. + + @type data: L{bytes} + @ivar data: Wire data which makes up this record. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds which this record should be cached. + + @since: 11.1 + """ + + TYPE = None + + fancybasename = "UNKNOWN" + compareAttributes = ("data", "ttl") + showAttributes = (("data", _nicebytes), "ttl") + + def __init__(self, data=b"", ttl=None): + self.data = data + self.ttl = str2time(ttl) + + def encode(self, strio, compDict=None): + """ + Write the raw bytes corresponding to this record's payload to the + stream. + """ + strio.write(self.data) + + def decode(self, strio, length=None): + """ + Load the bytes which are part of this record from the stream and store + them unparsed and unmodified. + """ + if length is None: + raise Exception("must know length for unknown record types") + self.data = readPrecisely(strio, length) + + def __hash__(self): + return hash((self.data, self.ttl)) + + +class Record_SPF(Record_TXT): + """ + Structurally, freeform text. Semantically, a policy definition, formatted + as defined in U{rfc 4408<http://www.faqs.org/rfcs/rfc4408.html>}. + + @type data: L{list} of L{bytes} + @ivar data: Freeform text which makes up this record. + + @type ttl: L{int} + @ivar ttl: The maximum number of seconds + which this record should be cached. + """ + + TYPE = SPF + fancybasename = "SPF" + + +@implementer(IEncodableRecord) +class Record_TSIG(tputil.FancyEqMixin, tputil.FancyStrMixin): + """ + A transaction signature, encapsulated in a RR, as described + in U{RFC 2845 <https://tools.ietf.org/html/rfc2845>}. + + @type algorithm: L{Name} + @ivar algorithm: The name of the signature or MAC algorithm. + + @type timeSigned: L{int} + @ivar timeSigned: Signing time, as seconds from the POSIX epoch. + + @type fudge: L{int} + @ivar fudge: Allowable time skew, in seconds. + + @type MAC: L{bytes} + @ivar MAC: The message digest or signature. + + @type originalID: L{int} + @ivar originalID: A message ID. + + @type error: L{int} + @ivar error: An error code (extended C{RCODE}) carried + in exceptional cases. + + @type otherData: L{bytes} + @ivar otherData: Other data carried in exceptional cases. + + """ + + fancybasename = "TSIG" + compareAttributes = ( + "algorithm", + "timeSigned", + "fudge", + "MAC", + "originalID", + "error", + "otherData", + "ttl", + ) + showAttributes = ["algorithm", "timeSigned", "MAC", "error", "otherData"] + + TYPE = TSIG + + def __init__( + self, + algorithm=None, + timeSigned=None, + fudge=5, + MAC=None, + originalID=0, + error=OK, + otherData=b"", + ttl=0, + ): + # All of our init arguments have to have defaults, because of + # the way IEncodable and Message.parseRecords() work, but for + # some of our arguments there is no reasonable default; we use + # invalid values here to prevent a user of this class from + # relying on what's really an internal implementation detail. + self.algorithm = None if algorithm is None else Name(algorithm) + self.timeSigned = timeSigned + self.fudge = str2time(fudge) + self.MAC = MAC + self.originalID = originalID + self.error = error + self.otherData = otherData + self.ttl = ttl + + def encode(self, strio, compDict=None): + self.algorithm.encode(strio, compDict) + strio.write(struct.pack("!Q", self.timeSigned)[2:]) # 48-bit number + strio.write(struct.pack("!HH", self.fudge, len(self.MAC))) + strio.write(self.MAC) + strio.write( + struct.pack("!HHH", self.originalID, self.error, len(self.otherData)) + ) + strio.write(self.otherData) + + def decode(self, strio, length=None): + algorithm = Name() + algorithm.decode(strio) + self.algorithm = algorithm + fields = struct.unpack("!QHH", b"\x00\x00" + readPrecisely(strio, 10)) + self.timeSigned, self.fudge, macLength = fields + self.MAC = readPrecisely(strio, macLength) + fields = struct.unpack("!HHH", readPrecisely(strio, 6)) + self.originalID, self.error, otherLength = fields + self.otherData = readPrecisely(strio, otherLength) + + def __hash__(self): + return hash((self.algorithm, self.timeSigned, self.MAC, self.originalID)) + + +def _responseFromMessage(responseConstructor, message, **kwargs): + """ + Generate a L{Message} like instance suitable for use as the response to + C{message}. + + The C{queries}, C{id} attributes will be copied from C{message} and the + C{answer} flag will be set to L{True}. + + @param responseConstructor: A response message constructor with an + initializer signature matching L{dns.Message.__init__}. + @type responseConstructor: C{callable} + + @param message: A request message. + @type message: L{Message} + + @param kwargs: Keyword arguments which will be passed to the initialiser + of the response message. + @type kwargs: L{dict} + + @return: A L{Message} like response instance. + @rtype: C{responseConstructor} + """ + response = responseConstructor(id=message.id, answer=True, **kwargs) + response.queries = message.queries[:] + return response + + +def _getDisplayableArguments(obj, alwaysShow, fieldNames): + """ + Inspect the function signature of C{obj}'s constructor, + and get a list of which arguments should be displayed. + This is a helper function for C{_compactRepr}. + + @param obj: The instance whose repr is being generated. + @param alwaysShow: A L{list} of field names which should always be shown. + @param fieldNames: A L{list} of field attribute names which should be shown + if they have non-default values. + @return: A L{list} of displayable arguments. + """ + displayableArgs = [] + # Get the argument names and values from the constructor. + signature = inspect.signature(obj.__class__.__init__) + for name in fieldNames: + defaultValue = signature.parameters[name].default + fieldValue = getattr(obj, name, defaultValue) + if (name in alwaysShow) or (fieldValue != defaultValue): + displayableArgs.append(f" {name}={fieldValue!r}") + + return displayableArgs + + +def _compactRepr( + obj: object, + alwaysShow: Sequence[str] | None = None, + flagNames: Sequence[str] | None = None, + fieldNames: Sequence[str] | None = None, + sectionNames: Sequence[str] | None = None, +) -> str: + """ + Return a L{str} representation of C{obj} which only shows fields with + non-default values, flags which are True and sections which have been + explicitly set. + + @param obj: The instance whose repr is being generated. + @param alwaysShow: A L{list} of field names which should always be shown. + @param flagNames: A L{list} of flag attribute names which should be shown if + they are L{True}. + @param fieldNames: A L{list} of field attribute names which should be shown + if they have non-default values. + @param sectionNames: A L{list} of section attribute names which should be + shown if they have been assigned a value. + + @return: A L{str} representation of C{obj}. + """ + if alwaysShow is None: + alwaysShow = [] + + if flagNames is None: + flagNames = [] + + if fieldNames is None: + fieldNames = [] + + if sectionNames is None: + sectionNames = [] + + setFlags = [] + for name in flagNames: + if name in alwaysShow or getattr(obj, name, False) == True: + setFlags.append(name) + + displayableArgs = _getDisplayableArguments(obj, alwaysShow, fieldNames) + out = ["<", obj.__class__.__name__] + displayableArgs + + if setFlags: + out.append(" flags={}".format(",".join(setFlags))) + + for name in sectionNames: + section = getattr(obj, name, []) + if section: + out.append(f" {name}={section!r}") + + out.append(">") + + return "".join(out) + + +class Message(tputil.FancyEqMixin): + """ + L{Message} contains all the information represented by a single + DNS request or response. + + @ivar id: See L{__init__} + @ivar answer: See L{__init__} + @ivar opCode: See L{__init__} + @ivar recDes: See L{__init__} + @ivar recAv: See L{__init__} + @ivar auth: See L{__init__} + @ivar rCode: See L{__init__} + @ivar trunc: See L{__init__} + @ivar maxSize: See L{__init__} + @ivar authenticData: See L{__init__} + @ivar checkingDisabled: See L{__init__} + + @ivar queries: The queries which are being asked of or answered by + DNS server. + @type queries: L{list} of L{Query} + + @ivar answers: Records containing the answers to C{queries} if + this is a response message. + @type answers: L{list} of L{RRHeader} + + @ivar authority: Records containing information about the + authoritative DNS servers for the names in C{queries}. + @type authority: L{list} of L{RRHeader} + + @ivar additional: Records containing IP addresses of host names + in C{answers} and C{authority}. + @type additional: L{list} of L{RRHeader} + + @ivar _flagNames: The names of attributes representing the flag header + fields. + @ivar _fieldNames: The names of attributes representing non-flag fixed + header fields. + @ivar _sectionNames: The names of attributes representing the record + sections of this message. + """ + + compareAttributes = ( + "id", + "answer", + "opCode", + "recDes", + "recAv", + "auth", + "rCode", + "trunc", + "maxSize", + "authenticData", + "checkingDisabled", + "queries", + "answers", + "authority", + "additional", + ) + + headerFmt = "!H2B4H" + headerSize = struct.calcsize(headerFmt) + + # Question, answer, additional, and nameserver lists + queries = answers = add = ns = None + + def __init__( + self, + id=0, + answer=0, + opCode=0, + recDes=0, + recAv=0, + auth=0, + rCode=OK, + trunc=0, + maxSize=512, + authenticData=0, + checkingDisabled=0, + ): + """ + @param id: A 16 bit identifier assigned by the program that + generates any kind of query. This identifier is copied to + the corresponding reply and can be used by the requester + to match up replies to outstanding queries. + @type id: L{int} + + @param answer: A one bit field that specifies whether this + message is a query (0), or a response (1). + @type answer: L{int} + + @param opCode: A four bit field that specifies kind of query in + this message. This value is set by the originator of a query + and copied into the response. + @type opCode: L{int} + + @param recDes: Recursion Desired - this bit may be set in a + query and is copied into the response. If RD is set, it + directs the name server to pursue the query recursively. + Recursive query support is optional. + @type recDes: L{int} + + @param recAv: Recursion Available - this bit is set or cleared + in a response and denotes whether recursive query support + is available in the name server. + @type recAv: L{int} + + @param auth: Authoritative Answer - this bit is valid in + responses and specifies that the responding name server + is an authority for the domain name in question section. + @type auth: L{int} + + @ivar rCode: A response code, used to indicate success or failure in a + message which is a response from a server to a client request. + @type rCode: C{0 <= int < 16} + + @param trunc: A flag indicating that this message was + truncated due to length greater than that permitted on the + transmission channel. + @type trunc: L{int} + + @param maxSize: The requestor's UDP payload size is the number + of octets of the largest UDP payload that can be + reassembled and delivered in the requestor's network + stack. + @type maxSize: L{int} + + @param authenticData: A flag indicating in a response that all + the data included in the answer and authority portion of + the response has been authenticated by the server + according to the policies of that server. + See U{RFC2535 section-6.1<https://tools.ietf.org/html/rfc2535#section-6.1>}. + @type authenticData: L{int} + + @param checkingDisabled: A flag indicating in a query that + pending (non-authenticated) data is acceptable to the + resolver sending the query. + See U{RFC2535 section-6.1<https://tools.ietf.org/html/rfc2535#section-6.1>}. + @type authenticData: L{int} + """ + self.maxSize = maxSize + self.id = id + self.answer = answer + self.opCode = opCode + self.auth = auth + self.trunc = trunc + self.recDes = recDes + self.recAv = recAv + self.rCode = rCode + self.authenticData = authenticData + self.checkingDisabled = checkingDisabled + + self.queries = [] + self.answers = [] + self.authority = [] + self.additional = [] + + def __repr__(self) -> str: + """ + Generate a repr of this L{Message}. + + Only includes the non-default fields and sections and only includes + flags which are set. The C{id} is always shown. + + @return: The native string repr. + """ + return _compactRepr( + self, + flagNames=( + "answer", + "auth", + "trunc", + "recDes", + "recAv", + "authenticData", + "checkingDisabled", + ), + fieldNames=("id", "opCode", "rCode", "maxSize"), + sectionNames=("queries", "answers", "authority", "additional"), + alwaysShow=("id",), + ) + + def addQuery(self, name, type=ALL_RECORDS, cls=IN): + """ + Add another query to this Message. + + @type name: L{bytes} + @param name: The name to query. + + @type type: L{int} + @param type: Query type + + @type cls: L{int} + @param cls: Query class + """ + self.queries.append(Query(name, type, cls)) + + def encode(self, strio): + compDict = {} + body_tmp = BytesIO() + for q in self.queries: + q.encode(body_tmp, compDict) + for q in self.answers: + q.encode(body_tmp, compDict) + for q in self.authority: + q.encode(body_tmp, compDict) + for q in self.additional: + q.encode(body_tmp, compDict) + body = body_tmp.getvalue() + size = len(body) + self.headerSize + if self.maxSize and size > self.maxSize: + self.trunc = 1 + body = body[: self.maxSize - self.headerSize] + byte3 = ( + ((self.answer & 1) << 7) + | ((self.opCode & 0xF) << 3) + | ((self.auth & 1) << 2) + | ((self.trunc & 1) << 1) + | (self.recDes & 1) + ) + byte4 = ( + ((self.recAv & 1) << 7) + | ((self.authenticData & 1) << 5) + | ((self.checkingDisabled & 1) << 4) + | (self.rCode & 0xF) + ) + + strio.write( + struct.pack( + self.headerFmt, + self.id, + byte3, + byte4, + len(self.queries), + len(self.answers), + len(self.authority), + len(self.additional), + ) + ) + strio.write(body) + + def decode(self, strio, length=None): + self.maxSize = 0 + header = readPrecisely(strio, self.headerSize) + r = struct.unpack(self.headerFmt, header) + self.id, byte3, byte4, nqueries, nans, nns, nadd = r + self.answer = (byte3 >> 7) & 1 + self.opCode = (byte3 >> 3) & 0xF + self.auth = (byte3 >> 2) & 1 + self.trunc = (byte3 >> 1) & 1 + self.recDes = byte3 & 1 + self.recAv = (byte4 >> 7) & 1 + self.authenticData = (byte4 >> 5) & 1 + self.checkingDisabled = (byte4 >> 4) & 1 + self.rCode = byte4 & 0xF + + self.queries = [] + for i in range(nqueries): + q = Query() + try: + q.decode(strio) + except EOFError: + return + self.queries.append(q) + + items = ((self.answers, nans), (self.authority, nns), (self.additional, nadd)) + + for l, n in items: + self.parseRecords(l, n, strio) + + def parseRecords(self, list, num, strio): + for i in range(num): + header = RRHeader(auth=self.auth) + try: + header.decode(strio) + except EOFError: + return + t = self.lookupRecordType(header.type) + if not t: + continue + header.payload = t(ttl=header.ttl) + try: + header.payload.decode(strio, header.rdlength) + except EOFError: + return + list.append(header) + + # Create a mapping from record types to their corresponding Record_* + # classes. This relies on the global state which has been created so + # far in initializing this module (so don't define Record classes after + # this). + _recordTypes = {} + for name in globals(): + if name.startswith("Record_"): + _recordTypes[globals()[name].TYPE] = globals()[name] + + # Clear the iteration variable out of the class namespace so it + # doesn't become an attribute. + del name + + def lookupRecordType(self, type): + """ + Retrieve the L{IRecord} implementation for the given record type. + + @param type: A record type, such as C{A} or L{NS}. + @type type: L{int} + + @return: An object which implements L{IRecord} or L{None} if none + can be found for the given type. + @rtype: C{Type[IRecord]} + """ + return self._recordTypes.get(type, UnknownRecord) + + def toStr(self): + """ + Encode this L{Message} into a byte string in the format described by RFC + 1035. + + @rtype: L{bytes} + """ + strio = BytesIO() + self.encode(strio) + return strio.getvalue() + + def fromStr(self, str): + """ + Decode a byte string in the format described by RFC 1035 into this + L{Message}. + + @param str: L{bytes} + """ + strio = BytesIO(str) + self.decode(strio) + + +class _EDNSMessage(tputil.FancyEqMixin): + """ + An I{EDNS} message. + + Designed for compatibility with L{Message} but with a narrower public + interface. + + Most importantly, L{_EDNSMessage.fromStr} will interpret and remove I{OPT} + records that are present in the additional records section. + + The I{OPT} records are used to populate certain I{EDNS} specific attributes. + + L{_EDNSMessage.toStr} will add suitable I{OPT} records to the additional + section to represent the extended EDNS information. + + @see: U{https://tools.ietf.org/html/rfc6891} + + @ivar id: See L{__init__} + @ivar answer: See L{__init__} + @ivar opCode: See L{__init__} + @ivar auth: See L{__init__} + @ivar trunc: See L{__init__} + @ivar recDes: See L{__init__} + @ivar recAv: See L{__init__} + @ivar rCode: See L{__init__} + @ivar ednsVersion: See L{__init__} + @ivar dnssecOK: See L{__init__} + @ivar authenticData: See L{__init__} + @ivar checkingDisabled: See L{__init__} + @ivar maxSize: See L{__init__} + + @ivar queries: See L{__init__} + @ivar answers: See L{__init__} + @ivar authority: See L{__init__} + @ivar additional: See L{__init__} + + @ivar _messageFactory: A constructor of L{Message} instances. Called by + C{_toMessage} and C{_fromMessage}. + """ + + compareAttributes = ( + "id", + "answer", + "opCode", + "auth", + "trunc", + "recDes", + "recAv", + "rCode", + "ednsVersion", + "dnssecOK", + "authenticData", + "checkingDisabled", + "maxSize", + "queries", + "answers", + "authority", + "additional", + ) + + _messageFactory = Message + + def __init__( + self, + id=0, + answer=False, + opCode=OP_QUERY, + auth=False, + trunc=False, + recDes=False, + recAv=False, + rCode=0, + ednsVersion=0, + dnssecOK=False, + authenticData=False, + checkingDisabled=False, + maxSize=512, + queries=None, + answers=None, + authority=None, + additional=None, + ): + """ + Construct a new L{_EDNSMessage} + + @see: U{RFC1035 section-4.1.1<https://tools.ietf.org/html/rfc1035#section-4.1.1>} + @see: U{RFC2535 section-6.1<https://tools.ietf.org/html/rfc2535#section-6.1>} + @see: U{RFC3225 section-3<https://tools.ietf.org/html/rfc3225#section-3>} + @see: U{RFC6891 section-6.1.3<https://tools.ietf.org/html/rfc6891#section-6.1.3>} + + @param id: A 16 bit identifier assigned by the program that generates + any kind of query. This identifier is copied the corresponding + reply and can be used by the requester to match up replies to + outstanding queries. + @type id: L{int} + + @param answer: A one bit field that specifies whether this message is a + query (0), or a response (1). + @type answer: L{bool} + + @param opCode: A four bit field that specifies kind of query in this + message. This value is set by the originator of a query and copied + into the response. + @type opCode: L{int} + + @param auth: Authoritative Answer - this bit is valid in responses, and + specifies that the responding name server is an authority for the + domain name in question section. + @type auth: L{bool} + + @param trunc: Truncation - specifies that this message was truncated due + to length greater than that permitted on the transmission channel. + @type trunc: L{bool} + + @param recDes: Recursion Desired - this bit may be set in a query and is + copied into the response. If set, it directs the name server to + pursue the query recursively. Recursive query support is optional. + @type recDes: L{bool} + + @param recAv: Recursion Available - this bit is set or cleared in a + response, and denotes whether recursive query support is available + in the name server. + @type recAv: L{bool} + + @param rCode: Extended 12-bit RCODE. Derived from the 4 bits defined in + U{RFC1035 4.1.1<https://tools.ietf.org/html/rfc1035#section-4.1.1>} + and the upper 8bits defined in U{RFC6891 + 6.1.3<https://tools.ietf.org/html/rfc6891#section-6.1.3>}. + @type rCode: L{int} + + @param ednsVersion: Indicates the EDNS implementation level. Set to + L{None} to prevent any EDNS attributes and options being added to + the encoded byte string. + @type ednsVersion: L{int} or L{None} + + @param dnssecOK: DNSSEC OK bit as defined by + U{RFC3225 3<https://tools.ietf.org/html/rfc3225#section-3>}. + @type dnssecOK: L{bool} + + @param authenticData: A flag indicating in a response that all the data + included in the answer and authority portion of the response has + been authenticated by the server according to the policies of that + server. + See U{RFC2535 section-6.1<https://tools.ietf.org/html/rfc2535#section-6.1>}. + @type authenticData: L{bool} + + @param checkingDisabled: A flag indicating in a query that pending + (non-authenticated) data is acceptable to the resolver sending the + query. + See U{RFC2535 section-6.1<https://tools.ietf.org/html/rfc2535#section-6.1>}. + @type authenticData: L{bool} + + @param maxSize: The requestor's UDP payload size is the number of octets + of the largest UDP payload that can be reassembled and delivered in + the requestor's network stack. + @type maxSize: L{int} + + @param queries: The L{list} of L{Query} associated with this message. + @type queries: L{list} of L{Query} + + @param answers: The L{list} of answers associated with this message. + @type answers: L{list} of L{RRHeader} + + @param authority: The L{list} of authority records associated with this + message. + @type authority: L{list} of L{RRHeader} + + @param additional: The L{list} of additional records associated with + this message. + @type additional: L{list} of L{RRHeader} + """ + self.id = id + self.answer = answer + self.opCode = opCode + self.auth = auth + self.trunc = trunc + self.recDes = recDes + self.recAv = recAv + self.rCode = rCode + self.ednsVersion = ednsVersion + self.dnssecOK = dnssecOK + self.authenticData = authenticData + self.checkingDisabled = checkingDisabled + self.maxSize = maxSize + + if queries is None: + queries = [] + self.queries = queries + + if answers is None: + answers = [] + self.answers = answers + + if authority is None: + authority = [] + self.authority = authority + + if additional is None: + additional = [] + self.additional = additional + + def __repr__(self) -> str: + return _compactRepr( + self, + flagNames=( + "answer", + "auth", + "trunc", + "recDes", + "recAv", + "authenticData", + "checkingDisabled", + "dnssecOK", + ), + fieldNames=("id", "opCode", "rCode", "maxSize", "ednsVersion"), + sectionNames=("queries", "answers", "authority", "additional"), + alwaysShow=("id",), + ) + + def _toMessage(self): + """ + Convert to a standard L{dns.Message}. + + If C{ednsVersion} is not None, an L{_OPTHeader} instance containing all + the I{EDNS} specific attributes and options will be appended to the list + of C{additional} records. + + @return: A L{dns.Message} + @rtype: L{dns.Message} + """ + m = self._messageFactory( + id=self.id, + answer=self.answer, + opCode=self.opCode, + auth=self.auth, + trunc=self.trunc, + recDes=self.recDes, + recAv=self.recAv, + # Assign the lower 4 bits to the message + rCode=self.rCode & 0xF, + authenticData=self.authenticData, + checkingDisabled=self.checkingDisabled, + ) + + m.queries = self.queries[:] + m.answers = self.answers[:] + m.authority = self.authority[:] + m.additional = self.additional[:] + + if self.ednsVersion is not None: + o = _OPTHeader( + version=self.ednsVersion, + dnssecOK=self.dnssecOK, + udpPayloadSize=self.maxSize, + # Assign the upper 8 bits to the OPT record + extendedRCODE=self.rCode >> 4, + ) + m.additional.append(o) + + return m + + def toStr(self): + """ + Encode to wire format by first converting to a standard L{dns.Message}. + + @return: A L{bytes} string. + """ + return self._toMessage().toStr() + + @classmethod + def _fromMessage(cls, message): + """ + Construct and return a new L{_EDNSMessage} whose attributes and records + are derived from the attributes and records of C{message} (a L{Message} + instance). + + If present, an C{OPT} record will be extracted from the C{additional} + section and its attributes and options will be used to set the EDNS + specific attributes C{extendedRCODE}, C{ednsVersion}, C{dnssecOK}, + C{ednsOptions}. + + The C{extendedRCODE} will be combined with C{message.rCode} and assigned + to C{self.rCode}. + + @param message: The source L{Message}. + @type message: L{Message} + + @return: A new L{_EDNSMessage} + @rtype: L{_EDNSMessage} + """ + additional = [] + optRecords = [] + for r in message.additional: + if r.type == OPT: + optRecords.append(_OPTHeader.fromRRHeader(r)) + else: + additional.append(r) + + newMessage = cls( + id=message.id, + answer=message.answer, + opCode=message.opCode, + auth=message.auth, + trunc=message.trunc, + recDes=message.recDes, + recAv=message.recAv, + rCode=message.rCode, + authenticData=message.authenticData, + checkingDisabled=message.checkingDisabled, + # Default to None, it will be updated later when the OPT records are + # parsed. + ednsVersion=None, + dnssecOK=False, + queries=message.queries[:], + answers=message.answers[:], + authority=message.authority[:], + additional=additional, + ) + + if len(optRecords) == 1: + # XXX: If multiple OPT records are received, an EDNS server should + # respond with FORMERR. See ticket:5669#comment:1. + opt = optRecords[0] + newMessage.ednsVersion = opt.version + newMessage.dnssecOK = opt.dnssecOK + newMessage.maxSize = opt.udpPayloadSize + newMessage.rCode = opt.extendedRCODE << 4 | message.rCode + + return newMessage + + def fromStr(self, bytes): + """ + Decode from wire format, saving flags, values and records to this + L{_EDNSMessage} instance in place. + + @param bytes: The full byte string to be decoded. + @type bytes: L{bytes} + """ + m = self._messageFactory() + m.fromStr(bytes) + + ednsMessage = self._fromMessage(m) + for attrName in self.compareAttributes: + setattr(self, attrName, getattr(ednsMessage, attrName)) + + +class DNSMixin: + """ + DNS protocol mixin shared by UDP and TCP implementations. + + @ivar _reactor: A L{IReactorTime} and L{IReactorUDP} provider which will + be used to issue DNS queries and manage request timeouts. + """ + + id = None + liveMessages = None + + def __init__(self, controller, reactor=None): + self.controller = controller + self.id = random.randrange(2**10, 2**15) + if reactor is None: + from twisted.internet import reactor + self._reactor = reactor + + def pickID(self): + """ + Return a unique ID for queries. + """ + while True: + id = randomSource() + if id not in self.liveMessages: + return id + + def callLater(self, period, func, *args): + """ + Wrapper around reactor.callLater, mainly for test purpose. + """ + return self._reactor.callLater(period, func, *args) + + def _query(self, queries, timeout, id, writeMessage): + """ + Send out a message with the given queries. + + @type queries: L{list} of C{Query} instances + @param queries: The queries to transmit + + @type timeout: L{int} or C{float} + @param timeout: How long to wait before giving up + + @type id: L{int} + @param id: Unique key for this request + + @type writeMessage: C{callable} + @param writeMessage: One-parameter callback which writes the message + + @rtype: C{Deferred} + @return: a C{Deferred} which will be fired with the result of the + query, or errbacked with any errors that could happen (exceptions + during writing of the query, timeout errors, ...). + """ + m = Message(id, recDes=1) + m.queries = queries + + try: + writeMessage(m) + except BaseException: + return defer.fail() + + resultDeferred = defer.Deferred() + cancelCall = self.callLater(timeout, self._clearFailed, resultDeferred, id) + self.liveMessages[id] = (resultDeferred, cancelCall) + + return resultDeferred + + def _clearFailed(self, deferred, id): + """ + Clean the Deferred after a timeout. + """ + try: + del self.liveMessages[id] + except KeyError: + pass + deferred.errback(failure.Failure(DNSQueryTimeoutError(id))) + + +class DNSDatagramProtocol(DNSMixin, protocol.DatagramProtocol): + """ + DNS protocol over UDP. + """ + + resends = None + + def stopProtocol(self): + """ + Stop protocol: reset state variables. + """ + self.liveMessages = {} + self.resends = {} + self.transport = None + + def startProtocol(self): + """ + Upon start, reset internal state. + """ + self.liveMessages = {} + self.resends = {} + + def writeMessage(self, message, address): + """ + Send a message holding DNS queries. + + @type message: L{Message} + """ + self.transport.write(message.toStr(), address) + + def startListening(self): + self._reactor.listenUDP(0, self, maxPacketSize=512) + + def datagramReceived(self, data, addr): + """ + Read a datagram, extract the message in it and trigger the associated + Deferred. + """ + m = Message() + try: + m.fromStr(data) + except EOFError: + log.msg("Truncated packet (%d bytes) from %s" % (len(data), addr)) + return + except ValueError as ex: + log.msg(f"Invalid packet ({ex}) from {addr}") + return + except BaseException: + # Nothing should trigger this, but since we're potentially + # invoking a lot of different decoding methods, we might as well + # be extra cautious. Anything that triggers this is itself + # buggy. + log.err(failure.Failure(), "Unexpected decoding error") + return + + if m.id in self.liveMessages: + d, canceller = self.liveMessages[m.id] + del self.liveMessages[m.id] + canceller.cancel() + # XXX we shouldn't need this hack of catching exception on callback() + try: + d.callback(m) + except BaseException: + log.err() + else: + if m.id not in self.resends: + self.controller.messageReceived(m, self, addr) + + def removeResend(self, id): + """ + Mark message ID as no longer having duplication suppression. + """ + try: + del self.resends[id] + except KeyError: + pass + + def query(self, address, queries, timeout=10, id=None): + """ + Send out a message with the given queries. + + @type address: L{tuple} of L{str} and L{int} + @param address: The address to which to send the query + + @type queries: L{list} of C{Query} instances + @param queries: The queries to transmit + + @rtype: C{Deferred} + """ + if not self.transport: + # XXX transport might not get created automatically, use callLater? + try: + self.startListening() + except CannotListenError: + return defer.fail() + + if id is None: + id = self.pickID() + else: + self.resends[id] = 1 + + def writeMessage(m): + self.writeMessage(m, address) + + return self._query(queries, timeout, id, writeMessage) + + +class DNSProtocol(DNSMixin, protocol.Protocol): + """ + DNS protocol over TCP. + """ + + length = None + buffer = b"" + + def writeMessage(self, message): + """ + Send a message holding DNS queries. + + @type message: L{Message} + """ + s = message.toStr() + self.transport.write(struct.pack("!H", len(s)) + s) + + def connectionMade(self): + """ + Connection is made: reset internal state, and notify the controller. + """ + self.liveMessages = {} + self.controller.connectionMade(self) + + def connectionLost(self, reason): + """ + Notify the controller that this protocol is no longer + connected. + """ + self.controller.connectionLost(self) + + def dataReceived(self, data): + self.buffer += data + + while self.buffer: + if self.length is None and len(self.buffer) >= 2: + self.length = struct.unpack("!H", self.buffer[:2])[0] + self.buffer = self.buffer[2:] + + if len(self.buffer) >= self.length: + myChunk = self.buffer[: self.length] + m = Message() + m.fromStr(myChunk) + + try: + d, canceller = self.liveMessages[m.id] + except KeyError: + self.controller.messageReceived(m, self) + else: + del self.liveMessages[m.id] + canceller.cancel() + # XXX we shouldn't need this hack + try: + d.callback(m) + except BaseException: + log.err() + + self.buffer = self.buffer[self.length :] + self.length = None + else: + break + + def query(self, queries, timeout=60): + """ + Send out a message with the given queries. + + @type queries: L{list} of C{Query} instances + @param queries: The queries to transmit + + @rtype: C{Deferred} + """ + id = self.pickID() + return self._query(queries, timeout, id, self.writeMessage) diff --git a/contrib/python/Twisted/py3/twisted/names/error.py b/contrib/python/Twisted/py3/twisted/names/error.py new file mode 100644 index 0000000000..185c804472 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/error.py @@ -0,0 +1,94 @@ +# -*- test-case-name: twisted.names.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Exception class definitions for Twisted Names. +""" + + +from twisted.internet.defer import TimeoutError + + +class DomainError(ValueError): + """ + Indicates a lookup failed because there were no records matching the given + C{name, class, type} triple. + """ + + +class AuthoritativeDomainError(ValueError): + """ + Indicates a lookup failed for a name for which this server is authoritative + because there were no records matching the given C{name, class, type} + triple. + """ + + +class DNSQueryTimeoutError(TimeoutError): + """ + Indicates a lookup failed due to a timeout. + + @ivar id: The id of the message which timed out. + """ + + def __init__(self, id): + TimeoutError.__init__(self) + self.id = id + + +class DNSFormatError(DomainError): + """ + Indicates a query failed with a result of C{twisted.names.dns.EFORMAT}. + """ + + +class DNSServerError(DomainError): + """ + Indicates a query failed with a result of C{twisted.names.dns.ESERVER}. + """ + + +class DNSNameError(DomainError): + """ + Indicates a query failed with a result of C{twisted.names.dns.ENAME}. + """ + + +class DNSNotImplementedError(DomainError): + """ + Indicates a query failed with a result of C{twisted.names.dns.ENOTIMP}. + """ + + +class DNSQueryRefusedError(DomainError): + """ + Indicates a query failed with a result of C{twisted.names.dns.EREFUSED}. + """ + + +class DNSUnknownError(DomainError): + """ + Indicates a query failed with an unknown result. + """ + + +class ResolverError(Exception): + """ + Indicates a query failed because of a decision made by the local + resolver object. + """ + + +__all__ = [ + "DomainError", + "AuthoritativeDomainError", + "DNSQueryTimeoutError", + "DNSFormatError", + "DNSServerError", + "DNSNameError", + "DNSNotImplementedError", + "DNSQueryRefusedError", + "DNSUnknownError", + "ResolverError", +] diff --git a/contrib/python/Twisted/py3/twisted/names/hosts.py b/contrib/python/Twisted/py3/twisted/names/hosts.py new file mode 100644 index 0000000000..7d77aa4521 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/hosts.py @@ -0,0 +1,151 @@ +# -*- test-case-name: twisted.names.test.test_hosts -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +hosts(5) support. +""" + + +from twisted.internet import defer +from twisted.internet.abstract import isIPAddress, isIPv6Address +from twisted.names import common, dns +from twisted.python import failure +from twisted.python.compat import nativeString +from twisted.python.filepath import FilePath + + +def searchFileForAll(hostsFile, name): + """ + Search the given file, which is in hosts(5) standard format, for addresses + associated with a given name. + + @param hostsFile: The name of the hosts(5)-format file to search. + @type hostsFile: L{FilePath} + + @param name: The name to search for. + @type name: C{bytes} + + @return: L{None} if the name is not found in the file, otherwise a + C{str} giving the address in the file associated with the name. + """ + results = [] + try: + lines = hostsFile.getContent().splitlines() + except BaseException: + return results + + name = name.lower() + for line in lines: + idx = line.find(b"#") + if idx != -1: + line = line[:idx] + if not line: + continue + parts = line.split() + + if name.lower() in [s.lower() for s in parts[1:]]: + try: + maybeIP = nativeString(parts[0]) + except ValueError: # Not ASCII. + continue + if isIPAddress(maybeIP) or isIPv6Address(maybeIP): + results.append(maybeIP) + return results + + +def searchFileFor(file, name): + """ + Grep given file, which is in hosts(5) standard format, for an address + entry with a given name. + + @param file: The name of the hosts(5)-format file to search. + @type file: C{str} or C{bytes} + + @param name: The name to search for. + @type name: C{bytes} + + @return: L{None} if the name is not found in the file, otherwise a + C{str} giving the first address in the file associated with + the name. + """ + addresses = searchFileForAll(FilePath(file), name) + if addresses: + return addresses[0] + return None + + +class Resolver(common.ResolverBase): + """ + A resolver that services hosts(5) format files. + """ + + def __init__(self, file=b"/etc/hosts", ttl=60 * 60): + common.ResolverBase.__init__(self) + self.file = file + self.ttl = ttl + + def _aRecords(self, name): + """ + Return a tuple of L{dns.RRHeader} instances for all of the IPv4 + addresses in the hosts file. + """ + return tuple( + dns.RRHeader(name, dns.A, dns.IN, self.ttl, dns.Record_A(addr, self.ttl)) + for addr in searchFileForAll(FilePath(self.file), name) + if isIPAddress(addr) + ) + + def _aaaaRecords(self, name): + """ + Return a tuple of L{dns.RRHeader} instances for all of the IPv6 + addresses in the hosts file. + """ + return tuple( + dns.RRHeader( + name, dns.AAAA, dns.IN, self.ttl, dns.Record_AAAA(addr, self.ttl) + ) + for addr in searchFileForAll(FilePath(self.file), name) + if isIPv6Address(addr) + ) + + def _respond(self, name, records): + """ + Generate a response for the given name containing the given result + records, or a failure if there are no result records. + + @param name: The DNS name the response is for. + @type name: C{str} + + @param records: A tuple of L{dns.RRHeader} instances giving the results + that will go into the response. + + @return: A L{Deferred} which will fire with a three-tuple of result + records, authority records, and additional records, or which will + fail with L{dns.DomainError} if there are no result records. + """ + if records: + return defer.succeed((records, (), ())) + return defer.fail(failure.Failure(dns.DomainError(name))) + + def lookupAddress(self, name, timeout=None): + """ + Read any IPv4 addresses from C{self.file} and return them as + L{Record_A} instances. + """ + name = dns.domainString(name) + return self._respond(name, self._aRecords(name)) + + def lookupIPV6Address(self, name, timeout=None): + """ + Read any IPv6 addresses from C{self.file} and return them as + L{Record_AAAA} instances. + """ + name = dns.domainString(name) + return self._respond(name, self._aaaaRecords(name)) + + # Someday this should include IPv6 addresses too, but that will cause + # problems if users of the API (mainly via getHostByName) aren't updated to + # know about IPv6 first. + # FIXME - getHostByName knows about IPv6 now. + lookupAllRecords = lookupAddress diff --git a/contrib/python/Twisted/py3/twisted/names/newsfragments/.gitignore b/contrib/python/Twisted/py3/twisted/names/newsfragments/.gitignore new file mode 100644 index 0000000000..f935021a8f --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/newsfragments/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/contrib/python/Twisted/py3/twisted/names/resolve.py b/contrib/python/Twisted/py3/twisted/names/resolve.py new file mode 100644 index 0000000000..af4f40fea9 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/resolve.py @@ -0,0 +1,91 @@ +# -*- test-case-name: twisted.names.test.test_resolve -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Lookup a name using multiple resolvers. + +Future Plans: This needs someway to specify which resolver answered +the query, or someway to specify (authority|ttl|cache behavior|more?) +""" + + +from zope.interface import implementer + +from twisted.internet import defer, interfaces +from twisted.names import common, dns, error + + +class FailureHandler: + def __init__(self, resolver, query, timeout): + self.resolver = resolver + self.query = query + self.timeout = timeout + + def __call__(self, failure): + # AuthoritativeDomainErrors should halt resolution attempts + failure.trap(dns.DomainError, defer.TimeoutError, NotImplementedError) + return self.resolver(self.query, self.timeout) + + +@implementer(interfaces.IResolver) +class ResolverChain(common.ResolverBase): + """ + Lookup an address using multiple L{IResolver}s + """ + + def __init__(self, resolvers): + """ + @type resolvers: L{list} + @param resolvers: A L{list} of L{IResolver} providers. + """ + common.ResolverBase.__init__(self) + self.resolvers = resolvers + + def _lookup(self, name, cls, type, timeout): + """ + Build a L{dns.Query} for the given parameters and dispatch it + to each L{IResolver} in C{self.resolvers} until an answer or + L{error.AuthoritativeDomainError} is returned. + + @type name: C{str} + @param name: DNS name to resolve. + + @type type: C{int} + @param type: DNS record type. + + @type cls: C{int} + @param cls: DNS record class. + + @type timeout: Sequence of C{int} + @param timeout: Number of seconds after which to reissue the query. + When the last timeout expires, the query is considered failed. + + @rtype: L{Deferred} + @return: A L{Deferred} which fires with a three-tuple of lists of + L{twisted.names.dns.RRHeader} instances. The first element of the + tuple gives answers. The second element of the tuple gives + authorities. The third element of the tuple gives additional + information. The L{Deferred} may instead fail with one of the + exceptions defined in L{twisted.names.error} or with + C{NotImplementedError}. + """ + if not self.resolvers: + return defer.fail(error.DomainError()) + q = dns.Query(name, type, cls) + d = self.resolvers[0].query(q, timeout) + for r in self.resolvers[1:]: + d = d.addErrback(FailureHandler(r.query, q, timeout)) + return d + + def lookupAllRecords(self, name, timeout=None): + # XXX: Why is this necessary? dns.ALL_RECORDS queries should + # be handled just the same as any other type by _lookup + # above. If I remove this method all names tests still + # pass. See #6604 -rwall + if not self.resolvers: + return defer.fail(error.DomainError()) + d = self.resolvers[0].lookupAllRecords(name, timeout) + for r in self.resolvers[1:]: + d = d.addErrback(FailureHandler(r.lookupAllRecords, name, timeout)) + return d diff --git a/contrib/python/Twisted/py3/twisted/names/root.py b/contrib/python/Twisted/py3/twisted/names/root.py new file mode 100644 index 0000000000..3531dbfede --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/root.py @@ -0,0 +1,331 @@ +# -*- test-case-name: twisted.names.test.test_rootresolve -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Resolver implementation for querying successive authoritative servers to +lookup a record, starting from the root nameservers. + +@author: Jp Calderone + +todo:: + robustify it + documentation +""" + +from twisted.internet import defer +from twisted.names import common, dns, error +from twisted.python.failure import Failure + + +class _DummyController: + """ + A do-nothing DNS controller. This is useful when all messages received + will be responses to previously issued queries. Anything else received + will be ignored. + """ + + def messageReceived(self, *args): + pass + + +class Resolver(common.ResolverBase): + """ + L{Resolver} implements recursive lookup starting from a specified list of + root servers. + + @ivar hints: See C{hints} parameter of L{__init__} + @ivar _maximumQueries: See C{maximumQueries} parameter of L{__init__} + @ivar _reactor: See C{reactor} parameter of L{__init__} + @ivar _resolverFactory: See C{resolverFactory} parameter of L{__init__} + """ + + def __init__(self, hints, maximumQueries=10, reactor=None, resolverFactory=None): + """ + @param hints: A L{list} of L{str} giving the dotted quad + representation of IP addresses of root servers at which to + begin resolving names. + @type hints: L{list} of L{str} + + @param maximumQueries: An optional L{int} giving the maximum + number of queries which will be attempted to resolve a + single name. + @type maximumQueries: L{int} + + @param reactor: An optional L{IReactorTime} and L{IReactorUDP} + provider to use to bind UDP ports and manage timeouts. + @type reactor: L{IReactorTime} and L{IReactorUDP} provider + + @param resolverFactory: An optional callable which accepts C{reactor} + and C{servers} arguments and returns an instance that provides a + C{queryUDP} method. Defaults to L{twisted.names.client.Resolver}. + @type resolverFactory: callable + """ + common.ResolverBase.__init__(self) + self.hints = hints + self._maximumQueries = maximumQueries + self._reactor = reactor + if resolverFactory is None: + from twisted.names.client import Resolver as resolverFactory + self._resolverFactory = resolverFactory + + def _roots(self): + """ + Return a list of two-tuples representing the addresses of the root + servers, as defined by C{self.hints}. + """ + return [(ip, dns.PORT) for ip in self.hints] + + def _query(self, query, servers, timeout, filter): + """ + Issue one query and return a L{Deferred} which fires with its response. + + @param query: The query to issue. + @type query: L{dns.Query} + + @param servers: The servers which might have an answer for this + query. + @type servers: L{list} of L{tuple} of L{str} and L{int} + + @param timeout: A timeout on how long to wait for the response. + @type timeout: L{tuple} of L{int} + + @param filter: A flag indicating whether to filter the results. If + C{True}, the returned L{Deferred} will fire with a three-tuple of + lists of L{twisted.names.dns.RRHeader} (like the return value of + the I{lookup*} methods of L{IResolver}. IF C{False}, the result + will be a L{Message} instance. + @type filter: L{bool} + + @return: A L{Deferred} which fires with the response or a timeout + error. + @rtype: L{Deferred} + """ + r = self._resolverFactory(servers=servers, reactor=self._reactor) + d = r.queryUDP([query], timeout) + if filter: + d.addCallback(r.filterAnswers) + return d + + def _lookup(self, name, cls, type, timeout): + """ + Implement name lookup by recursively discovering the authoritative + server for the name and then asking it, starting at one of the servers + in C{self.hints}. + """ + if timeout is None: + # A series of timeouts for semi-exponential backoff, summing to an + # arbitrary total of 60 seconds. + timeout = (1, 3, 11, 45) + return self._discoverAuthority( + dns.Query(name, type, cls), self._roots(), timeout, self._maximumQueries + ) + + def _discoverAuthority(self, query, servers, timeout, queriesLeft): + """ + Issue a query to a server and follow a delegation if necessary. + + @param query: The query to issue. + @type query: L{dns.Query} + + @param servers: The servers which might have an answer for this + query. + @type servers: L{list} of L{tuple} of L{str} and L{int} + + @param timeout: A C{tuple} of C{int} giving the timeout to use for this + query. + + @param queriesLeft: A C{int} giving the number of queries which may + yet be attempted to answer this query before the attempt will be + abandoned. + + @return: A L{Deferred} which fires with a three-tuple of lists of + L{twisted.names.dns.RRHeader} giving the response, or with a + L{Failure} if there is a timeout or response error. + """ + # Stop now if we've hit the query limit. + if queriesLeft <= 0: + return Failure(error.ResolverError("Query limit reached without result")) + + d = self._query(query, servers, timeout, False) + d.addCallback(self._discoveredAuthority, query, timeout, queriesLeft - 1) + return d + + def _discoveredAuthority(self, response, query, timeout, queriesLeft): + """ + Interpret the response to a query, checking for error codes and + following delegations if necessary. + + @param response: The L{Message} received in response to issuing C{query}. + @type response: L{Message} + + @param query: The L{dns.Query} which was issued. + @type query: L{dns.Query}. + + @param timeout: The timeout to use if another query is indicated by + this response. + @type timeout: L{tuple} of L{int} + + @param queriesLeft: A C{int} giving the number of queries which may + yet be attempted to answer this query before the attempt will be + abandoned. + + @return: A L{Failure} indicating a response error, a three-tuple of + lists of L{twisted.names.dns.RRHeader} giving the response to + C{query} or a L{Deferred} which will fire with one of those. + """ + if response.rCode != dns.OK: + return Failure(self.exceptionForCode(response.rCode)(response)) + + # Turn the answers into a structure that's a little easier to work with. + records = {} + for answer in response.answers: + records.setdefault(answer.name, []).append(answer) + + def findAnswerOrCName(name, type, cls): + cname = None + for record in records.get(name, []): + if record.cls == cls: + if record.type == type: + return record + elif record.type == dns.CNAME: + cname = record + # If there were any CNAME records, return the last one. There's + # only supposed to be zero or one, though. + return cname + + seen = set() + name = query.name + record = None + while True: + seen.add(name) + previous = record + record = findAnswerOrCName(name, query.type, query.cls) + if record is None: + if name == query.name: + # If there's no answer for the original name, then this may + # be a delegation. Code below handles it. + break + else: + # Try to resolve the CNAME with another query. + d = self._discoverAuthority( + dns.Query(str(name), query.type, query.cls), + self._roots(), + timeout, + queriesLeft, + ) + # We also want to include the CNAME in the ultimate result, + # otherwise this will be pretty confusing. + + def cbResolved(results): + answers, authority, additional = results + answers.insert(0, previous) + return (answers, authority, additional) + + d.addCallback(cbResolved) + return d + elif record.type == query.type: + return (response.answers, response.authority, response.additional) + else: + # It's a CNAME record. Try to resolve it from the records + # in this response with another iteration around the loop. + if record.payload.name in seen: + raise error.ResolverError("Cycle in CNAME processing") + name = record.payload.name + + # Build a map to use to convert NS names into IP addresses. + addresses = {} + for rr in response.additional: + if rr.type == dns.A: + addresses[rr.name.name] = rr.payload.dottedQuad() + + hints = [] + traps = [] + for rr in response.authority: + if rr.type == dns.NS: + ns = rr.payload.name.name + if ns in addresses: + hints.append((addresses[ns], dns.PORT)) + else: + traps.append(ns) + if hints: + return self._discoverAuthority(query, hints, timeout, queriesLeft) + elif traps: + d = self.lookupAddress(traps[0], timeout) + + def getOneAddress(results): + answers, authority, additional = results + return answers[0].payload.dottedQuad() + + d.addCallback(getOneAddress) + d.addCallback( + lambda hint: self._discoverAuthority( + query, [(hint, dns.PORT)], timeout, queriesLeft - 1 + ) + ) + return d + else: + return Failure( + error.ResolverError("Stuck at response without answers or delegation") + ) + + +def makePlaceholder(deferred, name): + def placeholder(*args, **kw): + deferred.addCallback(lambda r: getattr(r, name)(*args, **kw)) + return deferred + + return placeholder + + +class DeferredResolver: + def __init__(self, resolverDeferred): + self.waiting = [] + resolverDeferred.addCallback(self.gotRealResolver) + + def gotRealResolver(self, resolver): + w = self.waiting + self.__dict__ = resolver.__dict__ + self.__class__ = resolver.__class__ + for d in w: + d.callback(resolver) + + def __getattr__(self, name): + if name.startswith("lookup") or name in ("getHostByName", "query"): + self.waiting.append(defer.Deferred()) + return makePlaceholder(self.waiting[-1], name) + raise AttributeError(name) + + +def bootstrap(resolver, resolverFactory=None): + """ + Lookup the root nameserver addresses using the given resolver + + Return a Resolver which will eventually become a C{root.Resolver} + instance that has references to all the root servers that we were able + to look up. + + @param resolver: The resolver instance which will be used to + lookup the root nameserver addresses. + @type resolver: L{twisted.internet.interfaces.IResolverSimple} + + @param resolverFactory: An optional callable which returns a + resolver instance. It will passed as the C{resolverFactory} + argument to L{Resolver.__init__}. + @type resolverFactory: callable + + @return: A L{DeferredResolver} which will be dynamically replaced + with L{Resolver} when the root nameservers have been looked up. + """ + domains = [chr(ord("a") + i) for i in range(13)] + L = [resolver.getHostByName("%s.root-servers.net" % d) for d in domains] + d = defer.DeferredList(L, consumeErrors=True) + + def buildResolver(res): + return Resolver( + hints=[e[1] for e in res if e[0]], resolverFactory=resolverFactory + ) + + d.addCallback(buildResolver) + + return DeferredResolver(d) diff --git a/contrib/python/Twisted/py3/twisted/names/secondary.py b/contrib/python/Twisted/py3/twisted/names/secondary.py new file mode 100644 index 0000000000..0b9e184b02 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/secondary.py @@ -0,0 +1,216 @@ +# -*- test-case-name: twisted.names.test.test_names -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +__all__ = ["SecondaryAuthority", "SecondaryAuthorityService"] + +from twisted.application import service +from twisted.internet import defer, task +from twisted.names import client, common, dns, resolve +from twisted.names.authority import FileAuthority +from twisted.python import failure, log +from twisted.python.compat import nativeString + + +class SecondaryAuthorityService(service.Service): + """ + A service that keeps one or more authorities up to date by doing hourly + zone transfers from a master. + + @ivar primary: IP address of the master. + @type primary: L{str} + + @ivar domains: An authority for each domain mirrored from the master. + @type domains: L{list} of L{SecondaryAuthority} + """ + + calls = None + + _port = 53 + + def __init__(self, primary, domains): + """ + @param primary: The IP address of the server from which to perform + zone transfers. + @type primary: L{str} + + @param domains: A sequence of domain names for which to perform + zone transfers. + @type domains: L{list} of L{bytes} + """ + self.primary = nativeString(primary) + self.domains = [SecondaryAuthority(primary, d) for d in domains] + + @classmethod + def fromServerAddressAndDomains(cls, serverAddress, domains): + """ + Construct a new L{SecondaryAuthorityService} from a tuple giving a + server address and a C{str} giving the name of a domain for which this + is an authority. + + @param serverAddress: A two-tuple, the first element of which is a + C{str} giving an IP address and the second element of which is a + C{int} giving a port number. Together, these define where zone + transfers will be attempted from. + + @param domains: Domain names for which to perform zone transfers. + @type domains: sequence of L{bytes} + + @return: A new instance of L{SecondaryAuthorityService}. + """ + primary, port = serverAddress + service = cls(primary, []) + service._port = port + service.domains = [ + SecondaryAuthority.fromServerAddressAndDomain(serverAddress, d) + for d in domains + ] + return service + + def getAuthority(self): + """ + Get a resolver for the transferred domains. + + @rtype: L{ResolverChain} + """ + return resolve.ResolverChain(self.domains) + + def startService(self): + service.Service.startService(self) + self.calls = [task.LoopingCall(d.transfer) for d in self.domains] + i = 0 + from twisted.internet import reactor + + for c in self.calls: + # XXX Add errbacks, respect proper timeouts + reactor.callLater(i, c.start, 60 * 60) + i += 1 + + def stopService(self): + service.Service.stopService(self) + for c in self.calls: + c.stop() + + +class SecondaryAuthority(FileAuthority): + """ + An Authority that keeps itself updated by performing zone transfers. + + @ivar primary: The IP address of the server from which zone transfers will + be attempted. + @type primary: L{str} + + @ivar _port: The port number of the server from which zone transfers will + be attempted. + @type _port: L{int} + + @ivar domain: The domain for which this is the secondary authority. + @type domain: L{bytes} + + @ivar _reactor: The reactor to use to perform the zone transfers, or + L{None} to use the global reactor. + """ + + transferring = False + soa = records = None + _port = 53 + _reactor = None + + def __init__(self, primaryIP, domain): + """ + @param domain: The domain for which this will be the secondary + authority. + @type domain: L{bytes} or L{str} + """ + # Yep. Skip over FileAuthority.__init__. This is a hack until we have + # a good composition-based API for the complicated DNS record lookup + # logic we want to share. + common.ResolverBase.__init__(self) + self.primary = nativeString(primaryIP) + self.domain = dns.domainString(domain) + + @classmethod + def fromServerAddressAndDomain(cls, serverAddress, domain): + """ + Construct a new L{SecondaryAuthority} from a tuple giving a server + address and a C{bytes} giving the name of a domain for which this is an + authority. + + @param serverAddress: A two-tuple, the first element of which is a + C{str} giving an IP address and the second element of which is a + C{int} giving a port number. Together, these define where zone + transfers will be attempted from. + + @param domain: A C{bytes} giving the domain to transfer. + @type domain: L{bytes} + + @return: A new instance of L{SecondaryAuthority}. + """ + primary, port = serverAddress + secondary = cls(primary, domain) + secondary._port = port + return secondary + + def transfer(self): + """ + Attempt a zone transfer. + + @returns: A L{Deferred} that fires with L{None} when attempted zone + transfer has completed. + """ + # FIXME: This logic doesn't avoid duplicate transfers + # https://twistedmatrix.com/trac/ticket/9754 + if self.transferring: # <-- never true + return + self.transfering = True # <-- speling + + reactor = self._reactor + if reactor is None: + from twisted.internet import reactor + + resolver = client.Resolver( + servers=[(self.primary, self._port)], reactor=reactor + ) + return ( + resolver.lookupZone(self.domain) + .addCallback(self._cbZone) + .addErrback(self._ebZone) + ) + + def _lookup(self, name, cls, type, timeout=None): + if not self.soa or not self.records: + # No transfer has occurred yet. Fail non-authoritatively so that + # the caller can try elsewhere. + return defer.fail(failure.Failure(dns.DomainError(name))) + return FileAuthority._lookup(self, name, cls, type, timeout) + + def _cbZone(self, zone): + ans, _, _ = zone + self.records = r = {} + for rec in ans: + if not self.soa and rec.type == dns.SOA: + self.soa = (rec.name.name.lower(), rec.payload) + else: + r.setdefault(rec.name.name.lower(), []).append(rec.payload) + + def _ebZone(self, failure): + log.msg( + "Updating %s from %s failed during zone transfer" + % (self.domain, self.primary) + ) + log.err(failure) + + def update(self): + self.transfer().addCallbacks(self._cbTransferred, self._ebTransferred) + + def _cbTransferred(self, result): + self.transferring = False + + def _ebTransferred(self, failure): + self.transferred = False + log.msg( + "Transferring %s from %s failed after zone transfer" + % (self.domain, self.primary) + ) + log.err(failure) diff --git a/contrib/python/Twisted/py3/twisted/names/server.py b/contrib/python/Twisted/py3/twisted/names/server.py new file mode 100644 index 0000000000..63fff7a277 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/server.py @@ -0,0 +1,569 @@ +# -*- test-case-name: twisted.names.test.test_names,twisted.names.test.test_server -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Async DNS server + +Future plans: + - Better config file format maybe + - Make sure to differentiate between different classes + - notice truncation bit + +Important: No additional processing is done on some of the record types. +This violates the most basic RFC and is just plain annoying +for resolvers to deal with. Fix it. + +@author: Jp Calderone +""" + +import time + +from twisted.internet import protocol +from twisted.names import dns, resolve +from twisted.python import log + + +class DNSServerFactory(protocol.ServerFactory): + """ + Server factory and tracker for L{DNSProtocol} connections. This class also + provides records for responses to DNS queries. + + @ivar cache: A L{Cache<twisted.names.cache.CacheResolver>} instance whose + C{cacheResult} method is called when a response is received from one of + C{clients}. Defaults to L{None} if no caches are specified. See + C{caches} of L{__init__} for more details. + @type cache: L{Cache<twisted.names.cache.CacheResolver>} or L{None} + + @ivar canRecurse: A flag indicating whether this server is capable of + performing recursive DNS resolution. + @type canRecurse: L{bool} + + @ivar resolver: A L{resolve.ResolverChain} containing an ordered list of + C{authorities}, C{caches} and C{clients} to which queries will be + dispatched. + @type resolver: L{resolve.ResolverChain} + + @ivar verbose: See L{__init__} + + @ivar connections: A list of all the connected L{DNSProtocol} instances + using this object as their controller. + @type connections: C{list} of L{DNSProtocol} instances + + @ivar protocol: A callable used for building a DNS stream protocol. Called + by L{DNSServerFactory.buildProtocol} and passed the L{DNSServerFactory} + instance as the one and only positional argument. Defaults to + L{dns.DNSProtocol}. + @type protocol: L{IProtocolFactory} constructor + + @ivar _messageFactory: A response message constructor with an initializer + signature matching L{dns.Message.__init__}. + @type _messageFactory: C{callable} + """ + + # Type is wrong. See: https://twistedmatrix.com/trac/ticket/10004#ticket + protocol = dns.DNSProtocol # type: ignore[assignment] + cache = None + _messageFactory = dns.Message + + def __init__(self, authorities=None, caches=None, clients=None, verbose=0): + """ + @param authorities: Resolvers which provide authoritative answers. + @type authorities: L{list} of L{IResolver} providers + + @param caches: Resolvers which provide cached non-authoritative + answers. The first cache instance is assigned to + C{DNSServerFactory.cache} and its C{cacheResult} method will be + called when a response is received from one of C{clients}. + @type caches: L{list} of L{Cache<twisted.names.cache.CacheResolver>} instances + + @param clients: Resolvers which are capable of performing recursive DNS + lookups. + @type clients: L{list} of L{IResolver} providers + + @param verbose: An integer controlling the verbosity of logging of + queries and responses. Default is C{0} which means no logging. Set + to C{2} to enable logging of full query and response messages. + @type verbose: L{int} + """ + resolvers = [] + if authorities is not None: + resolvers.extend(authorities) + if caches is not None: + resolvers.extend(caches) + if clients is not None: + resolvers.extend(clients) + + self.canRecurse = not not clients + self.resolver = resolve.ResolverChain(resolvers) + self.verbose = verbose + if caches: + self.cache = caches[-1] + self.connections = [] + + def _verboseLog(self, *args, **kwargs): + """ + Log a message only if verbose logging is enabled. + + @param args: Positional arguments which will be passed to C{log.msg} + @param kwargs: Keyword arguments which will be passed to C{log.msg} + """ + if self.verbose > 0: + log.msg(*args, **kwargs) + + def buildProtocol(self, addr): + p = self.protocol(self) + p.factory = self + return p + + def connectionMade(self, protocol): + """ + Track a newly connected L{DNSProtocol}. + + @param protocol: The protocol instance to be tracked. + @type protocol: L{dns.DNSProtocol} + """ + self.connections.append(protocol) + + def connectionLost(self, protocol): + """ + Stop tracking a no-longer connected L{DNSProtocol}. + + @param protocol: The tracked protocol instance to be which has been + lost. + @type protocol: L{dns.DNSProtocol} + """ + self.connections.remove(protocol) + + def sendReply(self, protocol, message, address): + """ + Send a response C{message} to a given C{address} via the supplied + C{protocol}. + + Message payload will be logged if C{DNSServerFactory.verbose} is C{>1}. + + @param protocol: The DNS protocol instance to which to send the message. + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param message: The DNS message to be sent. + @type message: L{dns.Message} + + @param address: The address to which the message will be sent or L{None} + if C{protocol} is a stream protocol. + @type address: L{tuple} or L{None} + """ + if self.verbose > 1: + s = " ".join([str(a.payload) for a in message.answers]) + auth = " ".join([str(a.payload) for a in message.authority]) + add = " ".join([str(a.payload) for a in message.additional]) + if not s: + log.msg("Replying with no answers") + else: + log.msg("Answers are " + s) + log.msg("Authority is " + auth) + log.msg("Additional is " + add) + + if address is None: + protocol.writeMessage(message) + else: + protocol.writeMessage(message, address) + + self._verboseLog( + "Processed query in %0.3f seconds" % (time.time() - message.timeReceived) + ) + + def _responseFromMessage( + self, message, rCode=dns.OK, answers=None, authority=None, additional=None + ): + """ + Generate a L{Message} instance suitable for use as the response to + C{message}. + + C{queries} will be copied from the request to the response. + + C{rCode}, C{answers}, C{authority} and C{additional} will be assigned to + the response, if supplied. + + The C{recAv} flag will be set on the response if the C{canRecurse} flag + on this L{DNSServerFactory} is set to L{True}. + + The C{auth} flag will be set on the response if *any* of the supplied + C{answers} have their C{auth} flag set to L{True}. + + The response will have the same C{maxSize} as the request. + + Additionally, the response will have a C{timeReceived} attribute whose + value is that of the original request and the + + @see: L{dns._responseFromMessage} + + @param message: The request message + @type message: L{Message} + + @param rCode: The response code which will be assigned to the response. + @type message: L{int} + + @param answers: An optional list of answer records which will be + assigned to the response. + @type answers: L{list} of L{dns.RRHeader} + + @param authority: An optional list of authority records which will be + assigned to the response. + @type authority: L{list} of L{dns.RRHeader} + + @param additional: An optional list of additional records which will be + assigned to the response. + @type additional: L{list} of L{dns.RRHeader} + + @return: A response L{Message} instance. + @rtype: L{Message} + """ + if answers is None: + answers = [] + if authority is None: + authority = [] + if additional is None: + additional = [] + authoritativeAnswer = False + for x in answers: + if x.isAuthoritative(): + authoritativeAnswer = True + break + + response = dns._responseFromMessage( + responseConstructor=self._messageFactory, + message=message, + recAv=self.canRecurse, + rCode=rCode, + auth=authoritativeAnswer, + ) + + # XXX: Timereceived is a hack which probably shouldn't be tacked onto + # the message. Use getattr here so that we don't have to set the + # timereceived on every message in the tests. See #6957. + response.timeReceived = getattr(message, "timeReceived", None) + + # XXX: This is another hack. dns.Message.decode sets maxSize=0 which + # means that responses are never truncated. I'll maintain that behaviour + # here until #6949 is resolved. + response.maxSize = message.maxSize + + response.answers = answers + response.authority = authority + response.additional = additional + + return response + + def gotResolverResponse(self, response, protocol, message, address): + """ + A callback used by L{DNSServerFactory.handleQuery} for handling the + deferred response from C{self.resolver.query}. + + Constructs a response message by combining the original query message + with the resolved answer, authority and additional records. + + Marks the response message as authoritative if any of the resolved + answers are found to be authoritative. + + The resolved answers count will be logged if C{DNSServerFactory.verbose} + is C{>1}. + + @param response: Answer records, authority records and additional records + @type response: L{tuple} of L{list} of L{dns.RRHeader} instances + + @param protocol: The DNS protocol instance to which to send a response + message. + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param message: The original DNS query message for which a response + message will be constructed. + @type message: L{dns.Message} + + @param address: The address to which the response message will be sent + or L{None} if C{protocol} is a stream protocol. + @type address: L{tuple} or L{None} + """ + ans, auth, add = response + response = self._responseFromMessage( + message=message, rCode=dns.OK, answers=ans, authority=auth, additional=add + ) + self.sendReply(protocol, response, address) + + l = len(ans) + len(auth) + len(add) + self._verboseLog("Lookup found %d record%s" % (l, l != 1 and "s" or "")) + + if self.cache and l: + self.cache.cacheResult(message.queries[0], (ans, auth, add)) + + def gotResolverError(self, failure, protocol, message, address): + """ + A callback used by L{DNSServerFactory.handleQuery} for handling deferred + errors from C{self.resolver.query}. + + Constructs a response message from the original query message by + assigning a suitable error code to C{rCode}. + + An error message will be logged if C{DNSServerFactory.verbose} is C{>1}. + + @param failure: The reason for the failed resolution (as reported by + C{self.resolver.query}). + @type failure: L{Failure<twisted.python.failure.Failure>} + + @param protocol: The DNS protocol instance to which to send a response + message. + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param message: The original DNS query message for which a response + message will be constructed. + @type message: L{dns.Message} + + @param address: The address to which the response message will be sent + or L{None} if C{protocol} is a stream protocol. + @type address: L{tuple} or L{None} + """ + if failure.check(dns.DomainError, dns.AuthoritativeDomainError): + rCode = dns.ENAME + else: + rCode = dns.ESERVER + log.err(failure) + + response = self._responseFromMessage(message=message, rCode=rCode) + + self.sendReply(protocol, response, address) + self._verboseLog("Lookup failed") + + def handleQuery(self, message, protocol, address): + """ + Called by L{DNSServerFactory.messageReceived} when a query message is + received. + + Takes the first query from the received message and dispatches it to + C{self.resolver.query}. + + Adds callbacks L{DNSServerFactory.gotResolverResponse} and + L{DNSServerFactory.gotResolverError} to the resulting deferred. + + Note: Multiple queries in a single message are not supported because + there is no standard way to respond with multiple rCodes, auth, + etc. This is consistent with other DNS server implementations. See + U{http://tools.ietf.org/html/draft-ietf-dnsext-edns1-03} for a proposed + solution. + + @param protocol: The DNS protocol instance to which to send a response + message. + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param message: The original DNS query message for which a response + message will be constructed. + @type message: L{dns.Message} + + @param address: The address to which the response message will be sent + or L{None} if C{protocol} is a stream protocol. + @type address: L{tuple} or L{None} + + @return: A C{deferred} which fires with the resolved result or error of + the first query in C{message}. + @rtype: L{Deferred<twisted.internet.defer.Deferred>} + """ + query = message.queries[0] + + return ( + self.resolver.query(query) + .addCallback(self.gotResolverResponse, protocol, message, address) + .addErrback(self.gotResolverError, protocol, message, address) + ) + + def handleInverseQuery(self, message, protocol, address): + """ + Called by L{DNSServerFactory.messageReceived} when an inverse query + message is received. + + Replies with a I{Not Implemented} error by default. + + An error message will be logged if C{DNSServerFactory.verbose} is C{>1}. + + Override in a subclass. + + @param protocol: The DNS protocol instance to which to send a response + message. + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param message: The original DNS query message for which a response + message will be constructed. + @type message: L{dns.Message} + + @param address: The address to which the response message will be sent + or L{None} if C{protocol} is a stream protocol. + @type address: L{tuple} or L{None} + """ + message.rCode = dns.ENOTIMP + self.sendReply(protocol, message, address) + self._verboseLog(f"Inverse query from {address!r}") + + def handleStatus(self, message, protocol, address): + """ + Called by L{DNSServerFactory.messageReceived} when a status message is + received. + + Replies with a I{Not Implemented} error by default. + + An error message will be logged if C{DNSServerFactory.verbose} is C{>1}. + + Override in a subclass. + + @param protocol: The DNS protocol instance to which to send a response + message. + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param message: The original DNS query message for which a response + message will be constructed. + @type message: L{dns.Message} + + @param address: The address to which the response message will be sent + or L{None} if C{protocol} is a stream protocol. + @type address: L{tuple} or L{None} + """ + message.rCode = dns.ENOTIMP + self.sendReply(protocol, message, address) + self._verboseLog(f"Status request from {address!r}") + + def handleNotify(self, message, protocol, address): + """ + Called by L{DNSServerFactory.messageReceived} when a notify message is + received. + + Replies with a I{Not Implemented} error by default. + + An error message will be logged if C{DNSServerFactory.verbose} is C{>1}. + + Override in a subclass. + + @param protocol: The DNS protocol instance to which to send a response + message. + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param message: The original DNS query message for which a response + message will be constructed. + @type message: L{dns.Message} + + @param address: The address to which the response message will be sent + or L{None} if C{protocol} is a stream protocol. + @type address: L{tuple} or L{None} + """ + message.rCode = dns.ENOTIMP + self.sendReply(protocol, message, address) + self._verboseLog(f"Notify message from {address!r}") + + def handleOther(self, message, protocol, address): + """ + Called by L{DNSServerFactory.messageReceived} when a message with + unrecognised I{OPCODE} is received. + + Replies with a I{Not Implemented} error by default. + + An error message will be logged if C{DNSServerFactory.verbose} is C{>1}. + + Override in a subclass. + + @param protocol: The DNS protocol instance to which to send a response + message. + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param message: The original DNS query message for which a response + message will be constructed. + @type message: L{dns.Message} + + @param address: The address to which the response message will be sent + or L{None} if C{protocol} is a stream protocol. + @type address: L{tuple} or L{None} + """ + message.rCode = dns.ENOTIMP + self.sendReply(protocol, message, address) + self._verboseLog("Unknown op code (%d) from %r" % (message.opCode, address)) + + def messageReceived(self, message, proto, address=None): + """ + L{DNSServerFactory.messageReceived} is called by protocols which are + under the control of this L{DNSServerFactory} whenever they receive a + DNS query message or an unexpected / duplicate / late DNS response + message. + + L{DNSServerFactory.allowQuery} is called with the received message, + protocol and origin address. If it returns L{False}, a C{dns.EREFUSED} + response is sent back to the client. + + Otherwise the received message is dispatched to one of + L{DNSServerFactory.handleQuery}, L{DNSServerFactory.handleInverseQuery}, + L{DNSServerFactory.handleStatus}, L{DNSServerFactory.handleNotify}, or + L{DNSServerFactory.handleOther} depending on the I{OPCODE} of the + received message. + + If C{DNSServerFactory.verbose} is C{>0} all received messages will be + logged in more or less detail depending on the value of C{verbose}. + + @param message: The DNS message that was received. + @type message: L{dns.Message} + + @param proto: The DNS protocol instance which received the message + @type proto: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param address: The address from which the message was received. Only + provided for messages received by datagram protocols. The origin of + Messages received from stream protocols can be gleaned from the + protocol C{transport} attribute. + @type address: L{tuple} or L{None} + """ + message.timeReceived = time.time() + + if self.verbose: + if self.verbose > 1: + s = " ".join([str(q) for q in message.queries]) + else: + s = " ".join( + [dns.QUERY_TYPES.get(q.type, "UNKNOWN") for q in message.queries] + ) + if not len(s): + log.msg(f"Empty query from {address or proto.transport.getPeer()!r}") + else: + log.msg(f"{s} query from {address or proto.transport.getPeer()!r}") + + if not self.allowQuery(message, proto, address): + message.rCode = dns.EREFUSED + self.sendReply(proto, message, address) + elif message.opCode == dns.OP_QUERY: + self.handleQuery(message, proto, address) + elif message.opCode == dns.OP_INVERSE: + self.handleInverseQuery(message, proto, address) + elif message.opCode == dns.OP_STATUS: + self.handleStatus(message, proto, address) + elif message.opCode == dns.OP_NOTIFY: + self.handleNotify(message, proto, address) + else: + self.handleOther(message, proto, address) + + def allowQuery(self, message, protocol, address): + """ + Called by L{DNSServerFactory.messageReceived} to decide whether to + process a received message or to reply with C{dns.EREFUSED}. + + This default implementation permits anything but empty queries. + + Override in a subclass to implement alternative policies. + + @param message: The DNS message that was received. + @type message: L{dns.Message} + + @param protocol: The DNS protocol instance which received the message + @type protocol: L{dns.DNSDatagramProtocol} or L{dns.DNSProtocol} + + @param address: The address from which the message was received. Only + provided for messages received by datagram protocols. The origin of + Messages received from stream protocols can be gleaned from the + protocol C{transport} attribute. + @type address: L{tuple} or L{None} + + @return: L{True} if the received message contained one or more queries, + else L{False}. + @rtype: L{bool} + """ + return len(message.queries) diff --git a/contrib/python/Twisted/py3/twisted/names/srvconnect.py b/contrib/python/Twisted/py3/twisted/names/srvconnect.py new file mode 100644 index 0000000000..6cad7a2539 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/srvconnect.py @@ -0,0 +1,271 @@ +# -*- test-case-name: twisted.names.test.test_srvconnect -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +import random + +from zope.interface import implementer + +from twisted.internet import error, interfaces +from twisted.names import client, dns +from twisted.names.error import DNSNameError +from twisted.python.compat import nativeString + + +class _SRVConnector_ClientFactoryWrapper: + def __init__(self, connector, wrappedFactory): + self.__connector = connector + self.__wrappedFactory = wrappedFactory + + def startedConnecting(self, connector): + self.__wrappedFactory.startedConnecting(self.__connector) + + def clientConnectionFailed(self, connector, reason): + self.__connector.connectionFailed(reason) + + def clientConnectionLost(self, connector, reason): + self.__connector.connectionLost(reason) + + def __getattr__(self, key): + return getattr(self.__wrappedFactory, key) + + +@implementer(interfaces.IConnector) +class SRVConnector: + """ + A connector that looks up DNS SRV records. + + RFC 2782 details how SRV records should be interpreted and selected + for subsequent connection attempts. The algorithm for using the records' + priority and weight is implemented in L{pickServer}. + + @ivar servers: List of candidate server records for future connection + attempts. + @type servers: L{list} of L{dns.Record_SRV} + + @ivar orderedServers: List of server records that have already been tried + in this round of connection attempts. + @type orderedServers: L{list} of L{dns.Record_SRV} + """ + + stopAfterDNS = 0 + + def __init__( + self, + reactor, + service, + domain, + factory, + protocol="tcp", + connectFuncName="connectTCP", + connectFuncArgs=(), + connectFuncKwArgs={}, + defaultPort=None, + ): + """ + @param domain: The domain to connect to. If passed as a text + string, it will be encoded using C{idna} encoding. + @type domain: L{bytes} or L{str} + + @param defaultPort: Optional default port number to be used when SRV + lookup fails and the service name is unknown. This should be the + port number associated with the service name as defined by the IANA + registry. + @type defaultPort: L{int} + """ + self.reactor = reactor + self.service = service + self.domain = None if domain is None else dns.domainString(domain) + self.factory = factory + + self.protocol = protocol + self.connectFuncName = connectFuncName + self.connectFuncArgs = connectFuncArgs + self.connectFuncKwArgs = connectFuncKwArgs + self._defaultPort = defaultPort + + self.connector = None + self.servers = None + # list of servers already used in this round: + self.orderedServers = None + + def connect(self): + """Start connection to remote server.""" + self.factory.doStart() + self.factory.startedConnecting(self) + + if not self.servers: + if self.domain is None: + self.connectionFailed( + error.DNSLookupError("Domain is not defined."), + ) + return + d = client.lookupService( + "_%s._%s.%s" + % ( + nativeString(self.service), + nativeString(self.protocol), + nativeString(self.domain), + ), + ) + d.addCallbacks(self._cbGotServers, self._ebGotServers) + d.addCallback(lambda x, self=self: self._reallyConnect()) + if self._defaultPort: + d.addErrback(self._ebServiceUnknown) + d.addErrback(self.connectionFailed) + elif self.connector is None: + self._reallyConnect() + else: + self.connector.connect() + + def _ebGotServers(self, failure): + failure.trap(DNSNameError) + + # Some DNS servers reply with NXDOMAIN when in fact there are + # just no SRV records for that domain. Act as if we just got an + # empty response and use fallback. + + self.servers = [] + self.orderedServers = [] + + def _cbGotServers(self, result): + answers, auth, add = result + if ( + len(answers) == 1 + and answers[0].type == dns.SRV + and answers[0].payload + and answers[0].payload.target == dns.Name(b".") + ): + # decidedly not available + raise error.DNSLookupError( + "Service %s not available for domain %s." + % (repr(self.service), repr(self.domain)) + ) + + self.servers = [] + self.orderedServers = [] + for a in answers: + if a.type != dns.SRV or not a.payload: + continue + + self.orderedServers.append(a.payload) + + def _ebServiceUnknown(self, failure): + """ + Connect to the default port when the service name is unknown. + + If no SRV records were found, the service name will be passed as the + port. If resolving the name fails with + L{error.ServiceNameUnknownError}, a final attempt is done using the + default port. + """ + failure.trap(error.ServiceNameUnknownError) + self.servers = [dns.Record_SRV(0, 0, self._defaultPort, self.domain)] + self.orderedServers = [] + self.connect() + + def pickServer(self): + """ + Pick the next server. + + This selects the next server from the list of SRV records according + to their priority and weight values, as set out by the default + algorithm specified in RFC 2782. + + At the beginning of a round, L{servers} is populated with + L{orderedServers}, and the latter is made empty. L{servers} + is the list of candidates, and L{orderedServers} is the list of servers + that have already been tried. + + First, all records are ordered by priority and weight in ascending + order. Then for each priority level, a running sum is calculated + over the sorted list of records for that priority. Then a random value + between 0 and the final sum is compared to each record in order. The + first record that is greater than or equal to that random value is + chosen and removed from the list of candidates for this round. + + @return: A tuple of target hostname and port from the chosen DNS SRV + record. + @rtype: L{tuple} of native L{str} and L{int} + """ + assert self.servers is not None + assert self.orderedServers is not None + + if not self.servers and not self.orderedServers: + # no SRV record, fall back.. + return nativeString(self.domain), self.service + + if not self.servers and self.orderedServers: + # start new round + self.servers = self.orderedServers + self.orderedServers = [] + + assert self.servers + + self.servers.sort(key=lambda record: (record.priority, record.weight)) + minPriority = self.servers[0].priority + + index = 0 + weightSum = 0 + weightIndex = [] + for x in self.servers: + if x.priority == minPriority: + weightSum += x.weight + weightIndex.append((index, weightSum)) + index += 1 + + rand = random.randint(0, weightSum) + for index, weight in weightIndex: + if weight >= rand: + chosen = self.servers[index] + del self.servers[index] + self.orderedServers.append(chosen) + + return str(chosen.target), chosen.port + + raise RuntimeError(f"Impossible {self.__class__.__name__} pickServer result.") + + def _reallyConnect(self): + if self.stopAfterDNS: + self.stopAfterDNS = 0 + return + + self.host, self.port = self.pickServer() + assert self.host is not None, "Must have a host to connect to." + assert self.port is not None, "Must have a port to connect to." + + connectFunc = getattr(self.reactor, self.connectFuncName) + self.connector = connectFunc( + self.host, + self.port, + _SRVConnector_ClientFactoryWrapper(self, self.factory), + *self.connectFuncArgs, + **self.connectFuncKwArgs, + ) + + def stopConnecting(self): + """Stop attempting to connect.""" + if self.connector: + self.connector.stopConnecting() + else: + self.stopAfterDNS = 1 + + def disconnect(self): + """Disconnect whatever our are state is.""" + if self.connector is not None: + self.connector.disconnect() + else: + self.stopConnecting() + + def getDestination(self): + assert self.connector + return self.connector.getDestination() + + def connectionFailed(self, reason): + self.factory.clientConnectionFailed(self, reason) + self.factory.doStop() + + def connectionLost(self, reason): + self.factory.clientConnectionLost(self, reason) + self.factory.doStop() diff --git a/contrib/python/Twisted/py3/twisted/names/tap.py b/contrib/python/Twisted/py3/twisted/names/tap.py new file mode 100644 index 0000000000..c971c10386 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/names/tap.py @@ -0,0 +1,149 @@ +# -*- test-case-name: twisted.names.test.test_tap -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Domain Name Server +""" + +import os +import traceback + +from twisted.application import internet, service +from twisted.names import authority, dns, secondary, server +from twisted.python import usage + + +class Options(usage.Options): + optParameters = [ + ["interface", "i", "", "The interface to which to bind"], + ["port", "p", "53", "The port on which to listen"], + [ + "resolv-conf", + None, + None, + "Override location of resolv.conf (implies --recursive)", + ], + ["hosts-file", None, None, "Perform lookups with a hosts file"], + ] + + optFlags = [ + ["cache", "c", "Enable record caching"], + ["recursive", "r", "Perform recursive lookups"], + ["verbose", "v", "Log verbosely"], + ] + + compData = usage.Completions( + optActions={"interface": usage.CompleteNetInterfaces()} + ) + + zones = None + zonefiles = None + + def __init__(self): + usage.Options.__init__(self) + self["verbose"] = 0 + self.bindfiles = [] + self.zonefiles = [] + self.secondaries = [] + + def opt_pyzone(self, filename): + """Specify the filename of a Python syntax zone definition""" + if not os.path.exists(filename): + raise usage.UsageError(filename + ": No such file") + self.zonefiles.append(filename) + + def opt_bindzone(self, filename): + """Specify the filename of a BIND9 syntax zone definition""" + if not os.path.exists(filename): + raise usage.UsageError(filename + ": No such file") + self.bindfiles.append(filename) + + def opt_secondary(self, ip_domain): + """Act as secondary for the specified domain, performing + zone transfers from the specified IP (IP/domain) + """ + args = ip_domain.split("/", 1) + if len(args) != 2: + raise usage.UsageError("Argument must be of the form IP[:port]/domain") + address = args[0].split(":") + if len(address) == 1: + address = (address[0], dns.PORT) + else: + try: + port = int(address[1]) + except ValueError: + raise usage.UsageError( + f"Specify an integer port number, not {address[1]!r}" + ) + address = (address[0], port) + self.secondaries.append((address, [args[1]])) + + def opt_verbose(self): + """Increment verbosity level""" + self["verbose"] += 1 + + def postOptions(self): + if self["resolv-conf"]: + self["recursive"] = True + + self.svcs = [] + self.zones = [] + for f in self.zonefiles: + try: + self.zones.append(authority.PySourceAuthority(f)) + except Exception: + traceback.print_exc() + raise usage.UsageError("Invalid syntax in " + f) + for f in self.bindfiles: + try: + self.zones.append(authority.BindAuthority(f)) + except Exception: + traceback.print_exc() + raise usage.UsageError("Invalid syntax in " + f) + for f in self.secondaries: + svc = secondary.SecondaryAuthorityService.fromServerAddressAndDomains(*f) + self.svcs.append(svc) + self.zones.append(self.svcs[-1].getAuthority()) + try: + self["port"] = int(self["port"]) + except ValueError: + raise usage.UsageError("Invalid port: {!r}".format(self["port"])) + + +def _buildResolvers(config): + """ + Build DNS resolver instances in an order which leaves recursive + resolving as a last resort. + + @type config: L{Options} instance + @param config: Parsed command-line configuration + + @return: Two-item tuple of a list of cache resovers and a list of client + resolvers + """ + from twisted.names import cache, client, hosts + + ca, cl = [], [] + if config["cache"]: + ca.append(cache.CacheResolver(verbose=config["verbose"])) + if config["hosts-file"]: + cl.append(hosts.Resolver(file=config["hosts-file"])) + if config["recursive"]: + cl.append(client.createResolver(resolvconf=config["resolv-conf"])) + return ca, cl + + +def makeService(config): + ca, cl = _buildResolvers(config) + + f = server.DNSServerFactory(config.zones, ca, cl, config["verbose"]) + p = dns.DNSDatagramProtocol(f) + f.noisy = 0 + ret = service.MultiService() + for klass, arg in [(internet.TCPServer, f), (internet.UDPServer, p)]: + s = klass(config["port"], arg, interface=config["interface"]) + s.setServiceParent(ret) + for svc in config.svcs: + svc.setServiceParent(ret) + return ret |