dnspython: Update to newer upstream snapshot.
authorJelmer Vernooij <jelmer@samba.org>
Thu, 9 Dec 2010 13:53:45 +0000 (14:53 +0100)
committerJelmer Vernooij <jelmer@samba.org>
Fri, 10 Dec 2010 02:04:06 +0000 (03:04 +0100)
22 files changed:
lib/dnspython/.gitignore
lib/dnspython/ChangeLog
lib/dnspython/Makefile [new file with mode: 0644]
lib/dnspython/README
lib/dnspython/dns/__init__.py
lib/dnspython/dns/dnssec.py
lib/dnspython/dns/hash.py [new file with mode: 0644]
lib/dnspython/dns/message.py
lib/dnspython/dns/node.py
lib/dnspython/dns/query.py
lib/dnspython/dns/rdataset.py
lib/dnspython/dns/resolver.py
lib/dnspython/dns/rrset.py
lib/dnspython/dns/tsig.py
lib/dnspython/dns/update.py
lib/dnspython/dns/version.py
lib/dnspython/dns/zone.py
lib/dnspython/examples/ddns.py
lib/dnspython/examples/zonediff.py [new file with mode: 0755]
lib/dnspython/setup.py
lib/dnspython/tests/dnssec.py [new file with mode: 0644]
lib/dnspython/tests/resolver.py

index 2abcfc47d78727d7e77659a167ffdb3765021f24..5592c971b018ecdfd08b3908e8facafe07946aa5 100644 (file)
@@ -2,4 +2,6 @@ build
 dist
 MANIFEST
 html
+html.zip
+html.tar.gz
 tests/*.out
index 73a66edef68ca86dd362791e1e5029310f9a9d40..91e69d3ea2f2d45ec39ff17afb1e2dce36f152d7 100644 (file)
@@ -1,3 +1,82 @@
+2010-11-23  Bob Halley  <halley@dnspython.org>
+
+       * (Version 1.9.2 released)
+
+2010-11-23  Bob Halley  <halley@dnspython.org>
+
+       * dns/dnssec.py (_need_pycrypto): DSA and RSA are modules, not
+         functions, and I didn't notice because the test suite masked
+         the bug!  *sigh*
+
+2010-11-22  Bob Halley  <halley@dnspython.org>
+
+       * (Version 1.9.1 released)
+
+2010-11-22  Bob Halley  <halley@dnspython.org>
+
+       * dns/dnssec.py: the "from" style import used to get DSA from
+         PyCrypto trashed a DSA constant.  Now a normal import is used
+         to avoid namespace contamination.
+
+2010-11-20  Bob Halley  <halley@dnspython.org>
+
+       * (Version 1.9.0 released)
+
+2010-11-07  Bob Halley  <halley@dnspython.org>
+
+       * dns/dnssec.py: Added validate() to do basic DNSSEC validation
+         (requires PyCrypto). Thanks to Brian Wellington for the patch.
+
+       * dns/hash.py: Hash compatibility handling is now its own module.
+
+2010-10-31  Bob Halley  <halley@dnspython.org>
+
+       * dns/resolver.py (zone_for_name): A query name resulting in a
+         CNAME or DNAME response to a node which had an SOA was incorrectly
+         treated as a zone origin.  In these cases, we should just look
+         higher.  Thanks to Gert Berger for reporting this problem.
+
+       * Added zonediff.py to examples.  This program compares two zones
+         and shows the differences either in diff-like plain text, or
+         HTML.  Thanks to Dennis Kaarsemaker for contributing this
+         useful program.
+
+2010-10-27  Bob Halley  <halley@dnspython.org>
+
+       * Incorporate a patch to use poll() instead of select() by
+         default on platforms which support it.  Thanks to
+         Peter Schüller and Spotify for the contribution.
+
+2010-10-17  Bob Halley  <halley@dnspython.org>
+
+       * Python prior to 2.5.2 doesn't compute the correct values for
+         HMAC-SHA384 and HMAC-SHA512.  We now detect attempts to use
+         them and raise NotImplemented if the Python version is too old.
+         Thanks to Kevin Chen for reporting the problem.
+
+       * Various routines that took the string forms of rdata types and
+         classes did not permit the strings to be Unicode strings.
+         Thanks to Ryan Workman for reporting the issue.
+
+       * dns/tsig.py: Added symbolic constants for the algorithm strings.
+         E.g. you can now say dns.tsig.HMAC_MD5 instead of
+         "HMAC-MD5.SIG-ALG.REG.INT".  Thanks to Cillian Sharkey for
+         suggesting this improvement.
+
+       * dns/tsig.py (get_algorithm): fix hashlib compatibility; thanks to
+         Kevin Chen for the patch.
+
+       * dns/dnssec.py: Added key_id() and make_ds().
+
+       * dns/message.py: message.py needs to import dns.edns since it uses
+         it.
+
+2010-05-04  Bob Halley  <halley@dnspython.org>
+
+       * dns/rrset.py (RRset.__init__): "covers" was not passed to the
+         superclass __init__().  Thanks to Shanmuga Rajan for reporting
+         the problem.
+
 2010-03-10  Bob Halley  <halley@dnspython.org>
 
        * The TSIG algorithm value was passed to use_tsig() incorrectly
diff --git a/lib/dnspython/Makefile b/lib/dnspython/Makefile
new file mode 100644 (file)
index 0000000..3dbfe95
--- /dev/null
@@ -0,0 +1,56 @@
+# Copyright (C) 2003-2007, 2009 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.
+
+# $Id: Makefile,v 1.16 2004/03/19 00:17:27 halley Exp $
+
+PYTHON=python
+
+all:
+       ${PYTHON} ./setup.py build
+
+install:
+       ${PYTHON} ./setup.py install
+
+clean:
+       ${PYTHON} ./setup.py clean --all
+       find . -name '*.pyc' -exec rm {} \;
+       find . -name '*.pyo' -exec rm {} \;
+       rm -f TAGS
+
+distclean: clean docclean
+       rm -rf build dist
+       rm -f MANIFEST
+
+doc:
+       epydoc -n dnspython -u http://www.dnspython.org \
+               dns/*.py dns/rdtypes/*.py dns/rdtypes/ANY/*.py \
+               dns/rdtypes/IN/*.py
+
+dockits: doc
+       mv html dnspython-html
+       tar czf html.tar.gz dnspython-html
+       zip -r html.zip dnspython-html
+       mv dnspython-html html
+
+docclean:
+       rm -rf html.tar.gz html.zip html
+
+kits:
+       ${PYTHON} ./setup.py sdist --formats=gztar,zip
+#      ${PYTHON} ./setup.py bdist_wininst
+#      ${PYTHON} ./setup.py bdist_rpm
+
+tags:
+       find . -name '*.py' -print | etags -
index b313d1c132e6852ab275a6fb474c6de73693748d..d53dac61aba9e8b6736f69f650988f1b795d0d9d 100644 (file)
@@ -22,7 +22,62 @@ development by continuing to employ the author :).
 
 ABOUT THIS RELEASE
 
-This is dnspython 1.8.0
+This is dnspython 1.9.2
+
+New since 1.9.1:
+
+       Nothing.
+
+Bugs fixed since 1.9.1:
+
+       The dns.dnssec module didn't work at all due to missing
+       imports that escaped detection in testing because the test
+       suite also did the imports.  The third time is the charm!
+
+New since 1.9.0:
+
+       Nothing.
+
+Bugs fixed since 1.9.0:
+
+        The dns.dnssec module didn't work with DSA due to namespace
+       contamination from a "from"-style import.
+
+New since 1.8.0:
+
+       dnspython now uses poll() instead of select() when available.
+
+       Basic DNSSEC validation can be done using dns.dnsec.validate()
+       and dns.dnssec.validate_rrsig() if you have PyCrypto 2.3 or
+       later installed.  Complete secure resolution is not yet
+       available.
+
+       Added key_id() to the DNSSEC module, which computes the DNSSEC
+       key id of a DNSKEY rdata.
+
+       Added make_ds() to the DNSSEC module, which returns the DS RR
+       for a given DNSKEY rdata.
+
+       dnspython now raises an exception if HMAC-SHA284 or
+       HMAC-SHA512 are used with a Python older than 2.5.2.  (Older
+       Pythons do not compute the correct value.)
+
+       Symbolic constants are now available for TSIG algorithm names.
+
+Bugs fixed since 1.8.0
+
+        dns.resolver.zone_for_name() didn't handle a query response
+       with a CNAME or DNAME correctly in some cases.
+
+        When specifying rdata types and classes as text, Unicode
+       strings may now be used.
+
+       Hashlib compatibility issues have been fixed.
+
+       dns.message now imports dns.edns.
+
+       The TSIG algorithm value was passed incorrectly to use_tsig()
+       in some cases.
 
 New since 1.7.1:
 
@@ -310,7 +365,7 @@ the prior release.
 
 REQUIREMENTS
 
-Python 2.2 or later.
+Python 2.4 or later.
 
 
 INSTALLATION
index 5ad5737cfa24faf48678631674aad07a17dcda38..56e1e8a2ea8085d3e516026e2ef41c78cf0e19d6 100644 (file)
@@ -22,6 +22,7 @@ __all__ = [
     'entropy',
     'exception',
     'flags',
+    'hash',
     'inet',
     'ipv4',
     'ipv6',
index 54fd78d9c9b91491e5736627dd0662863d888297..a595fd44782534cd8a9038185d109eeb7ea4283e 100644 (file)
 
 """Common DNSSEC-related functions and constants."""
 
+import cStringIO
+import struct
+import time
+
+import dns.exception
+import dns.hash
+import dns.name
+import dns.node
+import dns.rdataset
+import dns.rdata
+import dns.rdatatype
+import dns.rdataclass
+
+class UnsupportedAlgorithm(dns.exception.DNSException):
+    """Raised if an algorithm is not supported."""
+    pass
+
+class ValidationFailure(dns.exception.DNSException):
+    """The DNSSEC signature is invalid."""
+    pass
+
 RSAMD5 = 1
 DH = 2
 DSA = 3
@@ -49,14 +70,10 @@ _algorithm_by_text = {
 
 _algorithm_by_value = dict([(y, x) for x, y in _algorithm_by_text.iteritems()])
 
-class UnknownAlgorithm(Exception):
-    """Raised if an algorithm is unknown."""
-    pass
-
 def algorithm_from_text(text):
     """Convert text into a DNSSEC algorithm value
     @rtype: int"""
-    
+
     value = _algorithm_by_text.get(text.upper())
     if value is None:
         value = int(text)
@@ -65,8 +82,291 @@ def algorithm_from_text(text):
 def algorithm_to_text(value):
     """Convert a DNSSEC algorithm value to text
     @rtype: string"""
-    
+
     text = _algorithm_by_value.get(value)
     if text is None:
         text = str(value)
     return text
+
+def _to_rdata(record, origin):
+    s = cStringIO.StringIO()
+    record.to_wire(s, origin=origin)
+    return s.getvalue()
+
+def key_id(key, origin=None):
+    rdata = _to_rdata(key, origin)
+    if key.algorithm == RSAMD5:
+        return (ord(rdata[-3]) << 8) + ord(rdata[-2])
+    else:
+        total = 0
+        for i in range(len(rdata) / 2):
+            total += (ord(rdata[2 * i]) << 8) + ord(rdata[2 * i + 1])
+        if len(rdata) % 2 != 0:
+            total += ord(rdata[len(rdata) - 1]) << 8
+        total += ((total >> 16) & 0xffff);
+        return total & 0xffff
+
+def make_ds(name, key, algorithm, origin=None):
+    if algorithm.upper() == 'SHA1':
+        dsalg = 1
+        hash = dns.hash.get('SHA1')()
+    elif algorithm.upper() == 'SHA256':
+        dsalg = 2
+        hash = dns.hash.get('SHA256')()
+    else:
+        raise UnsupportedAlgorithm, 'unsupported algorithm "%s"' % algorithm
+
+    if isinstance(name, (str, unicode)):
+        name = dns.name.from_text(name, origin)
+    hash.update(name.canonicalize().to_wire())
+    hash.update(_to_rdata(key, origin))
+    digest = hash.digest()
+
+    dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, dsalg) + digest
+    return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0,
+                               len(dsrdata))
+
+def _find_key(keys, rrsig):
+    value = keys.get(rrsig.signer)
+    if value is None:
+        return None
+    if isinstance(value, dns.node.Node):
+        try:
+            rdataset = node.find_rdataset(dns.rdataclass.IN,
+                                          dns.rdatatype.DNSKEY)
+        except KeyError:
+            return None
+    else:
+        rdataset = value
+    for rdata in rdataset:
+        if rdata.algorithm == rrsig.algorithm and \
+               key_id(rdata) == rrsig.key_tag:
+            return rdata
+    return None
+
+def _is_rsa(algorithm):
+    return algorithm in (RSAMD5, RSASHA1,
+                         RSASHA1NSEC3SHA1, RSASHA256,
+                         RSASHA512)
+
+def _is_dsa(algorithm):
+    return algorithm in (DSA, DSANSEC3SHA1)
+
+def _is_md5(algorithm):
+    return algorithm == RSAMD5
+
+def _is_sha1(algorithm):
+    return algorithm in (DSA, RSASHA1,
+                         DSANSEC3SHA1, RSASHA1NSEC3SHA1)
+
+def _is_sha256(algorithm):
+    return algorithm == RSASHA256
+
+def _is_sha512(algorithm):
+    return algorithm == RSASHA512
+
+def _make_hash(algorithm):
+    if _is_md5(algorithm):
+        return dns.hash.get('MD5')()
+    if _is_sha1(algorithm):
+        return dns.hash.get('SHA1')()
+    if _is_sha256(algorithm):
+        return dns.hash.get('SHA256')()
+    if _is_sha512(algorithm):
+        return dns.hash.get('SHA512')()
+    raise ValidationFailure, 'unknown hash for algorithm %u' % algorithm
+
+def _make_algorithm_id(algorithm):
+    if _is_md5(algorithm):
+        oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05]
+    elif _is_sha1(algorithm):
+        oid = [0x2b, 0x0e, 0x03, 0x02, 0x1a]
+    elif _is_sha256(algorithm):
+        oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]
+    elif _is_sha512(algorithm):
+        oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]
+    else:
+        raise ValidationFailure, 'unknown algorithm %u' % algorithm
+    olen = len(oid)
+    dlen = _make_hash(algorithm).digest_size
+    idbytes = [0x30] + [8 + olen + dlen] + \
+              [0x30, olen + 4] + [0x06, olen] + oid + \
+              [0x05, 0x00] + [0x04, dlen]
+    return ''.join(map(chr, idbytes))
+
+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.
+
+    @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
+    """
+
+    if isinstance(origin, (str, unicode)):
+        origin = dns.name.from_text(origin, dns.name.root)
+
+    key = _find_key(keys, rrsig)
+    if not key:
+        raise ValidationFailure, 'unknown key'
+
+    # For convenience, allow the rrset to be specified as a (name, rdataset)
+    # tuple as well as a proper rrset
+    if isinstance(rrset, tuple):
+        rrname = rrset[0]
+        rdataset = rrset[1]
+    else:
+        rrname = rrset.name
+        rdataset = rrset
+
+    if now is None:
+        now = time.time()
+    if rrsig.expiration < now:
+        raise ValidationFailure, 'expired'
+    if rrsig.inception > now:
+        raise ValidationFailure, 'not yet valid'
+
+    hash = _make_hash(rrsig.algorithm)
+
+    if _is_rsa(rrsig.algorithm):
+        keyptr = key.key
+        (bytes,) = struct.unpack('!B', keyptr[0:1])
+        keyptr = keyptr[1:]
+        if bytes == 0:
+            (bytes,) = struct.unpack('!H', keyptr[0:2])
+            keyptr = keyptr[2:]
+        rsa_e = keyptr[0:bytes]
+        rsa_n = keyptr[bytes:]
+        keylen = len(rsa_n) * 8
+        pubkey = Crypto.PublicKey.RSA.construct(
+            (Crypto.Util.number.bytes_to_long(rsa_n),
+             Crypto.Util.number.bytes_to_long(rsa_e)))
+        sig = (Crypto.Util.number.bytes_to_long(rrsig.signature),)
+    elif _is_dsa(rrsig.algorithm):
+        keyptr = key.key
+        (t,) = struct.unpack('!B', keyptr[0:1])
+        keyptr = keyptr[1:]
+        octets = 64 + t * 8
+        dsa_q = keyptr[0:20]
+        keyptr = keyptr[20:]
+        dsa_p = keyptr[0:octets]
+        keyptr = keyptr[octets:]
+        dsa_g = keyptr[0:octets]
+        keyptr = keyptr[octets:]
+        dsa_y = keyptr[0:octets]
+        pubkey = Crypto.PublicKey.DSA.construct(
+            (Crypto.Util.number.bytes_to_long(dsa_y),
+             Crypto.Util.number.bytes_to_long(dsa_g),
+             Crypto.Util.number.bytes_to_long(dsa_p),
+             Crypto.Util.number.bytes_to_long(dsa_q)))
+        (dsa_r, dsa_s) = struct.unpack('!20s20s', rrsig.signature[1:])
+        sig = (Crypto.Util.number.bytes_to_long(dsa_r),
+               Crypto.Util.number.bytes_to_long(dsa_s))
+    else:
+        raise ValidationFailure, 'unknown algorithm %u' % rrsig.algorithm
+
+    hash.update(_to_rdata(rrsig, origin)[:18])
+    hash.update(rrsig.signer.to_digestable(origin))
+
+    if rrsig.labels < len(rrname) - 1:
+        suffix = rrname.split(rrsig.labels + 1)[1]
+        rrname = dns.name.from_text('*', suffix)
+    rrnamebuf = rrname.to_digestable(origin)
+    rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
+                          rrsig.original_ttl)
+    rrlist = sorted(rdataset);
+    for rr in rrlist:
+        hash.update(rrnamebuf)
+        hash.update(rrfixed)
+        rrdata = rr.to_digestable(origin)
+        rrlen = struct.pack('!H', len(rrdata))
+        hash.update(rrlen)
+        hash.update(rrdata)
+
+    digest = hash.digest()
+
+    if _is_rsa(rrsig.algorithm):
+        # PKCS1 algorithm identifier goop
+        digest = _make_algorithm_id(rrsig.algorithm) + digest
+        padlen = keylen / 8 - len(digest) - 3
+        digest = chr(0) + chr(1) + chr(0xFF) * padlen + chr(0) + digest
+    elif _is_dsa(rrsig.algorithm):
+        pass
+    else:
+        # Raise here for code clarity; this won't actually ever happen
+        # since if the algorithm is really unknown we'd already have
+        # raised an exception above
+        raise ValidationFailure, 'unknown algorithm %u' % rrsig.algorithm
+
+    if not pubkey.verify(digest, sig):
+        raise ValidationFailure, 'verify failure'
+
+def _validate(rrset, rrsigset, keys, origin=None, now=None):
+    """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
+    """
+
+    if isinstance(origin, (str, unicode)):
+        origin = dns.name.from_text(origin, dns.name.root)
+
+    if isinstance(rrset, tuple):
+        rrname = rrset[0]
+    else:
+        rrname = rrset.name
+
+    if isinstance(rrsigset, tuple):
+        rrsigname = rrsigset[0]
+        rrsigrdataset = rrsigset[1]
+    else:
+        rrsigname = rrsigset.name
+        rrsigrdataset = rrsigset
+
+    rrname = rrname.choose_relativity(origin)
+    rrsigname = rrname.choose_relativity(origin)
+    if rrname != rrsigname:
+        raise ValidationFailure, "owner names do not match"
+
+    for rrsig in rrsigrdataset:
+        try:
+            _validate_rrsig(rrset, rrsig, keys, origin, now)
+            return
+        except ValidationFailure, e:
+            pass
+    raise ValidationFailure, "no RRSIGs validated"
+
+def _need_pycrypto(*args, **kwargs):
+    raise NotImplementedError, "DNSSEC validation requires pycrypto"
+
+try:
+    import Crypto.PublicKey.RSA
+    import Crypto.PublicKey.DSA
+    import Crypto.Util.number
+    validate = _validate
+    validate_rrsig = _validate_rrsig
+except ImportError:
+    validate = _need_pycrypto
+    validate_rrsig = _need_pycrypto
diff --git a/lib/dnspython/dns/hash.py b/lib/dnspython/dns/hash.py
new file mode 100644 (file)
index 0000000..7bd5ae5
--- /dev/null
@@ -0,0 +1,67 @@
+# Copyright (C) 2010 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.
+
+"""Hashing backwards compatibility wrapper"""
+
+import sys
+
+_hashes = None
+
+def _need_later_python(alg):
+    def func(*args, **kwargs):
+        raise NotImplementedError("TSIG algorithm " + alg +
+                                  " requires Python 2.5.2 or later")
+    return func
+
+def _setup():
+    global _hashes
+    _hashes = {}
+    try:
+        import hashlib
+        _hashes['MD5'] = hashlib.md5
+        _hashes['SHA1'] = hashlib.sha1
+        _hashes['SHA224'] = hashlib.sha224
+        _hashes['SHA256'] = hashlib.sha256
+        if sys.hexversion >= 0x02050200:
+            _hashes['SHA384'] = hashlib.sha384
+            _hashes['SHA512'] = hashlib.sha512
+        else:
+            _hashes['SHA384'] = _need_later_python('SHA384')
+            _hashes['SHA512'] = _need_later_python('SHA512')
+
+        if sys.hexversion < 0x02050000:
+            # hashlib doesn't conform to PEP 247: API for
+            # Cryptographic Hash Functions, which hmac before python
+            # 2.5 requires, so add the necessary items.
+            class HashlibWrapper:
+                def __init__(self, basehash):
+                    self.basehash = basehash
+                    self.digest_size = self.basehash().digest_size
+
+                def new(self, *args, **kwargs):
+                    return self.basehash(*args, **kwargs)
+
+            for name in _hashes:
+                _hashes[name] = HashlibWrapper(_hashes[name])
+
+    except ImportError:
+        import md5, sha
+        _hashes['MD5'] =  md5
+        _hashes['SHA1'] = sha
+
+def get(algorithm):
+    if _hashes is None:
+        _setup()
+    return _hashes[algorithm.upper()]
index ba0ebf65f14c7b8966fda39e35462b838f29c2be..5ec711e1ebb68d17ceefbb02835c32b7684cec3a 100644 (file)
@@ -21,6 +21,7 @@ import struct
 import sys
 import time
 
+import dns.edns
 import dns.exception
 import dns.flags
 import dns.name
@@ -92,8 +93,11 @@ class Message(object):
     @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.
+    @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
@@ -1035,9 +1039,9 @@ def make_query(qname, rdtype, rdclass = dns.rdataclass.IN, use_edns=None,
 
     if isinstance(qname, (str, unicode)):
         qname = dns.name.from_text(qname)
-    if isinstance(rdtype, str):
+    if isinstance(rdtype, (str, unicode)):
         rdtype = dns.rdatatype.from_text(rdtype)
-    if isinstance(rdclass, str):
+    if isinstance(rdclass, (str, unicode)):
         rdclass = dns.rdataclass.from_text(rdclass)
     m = Message()
     m.flags |= dns.flags.RD
index 07fff9293c43ad066e1a4e6bb80acea46fe3d9cb..785a2454641067a7d46f9395137cea05a9985995 100644 (file)
@@ -23,18 +23,18 @@ 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"""
 
     __slots__ = ['rdatasets']
-    
+
     def __init__(self):
         """Initialize a DNS node.
         """
-        
+
         self.rdatasets = [];
 
     def to_text(self, name, **kw):
@@ -46,7 +46,7 @@ class Node(object):
         @type name: dns.name.Name object
         @rtype: string
         """
-        
+
         s = StringIO.StringIO()
         for rds in self.rdatasets:
             print >> s, rds.to_text(name, **kw)
@@ -54,7 +54,7 @@ class Node(object):
 
     def __repr__(self):
         return '<DNS node ' + str(id(self)) + '>'
-    
+
     def __eq__(self, other):
         """Two nodes are equal if they have the same rdatasets.
 
@@ -73,7 +73,7 @@ class Node(object):
 
     def __ne__(self, other):
         return not self.__eq__(other)
-        
+
     def __len__(self):
         return len(self.rdatasets)
 
@@ -159,7 +159,7 @@ class Node(object):
 
     def replace_rdataset(self, replacement):
         """Replace an rdataset.
-        
+
         It is not an error if there is no rdataset matching I{replacement}.
 
         Ownership of the I{replacement} object is transferred to the node;
index c023b140aff67d1d0748c87718849ebc7076d7e5..9dc88a635c4bb82771effabbbb6a946cf2f8b2c0 100644 (file)
@@ -45,7 +45,59 @@ def _compute_expiration(timeout):
     else:
         return time.time() + timeout
 
-def _wait_for(ir, iw, ix, expiration):
+def _poll_for(fd, readable, writable, error, timeout):
+    """
+    @param fd: File descriptor (int).
+    @param readable: Whether to wait for readability (bool).
+    @param writable: Whether to wait for writability (bool).
+    @param expiration: Deadline timeout (expiration time, in seconds (float)).
+
+    @return True on success, False on timeout
+    """
+    event_mask = 0
+    if readable:
+        event_mask |= select.POLLIN
+    if writable:
+        event_mask |= select.POLLOUT
+    if error:
+        event_mask |= select.POLLERR
+
+    pollable = select.poll()
+    pollable.register(fd, event_mask)
+
+    if timeout:
+        event_list = pollable.poll(long(timeout * 1000))
+    else:
+        event_list = pollable.poll()
+
+    return bool(event_list)
+
+def _select_for(fd, readable, writable, error, timeout):
+    """
+    @param fd: File descriptor (int).
+    @param readable: Whether to wait for readability (bool).
+    @param writable: Whether to wait for writability (bool).
+    @param expiration: Deadline timeout (expiration time, in seconds (float)).
+
+    @return True on success, False on timeout
+    """
+    rset, wset, xset = [], [], []
+
+    if readable:
+        rset = [fd]
+    if writable:
+        wset = [fd]
+    if error:
+        xset = [fd]
+
+    if timeout is None:
+        (rcount, wcount, xcount) = select.select(rset, wset, xset)
+    else:
+        (rcount, wcount, xcount) = select.select(rset, wset, xset, timeout)
+
+    return bool((rcount or wcount or xcount))
+
+def _wait_for(fd, readable, writable, error, expiration):
     done = False
     while not done:
         if expiration is None:
@@ -55,22 +107,34 @@ def _wait_for(ir, iw, ix, expiration):
             if timeout <= 0.0:
                 raise dns.exception.Timeout
         try:
-            if timeout is None:
-                (r, w, x) = select.select(ir, iw, ix)
-            else:
-                (r, w, x) = select.select(ir, iw, ix, timeout)
+            if not _polling_backend(fd, readable, writable, error, timeout):
+                raise dns.exception.Timeout
         except select.error, e:
             if e.args[0] != errno.EINTR:
                 raise e
         done = True
-        if len(r) == 0 and len(w) == 0 and len(x) == 0:
-            raise dns.exception.Timeout
+
+def _set_polling_backend(fn):
+    """
+    Internal API. Do not use.
+    """
+    global _polling_backend
+
+    _polling_backend = fn
+
+if hasattr(select, 'poll'):
+    # Prefer poll() on platforms that support it because it has no
+    # limits on the maximum value of a file descriptor (plus it will
+    # be more efficient for high values).
+    _polling_backend = _poll_for
+else:
+    _polling_backend = _select_for
 
 def _wait_for_readable(s, expiration):
-    _wait_for([s], [], [s], expiration)
+    _wait_for(s, True, False, True, expiration)
 
 def _wait_for_writable(s, expiration):
-    _wait_for([], [s], [s], expiration)
+    _wait_for(s, False, True, True, expiration)
 
 def _addresses_equal(af, a1, a2):
     # Convert the first value of the tuple, which is a textual format
@@ -310,7 +374,7 @@ def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN,
 
     if isinstance(zone, (str, unicode)):
         zone = dns.name.from_text(zone)
-    if isinstance(rdtype, str):
+    if isinstance(rdtype, (str, unicode)):
         rdtype = dns.rdatatype.from_text(rdtype)
     q = dns.message.make_query(zone, rdtype, rdclass)
     if rdtype == dns.rdatatype.IXFR:
index 0af018bab5c0e715cede89f7fc7ab794e7faea9c..f556d2288b23ee7a7666fec84e813a5f0e2d12e7 100644 (file)
@@ -281,9 +281,9 @@ def from_text_list(rdclass, rdtype, ttl, text_rdatas):
     @rtype: dns.rdataset.Rdataset object
     """
 
-    if isinstance(rdclass, str):
+    if isinstance(rdclass, (str, unicode)):
         rdclass = dns.rdataclass.from_text(rdclass)
-    if isinstance(rdtype, str):
+    if isinstance(rdtype, (str, unicode)):
         rdtype = dns.rdatatype.from_text(rdtype)
     r = Rdataset(rdclass, rdtype)
     r.update_ttl(ttl)
index cd0e5f804bc70acf90416edb4bb8fc2e25b653c5..f803eb6d2087ef7243ee1f28ff830ce8f2d45563 100644 (file)
@@ -569,9 +569,9 @@ class Resolver(object):
 
         if isinstance(qname, (str, unicode)):
             qname = dns.name.from_text(qname, None)
-        if isinstance(rdtype, str):
+        if isinstance(rdtype, (str, unicode)):
             rdtype = dns.rdatatype.from_text(rdtype)
-        if isinstance(rdclass, str):
+        if isinstance(rdclass, (str, unicode)):
             rdclass = dns.rdataclass.from_text(rdclass)
         qnames_to_try = []
         if qname.is_absolute():
@@ -754,9 +754,12 @@ def zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False, resolver=None):
     while 1:
         try:
             answer = resolver.query(name, dns.rdatatype.SOA, rdclass, tcp)
-            return name
+            if answer.rrset.name == name:
+                return name
+            # otherwise we were CNAMEd or DNAMEd and need to look higher
         except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
-            try:
-                name = name.parent()
-            except dns.name.NoParent:
-                raise NoRootSOA
+            pass
+        try:
+            name = name.parent()
+        except dns.name.NoParent:
+            raise NoRootSOA
index 7f6c4afed4a0a2f60328e7720df21d0676f345a3..21468174d4ec188851c410293112ec1d4dc8c9ce 100644 (file)
@@ -36,7 +36,7 @@ class RRset(dns.rdataset.Rdataset):
                  deleting=None):
         """Create a new RRset."""
 
-        super(RRset, self).__init__(rdclass, rdtype)
+        super(RRset, self).__init__(rdclass, rdtype, covers)
         self.name = name
         self.deleting = deleting
 
@@ -124,9 +124,9 @@ def from_text_list(name, ttl, rdclass, rdtype, text_rdatas):
 
     if isinstance(name, (str, unicode)):
         name = dns.name.from_text(name, None)
-    if isinstance(rdclass, str):
+    if isinstance(rdclass, (str, unicode)):
         rdclass = dns.rdataclass.from_text(rdclass)
-    if isinstance(rdtype, str):
+    if isinstance(rdtype, (str, unicode)):
         rdtype = dns.rdatatype.from_text(rdtype)
     r = RRset(name, rdclass, rdtype)
     r.update_ttl(ttl)
index b4deeca859dcaac594acd4733326c17b64f5a4c7..5e58ea884135934481bf80f5d2a84d3a828f89dd 100644 (file)
 
 import hmac
 import struct
+import sys
 
 import dns.exception
+import dns.hash
 import dns.rdataclass
 import dns.name
 
@@ -50,7 +52,16 @@ class PeerBadTruncation(PeerError):
     """Raised if the peer didn't like amount of truncation in the TSIG we sent"""
     pass
 
-default_algorithm = "HMAC-MD5.SIG-ALG.REG.INT"
+# TSIG Algorithms
+
+HMAC_MD5 = dns.name.from_text("HMAC-MD5.SIG-ALG.REG.INT")
+HMAC_SHA1 = dns.name.from_text("hmac-sha1")
+HMAC_SHA224 = dns.name.from_text("hmac-sha224")
+HMAC_SHA256 = dns.name.from_text("hmac-sha256")
+HMAC_SHA384 = dns.name.from_text("hmac-sha384")
+HMAC_SHA512 = dns.name.from_text("hmac-sha512")
+
+default_algorithm = HMAC_MD5
 
 BADSIG = 16
 BADKEY = 17
@@ -167,6 +178,24 @@ def validate(wire, keyname, secret, now, request_mac, tsig_start, tsig_rdata,
         raise BadSignature
     return ctx
 
+_hashes = None
+
+def _maybe_add_hash(tsig_alg, hash_alg):
+    try:
+        _hashes[tsig_alg] = dns.hash.get(hash_alg)
+    except KeyError:
+        pass
+
+def _setup_hashes():
+    global _hashes
+    _hashes = {}
+    _maybe_add_hash(HMAC_SHA224, 'SHA224')
+    _maybe_add_hash(HMAC_SHA256, 'SHA256')
+    _maybe_add_hash(HMAC_SHA384, 'SHA384')
+    _maybe_add_hash(HMAC_SHA512, 'SHA512')
+    _maybe_add_hash(HMAC_SHA1, 'SHA1')
+    _maybe_add_hash(HMAC_MD5, 'MD5')
+
 def get_algorithm(algorithm):
     """Returns the wire format string and the hash module to use for the
     specified TSIG algorithm
@@ -175,42 +204,20 @@ def get_algorithm(algorithm):
     @raises NotImplementedError: I{algorithm} is not supported
     """
 
-    hashes = {}
-    try:
-        import hashlib
-        hashes[dns.name.from_text('hmac-sha224')] = hashlib.sha224
-        hashes[dns.name.from_text('hmac-sha256')] = hashlib.sha256
-        hashes[dns.name.from_text('hmac-sha384')] = hashlib.sha384
-        hashes[dns.name.from_text('hmac-sha512')] = hashlib.sha512
-        hashes[dns.name.from_text('hmac-sha1')] = hashlib.sha1
-        hashes[dns.name.from_text('HMAC-MD5.SIG-ALG.REG.INT')] = hashlib.md5
-
-        import sys
-        if sys.hexversion < 0x02050000:
-            # hashlib doesn't conform to PEP 247: API for
-            # Cryptographic Hash Functions, which hmac before python
-            # 2.5 requires, so add the necessary items.
-            class HashlibWrapper:
-                def __init__(self, basehash):
-                    self.basehash = basehash
-                    self.digest_size = self.basehash().digest_size
-
-                def new(self, *args, **kwargs):
-                    return self.basehash(*args, **kwargs)
-
-            for name in hashes:
-                hashes[name] = HashlibWrapper(hashes[name])
-
-    except ImportError:
-        import md5, sha
-        hashes[dns.name.from_text('HMAC-MD5.SIG-ALG.REG.INT')] =  md5.md5
-        hashes[dns.name.from_text('hmac-sha1')] = sha.sha
+    global _hashes
+    if _hashes is None:
+        _setup_hashes()
 
     if isinstance(algorithm, (str, unicode)):
         algorithm = dns.name.from_text(algorithm)
 
-    if algorithm in hashes:
-        return (algorithm.to_digestable(), hashes[algorithm])
+    if sys.hexversion < 0x02050200 and \
+       (algorithm == HMAC_SHA384 or algorithm == HMAC_SHA512):
+        raise NotImplementedError("TSIG algorithm " + str(algorithm) +
+                                  " requires Python 2.5.2 or later")
 
-    raise NotImplementedError("TSIG algorithm " + str(algorithm) +
-                              " is not supported")
+    try:
+        return (algorithm.to_digestable(), _hashes[algorithm])
+    except KeyError:
+        raise NotImplementedError("TSIG algorithm " + str(algorithm) +
+                                  " is not supported")
index 97aea18fb9cbdd3549869bf3997917670196767f..e67acafec9304cd77483be0c483234f8d77b26c9 100644 (file)
@@ -21,6 +21,7 @@ import dns.opcode
 import dns.rdata
 import dns.rdataclass
 import dns.rdataset
+import dns.tsig
 
 class Update(dns.message.Message):
     def __init__(self, zone, rdclass=dns.rdataclass.IN, keyring=None,
@@ -42,7 +43,10 @@ class Update(dns.message.Message):
         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
+        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
         """
         super(Update, self).__init__()
@@ -148,7 +152,7 @@ class Update(dns.message.Message):
                     self._add_rr(name, 0, rd, dns.rdataclass.NONE)
             else:
                 rdtype = args.pop(0)
-                if isinstance(rdtype, str):
+                if isinstance(rdtype, (str, unicode)):
                     rdtype = dns.rdatatype.from_text(rdtype)
                 if len(args) == 0:
                     rrset = self.find_rrset(self.authority, name,
@@ -206,7 +210,7 @@ class Update(dns.message.Message):
             self._add(False, self.answer, name, *args)
         else:
             rdtype = args[0]
-            if isinstance(rdtype, str):
+            if isinstance(rdtype, (str, unicode)):
                 rdtype = dns.rdatatype.from_text(rdtype)
             rrset = self.find_rrset(self.answer, name,
                                     dns.rdataclass.ANY, rdtype,
@@ -225,7 +229,7 @@ class Update(dns.message.Message):
                                     dns.rdatatype.NONE, None,
                                     True, True)
         else:
-            if isinstance(rdtype, str):
+            if isinstance(rdtype, (str, unicode)):
                 rdtype = dns.rdatatype.from_text(rdtype)
             rrset = self.find_rrset(self.answer, name,
                                     dns.rdataclass.NONE, rdtype,
index dd135a13e520b952b784b4ffb1f3e0d7b9623a01..fe0e324217be94fddf9604d2c16d0fcd9bfaa1f2 100644 (file)
@@ -16,8 +16,8 @@
 """dnspython release version information."""
 
 MAJOR = 1
-MINOR = 8
-MICRO = 1
+MINOR = 9
+MICRO = 2
 RELEASELEVEL = 0x0f
 SERIAL = 0
 
index 93c157d8f01d22f5f94eba6a5e9d503465a8554d..db5fd5df85bc48489d0ad84e2dc0fd42c0fd8969 100644 (file)
@@ -237,9 +237,9 @@ class Zone(object):
         """
 
         name = self._validate_name(name)
-        if isinstance(rdtype, str):
+        if isinstance(rdtype, (str, unicode)):
             rdtype = dns.rdatatype.from_text(rdtype)
-        if isinstance(covers, str):
+        if isinstance(covers, (str, unicode)):
             covers = dns.rdatatype.from_text(covers)
         node = self.find_node(name, create)
         return node.find_rdataset(self.rdclass, rdtype, covers, create)
@@ -300,9 +300,9 @@ class Zone(object):
         """
 
         name = self._validate_name(name)
-        if isinstance(rdtype, str):
+        if isinstance(rdtype, (str, unicode)):
             rdtype = dns.rdatatype.from_text(rdtype)
-        if isinstance(covers, str):
+        if isinstance(covers, (str, unicode)):
             covers = dns.rdatatype.from_text(covers)
         node = self.get_node(name)
         if not node is None:
@@ -363,9 +363,9 @@ class Zone(object):
         """
 
         name = self._validate_name(name)
-        if isinstance(rdtype, str):
+        if isinstance(rdtype, (str, unicode)):
             rdtype = dns.rdatatype.from_text(rdtype)
-        if isinstance(covers, str):
+        if isinstance(covers, (str, unicode)):
             covers = dns.rdatatype.from_text(covers)
         rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
         rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
@@ -419,9 +419,9 @@ class Zone(object):
         @type covers: int or string
         """
 
-        if isinstance(rdtype, str):
+        if isinstance(rdtype, (str, unicode)):
             rdtype = dns.rdatatype.from_text(rdtype)
-        if isinstance(covers, str):
+        if isinstance(covers, (str, unicode)):
             covers = dns.rdatatype.from_text(covers)
         for (name, node) in self.iteritems():
             for rds in node:
@@ -442,9 +442,9 @@ class Zone(object):
         @type covers: int or string
         """
 
-        if isinstance(rdtype, str):
+        if isinstance(rdtype, (str, unicode)):
             rdtype = dns.rdatatype.from_text(rdtype)
-        if isinstance(covers, str):
+        if isinstance(covers, (str, unicode)):
             covers = dns.rdatatype.from_text(covers)
         for (name, node) in self.iteritems():
             for rds in node:
index 27a5b932f47cc77effad07792707c124ccc90a2a..84814b73cf6cffa9edd7c120c0388f2fe7db0ad9 100755 (executable)
@@ -16,7 +16,7 @@
 #
 #      DEVICE=$1
 #
-#      if [ "X${DEVICE}" = "Xeth0" ]; then
+#      if [ "X${DEVICE}" == "Xeth0" ]; then
 #              IPADDR=`LANG= LC_ALL= ifconfig ${DEVICE} | grep 'inet addr' |
 #                      awk -F: '{ print $2 } ' | awk '{ print $1 }'`
 #              /usr/local/sbin/ddns.py $IPADDR
diff --git a/lib/dnspython/examples/zonediff.py b/lib/dnspython/examples/zonediff.py
new file mode 100755 (executable)
index 0000000..ad81fb1
--- /dev/null
@@ -0,0 +1,270 @@
+#!/usr/bin/env python
+# 
+# Small library and commandline tool to do logical diffs of zonefiles
+# ./zonediff -h gives you help output
+#
+# Requires dnspython to do all the heavy lifting
+#
+# (c)2009 Dennis Kaarsemaker <dennis@kaarsemaker.net>
+#
+# 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 THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR 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.
+"""See diff_zones.__doc__ for more information"""
+
+__all__ = ['diff_zones', 'format_changes_plain', 'format_changes_html']
+
+try:
+    import dns.zone
+except ImportError:
+    import sys
+    sys.stderr.write("Please install dnspython")
+    sys.exit(1)
+
+def diff_zones(zone1, zone2, ignore_ttl=False, ignore_soa=False):
+    """diff_zones(zone1, zone2, ignore_ttl=False, ignore_soa=False) -> changes
+    Compares two dns.zone.Zone objects and returns a list of all changes
+    in the format (name, oldnode, newnode).
+
+    If ignore_ttl is true, a node will not be added to this list if the
+    only change is its TTL.
+    
+    If ignore_soa is true, a node will not be added to this list if the
+    only changes is a change in a SOA Rdata set.
+
+    The returned nodes do include all Rdata sets, including unchanged ones.
+    """
+
+    changes = []
+    for name in zone1:
+        name = str(name)
+        n1 = zone1.get_node(name)
+        n2 = zone2.get_node(name)
+        if not n2:
+            changes.append((str(name), n1, n2))
+        elif _nodes_differ(n1, n2, ignore_ttl, ignore_soa):
+            changes.append((str(name), n1, n2))
+
+    for name in zone2:
+        n1 = zone1.get_node(name)
+        if not n1:
+            n2 = zone2.get_node(name)
+            changes.append((str(name), n1, n2))
+    return changes
+
+def _nodes_differ(n1, n2, ignore_ttl, ignore_soa):
+    if ignore_soa or not ignore_ttl:
+        # Compare datasets directly
+        for r in n1.rdatasets:
+            if ignore_soa and r.rdtype == dns.rdatatype.SOA:
+                continue
+            if r not in n2.rdatasets:
+                return True
+            if not ignore_ttl:
+                return r.ttl != n2.find_rdataset(r.rdclass, r.rdtype).ttl
+
+        for r in n2.rdatasets:
+            if ignore_soa and r.rdtype == dns.rdatatype.SOA:
+                continue
+            if r not in n1.rdatasets:
+                return True
+    else:
+        return n1 != n2
+
+def format_changes_plain(oldf, newf, changes, ignore_ttl=False):
+    """format_changes(oldfile, newfile, changes, ignore_ttl=False) -> str
+    Given 2 filenames and a list of changes from diff_zones, produce diff-like
+    output. If ignore_ttl is True, TTL-only changes are not displayed"""
+
+    ret = "--- %s\n+++ %s\n" % (oldf, newf)
+    for name, old, new in changes:
+        ret +=  "@ %s\n" % name
+        if not old:
+            for r in new.rdatasets:
+                ret += "+ %s\n" % str(r).replace('\n','\n+ ')
+        elif not new:
+            for r in old.rdatasets:
+                ret += "- %s\n" % str(r).replace('\n','\n+ ')
+        else:
+            for r in old.rdatasets:
+                if r not in new.rdatasets or (r.ttl != new.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl):
+                    ret += "- %s\n" % str(r).replace('\n','\n+ ')
+            for r in new.rdatasets:
+                if r not in old.rdatasets or (r.ttl != old.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl):
+                    ret += "+ %s\n" % str(r).replace('\n','\n+ ')
+    return ret
+
+def format_changes_html(oldf, newf, changes, ignore_ttl=False):
+    """format_changes(oldfile, newfile, changes, ignore_ttl=False) -> str
+    Given 2 filenames and a list of changes from diff_zones, produce nice html
+    output. If ignore_ttl is True, TTL-only changes are not displayed"""
+
+    ret = '''<table class="zonediff">
+  <thead>
+    <tr>
+      <th>&nbsp;</th>
+      <th class="old">%s</th>
+      <th class="new">%s</th>
+    </tr>
+  </thead>
+  <tbody>\n''' % (oldf, newf)
+
+    for name, old, new in changes:
+        ret +=  '    <tr class="rdata">\n      <td class="rdname">%s</td>\n' % name
+        if not old:
+            for r in new.rdatasets:
+                ret += '      <td class="old">&nbsp;</td>\n      <td class="new">%s</td>\n' % str(r).replace('\n','<br />')
+        elif not new:
+            for r in old.rdatasets:
+                ret += '      <td class="old">%s</td>\n      <td class="new">&nbsp;</td>\n' % str(r).replace('\n','<br />')
+        else:
+            ret += '      <td class="old">'
+            for r in old.rdatasets:
+                if r not in new.rdatasets or (r.ttl != new.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl):
+                    ret += str(r).replace('\n','<br />')
+            ret += '</td>\n'
+            ret += '      <td class="new">'
+            for r in new.rdatasets:
+                if r not in old.rdatasets or (r.ttl != old.find_rdataset(r.rdclass, r.rdtype).ttl and not ignore_ttl):
+                    ret += str(r).replace('\n','<br />')
+            ret += '</td>\n'
+        ret += '    </tr>\n'
+    return ret + '  </tbody>\n</table>'
+
+# Make this module usable as a script too.
+if __name__ == '__main__':
+    import optparse
+    import subprocess
+    import sys
+    import traceback
+
+    usage = """%prog zonefile1 zonefile2 - Show differences between zones in a diff-like format
+%prog [--git|--bzr|--rcs] zonefile rev1 [rev2] - Show differences between two revisions of a zonefile
+
+The differences shown will be logical differences, not textual differences.
+"""
+    p = optparse.OptionParser(usage=usage)
+    p.add_option('-s', '--ignore-soa', action="store_true", default=False, dest="ignore_soa",
+                 help="Ignore SOA-only changes to records")
+    p.add_option('-t', '--ignore-ttl', action="store_true", default=False, dest="ignore_ttl",
+                 help="Ignore TTL-only changes to Rdata")
+    p.add_option('-T', '--traceback', action="store_true", default=False, dest="tracebacks",
+                 help="Show python tracebacks when errors occur")
+    p.add_option('-H', '--html', action="store_true", default=False, dest="html",
+                 help="Print HTML output")
+    p.add_option('-g', '--git', action="store_true", default=False, dest="use_git",
+                 help="Use git revisions instead of real files")
+    p.add_option('-b', '--bzr', action="store_true", default=False, dest="use_bzr",
+                 help="Use bzr revisions instead of real files")
+    p.add_option('-r', '--rcs', action="store_true", default=False, dest="use_rcs",
+                 help="Use rcs revisions instead of real files")
+    opts, args = p.parse_args()
+    opts.use_vc = opts.use_git or opts.use_bzr or opts.use_rcs
+
+    def _open(what, err):
+        if isinstance(what, basestring):
+            # Open as normal file
+            try:
+                return open(what, 'rb')
+            except:
+                sys.stderr.write(err + "\n")
+                if opts.tracebacks:
+                    traceback.print_exc()
+        else:
+            # Must be a list, open subprocess
+            try:
+                proc = subprocess.Popen(what, stdout=subprocess.PIPE)
+                proc.wait()
+                if proc.returncode == 0:
+                    return proc.stdout
+                sys.stderr.write(err + "\n")
+            except:
+                sys.stderr.write(err + "\n")
+                if opts.tracebacks:
+                    traceback.print_exc()
+
+    if not opts.use_vc and len(args) != 2:
+        p.print_help()
+        sys.exit(64)
+    if opts.use_vc and len(args) not in (2,3):
+        p.print_help()
+        sys.exit(64)
+
+    # Open file desriptors
+    if not opts.use_vc:
+        oldn, newn = args
+    else:
+        if len(args) == 3:
+            filename, oldr, newr = args
+            oldn = "%s:%s" % (oldr, filename)
+            newn = "%s:%s" % (newr, filename)
+        else:
+            filename, oldr = args
+            newr = None
+            oldn = "%s:%s" % (oldr, filename)
+            newn = filename
+
+        
+    old, new = None, None
+    oldz, newz = None, None
+    if opts.use_bzr:
+        old = _open(["bzr", "cat", "-r" + oldr, filename],
+                    "Unable to retrieve revision %s of %s" % (oldr, filename))
+        if newr != None:
+            new = _open(["bzr", "cat", "-r" + newr, filename],
+                        "Unable to retrieve revision %s of %s" % (newr, filename))
+    elif opts.use_git:
+        old = _open(["git", "show", oldn],
+                    "Unable to retrieve revision %s of %s" % (oldr, filename))
+        if newr != None:
+            new = _open(["git", "show", newn],
+                        "Unable to retrieve revision %s of %s" % (newr, filename))
+    elif opts.use_rcs:
+        old = _open(["co", "-q", "-p", "-r" + oldr, filename],
+                    "Unable to retrieve revision %s of %s" % (oldr, filename))
+        if newr != None:
+            new = _open(["co", "-q", "-p", "-r" + newr, filename],
+                        "Unable to retrieve revision %s of %s" % (newr, filename))
+    if not opts.use_vc:
+        old = _open(oldn, "Unable to open %s" % oldn)
+    if not opts.use_vc or newr == None:
+        new = _open(newn, "Unable to open %s" % newn)
+
+    if not old or not new:
+        sys.exit(65)
+
+    # Parse the zones
+    try:
+        oldz = dns.zone.from_file(old, origin = '.', check_origin=False)
+    except dns.exception.DNSException:
+        sys.stderr.write("Incorrect zonefile: %s\n", old)
+        if opts.tracebacks:
+            traceback.print_exc()
+    try:
+        newz = dns.zone.from_file(new, origin = '.', check_origin=False)
+    except dns.exception.DNSException:
+        sys.stderr.write("Incorrect zonefile: %s\n" % new)
+        if opts.tracebacks:
+            traceback.print_exc()
+    if not oldz or not newz:
+        sys.exit(65)
+
+    changes = diff_zones(oldz, newz, opts.ignore_ttl, opts.ignore_soa)
+    changes.sort()
+
+    if not changes:
+        sys.exit(0)
+    if opts.html:
+        print format_changes_html(oldn, newn, changes, opts.ignore_ttl)
+    else:
+        print format_changes_plain(oldn, newn, changes, opts.ignore_ttl)
+    sys.exit(1)
index 21ebddfb5921eb00dc3c13bb81cd150615710b63..59bd0ebc952d748e2b76b5f5618d9f8df8433259 100755 (executable)
@@ -18,7 +18,7 @@
 import sys
 from distutils.core import setup
 
-version = '1.8.1'
+version = '1.9.2'
 
 kwargs = {
     'name' : 'dnspython',
diff --git a/lib/dnspython/tests/dnssec.py b/lib/dnspython/tests/dnssec.py
new file mode 100644 (file)
index 0000000..b30e847
--- /dev/null
@@ -0,0 +1,146 @@
+# Copyright (C) 2010 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 unittest
+
+import dns.dnssec
+import dns.name
+import dns.rdata
+import dns.rdataclass
+import dns.rdatatype
+import dns.rrset
+
+abs_dnspython_org = dns.name.from_text('dnspython.org')
+
+abs_keys = { abs_dnspython_org :
+             dns.rrset.from_text('dnspython.org.', 3600, 'IN', 'DNSKEY',
+                                 '257 3 5 AwEAAenVTr9L1OMlL1/N2ta0Qj9LLLnnmFWIr1dJoAsWM9BQfsbV7kFZ XbAkER/FY9Ji2o7cELxBwAsVBuWn6IUUAJXLH74YbC1anY0lifjgt29z SwDzuB7zmC7yVYZzUunBulVW4zT0tg1aePbpVL2EtTL8VzREqbJbE25R KuQYHZtFwG8S4iBxJUmT2Bbd0921LLxSQgVoFXlQx/gFV2+UERXcJ5ce iX6A6wc02M/pdg/YbJd2rBa0MYL3/Fz/Xltre0tqsImZGxzi6YtYDs45 NC8gH+44egz82e2DATCVM1ICPmRDjXYTLldQiWA2ZXIWnK0iitl5ue24 7EsWJefrIhE=',
+                                 '256 3 5 AwEAAdSSghOGjU33IQZgwZM2Hh771VGXX05olJK49FxpSyuEAjDBXY58 LGU9R2Zgeecnk/b9EAhFu/vCV9oECtiTCvwuVAkt9YEweqYDluQInmgP NGMJCKdSLlnX93DkjDw8rMYv5dqXCuSGPlKChfTJOLQxIAxGloS7lL+c 0CTZydAF')
+         }
+
+rel_keys = { dns.name.empty :
+             dns.rrset.from_text('@', 3600, 'IN', 'DNSKEY',
+                                 '257 3 5 AwEAAenVTr9L1OMlL1/N2ta0Qj9LLLnnmFWIr1dJoAsWM9BQfsbV7kFZ XbAkER/FY9Ji2o7cELxBwAsVBuWn6IUUAJXLH74YbC1anY0lifjgt29z SwDzuB7zmC7yVYZzUunBulVW4zT0tg1aePbpVL2EtTL8VzREqbJbE25R KuQYHZtFwG8S4iBxJUmT2Bbd0921LLxSQgVoFXlQx/gFV2+UERXcJ5ce iX6A6wc02M/pdg/YbJd2rBa0MYL3/Fz/Xltre0tqsImZGxzi6YtYDs45 NC8gH+44egz82e2DATCVM1ICPmRDjXYTLldQiWA2ZXIWnK0iitl5ue24 7EsWJefrIhE=',
+                                 '256 3 5 AwEAAdSSghOGjU33IQZgwZM2Hh771VGXX05olJK49FxpSyuEAjDBXY58 LGU9R2Zgeecnk/b9EAhFu/vCV9oECtiTCvwuVAkt9YEweqYDluQInmgP NGMJCKdSLlnX93DkjDw8rMYv5dqXCuSGPlKChfTJOLQxIAxGloS7lL+c 0CTZydAF')
+         }
+
+when = 1290250287
+
+abs_soa = dns.rrset.from_text('dnspython.org.', 3600, 'IN', 'SOA',
+                              'howl.dnspython.org. hostmaster.dnspython.org. 2010020047 3600 1800 604800 3600')
+
+abs_other_soa = dns.rrset.from_text('dnspython.org.', 3600, 'IN', 'SOA',
+                                    'foo.dnspython.org. hostmaster.dnspython.org. 2010020047 3600 1800 604800 3600')
+
+abs_soa_rrsig = dns.rrset.from_text('dnspython.org.', 3600, 'IN', 'RRSIG',
+                                    'SOA 5 2 3600 20101127004331 20101119213831 61695 dnspython.org. sDUlltRlFTQw5ITFxOXW3TgmrHeMeNpdqcZ4EXxM9FHhIlte6V9YCnDw t6dvM9jAXdIEi03l9H/RAd9xNNW6gvGMHsBGzpvvqFQxIBR2PoiZA1mX /SWHZFdbt4xjYTtXqpyYvrMK0Dt7bUYPadyhPFCJ1B+I8Zi7B5WJEOd0 8vs=')
+
+rel_soa = dns.rrset.from_text('@', 3600, 'IN', 'SOA',
+                              'howl hostmaster 2010020047 3600 1800 604800 3600')
+
+rel_other_soa = dns.rrset.from_text('@', 3600, 'IN', 'SOA',
+                                    'foo hostmaster 2010020047 3600 1800 604800 3600')
+
+rel_soa_rrsig = dns.rrset.from_text('@', 3600, 'IN', 'RRSIG',
+                                    'SOA 5 2 3600 20101127004331 20101119213831 61695 @ sDUlltRlFTQw5ITFxOXW3TgmrHeMeNpdqcZ4EXxM9FHhIlte6V9YCnDw t6dvM9jAXdIEi03l9H/RAd9xNNW6gvGMHsBGzpvvqFQxIBR2PoiZA1mX /SWHZFdbt4xjYTtXqpyYvrMK0Dt7bUYPadyhPFCJ1B+I8Zi7B5WJEOd0 8vs=')
+
+sep_key = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DNSKEY,
+                              '257 3 5 AwEAAenVTr9L1OMlL1/N2ta0Qj9LLLnnmFWIr1dJoAsWM9BQfsbV7kFZ XbAkER/FY9Ji2o7cELxBwAsVBuWn6IUUAJXLH74YbC1anY0lifjgt29z SwDzuB7zmC7yVYZzUunBulVW4zT0tg1aePbpVL2EtTL8VzREqbJbE25R KuQYHZtFwG8S4iBxJUmT2Bbd0921LLxSQgVoFXlQx/gFV2+UERXcJ5ce iX6A6wc02M/pdg/YbJd2rBa0MYL3/Fz/Xltre0tqsImZGxzi6YtYDs45 NC8gH+44egz82e2DATCVM1ICPmRDjXYTLldQiWA2ZXIWnK0iitl5ue24 7EsWJefrIhE=')
+
+good_ds = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS,
+                              '57349 5 2 53A79A3E7488AB44FFC56B2D1109F0699D1796DD977E72108B841F96 E47D7013')
+
+when2 = 1290425644
+
+abs_example = dns.name.from_text('example')
+
+abs_dsa_keys = { abs_example :
+                 dns.rrset.from_text('example.', 86400, 'IN', 'DNSKEY',
+                                     '257 3 3 CI3nCqyJsiCJHTjrNsJOT4RaszetzcJPYuoH3F9ZTVt3KJXncCVR3bwn 1w0iavKljb9hDlAYSfHbFCp4ic/rvg4p1L8vh5s8ToMjqDNl40A0hUGQ Ybx5hsECyK+qHoajilUX1phYSAD8d9WAGO3fDWzUPBuzR7o85NiZCDxz yXuNVfni0uhj9n1KYhEO5yAbbruDGN89wIZcxMKuQsdUY2GYD93ssnBv a55W6XRABYWayKZ90WkRVODLVYLSn53Pj/wwxGH+XdhIAZJXimrZL4yl My7rtBsLMqq8Ihs4Tows7LqYwY7cp6y/50tw6pj8tFqMYcPUjKZV36l1 M/2t5BVg3i7IK61Aidt6aoC3TDJtzAxg3ZxfjZWJfhHjMJqzQIfbW5b9 q1mjFsW5EUv39RaNnX+3JWPRLyDqD4pIwDyqfutMsdk/Py3paHn82FGp CaOg+nicqZ9TiMZURN/XXy5JoXUNQ3RNvbHCUiPUe18KUkY6mTfnyHld 1l9YCWmzXQVClkx/hOYxjJ4j8Ife58+Obu5X',
+                                     '256 3 3 CJE1yb9YRQiw5d2xZrMUMR+cGCTt1bp1KDCefmYKmS+Z1+q9f42ETVhx JRiQwXclYwmxborzIkSZegTNYIV6mrYwbNB27Q44c3UGcspb3PiOw5TC jNPRYEcdwGvDZ2wWy+vkSV/S9tHXY8O6ODiE6abZJDDg/RnITyi+eoDL R3KZ5n/V1f1T1b90rrV6EewhBGQJpQGDogaXb2oHww9Tm6NfXyo7SoMM pbwbzOckXv+GxRPJIQNSF4D4A9E8XCksuzVVdE/0lr37+uoiAiPia38U 5W2QWe/FJAEPLjIp2eTzf0TrADc1pKP1wrA2ASpdzpm/aX3IB5RPp8Ew S9U72eBFZJAUwg635HxJVxH1maG6atzorR566E+e0OZSaxXS9o1o6QqN 3oPlYLGPORDiExilKfez3C/x/yioOupW9K5eKF0gmtaqrHX0oq9s67f/ RIM2xVaKHgG9Vf2cgJIZkhv7sntujr+E4htnRmy9P9BxyFxsItYxPI6Z bzygHAZpGhlI/7ltEGlIwKxyTK3ZKBm67q7B')
+                 }
+
+abs_dsa_soa = dns.rrset.from_text('example.', 86400, 'IN', 'SOA',
+                                  'ns1.example. hostmaster.example. 2 10800 3600 604800 86400')
+
+abs_other_dsa_soa = dns.rrset.from_text('example.', 86400, 'IN', 'SOA',
+                                        'ns1.example. hostmaster.example. 2 10800 3600 604800 86401')
+
+abs_dsa_soa_rrsig = dns.rrset.from_text('example.', 86400, 'IN', 'RRSIG',
+                                        'SOA 3 1 86400 20101129143231 20101122112731 42088 example. CGul9SuBofsktunV8cJs4eRs6u+3NCS3yaPKvBbD+pB2C76OUXDZq9U=')
+
+example_sep_key = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DNSKEY,
+                                      '257 3 3 CI3nCqyJsiCJHTjrNsJOT4RaszetzcJPYuoH3F9ZTVt3KJXncCVR3bwn 1w0iavKljb9hDlAYSfHbFCp4ic/rvg4p1L8vh5s8ToMjqDNl40A0hUGQ Ybx5hsECyK+qHoajilUX1phYSAD8d9WAGO3fDWzUPBuzR7o85NiZCDxz yXuNVfni0uhj9n1KYhEO5yAbbruDGN89wIZcxMKuQsdUY2GYD93ssnBv a55W6XRABYWayKZ90WkRVODLVYLSn53Pj/wwxGH+XdhIAZJXimrZL4yl My7rtBsLMqq8Ihs4Tows7LqYwY7cp6y/50tw6pj8tFqMYcPUjKZV36l1 M/2t5BVg3i7IK61Aidt6aoC3TDJtzAxg3ZxfjZWJfhHjMJqzQIfbW5b9 q1mjFsW5EUv39RaNnX+3JWPRLyDqD4pIwDyqfutMsdk/Py3paHn82FGp CaOg+nicqZ9TiMZURN/XXy5JoXUNQ3RNvbHCUiPUe18KUkY6mTfnyHld 1l9YCWmzXQVClkx/hOYxjJ4j8Ife58+Obu5X')
+
+example_ds_sha1 = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS,
+                                      '18673 3 1 71b71d4f3e11bbd71b4eff12cde69f7f9215bbe7')
+
+example_ds_sha256 = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS,
+                                        '18673 3 2 eb8344cbbf07c9d3d3d6c81d10c76653e28d8611a65e639ef8f716e4e4e5d913')
+
+class DNSSECValidatorTestCase(unittest.TestCase):
+
+    def testAbsoluteRSAGood(self):
+        dns.dnssec.validate(abs_soa, abs_soa_rrsig, abs_keys, None, when)
+
+    def testAbsoluteRSABad(self):
+        def bad():
+            dns.dnssec.validate(abs_other_soa, abs_soa_rrsig, abs_keys, None,
+                                when)
+        self.failUnlessRaises(dns.dnssec.ValidationFailure, bad)
+
+    def testRelativeRSAGood(self):
+        dns.dnssec.validate(rel_soa, rel_soa_rrsig, rel_keys,
+                            abs_dnspython_org, when)
+
+    def testRelativeRSABad(self):
+        def bad():
+            dns.dnssec.validate(rel_other_soa, rel_soa_rrsig, rel_keys,
+                                abs_dnspython_org, when)
+        self.failUnlessRaises(dns.dnssec.ValidationFailure, bad)
+
+    def testMakeSHA256DS(self):
+        ds = dns.dnssec.make_ds(abs_dnspython_org, sep_key, 'SHA256')
+        self.failUnless(ds == good_ds)
+
+    def testAbsoluteDSAGood(self):
+        dns.dnssec.validate(abs_dsa_soa, abs_dsa_soa_rrsig, abs_dsa_keys, None,
+                            when2)
+
+    def testAbsoluteDSABad(self):
+        def bad():
+            dns.dnssec.validate(abs_other_dsa_soa, abs_dsa_soa_rrsig,
+                                abs_dsa_keys, None, when2)
+        self.failUnlessRaises(dns.dnssec.ValidationFailure, bad)
+
+    def testMakeExampleSHA1DS(self):
+        ds = dns.dnssec.make_ds(abs_example, example_sep_key, 'SHA1')
+        self.failUnless(ds == example_ds_sha1)
+
+    def testMakeExampleSHA256DS(self):
+        ds = dns.dnssec.make_ds(abs_example, example_sep_key, 'SHA256')
+        self.failUnless(ds == example_ds_sha256)
+
+if __name__ == '__main__':
+    import_ok = False
+    try:
+        import Crypto.Util.number
+        import_ok = True
+    except:
+        pass
+    if import_ok:
+        unittest.main()
+    else:
+        print 'skipping DNSSEC tests because pycrypto is not installed'
index 4cacbdc79d95e747448dcb08def95bca3edf54d1..bd6dc5fbc2725f430a8df5b1157004863bf38daf 100644 (file)
@@ -14,6 +14,7 @@
 # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 import cStringIO
+import select
 import sys
 import time
 import unittest
@@ -46,7 +47,7 @@ example. 1 IN A 10.0.0.1
 ;ADDITIONAL
 """
 
-class ResolverTestCase(unittest.TestCase):
+class BaseResolverTests(object):
 
     if sys.platform != 'win32':
         def testRead(self):
@@ -101,5 +102,26 @@ class ResolverTestCase(unittest.TestCase):
             zname = dns.resolver.zone_for_name(name)
         self.failUnlessRaises(dns.resolver.NotAbsolute, bad)
 
+class PollingMonkeyPatchMixin(object):
+    def setUp(self):
+        self.__native_polling_backend = dns.query._polling_backend
+        dns.query._set_polling_backend(self.polling_backend())
+
+        unittest.TestCase.setUp(self)
+
+    def tearDown(self):
+        dns.query._set_polling_backend(self.__native_polling_backend)
+
+        unittest.TestCase.tearDown(self)
+
+class SelectResolverTestCase(PollingMonkeyPatchMixin, BaseResolverTests, unittest.TestCase):
+    def polling_backend(self):
+        return dns.query._select_for
+
+if hasattr(select, 'poll'):
+    class PollResolverTestCase(PollingMonkeyPatchMixin, BaseResolverTests, unittest.TestCase):
+        def polling_backend(self):
+            return dns.query._poll_for
+
 if __name__ == '__main__':
     unittest.main()