From 2d4fa74645fa836e77bc2c932a4bf6dcdf5d4284 Mon Sep 17 00:00:00 2001 From: Sergey Shepelev Date: Tue, 25 Jul 2017 12:40:57 +0300 Subject: [PATCH] support: upgrade bundled dnspython to 1.16.0 (22e9de1d7957e) https://github.com/eventlet/eventlet/issues/427 --- bin/pull-dnspython | 5 +- eventlet/support/dns/_compat.py | 8 + eventlet/support/dns/dnssec.py | 123 ++-- eventlet/support/dns/e164.py | 64 +- eventlet/support/dns/edns.py | 177 ++++- eventlet/support/dns/entropy.py | 7 +- eventlet/support/dns/exception.py | 42 +- eventlet/support/dns/flags.py | 26 +- eventlet/support/dns/grange.py | 16 +- eventlet/support/dns/inet.py | 55 +- eventlet/support/dns/ipv4.py | 24 +- eventlet/support/dns/ipv6.py | 48 +- eventlet/support/dns/message.py | 629 +++++++++--------- eventlet/support/dns/name.py | 444 ++++++++----- eventlet/support/dns/namedict.py | 25 +- eventlet/support/dns/node.py | 98 +-- eventlet/support/dns/opcode.py | 44 +- eventlet/support/dns/query.py | 430 +++++++----- eventlet/support/dns/rcode.py | 57 +- eventlet/support/dns/rdata.py | 187 +++--- eventlet/support/dns/rdataclass.py | 52 +- eventlet/support/dns/rdataset.py | 139 ++-- eventlet/support/dns/rdatatype.py | 81 ++- .../support/dns/rdtypes/ANY/OPENPGPKEY.py | 58 ++ eventlet/support/dns/rdtypes/ANY/__init__.py | 7 +- eventlet/support/dns/rdtypes/IN/A.py | 2 +- eventlet/support/dns/rdtypes/IN/APL.py | 8 +- eventlet/support/dns/rdtypes/IN/__init__.py | 1 + eventlet/support/dns/rdtypes/txtbase.py | 17 +- eventlet/support/dns/renderer.py | 124 +--- eventlet/support/dns/resolver.py | 417 ++++++------ eventlet/support/dns/reversename.py | 34 +- eventlet/support/dns/rrset.py | 45 +- eventlet/support/dns/set.py | 84 +-- eventlet/support/dns/tokenizer.py | 155 ++--- eventlet/support/dns/ttl.py | 12 +- eventlet/support/dns/update.py | 94 ++- eventlet/support/dns/version.py | 11 +- eventlet/support/dns/wiredata.py | 4 +- 39 files changed, 2177 insertions(+), 1677 deletions(-) create mode 100644 eventlet/support/dns/rdtypes/ANY/OPENPGPKEY.py diff --git a/bin/pull-dnspython b/bin/pull-dnspython index 56d1a76..5eb6029 100755 --- a/bin/pull-dnspython +++ b/bin/pull-dnspython @@ -1,6 +1,7 @@ -#!/bin/bash -eux +#!/bin/bash +set -eux cd "$( dirname "${BASH_SOURCE[0]}" )/.." -version=${1-bb0c9f21f4a6f56f2fe8d7c1fc991080ef89d223} +version=${1-22e9de1d7957e558ea8f89f24e402cbbc8d50646} upstream_path=./dnspython-${version} if [[ ! -d "${upstream_path}" ]]; then curl -L -odnspython.zip "https://github.com/rthalley/dnspython/archive/${version}.zip" diff --git a/eventlet/support/dns/_compat.py b/eventlet/support/dns/_compat.py index 956f9a1..5c9f15e 100644 --- a/eventlet/support/dns/_compat.py +++ b/eventlet/support/dns/_compat.py @@ -19,6 +19,10 @@ if sys.version_info > (3,): return x.decode() def maybe_encode(x): return x.encode() + def maybe_chr(x): + return x + def maybe_ord(x): + return x else: text_type = unicode # pylint: disable=unicode-builtin, undefined-variable binary_type = str @@ -30,6 +34,10 @@ else: return x def maybe_encode(x): return x + def maybe_chr(x): + return chr(x) + def maybe_ord(x): + return ord(x) def round_py2_compat(what): diff --git a/eventlet/support/dns/dnssec.py b/eventlet/support/dns/dnssec.py index fec1208..b91a64f 100644 --- a/eventlet/support/dns/dnssec.py +++ b/eventlet/support/dns/dnssec.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009, 2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -31,27 +31,40 @@ from ._compat import string_types class UnsupportedAlgorithm(dns.exception.DNSException): - """The DNSSEC algorithm is not supported.""" class ValidationFailure(dns.exception.DNSException): - """The DNSSEC signature is invalid.""" + +#: RSAMD5 RSAMD5 = 1 +#: DH DH = 2 +#: DSA DSA = 3 +#: ECC ECC = 4 +#: RSASHA1 RSASHA1 = 5 +#: DSANSEC3SHA1 DSANSEC3SHA1 = 6 +#: RSASHA1NSEC3SHA1 RSASHA1NSEC3SHA1 = 7 +#: RSASHA256 RSASHA256 = 8 +#: RSASHA512 RSASHA512 = 10 +#: ECDSAP256SHA256 ECDSAP256SHA256 = 13 +#: ECDSAP384SHA384 ECDSAP384SHA384 = 14 +#: INDIRECT INDIRECT = 252 +#: PRIVATEDNS PRIVATEDNS = 253 +#: PRIVATEOID PRIVATEOID = 254 _algorithm_by_text = { @@ -79,8 +92,10 @@ _algorithm_by_value = dict((y, x) for x, y in _algorithm_by_text.items()) def algorithm_from_text(text): - """Convert text into a DNSSEC algorithm value - @rtype: int""" + """Convert text into a DNSSEC algorithm value. + + Returns an ``int``. + """ value = _algorithm_by_text.get(text.upper()) if value is None: @@ -90,7 +105,9 @@ def algorithm_from_text(text): def algorithm_to_text(value): """Convert a DNSSEC algorithm value to text - @rtype: string""" + + Returns a ``str``. + """ text = _algorithm_by_value.get(value) if text is None: @@ -105,6 +122,14 @@ def _to_rdata(record, origin): def key_id(key, origin=None): + """Return the key id (a 16-bit number) for the specified key. + + Note the *origin* parameter of this function is historical and + is not needed. + + Returns an ``int`` between 0 and 65535. + """ + rdata = _to_rdata(key, origin) rdata = bytearray(rdata) if key.algorithm == RSAMD5: @@ -121,6 +146,22 @@ def key_id(key, origin=None): def make_ds(name, key, algorithm, origin=None): + """Create a DS record for a DNSSEC key. + + *name* is the owner name of the DS record. + + *key* is a ``dns.rdtypes.ANY.DNSKEY``. + + *algorithm* is a string describing which hash algorithm to use. The + currently supported hashes are "SHA1" and "SHA256". Case does not + matter for these strings. + + *origin* is a ``dns.name.Name`` and will be used as the origin + if *key* is a relative name. + + Returns a ``dns.rdtypes.ANY.DS``. + """ + if algorithm.upper() == 'SHA1': dsalg = 1 hash = dns.hash.hashes['SHA1']() @@ -232,31 +273,32 @@ def _make_algorithm_id(algorithm): def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): """Validate an RRset against a single signature rdata - The owner name of the rrsig is assumed to be the same as the owner name - of the rrset. + The owner name of *rrsig* is assumed to be the same as the owner name + of *rrset*. - @param rrset: The RRset to validate - @type rrset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) - tuple - @param rrsig: The signature rdata - @type rrsig: dns.rrset.Rdata - @param keys: The key dictionary. - @type keys: a dictionary keyed by dns.name.Name with node or rdataset - values - @param origin: The origin to use for relative names - @type origin: dns.name.Name or None - @param now: The time to use when validating the signatures. The default - is the current time. - @type now: int + *rrset* is the RRset to validate. It can be a ``dns.rrset.RRset`` or + a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple. + + *rrsig* is a ``dns.rrset.Rdata``, the signature to validate. + + *keys* is the key dictionary, used to find the DNSKEY associated with + a given name. The dictionary is keyed by a ``dns.name.Name``, and has + ``dns.node.Node`` or ``dns.rdataset.Rdataset`` values. + + *origin* is a ``dns.name.Name``, the origin to use for relative names. + + *now* is an ``int``, the time to use when validating the signatures, + in seconds since the UNIX epoch. The default is the current time. """ if isinstance(origin, string_types): origin = dns.name.from_text(origin, dns.name.root) - for candidate_key in _find_candidate_keys(keys, rrsig): - if not candidate_key: - raise ValidationFailure('unknown key') + candidate_keys = _find_candidate_keys(keys, rrsig) + if candidate_keys is None: + raise ValidationFailure('unknown key') + for candidate_key in candidate_keys: # For convenience, allow the rrset to be specified as a (name, # rdataset) tuple as well as a proper rrset if isinstance(rrset, tuple): @@ -322,7 +364,8 @@ def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): keyptr = candidate_key.key x = Crypto.Util.number.bytes_to_long(keyptr[0:key_len]) y = Crypto.Util.number.bytes_to_long(keyptr[key_len:key_len * 2]) - assert ecdsa.ecdsa.point_is_valid(curve.generator, x, y) + if not ecdsa.ecdsa.point_is_valid(curve.generator, x, y): + raise ValidationFailure('invalid ECDSA key') point = ecdsa.ellipticcurve.Point(curve.curve, x, y, curve.order) verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point, curve) @@ -374,22 +417,22 @@ def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): def _validate(rrset, rrsigset, keys, origin=None, now=None): - """Validate an RRset + """Validate an RRset. - @param rrset: The RRset to validate - @type rrset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) - tuple - @param rrsigset: The signature RRset - @type rrsigset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset) - tuple - @param keys: The key dictionary. - @type keys: a dictionary keyed by dns.name.Name with node or rdataset - values - @param origin: The origin to use for relative names - @type origin: dns.name.Name or None - @param now: The time to use when validating the signatures. The default - is the current time. - @type now: int + *rrset* is the RRset to validate. It can be a ``dns.rrset.RRset`` or + a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple. + + *rrsigset* is the signature RRset to be validated. It can be a + ``dns.rrset.RRset`` or a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple. + + *keys* is the key dictionary, used to find the DNSKEY associated with + a given name. The dictionary is keyed by a ``dns.name.Name``, and has + ``dns.node.Node`` or ``dns.rdataset.Rdataset`` values. + + *origin* is a ``dns.name.Name``, the origin to use for relative names. + + *now* is an ``int``, the time to use when validating the signatures, + in seconds since the UNIX epoch. The default is the current time. """ if isinstance(origin, string_types): diff --git a/eventlet/support/dns/e164.py b/eventlet/support/dns/e164.py index 9930073..0dbd3ff 100644 --- a/eventlet/support/dns/e164.py +++ b/eventlet/support/dns/e164.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006, 2007, 2009, 2011 Nominum, Inc. +# Copyright (C) 2006-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -13,31 +13,32 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""DNS E.164 helpers - -@var public_enum_domain: The DNS public ENUM domain, e164.arpa. -@type public_enum_domain: dns.name.Name object -""" - +"""DNS E.164 helpers.""" import dns.exception import dns.name import dns.resolver -from ._compat import string_types +from ._compat import string_types, maybe_decode +#: The public E.164 domain. public_enum_domain = dns.name.from_text('e164.arpa.') def from_e164(text, origin=public_enum_domain): """Convert an E.164 number in textual form into a Name object whose value is the ENUM domain name for that number. - @param text: an E.164 number in textual form. - @type text: str - @param origin: The domain in which the number should be constructed. - The default is e164.arpa. - @type origin: dns.name.Name object or None - @rtype: dns.name.Name object + + Non-digits in the text are ignored, i.e. "16505551212", + "+1.650.555.1212" and "1 (650) 555-1212" are all the same. + + *text*, a ``text``, is an E.164 number in textual form. + + *origin*, a ``dns.name.Name``, the domain in which the number + should be constructed. The default is ``e164.arpa.``. + + Returns a ``dns.name.Name``. """ + parts = [d for d in text if d.isdigit()] parts.reverse() return dns.name.from_text('.'.join(parts), origin=origin) @@ -45,14 +46,23 @@ def from_e164(text, origin=public_enum_domain): def to_e164(name, origin=public_enum_domain, want_plus_prefix=True): """Convert an ENUM domain name into an E.164 number. - @param name: the ENUM domain name. - @type name: dns.name.Name object. - @param origin: A domain containing the ENUM domain name. The - name is relativized to this domain before being converted to text. - @type origin: dns.name.Name object or None - @param want_plus_prefix: if True, add a '+' to the beginning of the - returned number. - @rtype: str + + Note that dnspython does not have any information about preferred + number formats within national numbering plans, so all numbers are + emitted as a simple string of digits, prefixed by a '+' (unless + *want_plus_prefix* is ``False``). + + *name* is a ``dns.name.Name``, the ENUM domain name. + + *origin* is a ``dns.name.Name``, a domain containing the ENUM + domain name. The name is relativized to this domain before being + converted to text. If ``None``, no relativization is done. + + *want_plus_prefix* is a ``bool``. If True, add a '+' to the beginning of + the returned number. + + Returns a ``text``. + """ if origin is not None: name = name.relativize(origin) @@ -63,14 +73,22 @@ def to_e164(name, origin=public_enum_domain, want_plus_prefix=True): text = b''.join(dlabels) if want_plus_prefix: text = b'+' + text - return text + return maybe_decode(text) def query(number, domains, resolver=None): """Look for NAPTR RRs for the specified number in the specified domains. e.g. lookup('16505551212', ['e164.dnspython.org.', 'e164.arpa.']) + + *number*, a ``text`` is the number to look for. + + *domains* is an iterable containing ``dns.name.Name`` values. + + *resolver*, a ``dns.resolver.Resolver``, is the resolver to use. If + ``None``, the default resolver is used. """ + if resolver is None: resolver = dns.resolver.get_default_resolver() e_nx = dns.resolver.NXDOMAIN() diff --git a/eventlet/support/dns/edns.py b/eventlet/support/dns/edns.py index 8ac676b..22104d8 100644 --- a/eventlet/support/dns/edns.py +++ b/eventlet/support/dns/edns.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009, 2011 Nominum, Inc. +# Copyright (C) 2009-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -15,18 +15,42 @@ """EDNS Options""" -NSID = 3 +from __future__ import absolute_import +import math +import struct + +import dns.inet + +#: NSID +NSID = 3 +#: DAU +DAU = 5 +#: DHU +DHU = 6 +#: N3U +N3U = 7 +#: ECS (client-subnet) +ECS = 8 +#: EXPIRE +EXPIRE = 9 +#: COOKIE +COOKIE = 10 +#: KEEPALIVE +KEEPALIVE = 11 +#: PADDING +PADDING = 12 +#: CHAIN +CHAIN = 13 class Option(object): - """Base class for all EDNS option types. - """ + """Base class for all EDNS option types.""" def __init__(self, otype): """Initialize an option. - @param otype: The rdata type - @type otype: int + + *otype*, an ``int``, is the option type. """ self.otype = otype @@ -37,23 +61,26 @@ class Option(object): @classmethod def from_wire(cls, otype, wire, current, olen): - """Build an EDNS option object from wire format + """Build an EDNS option object from wire format. + + *otype*, an ``int``, is the option type. + + *wire*, a ``binary``, is the wire-format message. + + *current*, an ``int``, is the offset in *wire* of the beginning + of the rdata. + + *olen*, an ``int``, is the length of the wire-format option data + + Returns a ``dns.edns.Option``. + """ - @param otype: The option type - @type otype: int - @param wire: The wire-format message - @type wire: string - @param current: The offset in wire of the beginning of the rdata. - @type current: int - @param olen: The length of the wire-format option data - @type olen: int - @rtype: dns.edns.Option instance""" raise NotImplementedError def _cmp(self, other): """Compare an EDNS option with another option of the same type. - Return < 0 if self < other, 0 if self == other, - and > 0 if self > other. + + Returns < 0 if < *other*, 0 if == *other*, and > 0 if > *other*. """ raise NotImplementedError @@ -98,7 +125,7 @@ class Option(object): class GenericOption(Option): - """Generate Rdata Class + """Generic Option Class This class is used for EDNS option types for which we have no better implementation. @@ -111,6 +138,9 @@ class GenericOption(Option): def to_wire(self, file): file.write(self.data) + def to_text(self): + return "Generic %d" % self.otype + @classmethod def from_wire(cls, otype, wire, current, olen): return cls(otype, wire[current: current + olen]) @@ -122,11 +152,96 @@ class GenericOption(Option): return 1 return -1 + +class ECSOption(Option): + """EDNS Client Subnet (ECS, RFC7871)""" + + def __init__(self, address, srclen=None, scopelen=0): + """*address*, a ``text``, is the client address information. + + *srclen*, an ``int``, the source prefix length, which is the + leftmost number of bits of the address to be used for the + lookup. The default is 24 for IPv4 and 56 for IPv6. + + *scopelen*, an ``int``, the scope prefix length. This value + must be 0 in queries, and should be set in responses. + """ + + super(ECSOption, self).__init__(ECS) + af = dns.inet.af_for_address(address) + + if af == dns.inet.AF_INET6: + self.family = 2 + if srclen is None: + srclen = 56 + elif af == dns.inet.AF_INET: + self.family = 1 + if srclen is None: + srclen = 24 + else: + raise ValueError('Bad ip family') + + self.address = address + self.srclen = srclen + self.scopelen = scopelen + + addrdata = dns.inet.inet_pton(af, address) + nbytes = int(math.ceil(srclen/8.0)) + + # Truncate to srclen and pad to the end of the last octet needed + # See RFC section 6 + self.addrdata = addrdata[:nbytes] + nbits = srclen % 8 + if nbits != 0: + last = struct.pack('B', ord(self.addrdata[-1:]) & (0xff << nbits)) + self.addrdata = self.addrdata[:-1] + last + + def to_text(self): + return "ECS %s/%s scope/%s" % (self.address, self.srclen, + self.scopelen) + + def to_wire(self, file): + file.write(struct.pack('!H', self.family)) + file.write(struct.pack('!BB', self.srclen, self.scopelen)) + file.write(self.addrdata) + + @classmethod + def from_wire(cls, otype, wire, cur, olen): + family, src, scope = struct.unpack('!HBB', wire[cur:cur+4]) + cur += 4 + + addrlen = int(math.ceil(src/8.0)) + + if family == 1: + af = dns.inet.AF_INET + pad = 4 - addrlen + elif family == 2: + af = dns.inet.AF_INET6 + pad = 16 - addrlen + else: + raise ValueError('unsupported family') + + addr = dns.inet.inet_ntop(af, wire[cur:cur+addrlen] + b'\x00' * pad) + return cls(addr, src, scope) + + def _cmp(self, other): + if self.addrdata == other.addrdata: + return 0 + if self.addrdata > other.addrdata: + return 1 + return -1 + _type_to_class = { + ECS: ECSOption } - def get_option_class(otype): + """Return the class for the specified option type. + + The GenericOption class is used if a more specific class is not + known. + """ + cls = _type_to_class.get(otype) if cls is None: cls = GenericOption @@ -134,17 +249,19 @@ def get_option_class(otype): def option_from_wire(otype, wire, current, olen): - """Build an EDNS option object from wire format + """Build an EDNS option object from wire format. - @param otype: The option type - @type otype: int - @param wire: The wire-format message - @type wire: string - @param current: The offset in wire of the beginning of the rdata. - @type current: int - @param olen: The length of the wire-format option data - @type olen: int - @rtype: dns.edns.Option instance""" + *otype*, an ``int``, is the option type. + + *wire*, a ``binary``, is the wire-format message. + + *current*, an ``int``, is the offset in *wire* of the beginning + of the rdata. + + *olen*, an ``int``, is the length of the wire-format option data + + Returns an instance of a subclass of ``dns.edns.Option``. + """ cls = get_option_class(otype) return cls.from_wire(otype, wire, current, olen) diff --git a/eventlet/support/dns/entropy.py b/eventlet/support/dns/entropy.py index de7a70a..64e0b5d 100644 --- a/eventlet/support/dns/entropy.py +++ b/eventlet/support/dns/entropy.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009, 2011 Nominum, Inc. +# Copyright (C) 2009-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -25,6 +25,11 @@ except ImportError: class EntropyPool(object): + # This is an entropy pool for Python implementations that do not + # have a working SystemRandom. I'm not sure there are any, but + # leaving this code doesn't hurt anything as the library code + # is used if present. + def __init__(self, seed=None): self.pool_index = 0 self.digest = None diff --git a/eventlet/support/dns/exception.py b/eventlet/support/dns/exception.py index 6c0b1f4..89aee9c 100644 --- a/eventlet/support/dns/exception.py +++ b/eventlet/support/dns/exception.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -13,32 +13,35 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""Common DNS Exceptions.""" +"""Common DNS Exceptions. +Dnspython modules may also define their own exceptions, which will +always be subclasses of ``DNSException``. +""" class DNSException(Exception): - """Abstract base class shared by all dnspython exceptions. It supports two basic modes of operation: - a) Old/compatible mode is used if __init__ was called with - empty **kwargs. - In compatible mode all *args are passed to standard Python Exception class - as before and all *args are printed by standard __str__ implementation. - Class variable msg (or doc string if msg is None) is returned from str() - if *args is empty. + a) Old/compatible mode is used if ``__init__`` was called with + empty *kwargs*. In compatible mode all *args* are passed + to the standard Python Exception class as before and all *args* are + printed by the standard ``__str__`` implementation. Class variable + ``msg`` (or doc string if ``msg`` is ``None``) is returned from ``str()`` + if *args* is empty. - b) New/parametrized mode is used if __init__ was called with - non-empty **kwargs. - In the new mode *args has to be empty and all kwargs has to exactly match - set in class variable self.supp_kwargs. All kwargs are stored inside - self.kwargs and used in new __str__ implementation to construct - formatted message based on self.fmt string. + b) New/parametrized mode is used if ``__init__`` was called with + non-empty *kwargs*. + In the new mode *args* must be empty and all kwargs must match + those set in class variable ``supp_kwargs``. All kwargs are stored inside + ``self.kwargs`` and used in a new ``__str__`` implementation to construct + a formatted message based on the ``fmt`` class variable, a ``string``. - In the simplest case it is enough to override supp_kwargs and fmt - class variables to get nice parametrized messages. + In the simplest case it is enough to override the ``supp_kwargs`` + and ``fmt`` class variables to get nice parametrized messages. """ + msg = None # non-parametrized message supp_kwargs = set() # accepted parameters for _fmt_kwargs (sanity check) fmt = None # message parametrized with results from _fmt_kwargs @@ -102,27 +105,22 @@ class DNSException(Exception): class FormError(DNSException): - """DNS message is malformed.""" class SyntaxError(DNSException): - """Text input is malformed.""" class UnexpectedEnd(SyntaxError): - """Text input ended unexpectedly.""" class TooBig(DNSException): - """The DNS message is too big.""" class Timeout(DNSException): - """The DNS operation timed out.""" supp_kwargs = set(['timeout']) fmt = "The DNS operation timed out after {timeout} seconds" diff --git a/eventlet/support/dns/flags.py b/eventlet/support/dns/flags.py index 388d6aa..f9a62b3 100644 --- a/eventlet/support/dns/flags.py +++ b/eventlet/support/dns/flags.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -17,16 +17,24 @@ # Standard DNS flags +#: Query Response QR = 0x8000 +#: Authoritative Answer AA = 0x0400 +#: Truncated Response TC = 0x0200 +#: Recursion Desired RD = 0x0100 +#: Recursion Available RA = 0x0080 +#: Authentic Data AD = 0x0020 +#: Checking Disabled CD = 0x0010 # EDNS flags +#: DNSSEC answer OK DO = 0x8000 _by_text = { @@ -83,7 +91,9 @@ def _to_text(flags, table, order): def from_text(text): """Convert a space-separated list of flag text values into a flags value. - @rtype: int""" + + Returns an ``int`` + """ return _from_text(text, _by_text) @@ -91,7 +101,9 @@ def from_text(text): def to_text(flags): """Convert a flags value into a space-separated list of flag text values. - @rtype: string""" + + Returns a ``text``. + """ return _to_text(flags, _by_value, _flags_order) @@ -99,7 +111,9 @@ def to_text(flags): def edns_from_text(text): """Convert a space-separated list of EDNS flag text values into a EDNS flags value. - @rtype: int""" + + Returns an ``int`` + """ return _from_text(text, _edns_by_text) @@ -107,6 +121,8 @@ def edns_from_text(text): def edns_to_text(flags): """Convert an EDNS flags value into a space-separated list of EDNS flag text values. - @rtype: string""" + + Returns a ``text``. + """ return _to_text(flags, _edns_by_value, _edns_flags_order) diff --git a/eventlet/support/dns/grange.py b/eventlet/support/dns/grange.py index 9ce9f67..752858d 100644 --- a/eventlet/support/dns/grange.py +++ b/eventlet/support/dns/grange.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2012-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -17,18 +17,16 @@ import dns - def from_text(text): - """Convert the text form of a range in a GENERATE statement to an + """Convert the text form of a range in a ``$GENERATE`` statement to an integer. - @param text: the textual range - @type text: string - @return: The start, stop and step values. - @rtype: tuple - """ - # TODO, figure out the bounds on start, stop and step. + *text*, a ``str``, the textual range in ``$GENERATE`` form. + Returns a tuple of three ``int`` values ``(start, stop, step)``. + """ + + # TODO, figure out the bounds on start, stop and step. step = 1 cur = '' state = 0 diff --git a/eventlet/support/dns/inet.py b/eventlet/support/dns/inet.py index 73490a9..f417749 100644 --- a/eventlet/support/dns/inet.py +++ b/eventlet/support/dns/inet.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -20,6 +20,7 @@ import socket import dns.ipv4 import dns.ipv6 +from ._compat import maybe_ord # We assume that AF_INET is always defined. @@ -38,13 +39,14 @@ except AttributeError: def inet_pton(family, text): """Convert the textual form of a network address into its binary form. - @param family: the address family - @type family: int - @param text: the textual address - @type text: string - @raises NotImplementedError: the address family specified is not + *family* is an ``int``, the address family. + + *text* is a ``text``, the textual address. + + Raises ``NotImplementedError`` if the address family specified is not implemented. - @rtype: string + + Returns a ``binary``. """ if family == AF_INET: @@ -58,14 +60,16 @@ def inet_pton(family, text): def inet_ntop(family, address): """Convert the binary form of a network address into its textual form. - @param family: the address family - @type family: int - @param address: the binary address - @type address: string - @raises NotImplementedError: the address family specified is not + *family* is an ``int``, the address family. + + *address* is a ``binary``, the network address in binary form. + + Raises ``NotImplementedError`` if the address family specified is not implemented. - @rtype: string + + Returns a ``text``. """ + if family == AF_INET: return dns.ipv4.inet_ntoa(address) elif family == AF_INET6: @@ -77,11 +81,14 @@ def inet_ntop(family, address): def af_for_address(text): """Determine the address family of a textual-form network address. - @param text: the textual address - @type text: string - @raises ValueError: the address family cannot be determined from the input. - @rtype: int + *text*, a ``text``, the textual address. + + Raises ``ValueError`` if the address family cannot be determined + from the input. + + Returns an ``int``. """ + try: dns.ipv4.inet_aton(text) return AF_INET @@ -96,16 +103,20 @@ def af_for_address(text): def is_multicast(text): """Is the textual-form network address a multicast address? - @param text: the textual address - @raises ValueError: the address family cannot be determined from the input. - @rtype: bool + *text*, a ``text``, the textual address. + + Raises ``ValueError`` if the address family cannot be determined + from the input. + + Returns a ``bool``. """ + try: - first = ord(dns.ipv4.inet_aton(text)[0]) + first = maybe_ord(dns.ipv4.inet_aton(text)[0]) return first >= 224 and first <= 239 except Exception: try: - first = ord(dns.ipv6.inet_aton(text)[0]) + first = maybe_ord(dns.ipv6.inet_aton(text)[0]) return first == 255 except Exception: raise ValueError diff --git a/eventlet/support/dns/ipv4.py b/eventlet/support/dns/ipv4.py index 3fef282..a67a84b 100644 --- a/eventlet/support/dns/ipv4.py +++ b/eventlet/support/dns/ipv4.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -21,26 +21,28 @@ import dns.exception from ._compat import binary_type def inet_ntoa(address): - """Convert an IPv4 address in network form to text form. + """Convert an IPv4 address in binary form to text form. - @param address: The IPv4 address - @type address: string - @returns: string + *address*, a ``binary``, the IPv4 address in binary form. + + Returns a ``text``. """ + if len(address) != 4: raise dns.exception.SyntaxError if not isinstance(address, bytearray): address = bytearray(address) - return (u'%u.%u.%u.%u' % (address[0], address[1], - address[2], address[3])).encode() + return ('%u.%u.%u.%u' % (address[0], address[1], + address[2], address[3])) def inet_aton(text): - """Convert an IPv4 address in text form to network form. + """Convert an IPv4 address in text form to binary form. - @param text: The IPv4 address - @type text: string - @returns: string + *text*, a ``text``, the IPv4 address in textual form. + + Returns a ``binary``. """ + if not isinstance(text, binary_type): text = text.encode() parts = text.split(b'.') diff --git a/eventlet/support/dns/ipv6.py b/eventlet/support/dns/ipv6.py index cbaee8e..fcc0d93 100644 --- a/eventlet/support/dns/ipv6.py +++ b/eventlet/support/dns/ipv6.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -22,15 +22,15 @@ import dns.exception import dns.ipv4 from ._compat import xrange, binary_type, maybe_decode -_leading_zero = re.compile(b'0+([0-9a-f]+)') +_leading_zero = re.compile('0+([0-9a-f]+)') def inet_ntoa(address): - """Convert a network format IPv6 address into text. + """Convert an IPv6 address in binary form to text form. - @param address: the binary address - @type address: string - @rtype: string - @raises ValueError: the address isn't 16 bytes long + *address*, a ``binary``, the IPv6 address in binary form. + + Raises ``ValueError`` if the address isn't 16 bytes long. + Returns a ``text``. """ if len(address) != 16: @@ -40,7 +40,7 @@ def inet_ntoa(address): i = 0 l = len(hex) while i < l: - chunk = hex[i : i + 4] + chunk = maybe_decode(hex[i : i + 4]) # strip leading zeros. we do this with an re instead of # with lstrip() because lstrip() didn't support chars until # python 2.2.2 @@ -57,7 +57,7 @@ def inet_ntoa(address): start = -1 last_was_zero = False for i in xrange(8): - if chunks[i] != b'0': + if chunks[i] != '0': if last_was_zero: end = i current_len = end - start @@ -77,31 +77,30 @@ def inet_ntoa(address): if best_len > 1: if best_start == 0 and \ (best_len == 6 or - best_len == 5 and chunks[5] == b'ffff'): + best_len == 5 and chunks[5] == 'ffff'): # We have an embedded IPv4 address if best_len == 6: - prefix = b'::' + prefix = '::' else: - prefix = b'::ffff:' + prefix = '::ffff:' hex = prefix + dns.ipv4.inet_ntoa(address[12:]) else: - hex = b':'.join(chunks[:best_start]) + b'::' + \ - b':'.join(chunks[best_start + best_len:]) + hex = ':'.join(chunks[:best_start]) + '::' + \ + ':'.join(chunks[best_start + best_len:]) else: - hex = b':'.join(chunks) - return maybe_decode(hex) + hex = ':'.join(chunks) + return hex _v4_ending = re.compile(b'(.*):(\d+\.\d+\.\d+\.\d+)$') _colon_colon_start = re.compile(b'::.*') _colon_colon_end = re.compile(b'.*::$') def inet_aton(text): - """Convert a text format IPv6 address into network format. + """Convert an IPv6 address in text form to binary form. - @param text: the textual address - @type text: string - @rtype: string - @raises dns.exception.SyntaxError: the text was not properly formatted + *text*, a ``text``, the IPv6 address in textual form. + + Returns a ``binary``. """ # @@ -169,4 +168,11 @@ def inet_aton(text): _mapped_prefix = b'\x00' * 10 + b'\xff\xff' def is_mapped(address): + """Is the specified address a mapped IPv4 address? + + *address*, a ``binary`` is an IPv6 address in binary form. + + Returns a ``bool``. + """ + return address.startswith(_mapped_prefix) diff --git a/eventlet/support/dns/message.py b/eventlet/support/dns/message.py index a0df18e..e7c6417 100644 --- a/eventlet/support/dns/message.py +++ b/eventlet/support/dns/message.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -40,114 +40,46 @@ from ._compat import long, xrange, string_types class ShortHeader(dns.exception.FormError): - """The DNS packet passed to from_wire() is too short.""" class TrailingJunk(dns.exception.FormError): - """The DNS packet passed to from_wire() has extra junk at the end of it.""" class UnknownHeaderField(dns.exception.DNSException): - """The header field name was not recognized when converting from text into a message.""" class BadEDNS(dns.exception.FormError): - - """OPT record occurred somewhere other than the start of + """An OPT record occurred somewhere other than the start of the additional data section.""" class BadTSIG(dns.exception.FormError): - """A TSIG record occurred somewhere other than the end of the additional data section.""" class UnknownTSIGKey(dns.exception.DNSException): - """A TSIG with an unknown key was received.""" +#: The question section number +QUESTION = 0 + +#: The answer section number +ANSWER = 1 + +#: The authority section number +AUTHORITY = 2 + +#: The additional section number +ADDITIONAL = 3 + class Message(object): - - """A DNS message. - - @ivar id: The query id; the default is a randomly chosen id. - @type id: int - @ivar flags: The DNS flags of the message. @see: RFC 1035 for an - explanation of these flags. - @type flags: int - @ivar question: The question section. - @type question: list of dns.rrset.RRset objects - @ivar answer: The answer section. - @type answer: list of dns.rrset.RRset objects - @ivar authority: The authority section. - @type authority: list of dns.rrset.RRset objects - @ivar additional: The additional data section. - @type additional: list of dns.rrset.RRset objects - @ivar edns: The EDNS level to use. The default is -1, no Edns. - @type edns: int - @ivar ednsflags: The EDNS flags - @type ednsflags: long - @ivar payload: The EDNS payload size. The default is 0. - @type payload: int - @ivar options: The EDNS options - @type options: list of dns.edns.Option objects - @ivar request_payload: The associated request's EDNS payload size. - @type request_payload: int - @ivar keyring: The TSIG keyring to use. The default is None. - @type keyring: dict - @ivar keyname: The TSIG keyname to use. The default is None. - @type keyname: dns.name.Name object - @ivar keyalgorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm. Constants for TSIG algorithms are defined - in dns.tsig, and the currently implemented algorithms are - HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, and - HMAC_SHA512. - @type keyalgorithm: string - @ivar request_mac: The TSIG MAC of the request message associated with - this message; used when validating TSIG signatures. @see: RFC 2845 for - more information on TSIG fields. - @type request_mac: string - @ivar fudge: TSIG time fudge; default is 300 seconds. - @type fudge: int - @ivar original_id: TSIG original id; defaults to the message's id - @type original_id: int - @ivar tsig_error: TSIG error code; default is 0. - @type tsig_error: int - @ivar other_data: TSIG other data. - @type other_data: string - @ivar mac: The TSIG MAC for this message. - @type mac: string - @ivar xfr: Is the message being used to contain the results of a DNS - zone transfer? The default is False. - @type xfr: bool - @ivar origin: The origin of the zone in messages which are used for - zone transfers or for DNS dynamic updates. The default is None. - @type origin: dns.name.Name object - @ivar tsig_ctx: The TSIG signature context associated with this - message. The default is None. - @type tsig_ctx: hmac.HMAC object - @ivar had_tsig: Did the message decoded from wire format have a TSIG - signature? - @type had_tsig: bool - @ivar multi: Is this message part of a multi-message sequence? The - default is false. This variable is used when validating TSIG signatures - on messages which are part of a zone transfer. - @type multi: bool - @ivar first: Is this message standalone, or the first of a multi - message sequence? This variable is used when validating TSIG signatures - on messages which are part of a zone transfer. - @type first: bool - @ivar index: An index of rrsets in the message. The index key is - (section, name, rdclass, rdtype, covers, deleting). Indexing can be - disabled by setting the index to None. - @type index: dict - """ + """A DNS message.""" def __init__(self, id=None): if id is None: @@ -167,12 +99,12 @@ class Message(object): self.keyring = None self.keyname = None self.keyalgorithm = dns.tsig.default_algorithm - self.request_mac = '' - self.other_data = '' + self.request_mac = b'' + self.other_data = b'' self.tsig_error = 0 self.fudge = 300 self.original_id = self.id - self.mac = '' + self.mac = b'' self.xfr = False self.origin = None self.tsig_ctx = None @@ -190,10 +122,10 @@ class Message(object): def to_text(self, origin=None, relativize=True, **kw): """Convert the message to text. - The I{origin}, I{relativize}, and any other keyword - arguments are passed to the rrset to_wire() method. + The *origin*, *relativize*, and any other keyword + arguments are passed to the RRset ``to_wire()`` method. - @rtype: string + Returns a ``text``. """ s = StringIO() @@ -209,6 +141,8 @@ class Message(object): s.write(u'eflags %s\n' % dns.flags.edns_to_text(self.ednsflags)) s.write(u'payload %d\n' % self.payload) + for opt in self.options: + s.write(u'option %s\n' % opt.to_text()) is_update = dns.opcode.is_update(self.flags) if is_update: s.write(u';ZONE\n') @@ -245,7 +179,10 @@ class Message(object): def __eq__(self, other): """Two messages are equal if they have the same content in the header, question, answer, and authority sections. - @rtype: bool""" + + Returns a ``bool``. + """ + if not isinstance(other, Message): return False if self.id != other.id: @@ -273,13 +210,14 @@ class Message(object): return True def __ne__(self, other): - """Are two messages not equal? - @rtype: bool""" return not self.__eq__(other) def is_response(self, other): - """Is other a response to self? - @rtype: bool""" + """Is this message a response to *other*? + + Returns a ``bool``. + """ + if other.flags & dns.flags.QR == 0 or \ self.id != other.id or \ dns.opcode.from_flags(self.flags) != \ @@ -299,14 +237,48 @@ class Message(object): return True def section_number(self, section): + """Return the "section number" of the specified section for use + in indexing. The question section is 0, the answer section is 1, + the authority section is 2, and the additional section is 3. + + *section* is one of the section attributes of this message. + + Raises ``ValueError`` if the section isn't known. + + Returns an ``int``. + """ + if section is self.question: - return 0 + return QUESTION elif section is self.answer: - return 1 + return ANSWER elif section is self.authority: - return 2 + return AUTHORITY elif section is self.additional: - return 3 + return ADDITIONAL + else: + raise ValueError('unknown section') + + def section_from_number(self, number): + """Return the "section number" of the specified section for use + in indexing. The question section is 0, the answer section is 1, + the authority section is 2, and the additional section is 3. + + *section* is one of the section attributes of this message. + + Raises ``ValueError`` if the section isn't known. + + Returns an ``int``. + """ + + if number == QUESTION: + return self.question + elif number == ANSWER: + return self.answer + elif number == AUTHORITY: + return self.authority + elif number == ADDITIONAL: + return self.additional else: raise ValueError('unknown section') @@ -315,30 +287,45 @@ class Message(object): force_unique=False): """Find the RRset with the given attributes in the specified section. - @param section: the section of the message to look in, e.g. - self.answer. - @type section: list of dns.rrset.RRset objects - @param name: the name of the RRset - @type name: dns.name.Name object - @param rdclass: the class of the RRset - @type rdclass: int - @param rdtype: the type of the RRset - @type rdtype: int - @param covers: the covers value of the RRset - @type covers: int - @param deleting: the deleting value of the RRset - @type deleting: int - @param create: If True, create the RRset if it is not found. - The created RRset is appended to I{section}. - @type create: bool - @param force_unique: If True and create is also True, create a - new RRset regardless of whether a matching RRset exists already. - @type force_unique: bool - @raises KeyError: the RRset was not found and create was False - @rtype: dns.rrset.RRset object""" + *section*, an ``int`` section number, or one of the section + attributes of this message. This specifies the + the section of the message to search. For example:: - key = (self.section_number(section), - name, rdclass, rdtype, covers, deleting) + my_message.find_rrset(my_message.answer, name, rdclass, rdtype) + my_message.find_rrset(dns.message.ANSWER, name, rdclass, rdtype) + + *name*, a ``dns.name.Name``, the name of the RRset. + + *rdclass*, an ``int``, the class of the RRset. + + *rdtype*, an ``int``, the type of the RRset. + + *covers*, an ``int`` or ``None``, the covers value of the RRset. + The default is ``None``. + + *deleting*, an ``int`` or ``None``, the deleting value of the RRset. + The default is ``None``. + + *create*, a ``bool``. If ``True``, create the RRset if it is not found. + The created RRset is appended to *section*. + + *force_unique*, a ``bool``. If ``True`` and *create* is also ``True``, + create a new RRset regardless of whether a matching RRset exists + already. The default is ``False``. This is useful when creating + DDNS Update messages, as order matters for them. + + Raises ``KeyError`` if the RRset was not found and create was + ``False``. + + Returns a ``dns.rrset.RRset object``. + """ + + if isinstance(section, int): + section_number = section + section = self.section_from_number(section_number) + else: + section_number = self.section_number(section) + key = (section_number, name, rdclass, rdtype, covers, deleting) if not force_unique: if self.index is not None: rrset = self.index.get(key) @@ -363,26 +350,35 @@ class Message(object): If the RRset is not found, None is returned. - @param section: the section of the message to look in, e.g. - self.answer. - @type section: list of dns.rrset.RRset objects - @param name: the name of the RRset - @type name: dns.name.Name object - @param rdclass: the class of the RRset - @type rdclass: int - @param rdtype: the type of the RRset - @type rdtype: int - @param covers: the covers value of the RRset - @type covers: int - @param deleting: the deleting value of the RRset - @type deleting: int - @param create: If True, create the RRset if it is not found. - The created RRset is appended to I{section}. - @type create: bool - @param force_unique: If True and create is also True, create a - new RRset regardless of whether a matching RRset exists already. - @type force_unique: bool - @rtype: dns.rrset.RRset object or None""" + *section*, an ``int`` section number, or one of the section + attributes of this message. This specifies the + the section of the message to search. For example:: + + my_message.get_rrset(my_message.answer, name, rdclass, rdtype) + my_message.get_rrset(dns.message.ANSWER, name, rdclass, rdtype) + + *name*, a ``dns.name.Name``, the name of the RRset. + + *rdclass*, an ``int``, the class of the RRset. + + *rdtype*, an ``int``, the type of the RRset. + + *covers*, an ``int`` or ``None``, the covers value of the RRset. + The default is ``None``. + + *deleting*, an ``int`` or ``None``, the deleting value of the RRset. + The default is ``None``. + + *create*, a ``bool``. If ``True``, create the RRset if it is not found. + The created RRset is appended to *section*. + + *force_unique*, a ``bool``. If ``True`` and *create* is also ``True``, + create a new RRset regardless of whether a matching RRset exists + already. The default is ``False``. This is useful when creating + DDNS Update messages, as order matters for them. + + Returns a ``dns.rrset.RRset object`` or ``None``. + """ try: rrset = self.find_rrset(section, name, rdclass, rdtype, covers, @@ -395,17 +391,19 @@ class Message(object): """Return a string containing the message in DNS compressed wire format. - Additional keyword arguments are passed to the rrset to_wire() + Additional keyword arguments are passed to the RRset ``to_wire()`` method. - @param origin: The origin to be appended to any relative names. - @type origin: dns.name.Name object - @param max_size: The maximum size of the wire format output; default - is 0, which means 'the message's request payload, if nonzero, or - 65536'. - @type max_size: int - @raises dns.exception.TooBig: max_size was exceeded - @rtype: string + *origin*, a ``dns.name.Name`` or ``None``, the origin to be appended + to any relative names. + + *max_size*, an ``int``, the maximum size of the wire format + output; default is 0, which means "the message's request + payload, if nonzero, or 65535". + + Raises ``dns.exception.TooBig`` if *max_size* was exceeded. + + Returns a ``binary``. """ if max_size == 0: @@ -438,30 +436,34 @@ class Message(object): return r.get_wire() def use_tsig(self, keyring, keyname=None, fudge=300, - original_id=None, tsig_error=0, other_data='', + original_id=None, tsig_error=0, other_data=b'', algorithm=dns.tsig.default_algorithm): """When sending, a TSIG signature using the specified keyring and keyname should be added. - @param keyring: The TSIG keyring to use; defaults to None. - @type keyring: dict - @param keyname: The name of the TSIG key to use; defaults to None. - The key must be defined in the keyring. If a keyring is specified - but a keyname is not, then the key used will be the first key in the - keyring. Note that the order of keys in a dictionary is not defined, - so applications should supply a keyname when a keyring is used, unless - they know the keyring contains only one key. - @type keyname: dns.name.Name or string - @param fudge: TSIG time fudge; default is 300 seconds. - @type fudge: int - @param original_id: TSIG original id; defaults to the message's id - @type original_id: int - @param tsig_error: TSIG error code; default is 0. - @type tsig_error: int - @param other_data: TSIG other data. - @type other_data: string - @param algorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm + See the documentation of the Message class for a complete + description of the keyring dictionary. + + *keyring*, a ``dict``, the TSIG keyring to use. If a + *keyring* is specified but a *keyname* is not, then the key + used will be the first key in the *keyring*. Note that the + order of keys in a dictionary is not defined, so applications + should supply a keyname when a keyring is used, unless they + know the keyring contains only one key. + + *keyname*, a ``dns.name.Name`` or ``None``, the name of the TSIG key + to use; defaults to ``None``. The key must be defined in the keyring. + + *fudge*, an ``int``, the TSIG time fudge. + + *original_id*, an ``int``, the TSIG original id. If ``None``, + the message's id is used. + + *tsig_error*, an ``int``, the TSIG error code. + + *other_data*, a ``binary``, the TSIG other data. + + *algorithm*, a ``dns.name.Name``, the TSIG algorithm to use. """ self.keyring = keyring @@ -483,23 +485,26 @@ class Message(object): def use_edns(self, edns=0, ednsflags=0, payload=1280, request_payload=None, options=None): """Configure EDNS behavior. - @param edns: The EDNS level to use. Specifying None, False, or -1 - means 'do not use EDNS', and in this case the other parameters are - ignored. Specifying True is equivalent to specifying 0, i.e. 'use - EDNS0'. - @type edns: int or bool or None - @param ednsflags: EDNS flag values. - @type ednsflags: int - @param payload: The EDNS sender's payload field, which is the maximum - size of UDP datagram the sender can handle. - @type payload: int - @param request_payload: The EDNS payload size to use when sending - this message. If not specified, defaults to the value of payload. - @type request_payload: int or None - @param options: The EDNS options - @type options: None or list of dns.edns.Option objects - @see: RFC 2671 + + *edns*, an ``int``, is the EDNS level to use. Specifying + ``None``, ``False``, or ``-1`` means "do not use EDNS", and in this case + the other parameters are ignored. Specifying ``True`` is + equivalent to specifying 0, i.e. "use EDNS0". + + *ednsflags*, an ``int``, the EDNS flag values. + + *payload*, an ``int``, is the EDNS sender's payload field, which is the + maximum size of UDP datagram the sender can handle. I.e. how big + a response to this message can be. + + *request_payload*, an ``int``, is the EDNS payload size to use when + sending this message. If not specified, defaults to the value of + *payload*. + + *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS + options. """ + if edns is None or edns is False: edns = -1 if edns is True: @@ -525,11 +530,13 @@ class Message(object): def want_dnssec(self, wanted=True): """Enable or disable 'DNSSEC desired' flag in requests. - @param wanted: Is DNSSEC desired? If True, EDNS is enabled if - required, and then the DO bit is set. If False, the DO bit is - cleared if EDNS is enabled. - @type wanted: bool + + *wanted*, a ``bool``. If ``True``, then DNSSEC data is + desired in the response, EDNS is enabled if required, and then + the DO bit is set. If ``False``, the DO bit is cleared if + EDNS is enabled. """ + if wanted: if self.edns < 0: self.use_edns() @@ -539,14 +546,15 @@ class Message(object): def rcode(self): """Return the rcode. - @rtype: int + + Returns an ``int``. """ return dns.rcode.from_flags(self.flags, self.ednsflags) def set_rcode(self, rcode): """Set the rcode. - @param rcode: the rcode - @type rcode: int + + *rcode*, an ``int``, is the rcode to set. """ (value, evalue) = dns.rcode.to_flags(rcode) self.flags &= 0xFFF0 @@ -558,14 +566,15 @@ class Message(object): def opcode(self): """Return the opcode. - @rtype: int + + Returns an ``int``. """ return dns.opcode.from_flags(self.flags) def set_opcode(self, opcode): """Set the opcode. - @param opcode: the opcode - @type opcode: int + + *opcode*, an ``int``, is the opcode to set. """ self.flags &= 0x87FF self.flags |= dns.opcode.to_flags(opcode) @@ -575,23 +584,16 @@ class _WireReader(object): """Wire format reader. - @ivar wire: the wire-format message. - @type wire: string - @ivar message: The message object being built - @type message: dns.message.Message object - @ivar current: When building a message object from wire format, this + wire: a binary, is the wire-format message. + message: The message object being built + current: When building a message object from wire format, this variable contains the offset from the beginning of wire of the next octet to be read. - @type current: int - @ivar updating: Is the message a dynamic update? - @type updating: bool - @ivar one_rr_per_rrset: Put each RR into its own RRset? - @type one_rr_per_rrset: bool - @ivar ignore_trailing: Ignore trailing junk at end of request? - @type ignore_trailing: bool - @ivar zone_rdclass: The class of the zone in messages which are + updating: Is the message a dynamic update? + one_rr_per_rrset: Put each RR into its own RRset? + ignore_trailing: Ignore trailing junk at end of request? + zone_rdclass: The class of the zone in messages which are DNS dynamic updates. - @type zone_rdclass: int """ def __init__(self, wire, message, question_only=False, @@ -606,10 +608,9 @@ class _WireReader(object): self.ignore_trailing = ignore_trailing def _get_question(self, qcount): - """Read the next I{qcount} records from the wire data and add them to + """Read the next *qcount* records from the wire data and add them to the question section. - @param qcount: the number of questions in the message - @type qcount: int""" + """ if self.updating and qcount > 1: raise dns.exception.FormError @@ -632,10 +633,10 @@ class _WireReader(object): def _get_section(self, section, count): """Read the next I{count} records from the wire data and add them to the specified section. - @param section: the section of the message to which to add records - @type section: list of dns.rrset.RRset objects - @param count: the number of records to read - @type count: int""" + + section: the section of the message to which to add records + count: the number of records to read + """ if self.updating or self.one_rr_per_rrset: force_unique = True @@ -753,45 +754,58 @@ class _WireReader(object): self.message.tsig_ctx.update(self.wire) -def from_wire(wire, keyring=None, request_mac='', xfr=False, origin=None, +def from_wire(wire, keyring=None, request_mac=b'', xfr=False, origin=None, tsig_ctx=None, multi=False, first=True, question_only=False, one_rr_per_rrset=False, ignore_trailing=False): """Convert a DNS wire format message into a message object. - @param keyring: The keyring to use if the message is signed. - @type keyring: dict - @param request_mac: If the message is a response to a TSIG-signed request, - I{request_mac} should be set to the MAC of that request. - @type request_mac: string - @param xfr: Is this message part of a zone transfer? - @type xfr: bool - @param origin: If the message is part of a zone transfer, I{origin} - should be the origin name of the zone. - @type origin: dns.name.Name object - @param tsig_ctx: The ongoing TSIG context, used when validating zone - transfers. - @type tsig_ctx: hmac.HMAC object - @param multi: Is this message part of a multiple message sequence? - @type multi: bool - @param first: Is this message standalone, or the first of a multi - message sequence? - @type first: bool - @param question_only: Read only up to the end of the question section? - @type question_only: bool - @param one_rr_per_rrset: Put each RR into its own RRset - @type one_rr_per_rrset: bool - @param ignore_trailing: Ignore trailing junk at end of request? - @type ignore_trailing: bool - @raises ShortHeader: The message is less than 12 octets long. - @raises TrailingJunk: There were octets in the message past the end - of the proper DNS message. - @raises BadEDNS: An OPT record was in the wrong section, or occurred more - than once. - @raises BadTSIG: A TSIG record was not the last record of the additional - data section. - @rtype: dns.message.Message object""" + *keyring*, a ``dict``, the keyring to use if the message is signed. + + *request_mac*, a ``binary``. If the message is a response to a + TSIG-signed request, *request_mac* should be set to the MAC of + that request. + + *xfr*, a ``bool``, should be set to ``True`` if this message is part of + a zone transfer. + + *origin*, a ``dns.name.Name`` or ``None``. If the message is part + of a zone transfer, *origin* should be the origin name of the + zone. + + *tsig_ctx*, a ``hmac.HMAC`` objext, the ongoing TSIG context, used + when validating zone transfers. + + *multi*, a ``bool``, should be set to ``True`` if this message + part of a multiple message sequence. + + *first*, a ``bool``, should be set to ``True`` if this message is + stand-alone, or the first message in a multi-message sequence. + + *question_only*, a ``bool``. If ``True``, read only up to + the end of the question section. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its + own RRset. + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing + junk at end of the message. + + Raises ``dns.message.ShortHeader`` if the message is less than 12 octets + long. + + Raises ``dns.messaage.TrailingJunk`` if there were octets in the message + past the end of the proper DNS message, and *ignore_trailing* is ``False``. + + Raises ``dns.message.BadEDNS`` if an OPT record was in the + wrong section, or occurred more than once. + + Raises ``dns.message.BadTSIG`` if a TSIG record was not the last + record of the additional data section. + + Returns a ``dns.message.Message``. + """ m = Message(id=0) m.keyring = keyring @@ -813,18 +827,12 @@ class _TextReader(object): """Text format reader. - @ivar tok: the tokenizer - @type tok: dns.tokenizer.Tokenizer object - @ivar message: The message object being built - @type message: dns.message.Message object - @ivar updating: Is the message a dynamic update? - @type updating: bool - @ivar zone_rdclass: The class of the zone in messages which are + tok: the tokenizer. + message: The message object being built. + updating: Is the message a dynamic update? + zone_rdclass: The class of the zone in messages which are DNS dynamic updates. - @type zone_rdclass: int - @ivar last_name: The most recently read name when building a message object - from text format. - @type last_name: dns.name.Name object + last_name: The most recently read name when building a message object. """ def __init__(self, text, message): @@ -997,11 +1005,14 @@ class _TextReader(object): def from_text(text): """Convert the text format message into a message object. - @param text: The text format message. - @type text: string - @raises UnknownHeaderField: - @raises dns.exception.SyntaxError: - @rtype: dns.message.Message object""" + *text*, a ``text``, the text format message. + + Raises ``dns.message.UnknownHeaderField`` if a header is unknown. + + Raises ``dns.exception.SyntaxError`` if the text is badly formed. + + Returns a ``dns.message.Message object`` + """ # 'text' can also be a file, but we don't publish that fact # since it's an implementation detail. The official file @@ -1018,11 +1029,15 @@ def from_text(text): def from_file(f): """Read the next text format message from the specified file. - @param f: file or string. If I{f} is a string, it is treated - as the name of a file to open. - @raises UnknownHeaderField: - @raises dns.exception.SyntaxError: - @rtype: dns.message.Message object""" + *f*, a ``file`` or ``text``. If *f* is text, it is treated as the + pathname of a file to open. + + Raises ``dns.message.UnknownHeaderField`` if a header is unknown. + + Raises ``dns.exception.SyntaxError`` if the text is badly formed. + + Returns a ``dns.message.Message object`` + """ str_type = string_types opts = 'rU' @@ -1052,30 +1067,35 @@ def make_query(qname, rdtype, rdclass=dns.rdataclass.IN, use_edns=None, The query will have a randomly chosen query id, and its DNS flags will be set to dns.flags.RD. - @param qname: The query name. - @type qname: dns.name.Name object or string - @param rdtype: The desired rdata type. - @type rdtype: int - @param rdclass: The desired rdata class; the default is class IN. - @type rdclass: int - @param use_edns: The EDNS level to use; the default is None (no EDNS). + qname, a ``dns.name.Name`` or ``text``, the query name. + + *rdtype*, an ``int`` or ``text``, the desired rdata type. + + *rdclass*, an ``int`` or ``text``, the desired rdata class; the default + is class IN. + + *use_edns*, an ``int``, ``bool`` or ``None``. The EDNS level to use; the + default is None (no EDNS). See the description of dns.message.Message.use_edns() for the possible values for use_edns and their meanings. - @type use_edns: int or bool or None - @param want_dnssec: Should the query indicate that DNSSEC is desired? - @type want_dnssec: bool - @param ednsflags: EDNS flag values. - @type ednsflags: int - @param payload: The EDNS sender's payload field, which is the maximum - size of UDP datagram the sender can handle. - @type payload: int - @param request_payload: The EDNS payload size to use when sending - this message. If not specified, defaults to the value of payload. - @type request_payload: int or None - @param options: The EDNS options - @type options: None or list of dns.edns.Option objects - @see: RFC 2671 - @rtype: dns.message.Message object""" + + *want_dnssec*, a ``bool``. If ``True``, DNSSEC data is desired. + + *ednsflags*, an ``int``, the EDNS flag values. + + *payload*, an ``int``, is the EDNS sender's payload field, which is the + maximum size of UDP datagram the sender can handle. I.e. how big + a response to this message can be. + + *request_payload*, an ``int``, is the EDNS payload size to use when + sending this message. If not specified, defaults to the value of + *payload*. + + *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS + options. + + Returns a ``dns.message.Message`` + """ if isinstance(qname, string_types): qname = dns.name.from_text(qname) @@ -1124,16 +1144,17 @@ def make_response(query, recursion_available=False, our_payload=8192, question section, so the query's question RRsets should not be changed. - @param query: the query to respond to - @type query: dns.message.Message object - @param recursion_available: should RA be set in the response? - @type recursion_available: bool - @param our_payload: payload size to advertise in EDNS responses; default - is 8192. - @type our_payload: int - @param fudge: TSIG time fudge; default is 300 seconds. - @type fudge: int - @rtype: dns.message.Message object""" + *query*, a ``dns.message.Message``, the query to respond to. + + *recursion_available*, a ``bool``, should RA be set in the response? + + *our_payload*, an ``int``, the payload size to advertise in EDNS + responses. + + *fudge*, an ``int``, the TSIG time fudge. + + Returns a ``dns.message.Message`` object. + """ if query.flags & dns.flags.QR: raise dns.exception.FormError('specified query message is not a query') @@ -1146,7 +1167,7 @@ def make_response(query, recursion_available=False, our_payload=8192, if query.edns >= 0: response.use_edns(0, 0, our_payload, query.payload) if query.had_tsig: - response.use_tsig(query.keyring, query.keyname, fudge, None, 0, '', + response.use_tsig(query.keyring, query.keyname, fudge, None, 0, b'', query.keyalgorithm) response.request_mac = query.mac return response diff --git a/eventlet/support/dns/name.py b/eventlet/support/dns/name.py index 758a394..7537792 100644 --- a/eventlet/support/dns/name.py +++ b/eventlet/support/dns/name.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -14,11 +14,6 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """DNS Names. - -@var root: The DNS root name. -@type root: dns.name.Name object -@var empty: The empty DNS name. -@type empty: dns.name.Name object """ from io import BytesIO @@ -35,95 +30,117 @@ except ImportError: import dns.exception import dns.wiredata -from ._compat import long, binary_type, text_type, unichr +from ._compat import long, binary_type, text_type, unichr, maybe_decode try: maxint = sys.maxint except AttributeError: maxint = (1 << (8 * struct.calcsize("P"))) // 2 - 1 + +# fullcompare() result values + +#: The compared names have no relationship to each other. NAMERELN_NONE = 0 +#: the first name is a superdomain of the second. NAMERELN_SUPERDOMAIN = 1 +#: The first name is a subdomain of the second. NAMERELN_SUBDOMAIN = 2 +#: The compared names are equal. NAMERELN_EQUAL = 3 +#: The compared names have a common ancestor. NAMERELN_COMMONANCESTOR = 4 class EmptyLabel(dns.exception.SyntaxError): - """A DNS label is empty.""" class BadEscape(dns.exception.SyntaxError): - """An escaped code in a text format of DNS name is invalid.""" class BadPointer(dns.exception.FormError): - """A DNS compression pointer points forward instead of backward.""" class BadLabelType(dns.exception.FormError): - """The label type in DNS name wire format is unknown.""" class NeedAbsoluteNameOrOrigin(dns.exception.DNSException): - """An attempt was made to convert a non-absolute name to wire when there was also a non-absolute (or missing) origin.""" class NameTooLong(dns.exception.FormError): - """A DNS name is > 255 octets long.""" class LabelTooLong(dns.exception.SyntaxError): - """A DNS label is > 63 octets long.""" class AbsoluteConcatenation(dns.exception.DNSException): - """An attempt was made to append anything other than the empty name to an absolute DNS name.""" class NoParent(dns.exception.DNSException): - """An attempt was made to get the parent of the root name or the empty name.""" class NoIDNA2008(dns.exception.DNSException): - """IDNA 2008 processing was requested but the idna module is not available.""" class IDNAException(dns.exception.DNSException): - - """IDNA 2008 processing raised an exception.""" + """IDNA processing raised an exception.""" supp_kwargs = set(['idna_exception']) fmt = "IDNA processing exception: {idna_exception}" -class IDNACodec(object): +class IDNACodec(object): """Abstract base class for IDNA encoder/decoders.""" + def __init__(self): + pass + def encode(self, label): raise NotImplementedError def decode(self, label): - raise NotImplementedError + # We do not apply any IDNA policy on decode; we just + downcased = label.lower() + if downcased.startswith(b'xn--'): + try: + label = downcased[4:].decode('punycode') + except Exception as e: + raise IDNAException(idna_exception=e) + else: + label = maybe_decode(label) + return _escapify(label, True) + class IDNA2003Codec(IDNACodec): - """IDNA 2003 encoder/decoder.""" + def __init__(self, strict_decode=False): + """Initialize the IDNA 2003 encoder/decoder. + + *strict_decode* is a ``bool``. If `True`, then IDNA2003 checking + is done when decoding. This can cause failures if the name + was encoded with IDNA2008. The default is `False`. + """ + + super(IDNA2003Codec, self).__init__() + self.strict_decode = strict_decode + def encode(self, label): + """Encode *label*.""" + if label == '': return b'' try: @@ -132,37 +149,49 @@ class IDNA2003Codec(IDNACodec): raise LabelTooLong def decode(self, label): + """Decode *label*.""" + if not self.strict_decode: + return super(IDNA2003Codec, self).decode(label) if label == b'': return u'' - return _escapify(encodings.idna.ToUnicode(label), True) + try: + return _escapify(encodings.idna.ToUnicode(label), True) + except Exception as e: + raise IDNAException(idna_exception=e) + class IDNA2008Codec(IDNACodec): + """IDNA 2008 encoder/decoder. - """IDNA 2008 encoder/decoder.""" + *uts_46* is a ``bool``. If True, apply Unicode IDNA + compatibility processing as described in Unicode Technical + Standard #46 (http://unicode.org/reports/tr46/). + If False, do not apply the mapping. The default is False. + + *transitional* is a ``bool``: If True, use the + "transitional" mode described in Unicode Technical Standard + #46. The default is False. + + *allow_pure_ascii* is a ``bool``. If True, then a label which + consists of only ASCII characters is allowed. This is less + strict than regular IDNA 2008, but is also necessary for mixed + names, e.g. a name with starting with "_sip._tcp." and ending + in an IDN suffix which would otherwise be disallowed. The + default is False. + + *strict_decode* is a ``bool``: If True, then IDNA2008 checking + is done when decoding. This can cause failures if the name + was encoded with IDNA2003. The default is False. + """ def __init__(self, uts_46=False, transitional=False, - allow_pure_ascii=False): - """Initialize the IDNA 2008 encoder/decoder. - @param uts_46: If True, apply Unicode IDNA compatibility processing - as described in Unicode Technical Standard #46 - (U{http://unicode.org/reports/tr46/}). This parameter is only - meaningful if IDNA 2008 is in use. If False, do not apply - the mapping. The default is False - @type uts_46: bool - @param transitional: If True, use the "transitional" mode described - in Unicode Technical Standard #46. This parameter is only - meaningful if IDNA 2008 is in use. The default is False. - @type transitional: bool - @param allow_pure_ascii: If True, then a label which - consists of only ASCII characters is allowed. This is less strict - than regular IDNA 2008, but is also necessary for mixed names, - e.g. a name with starting with "_sip._tcp." and ending in an IDN - suffixm which would otherwise be disallowed. The default is False - @type allow_pure_ascii: bool - """ + allow_pure_ascii=False, strict_decode=False): + """Initialize the IDNA 2008 encoder/decoder.""" + super(IDNA2008Codec, self).__init__() self.uts_46 = uts_46 self.transitional = transitional self.allow_pure_ascii = allow_pure_ascii + self.strict_decode = strict_decode def is_all_ascii(self, label): for c in label: @@ -185,6 +214,8 @@ class IDNA2008Codec(IDNACodec): raise IDNAException(idna_exception=e) def decode(self, label): + if not self.strict_decode: + return super(IDNA2008Codec, self).decode(label) if label == b'': return u'' if not have_idna_2008: @@ -196,14 +227,15 @@ class IDNA2008Codec(IDNACodec): except idna.IDNAError as e: raise IDNAException(idna_exception=e) - _escaped = bytearray(b'"().;\\@$') -IDNA_2003 = IDNA2003Codec() -IDNA_2008_Practical = IDNA2008Codec(True, False, True) -IDNA_2008_UTS_46 = IDNA2008Codec(True, False, False) -IDNA_2008_Strict = IDNA2008Codec(False, False, False) -IDNA_2008_Transitional = IDNA2008Codec(True, True, False) +IDNA_2003_Practical = IDNA2003Codec(False) +IDNA_2003_Strict = IDNA2003Codec(True) +IDNA_2003 = IDNA_2003_Practical +IDNA_2008_Practical = IDNA2008Codec(True, False, True, False) +IDNA_2008_UTS_46 = IDNA2008Codec(True, False, False, False) +IDNA_2008_Strict = IDNA2008Codec(False, False, False, True) +IDNA_2008_Transitional = IDNA2008Codec(True, True, False, False) IDNA_2008 = IDNA_2008_Practical def _escapify(label, unicode_mode=False): @@ -241,9 +273,14 @@ def _escapify(label, unicode_mode=False): def _validate_labels(labels): """Check for empty labels in the middle of a label sequence, labels that are too long, and for too many labels. - @raises NameTooLong: the name as a whole is too long - @raises EmptyLabel: a label is empty (i.e. the root label) and appears - in a position other than the end of the label sequence""" + + Raises ``dns.name.NameTooLong`` if the name as a whole is too long. + + Raises ``dns.name.EmptyLabel`` if a label is empty (i.e. the root + label) and appears in a position other than the end of the label + sequence + + """ l = len(labels) total = 0 @@ -263,7 +300,12 @@ def _validate_labels(labels): raise EmptyLabel -def _ensure_bytes(label): +def _maybe_convert_to_binary(label): + """If label is ``text``, convert it to ``binary``. If it is already + ``binary`` just return it. + + """ + if isinstance(label, binary_type): return label if isinstance(label, text_type): @@ -275,24 +317,23 @@ class Name(object): """A DNS name. - The dns.name.Name class represents a DNS name as a tuple of labels. - Instances of the class are immutable. - - @ivar labels: The tuple of labels in the name. Each label is a string of - up to 63 octets.""" + The dns.name.Name class represents a DNS name as a tuple of + labels. Each label is a `binary` in DNS wire format. Instances + of the class are immutable. + """ __slots__ = ['labels'] def __init__(self, labels): - """Initialize a domain name from a list of labels. - @param labels: the labels - @type labels: any iterable whose values are strings + """*labels* is any iterable whose values are ``text`` or ``binary``. """ - labels = [_ensure_bytes(x) for x in labels] + + labels = [_maybe_convert_to_binary(x) for x in labels] super(Name, self).__setattr__('labels', tuple(labels)) _validate_labels(self.labels) def __setattr__(self, name, value): + # Names are immutable raise TypeError("object doesn't support attribute assignment") def __copy__(self): @@ -302,6 +343,7 @@ class Name(object): return Name(copy.deepcopy(self.labels, memo)) def __getstate__(self): + # Names can be pickled return {'labels': self.labels} def __setstate__(self, state): @@ -310,21 +352,24 @@ class Name(object): def is_absolute(self): """Is the most significant label of this name the root label? - @rtype: bool + + Returns a ``bool``. """ return len(self.labels) > 0 and self.labels[-1] == b'' def is_wild(self): """Is this name wild? (I.e. Is the least significant label '*'?) - @rtype: bool + + Returns a ``bool``. """ return len(self.labels) > 0 and self.labels[0] == b'*' def __hash__(self): """Return a case-insensitive hash of the name. - @rtype: int + + Returns an ``int``. """ h = long(0) @@ -334,20 +379,35 @@ class Name(object): return int(h % maxint) def fullcompare(self, other): - """Compare two names, returning a 3-tuple (relation, order, nlabels). + """Compare two names, returning a 3-tuple + ``(relation, order, nlabels)``. - I{relation} describes the relation ship between the names, - and is one of: dns.name.NAMERELN_NONE, - dns.name.NAMERELN_SUPERDOMAIN, dns.name.NAMERELN_SUBDOMAIN, - dns.name.NAMERELN_EQUAL, or dns.name.NAMERELN_COMMONANCESTOR + *relation* describes the relation ship between the names, + and is one of: ``dns.name.NAMERELN_NONE``, + ``dns.name.NAMERELN_SUPERDOMAIN``, ``dns.name.NAMERELN_SUBDOMAIN``, + ``dns.name.NAMERELN_EQUAL``, or ``dns.name.NAMERELN_COMMONANCESTOR``. - I{order} is < 0 if self < other, > 0 if self > other, and == - 0 if self == other. A relative name is always less than an + *order* is < 0 if *self* < *other*, > 0 if *self* > *other*, and == + 0 if *self* == *other*. A relative name is always less than an absolute name. If both names have the same relativity, then the DNSSEC order relation is used to order them. - I{nlabels} is the number of significant labels that the two names + *nlabels* is the number of significant labels that the two names have in common. + + Here are some examples. Names ending in "." are absolute names, + those not ending in "." are relative names. + + ============= ============= =========== ===== ======= + self other relation order nlabels + ============= ============= =========== ===== ======= + www.example. www.example. equal 0 3 + www.example. example. subdomain > 0 2 + example. www.example. superdomain < 0 2 + example1.com. example2.com. common anc. < 0 2 + example1 example2. none < 0 0 + example1. example2 none > 0 0 + ============= ============= =========== ===== ======= """ sabs = self.is_absolute() @@ -397,8 +457,10 @@ class Name(object): def is_subdomain(self, other): """Is self a subdomain of other? - The notion of subdomain includes equality. - @rtype: bool + Note that the notion of subdomain includes equality, e.g. + "dnpython.org" is a subdomain of itself. + + Returns a ``bool``. """ (nr, o, nl) = self.fullcompare(other) @@ -409,8 +471,10 @@ class Name(object): def is_superdomain(self, other): """Is self a superdomain of other? - The notion of subdomain includes equality. - @rtype: bool + Note that the notion of superdomain includes equality, e.g. + "dnpython.org" is a superdomain of itself. + + Returns a ``bool``. """ (nr, o, nl) = self.fullcompare(other) @@ -421,7 +485,6 @@ class Name(object): def canonicalize(self): """Return a name which is equal to the current name, but is in DNSSEC canonical form. - @rtype: dns.name.Name object """ return Name([x.lower() for x in self.labels]) @@ -466,39 +529,45 @@ class Name(object): return '' def __str__(self): - return self.to_text(False).decode() + return self.to_text(False) def to_text(self, omit_final_dot=False): - """Convert name to text format. - @param omit_final_dot: If True, don't emit the final dot (denoting the - root label) for absolute names. The default is False. - @rtype: string + """Convert name to DNS text format. + + *omit_final_dot* is a ``bool``. If True, don't emit the final + dot (denoting the root label) for absolute names. The default + is False. + + Returns a ``text``. """ if len(self.labels) == 0: - return b'@' + return maybe_decode(b'@') if len(self.labels) == 1 and self.labels[0] == b'': - return b'.' + return maybe_decode(b'.') if omit_final_dot and self.is_absolute(): l = self.labels[:-1] else: l = self.labels s = b'.'.join(map(_escapify, l)) - return s + return maybe_decode(s) def to_unicode(self, omit_final_dot=False, idna_codec=None): """Convert name to Unicode text format. IDN ACE labels are converted to Unicode. - @param omit_final_dot: If True, don't emit the final dot (denoting the - root label) for absolute names. The default is False. - @type omit_final_dot: bool - @param: idna_codec: IDNA encoder/decoder. If None, the default IDNA - 2003 - encoder/decoder is used. - @type idna_codec: dns.name.IDNA - @rtype: string + *omit_final_dot* is a ``bool``. If True, don't emit the final + dot (denoting the root label) for absolute names. The default + is False. + *idna_codec* specifies the IDNA encoder/decoder. If None, the + dns.name.IDNA_2003_Practical encoder/decoder is used. + The IDNA_2003_Practical decoder does + not impose any policy, it just decodes punycode, so if you + don't want checking for compliance, you can use this decoder + for IDNA2008 as well. + + Returns a ``text``. """ if len(self.labels) == 0: @@ -510,21 +579,24 @@ class Name(object): else: l = self.labels if idna_codec is None: - idna_codec = IDNA_2003 + idna_codec = IDNA_2003_Practical return u'.'.join([idna_codec.decode(x) for x in l]) def to_digestable(self, origin=None): """Convert name to a format suitable for digesting in hashes. - The name is canonicalized and converted to uncompressed wire format. + The name is canonicalized and converted to uncompressed wire + format. All names in wire format are absolute. If the name + is a relative name, then an origin must be supplied. - @param origin: If the name is relative and origin is not None, then - origin will be appended to it. - @type origin: dns.name.Name object - @raises NeedAbsoluteNameOrOrigin: All names in wire format are - absolute. If self is a relative name, then an origin must be supplied; - if it is missing, then this exception is raised - @rtype: string + *origin* is a ``dns.name.Name`` or ``None``. If the name is + relative and origin is not ``None``, then origin will be appended + to the name. + + Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is + relative and no origin was provided. + + Returns a ``binary``. """ if not self.is_absolute(): @@ -541,19 +613,21 @@ class Name(object): def to_wire(self, file=None, compress=None, origin=None): """Convert name to wire format, possibly compressing it. - @param file: the file where the name is emitted (typically - a BytesIO file). If None, a string containing the wire name - will be returned. - @type file: file or None - @param compress: The compression table. If None (the default) names - will not be compressed. - @type compress: dict - @param origin: If the name is relative and origin is not None, then - origin will be appended to it. - @type origin: dns.name.Name object - @raises NeedAbsoluteNameOrOrigin: All names in wire format are - absolute. If self is a relative name, then an origin must be supplied; - if it is missing, then this exception is raised + *file* is the file where the name is emitted (typically a + BytesIO file). If ``None`` (the default), a ``binary`` + containing the wire name will be returned. + + *compress*, a ``dict``, is the compression table to use. If + ``None`` (the default), names will not be compressed. + + *origin* is a ``dns.name.Name`` or ``None``. If the name is + relative and origin is not ``None``, then *origin* will be appended + to it. + + Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is + relative and no origin was provided. + + Returns a ``binary`` or ``None``. """ if file is None: @@ -596,7 +670,8 @@ class Name(object): def __len__(self): """The length of the name (in labels). - @rtype: int + + Returns an ``int``. """ return len(self.labels) @@ -611,14 +686,14 @@ class Name(object): return self.relativize(other) def split(self, depth): - """Split a name into a prefix and suffix at depth. + """Split a name into a prefix and suffix names at the specified depth. - @param depth: the number of labels in the suffix - @type depth: int - @raises ValueError: the depth was not >= 0 and <= the length of the + *depth* is an ``int`` specifying the number of labels in the suffix + + Raises ``ValueError`` if *depth* was not >= 0 and <= the length of the name. - @returns: the tuple (prefix, suffix) - @rtype: tuple + + Returns the tuple ``(prefix, suffix)``. """ l = len(self.labels) @@ -633,9 +708,11 @@ class Name(object): def concatenate(self, other): """Return a new name which is the concatenation of self and other. - @rtype: dns.name.Name object - @raises AbsoluteConcatenation: self is absolute and other is - not the empty name + + Raises ``dns.name.AbsoluteConcatenation`` if the name is + absolute and *other* is not the empty name. + + Returns a ``dns.name.Name``. """ if self.is_absolute() and len(other) > 0: @@ -645,9 +722,14 @@ class Name(object): return Name(labels) def relativize(self, origin): - """If self is a subdomain of origin, return a new name which is self - relative to origin. Otherwise return self. - @rtype: dns.name.Name object + """If the name is a subdomain of *origin*, return a new name which is + the name relative to origin. Otherwise return the name. + + For example, relativizing ``www.dnspython.org.`` to origin + ``dnspython.org.`` returns the name ``www``. Relativizing ``example.`` + to origin ``dnspython.org.`` returns ``example.``. + + Returns a ``dns.name.Name``. """ if origin is not None and self.is_subdomain(origin): @@ -656,9 +738,14 @@ class Name(object): return self def derelativize(self, origin): - """If self is a relative name, return a new name which is the - concatenation of self and origin. Otherwise return self. - @rtype: dns.name.Name object + """If the name is a relative name, return a new name which is the + concatenation of the name and origin. Otherwise return the name. + + For example, derelativizing ``www`` to origin ``dnspython.org.`` + returns the name ``www.dnspython.org.``. Derelativizing ``example.`` + to origin ``dnspython.org.`` returns ``example.``. + + Returns a ``dns.name.Name``. """ if not self.is_absolute(): @@ -667,11 +754,14 @@ class Name(object): return self def choose_relativity(self, origin=None, relativize=True): - """Return a name with the relativity desired by the caller. If - origin is None, then self is returned. Otherwise, if - relativize is true the name is relativized, and if relativize is - false the name is derelativized. - @rtype: dns.name.Name object + """Return a name with the relativity desired by the caller. + + If *origin* is ``None``, then the name is returned. + Otherwise, if *relativize* is ``True`` the name is + relativized, and if *relativize* is ``False`` the name is + derelativized. + + Returns a ``dns.name.Name``. """ if origin: @@ -684,31 +774,41 @@ class Name(object): def parent(self): """Return the parent of the name. - @rtype: dns.name.Name object - @raises NoParent: the name is either the root name or the empty name, - and thus has no parent. + + For example, the parent of ``www.dnspython.org.`` is ``dnspython.org``. + + Raises ``dns.name.NoParent`` if the name is either the root name or the + empty name, and thus has no parent. + + Returns a ``dns.name.Name``. """ + if self == root or self == empty: raise NoParent return Name(self.labels[1:]) +#: The root name, '.' root = Name([b'']) -empty = Name([]) +#: The empty name. +empty = Name([]) def from_unicode(text, origin=root, idna_codec=None): """Convert unicode text into a Name object. - Labels are encoded in IDN ACE form. + Labels are encoded in IDN ACE form according to rules specified by + the IDNA codec. - @param text: The text to convert into a name. - @type text: Unicode string - @param origin: The origin to append to non-absolute names. - @type origin: dns.name.Name - @param: idna_codec: IDNA encoder/decoder. If None, the default IDNA 2003 - encoder/decoder is used. - @type idna_codec: dns.name.IDNA - @rtype: dns.name.Name object + *text*, a ``text``, is the text to convert into a name. + + *origin*, a ``dns.name.Name``, specifies the origin to + append to non-absolute names. The default is the root name. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + Returns a ``dns.name.Name``. """ if not isinstance(text, text_type): @@ -771,14 +871,16 @@ def from_unicode(text, origin=root, idna_codec=None): def from_text(text, origin=root, idna_codec=None): """Convert text into a Name object. - @param text: The text to convert into a name. - @type text: string - @param origin: The origin to append to non-absolute names. - @type origin: dns.name.Name - @param: idna_codec: IDNA encoder/decoder. If None, the default IDNA 2003 - encoder/decoder is used. - @type idna_codec: dns.name.IDNA - @rtype: dns.name.Name object + *text*, a ``text``, is the text to convert into a name. + + *origin*, a ``dns.name.Name``, specifies the origin to + append to non-absolute names. The default is the root name. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + Returns a ``dns.name.Name``. """ if isinstance(text, text_type): @@ -840,17 +942,21 @@ def from_text(text, origin=root, idna_codec=None): def from_wire(message, current): """Convert possibly compressed wire format into a Name. - @param message: the entire DNS message - @type message: string - @param current: the offset of the beginning of the name from the start - of the message - @type current: int - @raises dns.name.BadPointer: a compression pointer did not point backwards - in the message - @raises dns.name.BadLabelType: an invalid label type was encountered. - @returns: a tuple consisting of the name that was read and the number - of bytes of the wire format message which were consumed reading it - @rtype: (dns.name.Name object, int) tuple + + *message* is a ``binary`` containing an entire DNS message in DNS + wire form. + + *current*, an ``int``, is the offset of the beginning of the name + from the start of the message + + Raises ``dns.name.BadPointer`` if a compression pointer did not + point backwards in the message. + + Raises ``dns.name.BadLabelType`` if an invalid label type was encountered. + + Returns a ``(dns.name.Name, int)`` tuple consisting of the name + that was read and the number of bytes of the wire format message + which were consumed reading it. """ if not isinstance(message, binary_type): diff --git a/eventlet/support/dns/namedict.py b/eventlet/support/dns/namedict.py index 58e4034..2215af7 100644 --- a/eventlet/support/dns/namedict.py +++ b/eventlet/support/dns/namedict.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # Copyright (C) 2016 Coresec Systems AB # # Permission to use, copy, modify, and distribute this software and its @@ -31,20 +31,19 @@ from ._compat import xrange class NameDict(collections.MutableMapping): - """A dictionary whose keys are dns.name.Name objects. - @ivar max_depth: the maximum depth of the keys that have ever been - added to the dictionary. - @type max_depth: int - @ivar max_depth_items: the number of items of maximum depth - @type max_depth_items: int + + In addition to being like a regular Python dictionary, this + dictionary can also get the deepest match for a given key. """ __slots__ = ["max_depth", "max_depth_items", "__store"] def __init__(self, *args, **kwargs): self.__store = dict() + #: the maximum depth of the keys that have ever been added self.max_depth = 0 + #: the number of items of maximum depth self.max_depth_items = 0 self.update(dict(*args, **kwargs)) @@ -83,14 +82,16 @@ class NameDict(collections.MutableMapping): return key in self.__store def get_deepest_match(self, name): - """Find the deepest match to I{name} in the dictionary. + """Find the deepest match to *fname* in the dictionary. The deepest match is the longest name in the dictionary which is - a superdomain of I{name}. + a superdomain of *name*. Note that *superdomain* includes matching + *name* itself. - @param name: the name - @type name: dns.name.Name object - @rtype: (key, value) tuple + *name*, a ``dns.name.Name``, the name to find. + + Returns a ``(key, value)`` where *key* is the deepest + ``dns.name.Name``, and *value* is the value associated with *key*. """ depth = len(name) diff --git a/eventlet/support/dns/node.py b/eventlet/support/dns/node.py index 7c25060..cc507b5 100644 --- a/eventlet/support/dns/node.py +++ b/eventlet/support/dns/node.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -24,19 +24,12 @@ import dns.renderer class Node(object): - """A DNS node. - - A node is a set of rdatasets - - @ivar rdatasets: the node's rdatasets - @type rdatasets: list of dns.rdataset.Rdataset objects""" + """A Node is a set of rdatasets.""" __slots__ = ['rdatasets'] def __init__(self): - """Initialize a DNS node. - """ - + #: the set of rdatsets, represented as a list. self.rdatasets = [] def to_text(self, name, **kw): @@ -44,9 +37,10 @@ class Node(object): Each rdataset at the node is printed. Any keyword arguments to this method are passed on to the rdataset's to_text() method. - @param name: the owner name of the rdatasets - @type name: dns.name.Name object - @rtype: string + + *name*, a ``dns.name.Name``, the owner name of the rdatasets. + + Returns a ``text``. """ s = StringIO() @@ -60,10 +54,6 @@ class Node(object): return '' def __eq__(self, other): - """Two nodes are equal if they have the same rdatasets. - - @rtype: bool - """ # # This is inefficient. Good thing we don't need to do it much. # @@ -89,11 +79,11 @@ class Node(object): """Find an rdataset matching the specified properties in the current node. - @param rdclass: The class of the rdataset - @type rdclass: int - @param rdtype: The type of the rdataset - @type rdtype: int - @param covers: The covered type. Usually this value is + *rdclass*, an ``int``, the class of the rdataset. + + *rdtype*, an ``int``, the type of the rdataset. + + *covers*, an ``int``, the covered type. Usually this value is dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or dns.rdatatype.RRSIG, then the covers value will be the rdata type the SIG/RRSIG covers. The library treats the SIG and RRSIG @@ -101,12 +91,13 @@ class Node(object): types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This makes RRSIGs much easier to work with than if RRSIGs covering different rdata types were aggregated into a single RRSIG rdataset. - @type covers: int - @param create: If True, create the rdataset if it is not found. - @type create: bool - @raises KeyError: An rdataset of the desired type and class does - not exist and I{create} is not True. - @rtype: dns.rdataset.Rdataset object + + *create*, a ``bool``. If True, create the rdataset if it is not found. + + Raises ``KeyError`` if an rdataset of the desired type and class does + not exist and *create* is not ``True``. + + Returns a ``dns.rdataset.Rdataset``. """ for rds in self.rdatasets: @@ -124,17 +115,24 @@ class Node(object): current node. None is returned if an rdataset of the specified type and - class does not exist and I{create} is not True. + class does not exist and *create* is not ``True``. - @param rdclass: The class of the rdataset - @type rdclass: int - @param rdtype: The type of the rdataset - @type rdtype: int - @param covers: The covered type. - @type covers: int - @param create: If True, create the rdataset if it is not found. - @type create: bool - @rtype: dns.rdataset.Rdataset object or None + *rdclass*, an ``int``, the class of the rdataset. + + *rdtype*, an ``int``, the type of the rdataset. + + *covers*, an ``int``, the covered type. Usually this value is + dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or + dns.rdatatype.RRSIG, then the covers value will be the rdata + type the SIG/RRSIG covers. The library treats the SIG and RRSIG + types as if they were a family of + types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This makes RRSIGs much + easier to work with than if RRSIGs covering different rdata + types were aggregated into a single RRSIG rdataset. + + *create*, a ``bool``. If True, create the rdataset if it is not found. + + Returns a ``dns.rdataset.Rdataset`` or ``None``. """ try: @@ -149,12 +147,11 @@ class Node(object): If a matching rdataset does not exist, it is not an error. - @param rdclass: The class of the rdataset - @type rdclass: int - @param rdtype: The type of the rdataset - @type rdtype: int - @param covers: The covered type. - @type covers: int + *rdclass*, an ``int``, the class of the rdataset. + + *rdtype*, an ``int``, the type of the rdataset. + + *covers*, an ``int``, the covered type. """ rds = self.get_rdataset(rdclass, rdtype, covers) @@ -164,11 +161,16 @@ class Node(object): def replace_rdataset(self, replacement): """Replace an rdataset. - It is not an error if there is no rdataset matching I{replacement}. + It is not an error if there is no rdataset matching *replacement*. - Ownership of the I{replacement} object is transferred to the node; - in other words, this method does not store a copy of I{replacement} - at the node, it stores I{replacement} itself. + Ownership of the *replacement* object is transferred to the node; + in other words, this method does not store a copy of *replacement* + at the node, it stores *replacement* itself. + + *replacement*, a ``dns.rdataset.Rdataset``. + + Raises ``ValueError`` if *replacement* is not a + ``dns.rdataset.Rdataset``. """ if not isinstance(replacement, dns.rdataset.Rdataset): diff --git a/eventlet/support/dns/opcode.py b/eventlet/support/dns/opcode.py index 70d704f..5dcd2ab 100644 --- a/eventlet/support/dns/opcode.py +++ b/eventlet/support/dns/opcode.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -17,10 +17,15 @@ import dns.exception +#: Query QUERY = 0 +#: Inverse Query (historical) IQUERY = 1 +#: Server Status (unspecified and unimplemented anywhere) STATUS = 2 +#: Notify NOTIFY = 4 +#: Dynamic Update UPDATE = 5 _by_text = { @@ -39,17 +44,17 @@ _by_value = dict((y, x) for x, y in _by_text.items()) class UnknownOpcode(dns.exception.DNSException): - """An DNS opcode is unknown.""" def from_text(text): """Convert text into an opcode. - @param text: the textual opcode - @type text: string - @raises UnknownOpcode: the opcode is unknown - @rtype: int + *text*, a ``text``, the textual opcode + + Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown. + + Returns an ``int``. """ if text.isdigit(): @@ -65,8 +70,9 @@ def from_text(text): def from_flags(flags): """Extract an opcode from DNS message flags. - @param flags: int - @rtype: int + *flags*, an ``int``, the DNS flags. + + Returns an ``int``. """ return (flags & 0x7800) >> 11 @@ -75,7 +81,10 @@ def from_flags(flags): def to_flags(value): """Convert an opcode to a value suitable for ORing into DNS message flags. - @rtype: int + + *value*, an ``int``, the DNS opcode value. + + Returns an ``int``. """ return (value << 11) & 0x7800 @@ -84,10 +93,11 @@ def to_flags(value): def to_text(value): """Convert an opcode to text. - @param value: the opcdoe - @type value: int - @raises UnknownOpcode: the opcode is unknown - @rtype: string + *value*, an ``int`` the opcode value, + + Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown. + + Returns a ``text``. """ text = _by_value.get(value) @@ -97,11 +107,11 @@ def to_text(value): def is_update(flags): - """True if the opcode in flags is UPDATE. + """Is the opcode in flags UPDATE? - @param flags: DNS flags - @type flags: int - @rtype: bool + *flags*, an ``int``, the DNS message flags. + + Returns a ``bool``. """ return from_flags(flags) == UPDATE diff --git a/eventlet/support/dns/query.py b/eventlet/support/dns/query.py index bfecd43..f1f656e 100644 --- a/eventlet/support/dns/query.py +++ b/eventlet/support/dns/query.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -28,6 +28,7 @@ import dns.exception import dns.inet import dns.name import dns.message +import dns.rcode import dns.rdataclass import dns.rdatatype from ._compat import long, string_types @@ -42,34 +43,36 @@ else: socket_factory = socket.socket class UnexpectedSource(dns.exception.DNSException): - """A DNS query response came from an unexpected address or port.""" class BadResponse(dns.exception.FormError): - """A DNS query response does not respond to the question asked.""" +class TransferError(dns.exception.DNSException): + """A zone transfer response got a non-zero rcode.""" + + def __init__(self, rcode): + message = 'Zone transfer error: %s' % dns.rcode.to_text(rcode) + super(TransferError, self).__init__(message) + self.rcode = rcode + + def _compute_expiration(timeout): if timeout is None: return None else: return time.time() + timeout +# This module can use either poll() or select() as the "polling backend". +# +# A backend function takes an fd, bools for readability, writablity, and +# error detection, and a timeout. def _poll_for(fd, readable, writable, error, timeout): - """Poll polling backend. - @param fd: File descriptor - @type fd: int - @param readable: Whether to wait for readability - @type readable: bool - @param writable: Whether to wait for writability - @type writable: bool - @param timeout: Deadline timeout (expiration time, in seconds) - @type timeout: float - @return True on success, False on timeout - """ + """Poll polling backend.""" + event_mask = 0 if readable: event_mask |= select.POLLIN @@ -90,17 +93,8 @@ def _poll_for(fd, readable, writable, error, timeout): def _select_for(fd, readable, writable, error, timeout): - """Select polling backend. - @param fd: File descriptor - @type fd: int - @param readable: Whether to wait for readability - @type readable: bool - @param writable: Whether to wait for writability - @type writable: bool - @param timeout: Deadline timeout (expiration time, in seconds) - @type timeout: float - @return True on success, False on timeout - """ + """Select polling backend.""" + rset, wset, xset = [], [], [] if readable: @@ -119,6 +113,10 @@ def _select_for(fd, readable, writable, error, timeout): def _wait_for(fd, readable, writable, error, expiration): + # Use the selected polling backend to wait for any of the specified + # events. An "expiration" absolute time is converted into a relative + # timeout. + done = False while not done: if expiration is None: @@ -137,9 +135,8 @@ def _wait_for(fd, readable, writable, error, expiration): def _set_polling_backend(fn): - """ - Internal API. Do not use. - """ + # Internal API. Do not use. + global _polling_backend _polling_backend = fn @@ -165,8 +162,11 @@ def _addresses_equal(af, a1, a2): # Convert the first value of the tuple, which is a textual format # address into binary form, so that we are not confused by different # textual representations of the same address - n1 = dns.inet.inet_pton(af, a1[0]) - n2 = dns.inet.inet_pton(af, a2[0]) + try: + n1 = dns.inet.inet_pton(af, a1[0]) + n2 = dns.inet.inet_pton(af, a2[0]) + except dns.exception.SyntaxError: + return False return n1 == n2 and a1[1:] == a2[1:] @@ -193,68 +193,133 @@ def _destination_and_source(af, where, port, source, source_port): return (af, destination, source) +def send_udp(sock, what, destination, expiration=None): + """Send a DNS message to the specified UDP socket. + + *sock*, a ``socket``. + + *what*, a ``binary`` or ``dns.message.Message``, the message to send. + + *destination*, a destination tuple appropriate for the address family + of the socket, specifying where to send the query. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. + + Returns an ``(int, float)`` tuple of bytes sent and the sent time. + """ + + if isinstance(what, dns.message.Message): + what = what.to_wire() + _wait_for_writable(sock, expiration) + sent_time = time.time() + n = sock.sendto(what, destination) + return (n, sent_time) + + +def receive_udp(sock, destination, expiration=None, + ignore_unexpected=False, one_rr_per_rrset=False, + keyring=None, request_mac=b''): + """Read a DNS message from a UDP socket. + + *sock*, a ``socket``. + + *destination*, a destination tuple appropriate for the address family + of the socket, specifying where the associated query was sent. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. + + *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from + unexpected sources. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + *keyring*, a ``dict``, the keyring to use for TSIG. + + *request_mac*, a ``binary``, the MAC of the request (for TSIG). + + Raises if the message is malformed, if network errors occur, of if + there is a timeout. + + Returns a ``dns.message.Message`` object. + """ + + wire = b'' + while 1: + _wait_for_readable(sock, expiration) + (wire, from_address) = sock.recvfrom(65535) + if _addresses_equal(sock.family, from_address, destination) or \ + (dns.inet.is_multicast(destination[0]) and + from_address[1:] == destination[1:]): + break + if not ignore_unexpected: + raise UnexpectedSource('got a response from ' + '%s instead of %s' % (from_address, + destination)) + received_time = time.time() + r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac, + one_rr_per_rrset=one_rr_per_rrset) + return (r, received_time) + def udp(q, where, timeout=None, port=53, af=None, source=None, source_port=0, ignore_unexpected=False, one_rr_per_rrset=False): """Return the response obtained after sending a query via UDP. - @param q: the query - @type q: dns.message.Message - @param where: where to send the message - @type where: string containing an IPv4 or IPv6 address - @param timeout: The number of seconds to wait before the query times out. - If None, the default, wait forever. - @type timeout: float - @param port: The port to which to send the message. The default is 53. - @type port: int - @param af: the address family to use. The default is None, which - causes the address family to use to be inferred from the form of where. - If the inference attempt fails, AF_INET is used. - @type af: int - @rtype: dns.message.Message object - @param source: source address. The default is the wildcard address. - @type source: string - @param source_port: The port from which to send the message. + *q*, a ``dns.message.message``, the query to send + + *where*, a ``text`` containing an IPv4 or IPv6 address, where + to send the message. + + *timeout*, a ``float`` or ``None``, the number of seconds to wait before the + query times out. If ``None``, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 53. + + *af*, an ``int``, the address family to use. The default is ``None``, + which causes the address family to use to be inferred from the form of + *where*. If the inference attempt fails, AF_INET is used. This + parameter is historical; you need never set it. + + *source*, a ``text`` containing an IPv4 or IPv6 address, specifying + the source address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. The default is 0. - @type source_port: int - @param ignore_unexpected: If True, ignore responses from unexpected - sources. The default is False. - @type ignore_unexpected: bool - @param one_rr_per_rrset: Put each RR into its own RRset - @type one_rr_per_rrset: bool + + *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from + unexpected sources. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + Returns a ``dns.message.Message``. """ wire = q.to_wire() (af, destination, source) = _destination_and_source(af, where, port, source, source_port) s = socket_factory(af, socket.SOCK_DGRAM, 0) - begin_time = None + received_time = None + sent_time = None try: expiration = _compute_expiration(timeout) s.setblocking(0) if source is not None: s.bind(source) - _wait_for_writable(s, expiration) - begin_time = time.time() - s.sendto(wire, destination) - while 1: - _wait_for_readable(s, expiration) - (wire, from_address) = s.recvfrom(65535) - if _addresses_equal(af, from_address, destination) or \ - (dns.inet.is_multicast(where) and - from_address[1:] == destination[1:]): - break - if not ignore_unexpected: - raise UnexpectedSource('got a response from ' - '%s instead of %s' % (from_address, - destination)) + (_, sent_time) = send_udp(s, wire, destination, expiration) + (r, received_time) = receive_udp(s, destination, expiration, + ignore_unexpected, one_rr_per_rrset, + q.keyring, q.mac) finally: - if begin_time is None: + if sent_time is None or received_time is None: response_time = 0 else: - response_time = time.time() - begin_time + response_time = received_time - sent_time s.close() - r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac, - one_rr_per_rrset=one_rr_per_rrset) r.time = response_time if not q.is_response(r): raise BadResponse @@ -290,6 +355,63 @@ def _net_write(sock, data, expiration): current += sock.send(data[current:]) +def send_tcp(sock, what, expiration=None): + """Send a DNS message to the specified TCP socket. + + *sock*, a ``socket``. + + *what*, a ``binary`` or ``dns.message.Message``, the message to send. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. + + Returns an ``(int, float)`` tuple of bytes sent and the sent time. + """ + + if isinstance(what, dns.message.Message): + what = what.to_wire() + l = len(what) + # copying the wire into tcpmsg is inefficient, but lets us + # avoid writev() or doing a short write that would get pushed + # onto the net + tcpmsg = struct.pack("!H", l) + what + _wait_for_writable(sock, expiration) + sent_time = time.time() + _net_write(sock, tcpmsg, expiration) + return (len(tcpmsg), sent_time) + +def receive_tcp(sock, expiration=None, one_rr_per_rrset=False, + keyring=None, request_mac=b''): + """Read a DNS message from a TCP socket. + + *sock*, a ``socket``. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + *keyring*, a ``dict``, the keyring to use for TSIG. + + *request_mac*, a ``binary``, the MAC of the request (for TSIG). + + Raises if the message is malformed, if network errors occur, of if + there is a timeout. + + Returns a ``dns.message.Message`` object. + """ + + ldata = _net_read(sock, 2, expiration) + (l,) = struct.unpack("!H", ldata) + wire = _net_read(sock, l, expiration) + received_time = time.time() + r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac, + one_rr_per_rrset=one_rr_per_rrset) + return (r, received_time) + def _connect(s, address): try: s.connect(address) @@ -308,27 +430,31 @@ def tcp(q, where, timeout=None, port=53, af=None, source=None, source_port=0, one_rr_per_rrset=False): """Return the response obtained after sending a query via TCP. - @param q: the query - @type q: dns.message.Message object - @param where: where to send the message - @type where: string containing an IPv4 or IPv6 address - @param timeout: The number of seconds to wait before the query times out. - If None, the default, wait forever. - @type timeout: float - @param port: The port to which to send the message. The default is 53. - @type port: int - @param af: the address family to use. The default is None, which - causes the address family to use to be inferred from the form of where. - If the inference attempt fails, AF_INET is used. - @type af: int - @rtype: dns.message.Message object - @param source: source address. The default is the wildcard address. - @type source: string - @param source_port: The port from which to send the message. + *q*, a ``dns.message.message``, the query to send + + *where*, a ``text`` containing an IPv4 or IPv6 address, where + to send the message. + + *timeout*, a ``float`` or ``None``, the number of seconds to wait before the + query times out. If ``None``, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 53. + + *af*, an ``int``, the address family to use. The default is ``None``, + which causes the address family to use to be inferred from the form of + *where*. If the inference attempt fails, AF_INET is used. This + parameter is historical; you need never set it. + + *source*, a ``text`` containing an IPv4 or IPv6 address, specifying + the source address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. The default is 0. - @type source_port: int - @param one_rr_per_rrset: Put each RR into its own RRset - @type one_rr_per_rrset: bool + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + Returns a ``dns.message.Message``. """ wire = q.to_wire() @@ -336,6 +462,7 @@ def tcp(q, where, timeout=None, port=53, af=None, source=None, source_port=0, source, source_port) s = socket_factory(af, socket.SOCK_STREAM, 0) begin_time = None + received_time = None try: expiration = _compute_expiration(timeout) s.setblocking(0) @@ -343,25 +470,15 @@ def tcp(q, where, timeout=None, port=53, af=None, source=None, source_port=0, if source is not None: s.bind(source) _connect(s, destination) - - l = len(wire) - - # copying the wire into tcpmsg is inefficient, but lets us - # avoid writev() or doing a short write that would get pushed - # onto the net - tcpmsg = struct.pack("!H", l) + wire - _net_write(s, tcpmsg, expiration) - ldata = _net_read(s, 2, expiration) - (l,) = struct.unpack("!H", ldata) - wire = _net_read(s, l, expiration) + send_tcp(s, wire, expiration) + (r, received_time) = receive_tcp(s, expiration, one_rr_per_rrset, + q.keyring, q.mac) finally: - if begin_time is None: + if begin_time is None or received_time is None: response_time = 0 else: - response_time = time.time() - begin_time + response_time = received_time - begin_time s.close() - r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac, - one_rr_per_rrset=one_rr_per_rrset) r.time = response_time if not q.is_response(r): raise BadResponse @@ -374,51 +491,59 @@ def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN, use_udp=False, keyalgorithm=dns.tsig.default_algorithm): """Return a generator for the responses to a zone transfer. - @param where: where to send the message - @type where: string containing an IPv4 or IPv6 address - @param zone: The name of the zone to transfer - @type zone: dns.name.Name object or string - @param rdtype: The type of zone transfer. The default is - dns.rdatatype.AXFR. - @type rdtype: int or string - @param rdclass: The class of the zone transfer. The default is - dns.rdataclass.IN. - @type rdclass: int or string - @param timeout: The number of seconds to wait for each response message. - If None, the default, wait forever. - @type timeout: float - @param port: The port to which to send the message. The default is 53. - @type port: int - @param keyring: The TSIG keyring to use - @type keyring: dict - @param keyname: The name of the TSIG key to use - @type keyname: dns.name.Name object or string - @param relativize: If True, all names in the zone will be relativized to - the zone origin. It is essential that the relativize setting matches - the one specified to dns.zone.from_xfr(). - @type relativize: bool - @param af: the address family to use. The default is None, which - causes the address family to use to be inferred from the form of where. - If the inference attempt fails, AF_INET is used. - @type af: int - @param lifetime: The total number of seconds to spend doing the transfer. - If None, the default, then there is no limit on the time the transfer may - take. - @type lifetime: float - @rtype: generator of dns.message.Message objects. - @param source: source address. The default is the wildcard address. - @type source: string - @param source_port: The port from which to send the message. + *where*. If the inference attempt fails, AF_INET is used. This + parameter is historical; you need never set it. + + *zone*, a ``dns.name.Name`` or ``text``, the name of the zone to transfer. + + *rdtype*, an ``int`` or ``text``, the type of zone transfer. The + default is ``dns.rdatatype.AXFR``. ``dns.rdatatype.IXFR`` can be + used to do an incremental transfer instead. + + *rdclass*, an ``int`` or ``text``, the class of the zone transfer. + The default is ``dns.rdataclass.IN``. + + *timeout*, a ``float``, the number of seconds to wait for each + response message. If None, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 53. + + *keyring*, a ``dict``, the keyring to use for TSIG. + + *keyname*, a ``dns.name.Name`` or ``text``, the name of the TSIG + key to use. + + *relativize*, a ``bool``. If ``True``, all names in the zone will be + relativized to the zone origin. It is essential that the + relativize setting matches the one specified to + ``dns.zone.from_xfr()`` if using this generator to make a zone. + + *af*, an ``int``, the address family to use. The default is ``None``, + which causes the address family to use to be inferred from the form of + *where*. If the inference attempt fails, AF_INET is used. This + parameter is historical; you need never set it. + + *lifetime*, a ``float``, the total number of seconds to spend + doing the transfer. If ``None``, the default, then there is no + limit on the time the transfer may take. + + *source*, a ``text`` containing an IPv4 or IPv6 address, specifying + the source address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. The default is 0. - @type source_port: int - @param serial: The SOA serial number to use as the base for an IXFR diff - sequence (only meaningful if rdtype == dns.rdatatype.IXFR). - @type serial: int - @param use_udp: Use UDP (only meaningful for IXFR) - @type use_udp: bool - @param keyalgorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm - @type keyalgorithm: string + + *serial*, an ``int``, the SOA serial number to use as the base for + an IXFR diff sequence (only meaningful if *rdtype* is + ``dns.rdatatype.IXFR``). + + *use_udp*, a ``bool``. If ``True``, use UDP (only meaningful for IXFR). + + *keyalgorithm*, a ``dns.name.Name`` or ``text``, the TSIG algorithm to use. + + Raises on errors, and so does the generator. + + Returns a generator of ``dns.message.Message`` objects. """ if isinstance(zone, string_types): @@ -481,6 +606,9 @@ def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN, xfr=True, origin=origin, tsig_ctx=tsig_ctx, multi=True, first=first, one_rr_per_rrset=is_ixfr) + rcode = r.rcode() + if rcode != dns.rcode.NOERROR: + raise TransferError(rcode) tsig_ctx = r.tsig_ctx first = False answer_index = 0 diff --git a/eventlet/support/dns/rcode.py b/eventlet/support/dns/rcode.py index 314815f..eb4d1d6 100644 --- a/eventlet/support/dns/rcode.py +++ b/eventlet/support/dns/rcode.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -18,18 +18,29 @@ import dns.exception from ._compat import long - +#: No error NOERROR = 0 +#: Form error FORMERR = 1 +#: Server failure SERVFAIL = 2 +#: Name does not exist ("Name Error" in RFC 1025 terminology). NXDOMAIN = 3 +#: Not implemented NOTIMP = 4 +#: Refused REFUSED = 5 +#: Name exists. YXDOMAIN = 6 +#: RRset exists. YXRRSET = 7 +#: RRset does not exist. NXRRSET = 8 +#: Not authoritative. NOTAUTH = 9 +#: Name not in zone. NOTZONE = 10 +#: Bad EDNS version. BADVERS = 16 _by_text = { @@ -55,17 +66,17 @@ _by_value = dict((y, x) for x, y in _by_text.items()) class UnknownRcode(dns.exception.DNSException): - """A DNS rcode is unknown.""" def from_text(text): """Convert text into an rcode. - @param text: the textual rcode - @type text: string - @raises UnknownRcode: the rcode is unknown - @rtype: int + *text*, a ``text``, the textual rcode or an integer in textual form. + + Raises ``dns.rcode.UnknownRcode`` if the rcode mnemonic is unknown. + + Returns an ``int``. """ if text.isdigit(): @@ -81,12 +92,13 @@ def from_text(text): def from_flags(flags, ednsflags): """Return the rcode value encoded by flags and ednsflags. - @param flags: the DNS flags - @type flags: int - @param ednsflags: the EDNS flags - @type ednsflags: int - @raises ValueError: rcode is < 0 or > 4095 - @rtype: int + *flags*, an ``int``, the DNS flags field. + + *ednsflags*, an ``int``, the EDNS flags field. + + Raises ``ValueError`` if rcode is < 0 or > 4095 + + Returns an ``int``. """ value = (flags & 0x000f) | ((ednsflags >> 20) & 0xff0) @@ -98,10 +110,11 @@ def from_flags(flags, ednsflags): def to_flags(value): """Return a (flags, ednsflags) tuple which encodes the rcode. - @param value: the rcode - @type value: int - @raises ValueError: rcode is < 0 or > 4095 - @rtype: (int, int) tuple + *value*, an ``int``, the rcode. + + Raises ``ValueError`` if rcode is < 0 or > 4095. + + Returns an ``(int, int)`` tuple. """ if value < 0 or value > 4095: @@ -114,11 +127,15 @@ def to_flags(value): def to_text(value): """Convert rcode into text. - @param value: the rcode - @type value: int - @rtype: string + *value*, and ``int``, the rcode. + + Raises ``ValueError`` if rcode is < 0 or > 4095. + + Returns a ``text``. """ + if value < 0 or value > 4095: + raise ValueError('rcode must be >= 0 and <= 4095') text = _by_value.get(value) if text is None: text = str(value) diff --git a/eventlet/support/dns/rdata.py b/eventlet/support/dns/rdata.py index 9e9344d..5c6c34d 100644 --- a/eventlet/support/dns/rdata.py +++ b/eventlet/support/dns/rdata.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -13,17 +13,7 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""DNS rdata. - -@var _rdata_modules: A dictionary mapping a (rdclass, rdtype) tuple to -the module which implements that type. -@type _rdata_modules: dict -@var _module_prefix: The prefix to use when forming modules names. The -default is 'dns.rdtypes'. Changing this value will break the library. -@type _module_prefix: string -@var _hex_chunk: At most this many octets that will be represented in each -chunk of hexstring that _hexify() produces before whitespace occurs. -@type _hex_chunk: int""" +"""DNS rdata.""" from io import BytesIO import base64 @@ -42,12 +32,7 @@ _hex_chunksize = 32 def _hexify(data, chunksize=_hex_chunksize): """Convert a binary string into its hex encoding, broken up into chunks - of I{chunksize} characters separated by a space. - - @param data: the binary string - @type data: string - @param chunksize: the chunk size. Default is L{dns.rdata._hex_chunksize} - @rtype: string + of chunksize characters separated by a space. """ line = binascii.hexlify(data) @@ -60,13 +45,7 @@ _base64_chunksize = 32 def _base64ify(data, chunksize=_base64_chunksize): """Convert a binary string into its base64 encoding, broken up into chunks - of I{chunksize} characters separated by a space. - - @param data: the binary string - @type data: string - @param chunksize: the chunk size. Default is - L{dns.rdata._base64_chunksize} - @rtype: string + of chunksize characters separated by a space. """ line = base64.b64encode(data) @@ -77,13 +56,7 @@ def _base64ify(data, chunksize=_base64_chunksize): __escaped = bytearray(b'"\\') def _escapify(qstring): - """Escape the characters in a quoted string which need it. - - @param qstring: the string - @type qstring: string - @returns: the escaped string - @rtype: string - """ + """Escape the characters in a quoted string which need it.""" if isinstance(qstring, text_type): qstring = qstring.encode() @@ -104,10 +77,6 @@ def _escapify(qstring): def _truncate_bitmap(what): """Determine the index of greatest byte that isn't all zeros, and return the bitmap that contains all the bytes less than that index. - - @param what: a string of octets representing a bitmap. - @type what: string - @rtype: string """ for i in xrange(len(what) - 1, -1, -1): @@ -117,30 +86,30 @@ def _truncate_bitmap(what): class Rdata(object): - - """Base class for all DNS rdata types. - """ + """Base class for all DNS rdata types.""" __slots__ = ['rdclass', 'rdtype'] def __init__(self, rdclass, rdtype): """Initialize an rdata. - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int + + *rdclass*, an ``int`` is the rdataclass of the Rdata. + *rdtype*, an ``int`` is the rdatatype of the Rdata. """ self.rdclass = rdclass self.rdtype = rdtype def covers(self): - """DNS SIG/RRSIG rdatas apply to a specific type; this type is + """Return the type a Rdata covers. + + DNS SIG/RRSIG rdatas apply to a specific type; this type is returned by the covers() function. If the rdata type is not SIG or RRSIG, dns.rdatatype.NONE is returned. This is useful when creating rdatasets, allowing the rdataset to contain only RRSIGs of a particular type, e.g. RRSIG(NS). - @rtype: int + + Returns an ``int``. """ return dns.rdatatype.NONE @@ -149,37 +118,52 @@ class Rdata(object): """Return a 32-bit type value, the least significant 16 bits of which are the ordinary DNS type, and the upper 16 bits of which are the "covered" type, if any. - @rtype: int + + Returns an ``int``. """ return self.covers() << 16 | self.rdtype def to_text(self, origin=None, relativize=True, **kw): """Convert an rdata to text format. - @rtype: string + + Returns a ``text``. """ + raise NotImplementedError def to_wire(self, file, compress=None, origin=None): """Convert an rdata to wire format. - @rtype: string + + Returns a ``binary``. """ raise NotImplementedError def to_digestable(self, origin=None): """Convert rdata to a format suitable for digesting in hashes. This - is also the DNSSEC canonical form.""" + is also the DNSSEC canonical form. + + Returns a ``binary``. + """ + f = BytesIO() self.to_wire(f, None, origin) return f.getvalue() def validate(self): """Check that the current contents of the rdata's fields are - valid. If you change an rdata by assigning to its fields, + valid. + + If you change an rdata by assigning to its fields, it is a good idea to call validate() when you are done making changes. + + Raises various exceptions if there are problems. + + Returns ``None``. """ + dns.rdata.from_text(self.rdclass, self.rdtype, self.to_text()) def __repr__(self): @@ -197,17 +181,20 @@ class Rdata(object): def _cmp(self, other): """Compare an rdata with another rdata of the same rdtype and - rdclass. Return < 0 if self < other in the DNSSEC ordering, - 0 if self == other, and > 0 if self > other. + rdclass. + + Return < 0 if self < other in the DNSSEC ordering, 0 if self + == other, and > 0 if self > other. + """ our = self.to_digestable(dns.name.root) their = other.to_digestable(dns.name.root) if our == their: return 0 - if our > their: + elif our > their: return 1 - - return -1 + else: + return -1 def __eq__(self, other): if not isinstance(other, Rdata): @@ -253,42 +240,10 @@ class Rdata(object): @classmethod def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): - """Build an rdata object from text format. - - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int - @param tok: The tokenizer - @type tok: dns.tokenizer.Tokenizer - @param origin: The origin to use for relative names - @type origin: dns.name.Name - @param relativize: should names be relativized? - @type relativize: bool - @rtype: dns.rdata.Rdata instance - """ - raise NotImplementedError @classmethod def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - """Build an rdata object from wire format - - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int - @param wire: The wire-format message - @type wire: string - @param current: The offset in wire of the beginning of the rdata. - @type current: int - @param rdlen: The length of the wire-format rdata - @type rdlen: int - @param origin: The origin to use for relative names - @type origin: dns.name.Name - @rtype: dns.rdata.Rdata instance - """ - raise NotImplementedError def choose_relativity(self, origin=None, relativize=True): @@ -301,7 +256,7 @@ class Rdata(object): class GenericRdata(Rdata): - """Generate Rdata Class + """Generic Rdata Class This class is used for rdata types for which we have no better implementation. It implements the DNS "unknown RRs" scheme. @@ -392,20 +347,23 @@ def from_text(rdclass, rdtype, tok, origin=None, relativize=True): Once a class is chosen, its from_text() class method is called with the parameters to this function. - If I{tok} is a string, then a tokenizer is created and the string + If *tok* is a ``text``, then a tokenizer is created and the string is used as its input. - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int - @param tok: The tokenizer or input text - @type tok: dns.tokenizer.Tokenizer or string - @param origin: The origin to use for relative names - @type origin: dns.name.Name - @param relativize: Should names be relativized? - @type relativize: bool - @rtype: dns.rdata.Rdata instance""" + *rdclass*, an ``int``, the rdataclass. + + *rdtype*, an ``int``, the rdatatype. + + *tok*, a ``dns.tokenizer.Tokenizer`` or a ``text``. + + *origin*, a ``dns.name.Name`` (or ``None``), the + origin to use for relative names. + + *relativize*, a ``bool``. If true, name will be relativized to + the specified origin. + + Returns an instance of the chosen Rdata subclass. + """ if isinstance(tok, string_types): tok = dns.tokenizer.Tokenizer(tok) @@ -439,19 +397,22 @@ def from_wire(rdclass, rdtype, wire, current, rdlen, origin=None): Once a class is chosen, its from_wire() class method is called with the parameters to this function. - @param rdclass: The rdata class - @type rdclass: int - @param rdtype: The rdata type - @type rdtype: int - @param wire: The wire-format message - @type wire: string - @param current: The offset in wire of the beginning of the rdata. - @type current: int - @param rdlen: The length of the wire-format rdata - @type rdlen: int - @param origin: The origin to use for relative names - @type origin: dns.name.Name - @rtype: dns.rdata.Rdata instance""" + *rdclass*, an ``int``, the rdataclass. + + *rdtype*, an ``int``, the rdatatype. + + *wire*, a ``binary``, the wire-format message. + + *current*, an ``int``, the offset in wire of the beginning of + the rdata. + + *rdlen*, an ``int``, the length of the wire-format rdata + + *origin*, a ``dns.name.Name`` (or ``None``). If not ``None``, + then names will be relativized to this origin. + + Returns an instance of the chosen Rdata subclass. + """ wire = dns.wiredata.maybe_wrap(wire) cls = get_rdata_class(rdclass, rdtype) diff --git a/eventlet/support/dns/rdataclass.py b/eventlet/support/dns/rdataclass.py index 17a4810..916dc61 100644 --- a/eventlet/support/dns/rdataclass.py +++ b/eventlet/support/dns/rdataclass.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -13,15 +13,7 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""DNS Rdata Classes. - -@var _by_text: The rdata class textual name to value mapping -@type _by_text: dict -@var _by_value: The rdata class value to textual name mapping -@type _by_value: dict -@var _metaclasses: If an rdataclass is a metaclass, there will be a mapping -whose key is the rdatatype value and whose value is True in this dictionary. -@type _metaclasses: dict""" +"""DNS Rdata Classes.""" import re @@ -67,17 +59,22 @@ _unknown_class_pattern = re.compile('CLASS([0-9]+)$', re.I) class UnknownRdataclass(dns.exception.DNSException): - """A DNS class is unknown.""" def from_text(text): """Convert text into a DNS rdata class value. - @param text: the text - @type text: string - @rtype: int - @raises dns.rdataclass.UnknownRdataclass: the class is unknown - @raises ValueError: the rdata class value is not >= 0 and <= 65535 + + The input text can be a defined DNS RR class mnemonic or + instance of the DNS generic class syntax. + + For example, "IN" and "CLASS1" will both result in a value of 1. + + Raises ``dns.rdatatype.UnknownRdataclass`` if the class is unknown. + + Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535. + + Returns an ``int``. """ value = _by_text.get(text.upper()) @@ -92,11 +89,14 @@ def from_text(text): def to_text(value): - """Convert a DNS rdata class to text. - @param value: the rdata class value - @type value: int - @rtype: string - @raises ValueError: the rdata class value is not >= 0 and <= 65535 + """Convert a DNS rdata type value to text. + + If the value has a known mnemonic, it will be used, otherwise the + DNS generic class syntax will be used. + + Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535. + + Returns a ``str``. """ if value < 0 or value > 65535: @@ -108,10 +108,12 @@ def to_text(value): def is_metaclass(rdclass): - """True if the class is a metaclass. - @param rdclass: the rdata class - @type rdclass: int - @rtype: bool""" + """True if the specified class is a metaclass. + + The currently defined metaclasses are ANY and NONE. + + *rdclass* is an ``int``. + """ if rdclass in _metaclasses: return True diff --git a/eventlet/support/dns/rdataset.py b/eventlet/support/dns/rdataset.py index db266f2..bd8cfab 100644 --- a/eventlet/support/dns/rdataset.py +++ b/eventlet/support/dns/rdataset.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -31,50 +31,37 @@ SimpleSet = dns.set.Set class DifferingCovers(dns.exception.DNSException): - """An attempt was made to add a DNS SIG/RRSIG whose covered type is not the same as that of the other rdatas in the rdataset.""" class IncompatibleTypes(dns.exception.DNSException): - """An attempt was made to add DNS RR data of an incompatible type.""" class Rdataset(dns.set.Set): - """A DNS rdataset. - - @ivar rdclass: The class of the rdataset - @type rdclass: int - @ivar rdtype: The type of the rdataset - @type rdtype: int - @ivar covers: The covered type. Usually this value is - dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or - dns.rdatatype.RRSIG, then the covers value will be the rdata - type the SIG/RRSIG covers. The library treats the SIG and RRSIG - types as if they were a family of - types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This makes RRSIGs much - easier to work with than if RRSIGs covering different rdata - types were aggregated into a single RRSIG rdataset. - @type covers: int - @ivar ttl: The DNS TTL (Time To Live) value - @type ttl: int - """ + """A DNS rdataset.""" __slots__ = ['rdclass', 'rdtype', 'covers', 'ttl'] - def __init__(self, rdclass, rdtype, covers=dns.rdatatype.NONE): + def __init__(self, rdclass, rdtype, covers=dns.rdatatype.NONE, ttl=0): """Create a new rdataset of the specified class and type. - @see: the description of the class instance variables for the - meaning of I{rdclass} and I{rdtype}""" + *rdclass*, an ``int``, the rdataclass. + + *rdtype*, an ``int``, the rdatatype. + + *covers*, an ``int``, the covered rdatatype. + + *ttl*, an ``int``, the TTL. + """ super(Rdataset, self).__init__() self.rdclass = rdclass self.rdtype = rdtype self.covers = covers - self.ttl = 0 + self.ttl = ttl def _clone(self): obj = super(Rdataset, self)._clone() @@ -85,11 +72,14 @@ class Rdataset(dns.set.Set): return obj def update_ttl(self, ttl): - """Set the TTL of the rdataset to be the lesser of the set's current + """Perform TTL minimization. + + Set the TTL of the rdataset to be the lesser of the set's current TTL or the specified TTL. If the set contains no rdatas, set the TTL to the specified TTL. - @param ttl: The TTL - @type ttl: int""" + + *ttl*, an ``int``. + """ if len(self) == 0: self.ttl = ttl @@ -99,13 +89,19 @@ class Rdataset(dns.set.Set): def add(self, rd, ttl=None): """Add the specified rdata to the rdataset. - If the optional I{ttl} parameter is supplied, then - self.update_ttl(ttl) will be called prior to adding the rdata. + If the optional *ttl* parameter is supplied, then + ``self.update_ttl(ttl)`` will be called prior to adding the rdata. - @param rd: The rdata - @type rd: dns.rdata.Rdata object - @param ttl: The TTL - @type ttl: int""" + *rd*, a ``dns.rdata.Rdata``, the rdata + + *ttl*, an ``int``, the TTL. + + Raises ``dns.rdataset.IncompatibleTypes`` if the type and class + do not match the type and class of the rdataset. + + Raises ``dns.rdataset.DifferingCovers`` if the type is a signature + type and the covered type does not match that of the rdataset. + """ # # If we're adding a signature, do some special handling to @@ -139,8 +135,9 @@ class Rdataset(dns.set.Set): def update(self, other): """Add all rdatas in other to self. - @param other: The rdataset from which to update - @type other: dns.rdataset.Rdataset object""" + *other*, a ``dns.rdataset.Rdataset``, the rdataset from which + to update. + """ self.update_ttl(other.ttl) super(Rdataset, self).update(other) @@ -157,10 +154,6 @@ class Rdataset(dns.set.Set): return self.to_text() def __eq__(self, other): - """Two rdatasets are equal if they have the same class, type, and - covers, and contain the same rdata. - @rtype: bool""" - if not isinstance(other, Rdataset): return False if self.rdclass != other.rdclass or \ @@ -176,20 +169,23 @@ class Rdataset(dns.set.Set): override_rdclass=None, **kw): """Convert the rdataset into DNS master file format. - @see: L{dns.name.Name.choose_relativity} for more information - on how I{origin} and I{relativize} determine the way names + See ``dns.name.Name.choose_relativity`` for more information + on how *origin* and *relativize* determine the way names are emitted. Any additional keyword arguments are passed on to the rdata - to_text() method. + ``to_text()`` method. + + *name*, a ``dns.name.Name``. If name is not ``None``, emit RRs with + *name* as the owner name. + + *origin*, a ``dns.name.Name`` or ``None``, the origin for relative + names. + + *relativize*, a ``bool``. If ``True``, names will be relativized + to *origin*. + """ - @param name: If name is not None, emit a RRs with I{name} as - the owner name. - @type name: dns.name.Name object - @param origin: The origin for relative names, or None. - @type origin: dns.name.Name object - @param relativize: True if names should names be relativized - @type relativize: bool""" if name is not None: name = name.choose_relativity(origin, relativize) ntext = str(name) @@ -227,16 +223,26 @@ class Rdataset(dns.set.Set): override_rdclass=None, want_shuffle=True): """Convert the rdataset to wire format. - @param name: The owner name of the RRset that will be emitted - @type name: dns.name.Name object - @param file: The file to which the wire format data will be appended - @type file: file - @param compress: The compression table to use; the default is None. - @type compress: dict - @param origin: The origin to be appended to any relative names when - they are emitted. The default is None. - @returns: the number of records emitted - @rtype: int + *name*, a ``dns.name.Name`` is the owner name to use. + + *file* is the file where the name is emitted (typically a + BytesIO file). + + *compress*, a ``dict``, is the compression table to use. If + ``None`` (the default), names will not be compressed. + + *origin* is a ``dns.name.Name`` or ``None``. If the name is + relative and origin is not ``None``, then *origin* will be appended + to it. + + *override_rdclass*, an ``int``, is used as the class instead of the + class of the rdataset. This is useful when rendering rdatasets + associated with dynamic updates. + + *want_shuffle*, a ``bool``. If ``True``, then the order of the + Rdatas within the Rdataset will be shuffled before rendering. + + Returns an ``int``, the number of records emitted. """ if override_rdclass is not None: @@ -272,8 +278,9 @@ class Rdataset(dns.set.Set): return len(self) def match(self, rdclass, rdtype, covers): - """Returns True if this rdataset matches the specified class, type, - and covers""" + """Returns ``True`` if this rdataset matches the specified class, + type, and covers. + """ if self.rdclass == rdclass and \ self.rdtype == rdtype and \ self.covers == covers: @@ -285,7 +292,7 @@ def from_text_list(rdclass, rdtype, ttl, text_rdatas): """Create an rdataset with the specified class, type, and TTL, and with the specified list of rdatas in text format. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset`` object. """ if isinstance(rdclass, string_types): @@ -304,7 +311,7 @@ def from_text(rdclass, rdtype, ttl, *text_rdatas): """Create an rdataset with the specified class, type, and TTL, and with the specified rdatas in text format. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset`` object. """ return from_text_list(rdclass, rdtype, ttl, text_rdatas) @@ -314,7 +321,7 @@ def from_rdata_list(ttl, rdatas): """Create an rdataset with the specified TTL, and with the specified list of rdata objects. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset`` object. """ if len(rdatas) == 0: @@ -332,7 +339,7 @@ def from_rdata(ttl, *rdatas): """Create an rdataset with the specified TTL, and with the specified rdata objects. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset`` object. """ return from_rdata_list(ttl, rdatas) diff --git a/eventlet/support/dns/rdatatype.py b/eventlet/support/dns/rdatatype.py index 15284f6..48d2aa6 100644 --- a/eventlet/support/dns/rdatatype.py +++ b/eventlet/support/dns/rdatatype.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -13,18 +13,7 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""DNS Rdata Types. - -@var _by_text: The rdata type textual name to value mapping -@type _by_text: dict -@var _by_value: The rdata type value to textual name mapping -@type _by_value: dict -@var _metatypes: If an rdatatype is a metatype, there will be a mapping -whose key is the rdatatype value and whose value is True in this dictionary. -@type _metatypes: dict -@var _singletons: If an rdatatype is a singleton, there will be a mapping -whose key is the rdatatype value and whose value is True in this dictionary. -@type _singletons: dict""" +"""DNS Rdata Types.""" import re @@ -82,6 +71,7 @@ TLSA = 52 HIP = 55 CDS = 59 CDNSKEY = 60 +OPENPGPKEY = 61 CSYNC = 62 SPF = 99 UNSPEC = 103 @@ -153,6 +143,7 @@ _by_text = { 'HIP': HIP, 'CDS': CDS, 'CDNSKEY': CDNSKEY, + 'OPENPGPKEY': OPENPGPKEY, 'CSYNC': CSYNC, 'SPF': SPF, 'UNSPEC': UNSPEC, @@ -178,7 +169,6 @@ _by_text = { _by_value = dict((y, x) for x, y in _by_text.items()) - _metatypes = { OPT: True } @@ -188,24 +178,30 @@ _singletons = { NXT: True, DNAME: True, NSEC: True, - # CNAME is technically a singleton, but we allow multiple CNAMEs. + CNAME: True, } _unknown_type_pattern = re.compile('TYPE([0-9]+)$', re.I) class UnknownRdatatype(dns.exception.DNSException): - """DNS resource record type is unknown.""" def from_text(text): """Convert text into a DNS rdata type value. - @param text: the text - @type text: string - @raises dns.rdatatype.UnknownRdatatype: the type is unknown - @raises ValueError: the rdata type value is not >= 0 and <= 65535 - @rtype: int""" + + The input text can be a defined DNS RR type mnemonic or + instance of the DNS generic type syntax. + + For example, "NS" and "TYPE2" will both result in a value of 2. + + Raises ``dns.rdatatype.UnknownRdatatype`` if the type is unknown. + + Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535. + + Returns an ``int``. + """ value = _by_text.get(text.upper()) if value is None: @@ -219,11 +215,15 @@ def from_text(text): def to_text(value): - """Convert a DNS rdata type to text. - @param value: the rdata type value - @type value: int - @raises ValueError: the rdata type value is not >= 0 and <= 65535 - @rtype: string""" + """Convert a DNS rdata type value to text. + + If the value has a known mnemonic, it will be used, otherwise the + DNS generic type syntax will be used. + + Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535. + + Returns a ``str``. + """ if value < 0 or value > 65535: raise ValueError("type must be between >= 0 and <= 65535") @@ -234,10 +234,15 @@ def to_text(value): def is_metatype(rdtype): - """True if the type is a metatype. - @param rdtype: the type - @type rdtype: int - @rtype: bool""" + """True if the specified type is a metatype. + + *rdtype* is an ``int``. + + The currently defined metatypes are TKEY, TSIG, IXFR, AXFR, MAILA, + MAILB, ANY, and OPT. + + Returns a ``bool``. + """ if rdtype >= TKEY and rdtype <= ANY or rdtype in _metatypes: return True @@ -245,10 +250,18 @@ def is_metatype(rdtype): def is_singleton(rdtype): - """True if the type is a singleton. - @param rdtype: the type - @type rdtype: int - @rtype: bool""" + """Is the specified type a singleton type? + + Singleton types can only have a single rdata in an rdataset, or a single + RR in an RRset. + + The currently defined singleton types are CNAME, DNAME, NSEC, NXT, and + SOA. + + *rdtype* is an ``int``. + + Returns a ``bool``. + """ if rdtype in _singletons: return True diff --git a/eventlet/support/dns/rdtypes/ANY/OPENPGPKEY.py b/eventlet/support/dns/rdtypes/ANY/OPENPGPKEY.py new file mode 100644 index 0000000..70f57f0 --- /dev/null +++ b/eventlet/support/dns/rdtypes/ANY/OPENPGPKEY.py @@ -0,0 +1,58 @@ +# Copyright (C) 2016 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 + +import dns.exception +import dns.rdata +import dns.tokenizer + +class OPENPGPKEY(dns.rdata.Rdata): + + """OPENPGPKEY record + + @ivar key: the key + @type key: bytes + @see: RFC 7929 + """ + + def __init__(self, rdclass, rdtype, key): + super(OPENPGPKEY, self).__init__(rdclass, rdtype) + self.key = key + + def to_text(self, origin=None, relativize=True, **kw): + return dns.rdata._base64ify(self.key) + + @classmethod + def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True): + chunks = [] + while 1: + t = tok.get().unescape() + if t.is_eol_or_eof(): + break + if not t.is_identifier(): + raise dns.exception.SyntaxError + chunks.append(t.value.encode()) + b64 = b''.join(chunks) + key = base64.b64decode(b64) + return cls(rdclass, rdtype, key) + + def to_wire(self, file, compress=None, origin=None): + file.write(self.key) + + @classmethod + def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): + key = wire[current: current + rdlen].unwrap() + return cls(rdclass, rdtype, key) diff --git a/eventlet/support/dns/rdtypes/ANY/__init__.py b/eventlet/support/dns/rdtypes/ANY/__init__.py index ea9c3e2..77be2cd 100644 --- a/eventlet/support/dns/rdtypes/ANY/__init__.py +++ b/eventlet/support/dns/rdtypes/ANY/__init__.py @@ -17,10 +17,13 @@ __all__ = [ 'AFSDB', + 'AVC', + 'CAA', 'CDNSKEY', 'CDS', 'CERT', 'CNAME', + 'CSYNC', 'DLV', 'DNAME', 'DNSKEY', @@ -37,7 +40,7 @@ __all__ = [ 'NSEC', 'NSEC3', 'NSEC3PARAM', - 'TLSA', + 'OPENPGPKEY', 'PTR', 'RP', 'RRSIG', @@ -45,6 +48,8 @@ __all__ = [ 'SOA', 'SPF', 'SSHFP', + 'TLSA', 'TXT', + 'URI', 'X25', ] diff --git a/eventlet/support/dns/rdtypes/IN/A.py b/eventlet/support/dns/rdtypes/IN/A.py index 3775548..31f3e7f 100644 --- a/eventlet/support/dns/rdtypes/IN/A.py +++ b/eventlet/support/dns/rdtypes/IN/A.py @@ -48,5 +48,5 @@ class A(dns.rdata.Rdata): @classmethod def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None): - address = dns.ipv4.inet_ntoa(wire[current: current + rdlen]).decode() + address = dns.ipv4.inet_ntoa(wire[current: current + rdlen]) return cls(rdclass, rdtype, address) diff --git a/eventlet/support/dns/rdtypes/IN/APL.py b/eventlet/support/dns/rdtypes/IN/APL.py index 57ef6c0..ac9c1f0 100644 --- a/eventlet/support/dns/rdtypes/IN/APL.py +++ b/eventlet/support/dns/rdtypes/IN/APL.py @@ -20,7 +20,7 @@ import dns.exception import dns.inet import dns.rdata import dns.tokenizer -from dns._compat import xrange +from dns._compat import xrange, maybe_chr class APLItem(object): @@ -63,7 +63,7 @@ class APLItem(object): # last = 0 for i in xrange(len(address) - 1, -1, -1): - if address[i] != chr(0): + if address[i] != maybe_chr(0): last = i + 1 break address = address[0: last] @@ -142,11 +142,11 @@ class APL(dns.rdata.Rdata): l = len(address) if header[0] == 1: if l < 4: - address += '\x00' * (4 - l) + address += b'\x00' * (4 - l) address = dns.inet.inet_ntop(dns.inet.AF_INET, address) elif header[0] == 2: if l < 16: - address += '\x00' * (16 - l) + address += b'\x00' * (16 - l) address = dns.inet.inet_ntop(dns.inet.AF_INET6, address) else: # diff --git a/eventlet/support/dns/rdtypes/IN/__init__.py b/eventlet/support/dns/rdtypes/IN/__init__.py index 24cf1ec..8c16268 100644 --- a/eventlet/support/dns/rdtypes/IN/__init__.py +++ b/eventlet/support/dns/rdtypes/IN/__init__.py @@ -20,6 +20,7 @@ __all__ = [ 'AAAA', 'APL', 'DHCID', + 'IPSECKEY', 'KX', 'NAPTR', 'NSAP', diff --git a/eventlet/support/dns/rdtypes/txtbase.py b/eventlet/support/dns/rdtypes/txtbase.py index 352b027..aa341f1 100644 --- a/eventlet/support/dns/rdtypes/txtbase.py +++ b/eventlet/support/dns/rdtypes/txtbase.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2006-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -20,24 +20,29 @@ import struct import dns.exception import dns.rdata import dns.tokenizer -from dns._compat import binary_type +from dns._compat import binary_type, string_types class TXTBase(dns.rdata.Rdata): """Base class for rdata that is like a TXT record - @ivar strings: the text strings - @type strings: list of string + @ivar strings: the strings + @type strings: list of binary @see: RFC 1035""" __slots__ = ['strings'] def __init__(self, rdclass, rdtype, strings): super(TXTBase, self).__init__(rdclass, rdtype) - if isinstance(strings, str): + if isinstance(strings, binary_type) or \ + isinstance(strings, string_types): strings = [strings] - self.strings = strings[:] + self.strings = [] + for string in strings: + if isinstance(string, string_types): + string = string.encode() + self.strings.append(string) def to_text(self, origin=None, relativize=True, **kw): txt = '' diff --git a/eventlet/support/dns/renderer.py b/eventlet/support/dns/renderer.py index 670fb28..2025f68 100644 --- a/eventlet/support/dns/renderer.py +++ b/eventlet/support/dns/renderer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2001-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -32,7 +32,6 @@ ADDITIONAL = 3 class Renderer(object): - """Helper class for building DNS wire-format messages. Most applications can use the higher-level L{dns.message.Message} @@ -54,41 +53,27 @@ class Renderer(object): r.add_tsig(keyname, secret, 300, 1, 0, '', request_mac) wire = r.get_wire() - @ivar output: where rendering is written - @type output: BytesIO object - @ivar id: the message id - @type id: int - @ivar flags: the message flags - @type flags: int - @ivar max_size: the maximum size of the message - @type max_size: int - @ivar origin: the origin to use when rendering relative names - @type origin: dns.name.Name object - @ivar compress: the compression table - @type compress: dict - @ivar section: the section currently being rendered - @type section: int (dns.renderer.QUESTION, dns.renderer.ANSWER, - dns.renderer.AUTHORITY, or dns.renderer.ADDITIONAL) - @ivar counts: list of the number of RRs in each section - @type counts: int list of length 4 - @ivar mac: the MAC of the rendered message (if TSIG was used) - @type mac: string + output, a BytesIO, where rendering is written + + id: the message id + + flags: the message flags + + max_size: the maximum size of the message + + origin: the origin to use when rendering relative names + + compress: the compression table + + section: an int, the section currently being rendered + + counts: list of the number of RRs in each section + + mac: the MAC of the rendered message (if TSIG was used) """ def __init__(self, id=None, flags=0, max_size=65535, origin=None): - """Initialize a new renderer. - - @param id: the message id - @type id: int - @param flags: the DNS message flags - @type flags: int - @param max_size: the maximum message size; the default is 65535. - If rendering results in a message greater than I{max_size}, - then L{dns.exception.TooBig} will be raised. - @type max_size: int - @param origin: the origin to use when rendering relative names - @type origin: dns.name.Name or None. - """ + """Initialize a new renderer.""" self.output = BytesIO() if id is None: @@ -105,12 +90,9 @@ class Renderer(object): self.mac = '' def _rollback(self, where): - """Truncate the output buffer at offset I{where}, and remove any + """Truncate the output buffer at offset *where*, and remove any compression table entries that pointed beyond the truncation point. - - @param where: the offset - @type where: int """ self.output.seek(where) @@ -128,9 +110,7 @@ class Renderer(object): Sections must be rendered order: QUESTION, ANSWER, AUTHORITY, ADDITIONAL. Sections may be empty. - @param section: the section - @type section: int - @raises dns.exception.FormError: an attempt was made to set + Raises dns.exception.FormError if an attempt was made to set a section value less than the current section. """ @@ -140,15 +120,7 @@ class Renderer(object): self.section = section def add_question(self, qname, rdtype, rdclass=dns.rdataclass.IN): - """Add a question to the message. - - @param qname: the question name - @type qname: dns.name.Name - @param rdtype: the question rdata type - @type rdtype: int - @param rdclass: the question rdata class - @type rdclass: int - """ + """Add a question to the message.""" self._set_section(QUESTION) before = self.output.tell() @@ -165,11 +137,6 @@ class Renderer(object): Any keyword arguments are passed on to the rdataset's to_wire() routine. - - @param section: the section - @type section: int - @param rrset: the rrset - @type rrset: dns.rrset.RRset object """ self._set_section(section) @@ -187,13 +154,6 @@ class Renderer(object): Any keyword arguments are passed on to the rdataset's to_wire() routine. - - @param section: the section - @type section: int - @param name: the owner name - @type name: dns.name.Name object - @param rdataset: the rdataset - @type rdataset: dns.rdataset.Rdataset object """ self._set_section(section) @@ -207,19 +167,7 @@ class Renderer(object): self.counts[section] += n def add_edns(self, edns, ednsflags, payload, options=None): - """Add an EDNS OPT record to the message. - - @param edns: The EDNS level to use. - @type edns: int - @param ednsflags: EDNS flag values. - @type ednsflags: int - @param payload: The EDNS sender's payload field, which is the maximum - size of UDP datagram the sender can handle. - @type payload: int - @param options: The EDNS options list - @type options: list of dns.edns.Option instances - @see: RFC 2671 - """ + """Add an EDNS OPT record to the message.""" # make sure the EDNS version in ednsflags agrees with edns ednsflags &= long(0xFF00FFFF) @@ -255,26 +203,7 @@ class Renderer(object): def add_tsig(self, keyname, secret, fudge, id, tsig_error, other_data, request_mac, algorithm=dns.tsig.default_algorithm): - """Add a TSIG signature to the message. - - @param keyname: the TSIG key name - @type keyname: dns.name.Name object - @param secret: the secret to use - @type secret: string - @param fudge: TSIG time fudge - @type fudge: int - @param id: the message id to encode in the tsig signature - @type id: int - @param tsig_error: TSIG error code; default is 0. - @type tsig_error: int - @param other_data: TSIG other data. - @type other_data: string - @param request_mac: This message is a response to the request which - had the specified MAC. - @type request_mac: string - @param algorithm: the TSIG algorithm to use - @type algorithm: dns.name.Name object - """ + """Add a TSIG signature to the message.""" self._set_section(ADDITIONAL) before = self.output.tell() @@ -321,9 +250,6 @@ class Renderer(object): self.output.seek(0, 2) def get_wire(self): - """Return the wire format message. - - @rtype: string - """ + """Return the wire format message.""" return self.output.getvalue() diff --git a/eventlet/support/dns/resolver.py b/eventlet/support/dns/resolver.py index abc431d..0032383 100644 --- a/eventlet/support/dns/resolver.py +++ b/eventlet/support/dns/resolver.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -13,10 +13,7 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""DNS stub resolver. - -@var default_resolver: The default resolver object -@type default_resolver: dns.resolver.Resolver object""" +"""DNS stub resolver.""" import socket import sys @@ -49,7 +46,6 @@ if sys.platform == 'win32': import _winreg # pylint: disable=import-error class NXDOMAIN(dns.exception.DNSException): - """The DNS query name does not exist.""" supp_kwargs = set(['qnames', 'responses']) fmt = None # we have our own __str__ implementation @@ -107,9 +103,30 @@ class NXDOMAIN(dns.exception.DNSException): responses0[qname1] = responses1[qname1] return NXDOMAIN(qnames=qnames0, responses=responses0) + def qnames(self): + """All of the names that were tried. + + Returns a list of ``dns.name.Name``. + """ + return self.kwargs['qnames'] + + def responses(self): + """A map from queried names to their NXDOMAIN responses. + + Returns a dict mapping a ``dns.name.Name`` to a + ``dns.message.Message``. + """ + return self.kwargs['responses'] + + def response(self, qname): + """The response for query *qname*. + + Returns a ``dns.message.Message``. + """ + return self.kwargs['responses'][qname] + class YXDOMAIN(dns.exception.DNSException): - """The DNS query name is too long after DNAME substitution.""" # The definition of the Timeout exception has moved from here to the @@ -120,7 +137,6 @@ Timeout = dns.exception.Timeout class NoAnswer(dns.exception.DNSException): - """The DNS response does not contain an answer to the question.""" fmt = 'The DNS response does not contain an answer ' + \ 'to the question: {query}' @@ -132,12 +148,11 @@ class NoAnswer(dns.exception.DNSException): class NoNameservers(dns.exception.DNSException): - """All nameservers failed to answer the query. errors: list of servers and respective errors The type of errors is - [(server ip address, any object convertible to string)]. + [(server IP address, any object convertible to string)]. Non-empty errors list will add explanatory message () """ @@ -155,50 +170,31 @@ class NoNameservers(dns.exception.DNSException): class NotAbsolute(dns.exception.DNSException): - """An absolute domain name is required but a relative name was provided.""" class NoRootSOA(dns.exception.DNSException): - """There is no SOA RR at the DNS root name. This should never happen!""" class NoMetaqueries(dns.exception.DNSException): - """DNS metaqueries are not allowed.""" class Answer(object): - - """DNS stub resolver answer + """DNS stub resolver answer. Instances of this class bundle up the result of a successful DNS resolution. For convenience, the answer object implements much of the sequence - protocol, forwarding to its rrset. E.g. "for a in answer" is - equivalent to "for a in answer.rrset", "answer[i]" is equivalent - to "answer.rrset[i]", and "answer[i:j]" is equivalent to - "answer.rrset[i:j]". + protocol, forwarding to its ``rrset`` attribute. E.g. + ``for a in answer`` is equivalent to ``for a in answer.rrset``. + ``answer[i]`` is equivalent to ``answer.rrset[i]``, and + ``answer[i:j]`` is equivalent to ``answer.rrset[i:j]``. Note that CNAMEs or DNAMEs in the response may mean that answer - node's name might not be the query name. - - @ivar qname: The query name - @type qname: dns.name.Name object - @ivar rdtype: The query type - @type rdtype: int - @ivar rdclass: The query class - @type rdclass: int - @ivar response: The response message - @type response: dns.message.Message object - @ivar rrset: The answer - @type rrset: dns.rrset.RRset object - @ivar expiration: The time when the answer expires - @type expiration: float (seconds since the epoch) - @ivar canonical_name: The canonical name of the query name - @type canonical_name: dns.name.Name object + RRset's name might not be the query name. """ def __init__(self, qname, rdtype, rdclass, response, @@ -278,32 +274,22 @@ class Answer(object): return self.rrset and iter(self.rrset) or iter(tuple()) def __getitem__(self, i): + if self.rrset is None: + raise IndexError return self.rrset[i] def __delitem__(self, i): + if self.rrset is None: + raise IndexError del self.rrset[i] class Cache(object): - - """Simple DNS answer cache. - - @ivar data: A dictionary of cached data - @type data: dict - @ivar cleaning_interval: The number of seconds between cleanings. The - default is 300 (5 minutes). - @type cleaning_interval: float - @ivar next_cleaning: The time the cache should next be cleaned (in seconds - since the epoch.) - @type next_cleaning: float - """ + """Simple thread-safe DNS answer cache.""" def __init__(self, cleaning_interval=300.0): - """Initialize a DNS cache. - - @param cleaning_interval: the number of seconds between periodic - cleanings. The default is 300.0 - @type cleaning_interval: float. + """*cleaning_interval*, a ``float`` is the number of seconds between + periodic cleanings. """ self.data = {} @@ -326,12 +312,14 @@ class Cache(object): self.next_cleaning = now + self.cleaning_interval def get(self, key): - """Get the answer associated with I{key}. Returns None if - no answer is cached for the key. - @param key: the key - @type key: (dns.name.Name, int, int) tuple whose values are the - query name, rdtype, and rdclass. - @rtype: dns.resolver.Answer object or None + """Get the answer associated with *key*. + + Returns None if no answer is cached for the key. + + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. + + Returns a ``dns.resolver.Answer`` or ``None``. """ try: @@ -346,11 +334,11 @@ class Cache(object): def put(self, key, value): """Associate key and value in the cache. - @param key: the key - @type key: (dns.name.Name, int, int) tuple whose values are the - query name, rdtype, and rdclass. - @param value: The answer being cached - @type value: dns.resolver.Answer object + + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. + + *value*, a ``dns.resolver.Answer``, the answer. """ try: @@ -363,11 +351,11 @@ class Cache(object): def flush(self, key=None): """Flush the cache. - If I{key} is specified, only that item is flushed. Otherwise + If *key* is not ``None``, only that item is flushed. Otherwise the entire cache is flushed. - @param key: the key to flush - @type key: (dns.name.Name, int, int) tuple or None + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. """ try: @@ -383,9 +371,7 @@ class Cache(object): class LRUCacheNode(object): - - """LRUCache node. - """ + """LRUCache node.""" def __init__(self, key, value): self.key = key @@ -411,30 +397,20 @@ class LRUCacheNode(object): class LRUCache(object): - - """Bounded least-recently-used DNS answer cache. + """Thread-safe, bounded, least-recently-used DNS answer cache. This cache is better than the simple cache (above) if you're running a web crawler or other process that does a lot of resolutions. The LRUCache has a maximum number of nodes, and when it is full, the least-recently used node is removed to make space for a new one. - - @ivar data: A dictionary of cached data - @type data: dict - @ivar sentinel: sentinel node for circular doubly linked list of nodes - @type sentinel: LRUCacheNode object - @ivar max_size: The maximum number of nodes - @type max_size: int """ def __init__(self, max_size=100000): - """Initialize a DNS cache. - - @param max_size: The maximum number of nodes to cache; the default is - 100,000. Must be greater than 1. - @type max_size: int + """*max_size*, an ``int``, is the maximum number of nodes to cache; + it must be greater than 0. """ + self.data = {} self.set_max_size(max_size) self.sentinel = LRUCacheNode(None, None) @@ -446,13 +422,16 @@ class LRUCache(object): self.max_size = max_size def get(self, key): - """Get the answer associated with I{key}. Returns None if - no answer is cached for the key. - @param key: the key - @type key: (dns.name.Name, int, int) tuple whose values are the - query name, rdtype, and rdclass. - @rtype: dns.resolver.Answer object or None + """Get the answer associated with *key*. + + Returns None if no answer is cached for the key. + + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. + + Returns a ``dns.resolver.Answer`` or ``None``. """ + try: self.lock.acquire() node = self.data.get(key) @@ -471,12 +450,13 @@ class LRUCache(object): def put(self, key, value): """Associate key and value in the cache. - @param key: the key - @type key: (dns.name.Name, int, int) tuple whose values are the - query name, rdtype, and rdclass. - @param value: The answer being cached - @type value: dns.resolver.Answer object + + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. + + *value*, a ``dns.resolver.Answer``, the answer. """ + try: self.lock.acquire() node = self.data.get(key) @@ -496,12 +476,13 @@ class LRUCache(object): def flush(self, key=None): """Flush the cache. - If I{key} is specified, only that item is flushed. Otherwise + If *key* is not ``None``, only that item is flushed. Otherwise the entire cache is flushed. - @param key: the key to flush - @type key: (dns.name.Name, int, int) tuple or None + *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the + query name, rdtype, and rdclass respectively. """ + try: self.lock.acquire() if key is not None: @@ -522,62 +503,19 @@ class LRUCache(object): class Resolver(object): - - """DNS stub resolver - - @ivar domain: The domain of this host - @type domain: dns.name.Name object - @ivar nameservers: A list of nameservers to query. Each nameserver is - a string which contains the IP address of a nameserver. - @type nameservers: list of strings - @ivar search: The search list. If the query name is a relative name, - the resolver will construct an absolute query name by appending the search - names one by one to the query name. - @type search: list of dns.name.Name objects - @ivar port: The port to which to send queries. The default is 53. - @type port: int - @ivar timeout: The number of seconds to wait for a response from a - server, before timing out. - @type timeout: float - @ivar lifetime: The total number of seconds to spend trying to get an - answer to the question. If the lifetime expires, a Timeout exception - will occur. - @type lifetime: float - @ivar keyring: The TSIG keyring to use. The default is None. - @type keyring: dict - @ivar keyname: The TSIG keyname to use. The default is None. - @type keyname: dns.name.Name object - @ivar keyalgorithm: The TSIG key algorithm to use. The default is - dns.tsig.default_algorithm. - @type keyalgorithm: string - @ivar edns: The EDNS level to use. The default is -1, no Edns. - @type edns: int - @ivar ednsflags: The EDNS flags - @type ednsflags: int - @ivar payload: The EDNS payload size. The default is 0. - @type payload: int - @ivar flags: The message flags to use. The default is None (i.e. not - overwritten) - @type flags: int - @ivar cache: The cache to use. The default is None. - @type cache: dns.resolver.Cache object - @ivar retry_servfail: should we retry a nameserver if it says SERVFAIL? - The default is 'false'. - @type retry_servfail: bool - """ + """DNS stub resolver.""" def __init__(self, filename='/etc/resolv.conf', configure=True): - """Initialize a resolver instance. + """*filename*, a ``text`` or file object, specifying a file + in standard /etc/resolv.conf format. This parameter is meaningful + only when *configure* is true and the platform is POSIX. - @param filename: The filename of a configuration file in - standard /etc/resolv.conf format. This parameter is meaningful - only when I{configure} is true and the platform is POSIX. - @type filename: string or file object - @param configure: If True (the default), the resolver instance - is configured in the normal fashion for the operating system - the resolver is running on. (I.e. a /etc/resolv.conf file on - POSIX systems and from the registry on Windows systems.) - @type configure: bool""" + *configure*, a ``bool``. If True (the default), the resolver + instance is configured in the normal fashion for the operating + system the resolver is running on. (I.e. by reading a + /etc/resolv.conf file on POSIX systems and from the registry + on Windows systems.) + """ self.domain = None self.nameservers = None @@ -606,6 +544,7 @@ class Resolver(object): def reset(self): """Reset all resolver configuration to the defaults.""" + self.domain = \ dns.name.Name(dns.name.from_text(socket.gethostname())[1:]) if len(self.domain) == 0: @@ -628,9 +567,10 @@ class Resolver(object): self.rotate = False def read_resolv_conf(self, f): - """Process f as a file in the /etc/resolv.conf format. If f is - a string, it is used as the name of the file to open; otherwise it + """Process *f* as a file in the /etc/resolv.conf format. If f is + a ``text``, it is used as the name of the file to open; otherwise it is treated as the file itself.""" + if isinstance(f, string_types): try: f = open(f, 'r') @@ -684,7 +624,6 @@ class Resolver(object): return split_char def _config_win32_nameservers(self, nameservers): - """Configure a NameServer registry entry.""" # we call str() on nameservers to convert it from unicode to ascii nameservers = str(nameservers) split_char = self._determine_split_char(nameservers) @@ -694,12 +633,10 @@ class Resolver(object): self.nameservers.append(ns) def _config_win32_domain(self, domain): - """Configure a Domain registry entry.""" # we call str() on domain to convert it from unicode to ascii self.domain = dns.name.from_text(str(domain)) def _config_win32_search(self, search): - """Configure a Search registry entry.""" # we call str() on search to convert it from unicode to ascii search = str(search) split_char = self._determine_split_char(search) @@ -708,14 +645,14 @@ class Resolver(object): if s not in self.search: self.search.append(dns.name.from_text(s)) - def _config_win32_fromkey(self, key): - """Extract DNS info from a registry key.""" + def _config_win32_fromkey(self, key, always_try_domain): try: servers, rtype = _winreg.QueryValueEx(key, 'NameServer') except WindowsError: # pylint: disable=undefined-variable servers = None if servers: self._config_win32_nameservers(servers) + if servers or always_try_domain: try: dom, rtype = _winreg.QueryValueEx(key, 'Domain') if dom: @@ -744,6 +681,7 @@ class Resolver(object): def read_registry(self): """Extract resolver configuration from the Windows registry.""" + lm = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) want_scan = False try: @@ -759,7 +697,7 @@ class Resolver(object): r'SYSTEM\CurrentControlSet' r'\Services\VxD\MSTCP') try: - self._config_win32_fromkey(tcp_params) + self._config_win32_fromkey(tcp_params, True) finally: tcp_params.Close() if want_scan: @@ -777,7 +715,7 @@ class Resolver(object): if not self._win32_is_nic_enabled(lm, guid, key): continue try: - self._config_win32_fromkey(key) + self._config_win32_fromkey(key, False) finally: key.Close() except EnvironmentError: @@ -862,36 +800,43 @@ class Resolver(object): tcp=False, source=None, raise_on_no_answer=True, source_port=0): """Query nameservers to find the answer to the question. - The I{qname}, I{rdtype}, and I{rdclass} parameters may be objects + The *qname*, *rdtype*, and *rdclass* parameters may be objects of the appropriate type, or strings that can be converted into objects - of the appropriate type. E.g. For I{rdtype} the integer 2 and the - the string 'NS' both mean to query for records with DNS rdata type NS. + of the appropriate type. - @param qname: the query name - @type qname: dns.name.Name object or string - @param rdtype: the query type - @type rdtype: int or string - @param rdclass: the query class - @type rdclass: int or string - @param tcp: use TCP to make the query (default is False). - @type tcp: bool - @param source: bind to this IP address (defaults to machine default - IP). - @type source: IP address in dotted quad notation - @param raise_on_no_answer: raise NoAnswer if there's no answer - (defaults is True). - @type raise_on_no_answer: bool - @param source_port: The port from which to send the message. - The default is 0. - @type source_port: int - @rtype: dns.resolver.Answer instance - @raises Timeout: no answers could be found in the specified lifetime - @raises NXDOMAIN: the query name does not exist - @raises YXDOMAIN: the query name is too long after DNAME substitution - @raises NoAnswer: the response did not contain an answer and - raise_on_no_answer is True. - @raises NoNameservers: no non-broken nameservers are available to - answer the question.""" + *qname*, a ``dns.name.Name`` or ``text``, the query name. + + *rdtype*, an ``int`` or ``text``, the query type. + + *rdclass*, an ``int`` or ``text``, the query class. + + *tcp*, a ``bool``. If ``True``, use TCP to make the query. + + *source*, a ``text`` or ``None``. If not ``None``, bind to this IP + address when making queries. + + *raise_on_no_answer*, a ``bool``. If ``True``, raise + ``dns.resolver.NoAnswer`` if there's no answer to the question. + + *source_port*, an ``int``, the port from which to send the message. + + Raises ``dns.exception.Timeout`` if no answers could be found + in the specified lifetime. + + Raises ``dns.resolver.NXDOMAIN`` if the query name does not exist. + + Raises ``dns.resolver.YXDOMAIN`` if the query name is too long after + DNAME substitution. + + Raises ``dns.resolver.NoAnswer`` if *raise_on_no_answer* is + ``True`` and the query name exists but has no RRset of the + desired type and class. + + Raises ``dns.resolver.NoNameservers`` if no non-broken + nameservers are available to answer the question. + + Returns a ``dns.resolver.Answer`` instance. + """ if isinstance(qname, string_types): qname = dns.name.from_text(qname, None) @@ -1059,17 +1004,22 @@ class Resolver(object): algorithm=dns.tsig.default_algorithm): """Add a TSIG signature to the query. - @param keyring: The TSIG keyring to use; defaults to None. - @type keyring: dict - @param keyname: The name of the TSIG key to use; defaults to None. - The key must be defined in the keyring. If a keyring is specified - but a keyname is not, then the key used will be the first key in the - keyring. Note that the order of keys in a dictionary is not defined, - so applications should supply a keyname when a keyring is used, unless - they know the keyring contains only one key. - @param algorithm: The TSIG key algorithm to use. The default - is dns.tsig.default_algorithm. - @type algorithm: string""" + See the documentation of the Message class for a complete + description of the keyring dictionary. + + *keyring*, a ``dict``, the TSIG keyring to use. If a + *keyring* is specified but a *keyname* is not, then the key + used will be the first key in the *keyring*. Note that the + order of keys in a dictionary is not defined, so applications + should supply a keyname when a keyring is used, unless they + know the keyring contains only one key. + + *keyname*, a ``dns.name.Name`` or ``None``, the name of the TSIG key + to use; defaults to ``None``. The key must be defined in the keyring. + + *algorithm*, a ``dns.name.Name``, the TSIG algorithm to use. + """ + self.keyring = keyring if keyname is None: self.keyname = list(self.keyring.keys())[0] @@ -1078,14 +1028,19 @@ class Resolver(object): self.keyalgorithm = algorithm def use_edns(self, edns, ednsflags, payload): - """Configure Edns. + """Configure EDNS behavior. - @param edns: The EDNS level to use. The default is -1, no Edns. - @type edns: int - @param ednsflags: The EDNS flags - @type ednsflags: int - @param payload: The EDNS payload size. The default is 0. - @type payload: int""" + *edns*, an ``int``, is the EDNS level to use. Specifying + ``None``, ``False``, or ``-1`` means "do not use EDNS", and in this case + the other parameters are ignored. Specifying ``True`` is + equivalent to specifying 0, i.e. "use EDNS0". + + *ednsflags*, an ``int``, the EDNS flag values. + + *payload*, an ``int``, is the EDNS sender's payload field, which is the + maximum size of UDP datagram the sender can handle. I.e. how big + a response to this message can be. + """ if edns is None: edns = -1 @@ -1094,12 +1049,15 @@ class Resolver(object): self.payload = payload def set_flags(self, flags): - """Overrides the default flags with your own + """Overrides the default flags with your own. + + *flags*, an ``int``, the message flags to use. + """ - @param flags: The flags to overwrite the default with - @type flags: int""" self.flags = flags + +#: The default resolver. default_resolver = None @@ -1113,8 +1071,10 @@ def get_default_resolver(): def reset_default_resolver(): """Re-initialize default resolver. - resolv.conf will be re-read immediatelly. + Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX + systems) will be re-read immediately. """ + global default_resolver default_resolver = Resolver() @@ -1126,8 +1086,11 @@ def query(qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, This is a convenience function that uses the default resolver object to make the query. - @see: L{dns.resolver.Resolver.query} for more information on the - parameters.""" + + See ``dns.resolver.Resolver.query`` for more information on the + parameters. + """ + return get_default_resolver().query(qname, rdtype, rdclass, tcp, source, raise_on_no_answer, source_port) @@ -1135,15 +1098,21 @@ def query(qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, def zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False, resolver=None): """Find the name of the zone which contains the specified name. - @param name: the query name - @type name: absolute dns.name.Name object or string - @param rdclass: The query class - @type rdclass: int - @param tcp: use TCP to make the query (default is False). - @type tcp: bool - @param resolver: the resolver to use - @type resolver: dns.resolver.Resolver object or None - @rtype: dns.name.Name""" + *name*, an absolute ``dns.name.Name`` or ``text``, the query name. + + *rdclass*, an ``int``, the query class. + + *tcp*, a ``bool``. If ``True``, use TCP to make the query. + + *resolver*, a ``dns.resolver.Resolver`` or ``None``, the resolver to use. + If ``None``, the default resolver is used. + + Raises ``dns.resolver.NoRootSOA`` if there is no SOA RR at the DNS + root. (This is only likely to happen if you're using non-default + root servers in your network and they are misconfigured.) + + Returns a ``dns.name.Name``. + """ if isinstance(name, string_types): name = dns.name.from_text(name, dns.name.root) @@ -1379,9 +1348,9 @@ def override_system_resolver(resolver=None): The resolver to use may be specified; if it's not, the default resolver will be used. - @param resolver: the resolver to use - @type resolver: dns.resolver.Resolver object or None + resolver, a ``dns.resolver.Resolver`` or ``None``, the resolver to use. """ + if resolver is None: resolver = get_default_resolver() global _resolver @@ -1395,8 +1364,8 @@ def override_system_resolver(resolver=None): def restore_system_resolver(): - """Undo the effects of override_system_resolver(). - """ + """Undo the effects of prior override_system_resolver().""" + global _resolver _resolver = None socket.getaddrinfo = _original_getaddrinfo diff --git a/eventlet/support/dns/reversename.py b/eventlet/support/dns/reversename.py index 9ea9395..e2ec77d 100644 --- a/eventlet/support/dns/reversename.py +++ b/eventlet/support/dns/reversename.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2006-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -13,13 +13,7 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""DNS Reverse Map Names. - -@var ipv4_reverse_domain: The DNS IPv4 reverse-map domain, in-addr.arpa. -@type ipv4_reverse_domain: dns.name.Name object -@var ipv6_reverse_domain: The DNS IPv6 reverse-map domain, ip6.arpa. -@type ipv6_reverse_domain: dns.name.Name object -""" +"""DNS Reverse Map Names.""" import binascii import sys @@ -35,11 +29,15 @@ ipv6_reverse_domain = dns.name.from_text('ip6.arpa.') def from_address(text): """Convert an IPv4 or IPv6 address in textual form into a Name object whose value is the reverse-map domain name of the address. - @param text: an IPv4 or IPv6 address in textual form (e.g. '127.0.0.1', - '::1') - @type text: str - @rtype: dns.name.Name object + + *text*, a ``text``, is an IPv4 or IPv6 address in textual form + (e.g. '127.0.0.1', '::1') + + Raises ``dns.exception.SyntaxError`` if the address is badly formed. + + Returns a ``dns.name.Name``. """ + try: v6 = dns.ipv6.inet_aton(text) if dns.ipv6.is_mapped(v6): @@ -61,10 +59,16 @@ def from_address(text): def to_address(name): """Convert a reverse map domain name into textual address form. - @param name: an IPv4 or IPv6 address in reverse-map form. - @type name: dns.name.Name object - @rtype: str + + *name*, a ``dns.name.Name``, an IPv4 or IPv6 address in reverse-map name + form. + + Raises ``dns.exception.SyntaxError`` if the name does not have a + reverse-map form. + + Returns a ``text``. """ + if name.is_subdomain(ipv4_reverse_domain): name = name.relativize(ipv4_reverse_domain) labels = list(name.labels) diff --git a/eventlet/support/dns/rrset.py b/eventlet/support/dns/rrset.py index d0f8f93..6774647 100644 --- a/eventlet/support/dns/rrset.py +++ b/eventlet/support/dns/rrset.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -67,10 +67,6 @@ class RRset(dns.rdataset.Rdataset): return self.to_text() def __eq__(self, other): - """Two RRsets are equal if they have the same name and the same - rdataset - - @rtype: bool""" if not isinstance(other, RRset): return False if self.name != other.name: @@ -78,8 +74,9 @@ class RRset(dns.rdataset.Rdataset): return super(RRset, self).__eq__(other) def match(self, name, rdclass, rdtype, covers, deleting=None): - """Returns True if this rrset matches the specified class, type, - covers, and deletion state.""" + """Returns ``True`` if this rrset matches the specified class, type, + covers, and deletion state. + """ if not super(RRset, self).match(rdclass, rdtype, covers): return False @@ -90,23 +87,31 @@ class RRset(dns.rdataset.Rdataset): def to_text(self, origin=None, relativize=True, **kw): """Convert the RRset into DNS master file format. - @see: L{dns.name.Name.choose_relativity} for more information - on how I{origin} and I{relativize} determine the way names + See ``dns.name.Name.choose_relativity`` for more information + on how *origin* and *relativize* determine the way names are emitted. Any additional keyword arguments are passed on to the rdata - to_text() method. + ``to_text()`` method. - @param origin: The origin for relative names, or None. - @type origin: dns.name.Name object - @param relativize: True if names should names be relativized - @type relativize: bool""" + *origin*, a ``dns.name.Name`` or ``None``, the origin for relative + names. + + *relativize*, a ``bool``. If ``True``, names will be relativized + to *origin*. + """ return super(RRset, self).to_text(self.name, origin, relativize, self.deleting, **kw) def to_wire(self, file, compress=None, origin=None, **kw): - """Convert the RRset to wire format.""" + """Convert the RRset to wire format. + + All keyword arguments are passed to ``dns.rdataset.to_wire()``; see + that function for details. + + Returns an ``int``, the number of records emitted. + """ return super(RRset, self).to_wire(self.name, file, compress, origin, self.deleting, **kw) @@ -114,7 +119,7 @@ class RRset(dns.rdataset.Rdataset): def to_rdataset(self): """Convert an RRset into an Rdataset. - @rtype: dns.rdataset.Rdataset object + Returns a ``dns.rdataset.Rdataset``. """ return dns.rdataset.from_rdata_list(self.ttl, list(self)) @@ -124,7 +129,7 @@ def from_text_list(name, ttl, rdclass, rdtype, text_rdatas, """Create an RRset with the specified name, TTL, class, and type, and with the specified list of rdatas in text format. - @rtype: dns.rrset.RRset object + Returns a ``dns.rrset.RRset`` object. """ if isinstance(name, string_types): @@ -145,7 +150,7 @@ def from_text(name, ttl, rdclass, rdtype, *text_rdatas): """Create an RRset with the specified name, TTL, class, and type and with the specified rdatas in text format. - @rtype: dns.rrset.RRset object + Returns a ``dns.rrset.RRset`` object. """ return from_text_list(name, ttl, rdclass, rdtype, text_rdatas) @@ -155,7 +160,7 @@ def from_rdata_list(name, ttl, rdatas, idna_codec=None): """Create an RRset with the specified name and TTL, and with the specified list of rdata objects. - @rtype: dns.rrset.RRset object + Returns a ``dns.rrset.RRset`` object. """ if isinstance(name, string_types): @@ -176,7 +181,7 @@ def from_rdata(name, ttl, *rdatas): """Create an RRset with the specified name and TTL, and with the specified rdata objects. - @rtype: dns.rrset.RRset object + Returns a ``dns.rrset.RRset`` object. """ return from_rdata_list(name, ttl, rdatas) diff --git a/eventlet/support/dns/set.py b/eventlet/support/dns/set.py index ef7fd29..b419ff4 100644 --- a/eventlet/support/dns/set.py +++ b/eventlet/support/dns/set.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -13,27 +13,22 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -"""A simple Set class.""" - - class Set(object): """A simple set class. - Sets are not in Python until 2.3, and rdata are not immutable so - we cannot use sets.Set anyway. This class implements subset of - the 2.3 Set interface using a list as the container. - - @ivar items: A list of the items which are in the set - @type items: list""" + This class was originally used to deal with sets being missing in + ancient versions of python, but dnspython will continue to use it + as these sets are based on lists and are thus indexable, and this + ability is widely used in dnspython applications. + """ __slots__ = ['items'] def __init__(self, items=None): """Initialize the set. - @param items: the initial set of items - @type items: any iterable or None + *items*, an iterable or ``None``, the initial set of items. """ self.items = [] @@ -45,16 +40,22 @@ class Set(object): return "dns.simpleset.Set(%s)" % repr(self.items) def add(self, item): - """Add an item to the set.""" + """Add an item to the set. + """ + if item not in self.items: self.items.append(item) def remove(self, item): - """Remove an item from the set.""" + """Remove an item from the set. + """ + self.items.remove(item) def discard(self, item): - """Remove an item from the set if present.""" + """Remove an item from the set if present. + """ + try: self.items.remove(item) except ValueError: @@ -79,19 +80,22 @@ class Set(object): return obj def __copy__(self): - """Make a (shallow) copy of the set.""" + """Make a (shallow) copy of the set. + """ + return self._clone() def copy(self): - """Make a (shallow) copy of the set.""" + """Make a (shallow) copy of the set. + """ + return self._clone() def union_update(self, other): """Update the set, adding any elements from other which are not already in the set. - @param other: the collection of items with which to update the set - @type other: Set object """ + if not isinstance(other, Set): raise ValueError('other must be a Set instance') if self is other: @@ -102,9 +106,8 @@ class Set(object): def intersection_update(self, other): """Update the set, removing any elements from other which are not in both sets. - @param other: the collection of items with which to update the set - @type other: Set object """ + if not isinstance(other, Set): raise ValueError('other must be a Set instance') if self is other: @@ -118,9 +121,8 @@ class Set(object): def difference_update(self, other): """Update the set, removing any elements from other which are in the set. - @param other: the collection of items with which to update the set - @type other: Set object """ + if not isinstance(other, Set): raise ValueError('other must be a Set instance') if self is other: @@ -130,11 +132,9 @@ class Set(object): self.discard(item) def union(self, other): - """Return a new set which is the union of I{self} and I{other}. + """Return a new set which is the union of ``self`` and ``other``. - @param other: the other set - @type other: Set object - @rtype: the same type as I{self} + Returns the same Set type as this set. """ obj = self._clone() @@ -142,11 +142,10 @@ class Set(object): return obj def intersection(self, other): - """Return a new set which is the intersection of I{self} and I{other}. + """Return a new set which is the intersection of ``self`` and + ``other``. - @param other: the other set - @type other: Set object - @rtype: the same type as I{self} + Returns the same Set type as this set. """ obj = self._clone() @@ -154,12 +153,10 @@ class Set(object): return obj def difference(self, other): - """Return a new set which I{self} - I{other}, i.e. the items - in I{self} which are not also in I{other}. + """Return a new set which ``self`` - ``other``, i.e. the items + in ``self`` which are not also in ``other``. - @param other: the other set - @type other: Set object - @rtype: the same type as I{self} + Returns the same Set type as this set. """ obj = self._clone() @@ -197,8 +194,11 @@ class Set(object): def update(self, other): """Update the set, adding any elements from other which are not already in the set. - @param other: the collection of items with which to update the set - @type other: any iterable type""" + + *other*, the collection of items with which to update the set, which + may be any iterable type. + """ + for item in other: self.add(item) @@ -233,9 +233,9 @@ class Set(object): del self.items[i] def issubset(self, other): - """Is I{self} a subset of I{other}? + """Is this set a subset of *other*? - @rtype: bool + Returns a ``bool``. """ if not isinstance(other, Set): @@ -246,9 +246,9 @@ class Set(object): return True def issuperset(self, other): - """Is I{self} a superset of I{other}? + """Is this set a superset of *other*? - @rtype: bool + Returns a ``bool``. """ if not isinstance(other, Set): diff --git a/eventlet/support/dns/tokenizer.py b/eventlet/support/dns/tokenizer.py index 04b9825..3c9bd37 100644 --- a/eventlet/support/dns/tokenizer.py +++ b/eventlet/support/dns/tokenizer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -44,32 +44,20 @@ DELIMITER = 6 class UngetBufferFull(dns.exception.DNSException): - """An attempt was made to unget a token when the unget buffer was full.""" class Token(object): - """A DNS master file format token. - @ivar ttype: The token type - @type ttype: int - @ivar value: The token value - @type value: string - @ivar has_escape: Does the token value contain escapes? - @type has_escape: bool + ttype: The token type + value: The token value + has_escape: Does the token value contain escapes? """ def __init__(self, ttype, value='', has_escape=False): - """Initialize a token instance. + """Initialize a token instance.""" - @param ttype: The token type - @type ttype: int - @param value: The token value - @type value: string - @param has_escape: Does the token value contain escapes? - @type has_escape: bool - """ self.ttype = ttype self.value = value self.has_escape = has_escape @@ -160,46 +148,43 @@ class Token(object): class Tokenizer(object): - """A DNS master file format tokenizer. - A token is a (type, value) tuple, where I{type} is an int, and - I{value} is a string. The valid types are EOF, EOL, WHITESPACE, - IDENTIFIER, QUOTED_STRING, COMMENT, and DELIMITER. + A token object is basically a (type, value) tuple. The valid + types are EOF, EOL, WHITESPACE, IDENTIFIER, QUOTED_STRING, + COMMENT, and DELIMITER. - @ivar file: The file to tokenize - @type file: file - @ivar ungotten_char: The most recently ungotten character, or None. - @type ungotten_char: string - @ivar ungotten_token: The most recently ungotten token, or None. - @type ungotten_token: (int, string) token tuple - @ivar multiline: The current multiline level. This value is increased + file: The file to tokenize + + ungotten_char: The most recently ungotten character, or None. + + ungotten_token: The most recently ungotten token, or None. + + multiline: The current multiline level. This value is increased by one every time a '(' delimiter is read, and decreased by one every time a ')' delimiter is read. - @type multiline: int - @ivar quoting: This variable is true if the tokenizer is currently + + quoting: This variable is true if the tokenizer is currently reading a quoted string. - @type quoting: bool - @ivar eof: This variable is true if the tokenizer has encountered EOF. - @type eof: bool - @ivar delimiters: The current delimiter dictionary. - @type delimiters: dict - @ivar line_number: The current line number - @type line_number: int - @ivar filename: A filename that will be returned by the L{where} method. - @type filename: string + + eof: This variable is true if the tokenizer has encountered EOF. + + delimiters: The current delimiter dictionary. + + line_number: The current line number + + filename: A filename that will be returned by the where() method. """ def __init__(self, f=sys.stdin, filename=None): """Initialize a tokenizer instance. - @param f: The file to tokenize. The default is sys.stdin. + f: The file to tokenize. The default is sys.stdin. This parameter may also be a string, in which case the tokenizer will take its input from the contents of the string. - @type f: file or string - @param filename: the name of the filename that the L{where} method + + filename: the name of the filename that the where() method will return. - @type filename: string """ if isinstance(f, text_type): @@ -228,7 +213,6 @@ class Tokenizer(object): def _get_char(self): """Read a character from input. - @rtype: string """ if self.ungotten_char is None: @@ -248,7 +232,7 @@ class Tokenizer(object): def where(self): """Return the current location in the input. - @rtype: (string, int) tuple. The first item is the filename of + Returns a (string, int) tuple. The first item is the filename of the input, the second is the current line number. """ @@ -261,9 +245,8 @@ class Tokenizer(object): an error to try to unget a character when the unget buffer is not empty. - @param c: the character to unget - @type c: string - @raises UngetBufferFull: there is already an ungotten char + c: the character to unget + raises UngetBufferFull: there is already an ungotten char """ if self.ungotten_char is not None: @@ -278,7 +261,7 @@ class Tokenizer(object): If the tokenizer is in multiline mode, then newlines are whitespace. - @rtype: int + Returns the number of characters skipped. """ skipped = 0 @@ -293,15 +276,17 @@ class Tokenizer(object): def get(self, want_leading=False, want_comment=False): """Get the next token. - @param want_leading: If True, return a WHITESPACE token if the + want_leading: If True, return a WHITESPACE token if the first character read is whitespace. The default is False. - @type want_leading: bool - @param want_comment: If True, return a COMMENT token if the + + want_comment: If True, return a COMMENT token if the first token read is a comment. The default is False. - @type want_comment: bool - @rtype: Token object - @raises dns.exception.UnexpectedEnd: input ended prematurely - @raises dns.exception.SyntaxError: input was badly formed + + Raises dns.exception.UnexpectedEnd: input ended prematurely + + Raises dns.exception.SyntaxError: input was badly formed + + Returns a Token. """ if self.ungotten_token is not None: @@ -420,9 +405,9 @@ class Tokenizer(object): an error to try to unget a token when the unget buffer is not empty. - @param token: the token to unget - @type token: Token object - @raises UngetBufferFull: there is already an ungotten token + token: the token to unget + + Raises UngetBufferFull: there is already an ungotten token """ if self.ungotten_token is not None: @@ -431,7 +416,8 @@ class Tokenizer(object): def next(self): """Return the next item in an iteration. - @rtype: (int, string) + + Returns a Token. """ token = self.get() @@ -449,8 +435,9 @@ class Tokenizer(object): def get_int(self): """Read the next token and interpret it as an integer. - @raises dns.exception.SyntaxError: - @rtype: int + Raises dns.exception.SyntaxError if not an integer. + + Returns an int. """ token = self.get().unescape() @@ -464,8 +451,9 @@ class Tokenizer(object): """Read the next token and interpret it as an 8-bit unsigned integer. - @raises dns.exception.SyntaxError: - @rtype: int + Raises dns.exception.SyntaxError if not an 8-bit unsigned integer. + + Returns an int. """ value = self.get_int() @@ -478,8 +466,9 @@ class Tokenizer(object): """Read the next token and interpret it as a 16-bit unsigned integer. - @raises dns.exception.SyntaxError: - @rtype: int + Raises dns.exception.SyntaxError if not a 16-bit unsigned integer. + + Returns an int. """ value = self.get_int() @@ -492,8 +481,9 @@ class Tokenizer(object): """Read the next token and interpret it as a 32-bit unsigned integer. - @raises dns.exception.SyntaxError: - @rtype: int + Raises dns.exception.SyntaxError if not a 32-bit unsigned integer. + + Returns an int. """ token = self.get().unescape() @@ -510,8 +500,9 @@ class Tokenizer(object): def get_string(self, origin=None): """Read the next token and interpret it as a string. - @raises dns.exception.SyntaxError: - @rtype: string + Raises dns.exception.SyntaxError if not a string. + + Returns a string. """ token = self.get().unescape() @@ -520,10 +511,11 @@ class Tokenizer(object): return token.value def get_identifier(self, origin=None): - """Read the next token and raise an exception if it is not an identifier. + """Read the next token, which should be an identifier. - @raises dns.exception.SyntaxError: - @rtype: string + Raises dns.exception.SyntaxError if not an identifier. + + Returns a string. """ token = self.get().unescape() @@ -534,8 +526,10 @@ class Tokenizer(object): def get_name(self, origin=None): """Read the next token and interpret it as a DNS name. - @raises dns.exception.SyntaxError: - @rtype: dns.name.Name object""" + Raises dns.exception.SyntaxError if not a name. + + Returns a dns.name.Name. + """ token = self.get() if not token.is_identifier(): @@ -546,8 +540,7 @@ class Tokenizer(object): """Read the next token and raise an exception if it isn't EOL or EOF. - @raises dns.exception.SyntaxError: - @rtype: string + Returns a string. """ token = self.get() @@ -558,6 +551,14 @@ class Tokenizer(object): return token.value def get_ttl(self): + """Read the next token and interpret it as a DNS TTL. + + Raises dns.exception.SyntaxError or dns.ttl.BadTTL if not an + identifier or badly formed. + + Returns an int. + """ + token = self.get().unescape() if not token.is_identifier(): raise dns.exception.SyntaxError('expecting an identifier') diff --git a/eventlet/support/dns/ttl.py b/eventlet/support/dns/ttl.py index a27d825..4690728 100644 --- a/eventlet/support/dns/ttl.py +++ b/eventlet/support/dns/ttl.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -20,7 +20,6 @@ from ._compat import long class BadTTL(dns.exception.SyntaxError): - """DNS TTL value is not well-formed.""" @@ -29,10 +28,11 @@ def from_text(text): The BIND 8 units syntax for TTLs (e.g. '1w6d4h3m10s') is supported. - @param text: the textual TTL - @type text: string - @raises dns.ttl.BadTTL: the TTL is not well-formed - @rtype: int + *text*, a ``text``, the textual TTL. + + Raises ``dns.ttl.BadTTL`` if the TTL is not well-formed. + + Returns an ``int``. """ if text.isdigit(): diff --git a/eventlet/support/dns/update.py b/eventlet/support/dns/update.py index 59728d9..db0a61d 100644 --- a/eventlet/support/dns/update.py +++ b/eventlet/support/dns/update.py @@ -32,26 +32,25 @@ class Update(dns.message.Message): keyname=None, keyalgorithm=dns.tsig.default_algorithm): """Initialize a new DNS Update object. - @param zone: The zone which is being updated. - @type zone: A dns.name.Name or string - @param rdclass: The class of the zone; defaults to dns.rdataclass.IN. - @type rdclass: An int designating the class, or a string whose value - is the name of a class. - @param keyring: The TSIG keyring to use; defaults to None. - @type keyring: dict - @param keyname: The name of the TSIG key to use; defaults to None. - The key must be defined in the keyring. If a keyring is specified - but a keyname is not, then the key used will be the first key in the - keyring. Note that the order of keys in a dictionary is not defined, - so applications should supply a keyname when a keyring is used, unless - they know the keyring contains only one key. - @type keyname: dns.name.Name or string - @param keyalgorithm: The TSIG algorithm to use; defaults to - dns.tsig.default_algorithm. Constants for TSIG algorithms are defined - in dns.tsig, and the currently implemented algorithms are - HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, and - HMAC_SHA512. - @type keyalgorithm: string + See the documentation of the Message class for a complete + description of the keyring dictionary. + + *zone*, a ``dns.name.Name`` or ``text``, the zone which is being + updated. + + *rdclass*, an ``int`` or ``text``, the class of the zone. + + *keyring*, a ``dict``, the TSIG keyring to use. If a + *keyring* is specified but a *keyname* is not, then the key + used will be the first key in the *keyring*. Note that the + order of keys in a dictionary is not defined, so applications + should supply a keyname when a keyring is used, unless they + know the keyring contains only one key. + + *keyname*, a ``dns.name.Name`` or ``None``, the name of the TSIG key + to use; defaults to ``None``. The key must be defined in the keyring. + + *keyalgorithm*, a ``dns.name.Name``, the TSIG algorithm to use. """ super(Update, self).__init__() self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE) @@ -77,8 +76,10 @@ class Update(dns.message.Message): rrset.add(rd, ttl) def _add(self, replace, section, name, *args): - """Add records. The first argument is the replace mode. If - false, RRs are added to an existing RRset; if true, the RRset + """Add records. + + *replace* is the replacement mode. If ``False``, + RRs are added to an existing RRset; if ``True``, the RRset is replaced with the specified contents. The second argument is the section to add to. The third argument is always a name. The other arguments can be: @@ -87,7 +88,8 @@ class Update(dns.message.Message): - ttl, rdata... - - ttl, rdtype, string...""" + - ttl, rdtype, string... + """ if isinstance(name, string_types): name = dns.name.from_text(name, None) @@ -117,27 +119,34 @@ class Update(dns.message.Message): self._add_rr(name, ttl, rd, section=section) def add(self, name, *args): - """Add records. The first argument is always a name. The other + """Add records. + + The first argument is always a name. The other arguments can be: - rdataset... - ttl, rdata... - - ttl, rdtype, string...""" + - ttl, rdtype, string... + """ + self._add(False, self.authority, name, *args) def delete(self, name, *args): - """Delete records. The first argument is always a name. The other + """Delete records. + + The first argument is always a name. The other arguments can be: - - I{nothing} + - *empty* - rdataset... - rdata... - - rdtype, [string...]""" + - rdtype, [string...] + """ if isinstance(name, string_types): name = dns.name.from_text(name, None) @@ -171,7 +180,9 @@ class Update(dns.message.Message): self._add_rr(name, 0, rd, dns.rdataclass.NONE) def replace(self, name, *args): - """Replace records. The first argument is always a name. The other + """Replace records. + + The first argument is always a name. The other arguments can be: - rdataset... @@ -181,21 +192,25 @@ class Update(dns.message.Message): - ttl, rdtype, string... Note that if you want to replace the entire node, you should do - a delete of the name followed by one or more calls to add.""" + a delete of the name followed by one or more calls to add. + """ self._add(True, self.authority, name, *args) def present(self, name, *args): """Require that an owner name (and optionally an rdata type, or specific rdataset) exists as a prerequisite to the - execution of the update. The first argument is always a name. + execution of the update. + + The first argument is always a name. The other arguments can be: - rdataset... - rdata... - - rdtype, string...""" + - rdtype, string... + """ if isinstance(name, string_types): name = dns.name.from_text(name, None) @@ -243,7 +258,20 @@ class Update(dns.message.Message): def to_wire(self, origin=None, max_size=65535): """Return a string containing the update in DNS compressed wire format. - @rtype: string""" + + *origin*, a ``dns.name.Name`` or ``None``, the origin to be + appended to any relative names. If *origin* is ``None``, then + the origin of the ``dns.update.Update`` message object is used + (i.e. the *zone* parameter passed when the Update object was + created). + + *max_size*, an ``int``, the maximum size of the wire format + output; default is 0, which means "the message's request + payload, if nonzero, or 65535". + + Returns a ``binary``. + """ + if origin is None: origin = self.origin return super(Update, self).to_wire(origin, max_size) diff --git a/eventlet/support/dns/version.py b/eventlet/support/dns/version.py index 9e8dbb1..4e7023c 100644 --- a/eventlet/support/dns/version.py +++ b/eventlet/support/dns/version.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2003-2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -15,13 +15,19 @@ """dnspython release version information.""" +#: MAJOR MAJOR = 1 -MINOR = 15 +#: MINOR +MINOR = 16 +#: MICRO MICRO = 0 +#: RELEASELEVEL RELEASELEVEL = 0x0f +#: SERIAL SERIAL = 0 if RELEASELEVEL == 0x0f: + #: version version = '%d.%d.%d' % (MAJOR, MINOR, MICRO) elif RELEASELEVEL == 0x00: version = '%d.%d.%dx%d' % \ @@ -30,5 +36,6 @@ else: version = '%d.%d.%d%x%d' % \ (MAJOR, MINOR, MICRO, RELEASELEVEL, SERIAL) +#: hexversion hexversion = MAJOR << 24 | MINOR << 16 | MICRO << 8 | RELEASELEVEL << 4 | \ SERIAL diff --git a/eventlet/support/dns/wiredata.py b/eventlet/support/dns/wiredata.py index ccef595..6a058c6 100644 --- a/eventlet/support/dns/wiredata.py +++ b/eventlet/support/dns/wiredata.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011 Nominum, Inc. +# Copyright (C) 2011,2017 Nominum, Inc. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose with or without fee is hereby granted, @@ -40,7 +40,7 @@ _unspecified_bound = _SliceUnspecifiedBound()[1:] class WireData(binary_type): - # WireData is a string with stricter slicing + # WireData is a binary type with stricter slicing def __getitem__(self, key): try: