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/py2/twisted/names | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py2/twisted/names')
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/__init__.py | 6 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/_rfc1982.py | 278 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/authority.py | 543 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/cache.py | 125 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/client.py | 776 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/common.py | 289 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/dns.py | 3214 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/error.py | 97 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/hosts.py | 153 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/resolve.py | 99 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/root.py | 333 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/secondary.py | 221 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/server.py | 590 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/srvconnect.py | 273 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/names/tap.py | 150 |
15 files changed, 7147 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py2/twisted/names/__init__.py b/contrib/python/Twisted/py2/twisted/names/__init__.py new file mode 100644 index 0000000000..ccdf8ba331 --- /dev/null +++ b/contrib/python/Twisted/py2/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/py2/twisted/names/_rfc1982.py b/contrib/python/Twisted/py2/twisted/names/_rfc1982.py new file mode 100644 index 0000000000..a895d82c4e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/_rfc1982.py @@ -0,0 +1,278 @@ +# -*- 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>} +""" + +from __future__ import division, absolute_import + +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, object): + """ + 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, serialBits=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 = 2 ** (serialBits - 1) + self._maxAdd = 2 ** (serialBits - 1) - 1 + self._number = int(number) % self._modulo + + + def _convertOther(self, other): + """ + 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. + @raises: L{TypeError} if C{other} is not compatible. + """ + if not isinstance(other, SerialNumber): + raise TypeError( + 'cannot compare or combine %r and %r' % (self, other)) + + 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): + """ + 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): + """ + Allow rich equality comparison with another L{SerialNumber} instance. + + @type other: L{SerialNumber} + """ + other = self._convertOther(other) + return other._number == self._number + + + def __ne__(self, other): + """ + Allow rich equality comparison with another L{SerialNumber} instance. + + @type other: L{SerialNumber} + """ + return not self.__eq__(other) + + + def __lt__(self, other): + """ + Allow I{less than} comparison with another L{SerialNumber} instance. + + @type other: L{SerialNumber} + """ + other = self._convertOther(other) + 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): + """ + Allow I{greater than} comparison with another L{SerialNumber} instance. + + @type other: L{SerialNumber} + @rtype: L{bool} + """ + other = self._convertOther(other) + 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 __le__(self, other): + """ + Allow I{less than or equal} comparison with another L{SerialNumber} + instance. + + @type other: L{SerialNumber} + @rtype: L{bool} + """ + other = self._convertOther(other) + return self == other or self < other + + + def __ge__(self, other): + """ + Allow I{greater than or equal} comparison with another L{SerialNumber} + instance. + + @type other: L{SerialNumber} + @rtype: L{bool} + """ + other = self._convertOther(other) + return self == other or self > other + + + def __add__(self, other): + """ + 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} + + @type other: L{SerialNumber} + @rtype: L{SerialNumber} + @raises: L{ArithmeticError} if C{other} is more than C{_maxAdd} + ie more than half the maximum value of this serial number. + """ + other = self._convertOther(other) + 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/py2/twisted/names/authority.py b/contrib/python/Twisted/py2/twisted/names/authority.py new file mode 100644 index 0000000000..1abc4f828d --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/authority.py @@ -0,0 +1,543 @@ +# -*- test-case-name: twisted.names.test.test_names -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Authoritative resolvers. +""" + +from __future__ import absolute_import, division + +import os +import time + +from twisted.names import dns, error, common +from twisted.internet import defer +from twisted.python import failure +from twisted.python.compat import execfile, nativeString, _PY3 +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, 'r') 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{byte} 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): + return lambda name, *arg, **kw: (name, type(*arg, **kw)) + + + def setupConfigNamespace(self): + r = {} + items = dns.__dict__.iterkeys() + 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, 'class_%s' % (cls,), None) + if f: + f(ttl, type, domain, rdata) + else: + raise NotImplementedError( + "Record class %r not supported" % (cls,) + ) + + + 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 rdate: bytes + """ + record = getattr(dns, 'Record_%s' % (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( + "Record type %r not supported" % (nativeString(type),) + ) + + + 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} + """ + if _PY3: + queryClasses = set( + qc.encode("ascii") for qc in dns.QUERY_CLASSES.values() + ) + queryTypes = set( + qt.encode("ascii") for qt in dns.QUERY_TYPES.values() + ) + else: + queryClasses = set(dns.QUERY_CLASSES.values()) + queryTypes = set(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/py2/twisted/names/cache.py b/contrib/python/Twisted/py2/twisted/names/cache.py new file mode 100644 index 0000000000..c906216126 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/cache.py @@ -0,0 +1,125 @@ +# -*- test-case-name: twisted.names.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +An in-memory caching resolver. +""" + +from __future__ import division, absolute_import + +from twisted.names import dns, common +from twisted.python import failure, log +from twisted.internet import defer + + + +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/py2/twisted/names/client.py b/contrib/python/Twisted/py2/twisted/names/client.py new file mode 100644 index 0000000000..c89e0fc2a3 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/client.py @@ -0,0 +1,776 @@ +# -*- 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 os +import errno +import warnings + +from zope.interface import moduleProvides + +# Twisted imports +from twisted.python.compat import nativeString +from twisted.python.runtime import platform +from twisted.python.filepath import FilePath +from twisted.internet import error, defer, interfaces, protocol +from twisted.python import log, failure +from twisted.names import ( + dns, common, resolve, cache, root, hosts as hostsModule) +from twisted.internet.abstract import isIPv6Address + + + +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 IOError 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('%s changed, reparsing' % (self.resolv,)) + 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("Resolver added %r to server list" % (resolver,)) + 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): + """ + 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/py2/twisted/names/common.py b/contrib/python/Twisted/py2/twisted/names/common.py new file mode 100644 index 0000000000..c7dfdb8ad1 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/common.py @@ -0,0 +1,289 @@ +# -*- test-case-name: twisted.names.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Base functionality useful to various parts of Twisted Names. +""" + +from __future__ import division, absolute_import + +import socket + +from zope.interface import implementer + +from twisted.names import dns +from twisted.names.error import DNSFormatError, DNSServerError, DNSNameError +from twisted.names.error import DNSNotImplementedError, DNSQueryRefusedError +from twisted.names.error import DNSUnknownError + +from twisted.internet import defer, error, interfaces + +from twisted.logger import Logger + +# 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/py2/twisted/names/dns.py b/contrib/python/Twisted/py2/twisted/names/dns.py new file mode 100644 index 0000000000..89509a644e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/dns.py @@ -0,0 +1,3214 @@ +# -*- 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 division, absolute_import + +__all__ = [ + 'IEncodable', 'IRecord', + + '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', + ] + + +# System imports +import inspect, struct, random, socket +from itertools import chain + +from io import BytesIO + +AF_INET6 = socket.AF_INET6 + +from zope.interface import implementer, Interface, Attribute + + +# Twisted imports +from twisted.internet import protocol, defer +from twisted.internet.error import CannotListenError +from twisted.python import log, failure +from twisted.python import util as tputil +from twisted.python import randbytes +from twisted.python.compat import _PY3, unicode, comparable, cmp, nativeString + + +if _PY3: + 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 '[%s]' % ( + ', '.join([_nicebytes(b) for b in list]),) +else: + _ord2bytes = chr + _nicebytes = _nicebyteslist = repr + + + +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 = dict([ + (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 = dict([ + (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 DomainError, AuthoritativeDomainError +from twisted.names.error import DNSQueryTimeoutError + + + +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): + """ + 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, unicode): + domain = domain.encode('idna') + if not isinstance(domain, bytes): + raise TypeError('Expected {} or {} but found {!r} of type {}'.format( + type(b'').__name__, type(u'').__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): + """ + 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{unicode}) 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. + """ + suffixes = ( + ('S', 1), ('M', 60), ('H', 60 * 60), ('D', 60 * 60 * 24), + ('W', 60 * 60 * 24 * 7), ('Y', 60 * 60 * 24 * 365) + ) + if _PY3 and isinstance(s, bytes): + s = s.decode('ascii') + + if isinstance(s, str): + s = s.upper().strip() + for (suff, mult) in suffixes: + if s.endswith(suff): + return int(float(s[:-1]) * mult) + try: + s = int(s) + except ValueError: + raise ValueError("Invalid time interval specifier: " + 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. + """ + + + +@implementer(IEncodable) +class Charstr(object): + + def __init__(self, string=b''): + if not isinstance(string, bytes): + raise ValueError("%r is not a byte string" % (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): + if isinstance(other, Charstr): + return self.string == other.string + return NotImplemented + + + def __ne__(self, other): + if isinstance(other, Charstr): + return self.string != other.string + return NotImplemented + + + def __hash__(self): + return hash(self.string) + + + def __str__(self): + """ + 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=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): + if isinstance(other, Name): + return self.name.lower() == other.name.lower() + return NotImplemented + + + def __ne__(self, other): + if isinstance(other, Name): + return self.name.lower() != other.name.lower() + return NotImplemented + + + def __hash__(self): + return hash(self.name) + + + def __str__(self): + """ + 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} + """ + name = None + type = None + cls = None + + def __init__(self, name=b'', type=A, cls=IN): + """ + @type name: L{bytes} or L{unicode} + @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): + 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 '<Query %s %s %s>' % (self.name, t, c) + + + def __repr__(self): + return 'Query(%r, %r, %r)' % (self.name.name, self.type, self.cls) + + + +@implementer(IEncodable) +class _OPTHeader(tputil.FancyStrMixin, tputil.FancyEqMixin, object): + """ + 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 payload: 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: L{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: L{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, object): + """ + 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: L{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: L{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: An object that implements the L{IEncodable} interface + + @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" + + name = None + type = None + cls = None + ttl = None + payload = None + rdlength = None + + cachedResponse = None + + def __init__(self, name=b'', type=A, cls=IN, ttl=0, payload=None, + auth=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: An object implementing C{IEncodable} + @param payload: A Query Type specific data object. + + @raises TypeError: if the ttl cannot be converted to an L{int}. + @raises ValueError: if the ttl is negative. + """ + assert (payload is None) or isinstance(payload, UnknownRecord) or (payload.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): + 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(IEncodable, IRecord) +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 = 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(IEncodable, IRecord) +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{unicode} + @param address: The IPv4 address associated with this record, in + quad-dotted notation. + """ + if _PY3 and 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): + return '<A address=%s ttl=%s>' % (self.dottedQuad(), self.ttl) + __repr__ = __str__ + + + def dottedQuad(self): + return socket.inet_ntoa(self.address) + + + +@implementer(IEncodable, IRecord) +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{unicode} + + @param rname: See L{Record_SOA.rname} + @type rname: L{bytes} or L{unicode} + """ + 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(IEncodable, IRecord) +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(IEncodable, IRecord) +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 + + _address = property(lambda self: 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{unicode} + @param address: The IPv4 address associated with this record, in + quad-dotted notation. + """ + if _PY3 and 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(IEncodable, IRecord) +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') + + _address = property(lambda self: socket.inet_ntop(AF_INET6, self.address)) + + def __init__(self, address='::', ttl=None): + """ + @type address: L{bytes} or L{unicode} + @param address: The IPv6 address for this host, in RFC 2373 format. + """ + if _PY3 and 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(IEncodable, IRecord) +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') + + _suffix = property(lambda self: socket.inet_ntop(AF_INET6, self.suffix)) + + def __init__(self, prefixLen=0, suffix='::', prefix=b'', ttl=None): + """ + @param suffix: An IPv6 address suffix in in RFC 2373 format. + @type suffix: L{bytes} or L{unicode} + + @param prefix: An IPv6 address prefix for other A6 records. + @type prefix: L{bytes} or L{unicode} + """ + if _PY3 and 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): + 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): + return '<A6 %s %s (%d) ttl=%s>' % ( + self.prefix, + socket.inet_ntop(AF_INET6, self.suffix), + self.prefixLen, self.ttl + ) + + + +@implementer(IEncodable, IRecord) +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{unicode} + """ + 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(IEncodable, IRecord) +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{unicode} + """ + 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(IEncodable, IRecord) +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{unicode} + """ + 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(IEncodable, IRecord) +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{unicode} + + @param txt: See L{Record_RP.txt} + @type txt: L{bytes} or L{unicode} + """ + 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(IEncodable, IRecord) +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=b'', os=b'', ttl=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): + 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(IEncodable, IRecord) +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{unicode} + + @param emailbx: See L{Record_MINFO.rmailbx}. + @type emailbx: L{bytes} or L{unicode} + """ + 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(IEncodable, IRecord) +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{unicode} + """ + 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(IEncodable, IRecord) +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(IEncodable, IRecord) +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(IEncodable, IRecord) +class UnknownRecord(tputil.FancyEqMixin, tputil.FancyStrMixin, object): + """ + 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 + """ + 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(IEncodable, IRecord) +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 = [] + if _PY3: + # 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(' %s=%r' % (name, fieldValue)) + else: + # Get the argument names and values from the constructor. + argspec = inspect.getargspec(obj.__class__.__init__) + # Reverse the args and defaults to avoid mapping positional arguments + # which don't have a default. + defaults = dict(zip(reversed(argspec.args), reversed(argspec.defaults))) + for name in fieldNames: + defaultValue = defaults.get(name) + fieldValue = getattr(obj, name, defaultValue) + if (name in alwaysShow) or (fieldValue != defaultValue): + displayableArgs.append(' %s=%r' % (name, fieldValue)) + + return displayableArgs + + + +def _compactRepr(obj, alwaysShow=None, flagNames=None, fieldNames=None, + sectionNames=None): + """ + 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=%s' % (','.join(setFlags),)) + + for name in sectionNames: + section = getattr(obj, name, []) + if section: + out.append(' %s=%r' % (name, section)) + + 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): + """ + 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: L{types.ClassType} + """ + 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, object): + """ + 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): + 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(object): + """ + 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: + 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: + # 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: + 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: + 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/py2/twisted/names/error.py b/contrib/python/Twisted/py2/twisted/names/error.py new file mode 100644 index 0000000000..92a076b26e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/error.py @@ -0,0 +1,97 @@ +# -*- test-case-name: twisted.names.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Exception class definitions for Twisted Names. +""" + +from __future__ import division, absolute_import + +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/py2/twisted/names/hosts.py b/contrib/python/Twisted/py2/twisted/names/hosts.py new file mode 100644 index 0000000000..6673eac63e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/hosts.py @@ -0,0 +1,153 @@ +# -*- test-case-name: twisted.names.test.test_hosts -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +hosts(5) support. +""" + +from __future__ import division, absolute_import + +from twisted.python.compat import nativeString +from twisted.names import dns +from twisted.python import failure +from twisted.python.filepath import FilePath +from twisted.internet import defer +from twisted.internet.abstract import isIPAddress + +from twisted.names import common + +def searchFileForAll(hostsFile, name): + """ + Search the given file, which is in hosts(5) standard format, for an address + entry 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: + 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:]]: + results.append(nativeString(parts[0])) + 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 not isIPAddress(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. + lookupAllRecords = lookupAddress diff --git a/contrib/python/Twisted/py2/twisted/names/resolve.py b/contrib/python/Twisted/py2/twisted/names/resolve.py new file mode 100644 index 0000000000..0cd39eb1c3 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/resolve.py @@ -0,0 +1,99 @@ +# -*- 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 __future__ import division, absolute_import + +from zope.interface import implementer + +from twisted.internet import defer, interfaces +from twisted.names import dns, common, 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/py2/twisted/names/root.py b/contrib/python/Twisted/py2/twisted/names/root.py new file mode 100644 index 0000000000..5b94254109 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/root.py @@ -0,0 +1,333 @@ +# -*- 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.python.failure import Failure +from twisted.internet import defer +from twisted.names import dns, common, error + + + +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/py2/twisted/names/secondary.py b/contrib/python/Twisted/py2/twisted/names/secondary.py new file mode 100644 index 0000000000..ba73e1b757 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/secondary.py @@ -0,0 +1,221 @@ +# -*- test-case-name: twisted.names.test.test_names -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +from __future__ import absolute_import, division + +__all__ = ['SecondaryAuthority', 'SecondaryAuthorityService'] + +from twisted.internet import task, defer +from twisted.names import dns +from twisted.names import common +from twisted.names import client +from twisted.names import resolve +from twisted.names.authority import FileAuthority + +from twisted.python import log, failure +from twisted.python.compat import nativeString +from twisted.application import service + + + +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 domain: A C{bytes} giving the domain to transfer. + + @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: C{str} + + @ivar _port: The port number of the server from which zone transfers will + be attempted. + @type: C{int} + + @ivar domain: The domain for which this is the secondary authority. + @type: C{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/py2/twisted/names/server.py b/contrib/python/Twisted/py2/twisted/names/server.py new file mode 100644 index 0000000000..893821b95f --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/server.py @@ -0,0 +1,590 @@ +# -*- 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 +""" +from __future__ import division, absolute_import + +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} + """ + + protocol = dns.DNSProtocol + 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("Inverse query from %r" % (address,)) + + + 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("Status request from %r" % (address,)) + + + 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("Notify message from %r" % (address,)) + + + 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( + "Empty query from %r" % ( + (address or proto.transport.getPeer()),)) + else: + log.msg( + "%s query from %r" % ( + s, address or proto.transport.getPeer())) + + 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/py2/twisted/names/srvconnect.py b/contrib/python/Twisted/py2/twisted/names/srvconnect.py new file mode 100644 index 0000000000..5346808d01 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/srvconnect.py @@ -0,0 +1,273 @@ +# -*- test-case-name: twisted.names.test.test_srvconnect -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +from __future__ import absolute_import, division + +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( + 'Impossible %s pickServer result.' % (self.__class__.__name__,)) + + + 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/py2/twisted/names/tap.py b/contrib/python/Twisted/py2/twisted/names/tap.py new file mode 100644 index 0000000000..d0e3b1d062 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/names/tap.py @@ -0,0 +1,150 @@ +# -*- test-case-name: twisted.names.test.test_tap -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Domain Name Server +""" + +import os, traceback + +from twisted.python import usage +from twisted.names import dns +from twisted.application import internet, service + +from twisted.names import server +from twisted.names import authority +from twisted.names import secondary + +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( + "Specify an integer port number, not %r" % (address[1],)) + 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" % (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 client, cache, 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 |