]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
merge 1.9.2 master
authorBob Halley <halley@nominum.com>
Sun, 12 Dec 2010 17:25:58 +0000 (09:25 -0800)
committerBob Halley <halley@nominum.com>
Sun, 12 Dec 2010 17:25:58 +0000 (09:25 -0800)
17 files changed:
ChangeLog
README
dns/__init__.py
dns/dnssec.py
dns/hash.py [new file with mode: 0644]
dns/message.py
dns/query.py
dns/rdata.py
dns/resolver.py
dns/rrset.py
dns/tsig.py
dns/update.py
dns/version.py
examples/zonediff.py [new file with mode: 0755]
setup.py
tests/dnssec.py [new file with mode: 0644]
tests/resolver.py

index 73a66edef68ca86dd362791e1e5029310f9a9d40..6a07424d40e2593b9dd87b66266f99bc05a32b2c 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,88 @@
+2010-12-10  Bob Halley  <halley@dnspython.org>
+
+       * dns/resolver.py (Resolver.query): disallow metaqueries.
+
+       * dns/rdata.py (Rdata.__hash__): Added a __hash__ method for rdata.
+
+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/README b/README
index b313d1c132e6852ab275a6fb474c6de73693748d..bfc75b225d5c5619b06cc317aaff1f4583c81729 100644 (file)
--- a/README
+++ b/README
@@ -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 3.1 or later.
 
 
 INSTALLATION
index aef737e7e163a09205ac64201eac876319c9b137..87370581d8da8beec646a042933b79a2890edb58 100644 (file)
@@ -22,6 +22,7 @@ __all__ = [
     'entropy',
     'exception',
     'flags',
+    'hash',
     'inet',
     'ipv4',
     'ipv6',
index 5e3d49878a9231cfe7a0e3d2823630f8d781eecf..9c1d5f3da2f7f2d42e82c51676da973c1a523f5e 100644 (file)
 
 """Common DNSSEC-related functions and constants."""
 
+import io
+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.items()])
 
-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,292 @@ 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 = io.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 (rdata[-3] << 8) + rdata[-2]
+    else:
+        total = 0
+        for i in range(len(rdata) / 2):
+            total += (rdata[2 * i] << 8) + rdata[2 * i + 1]
+        if len(rdata) % 2 != 0:
+            total += 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 bytes(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 = bytes(0) + bytes(1) + bytes(0xFF) * padlen + bytes(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:
+            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/dns/hash.py b/dns/hash.py
new file mode 100644 (file)
index 0000000..00f979b
--- /dev/null
@@ -0,0 +1,30 @@
+# 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
+import hashlib
+
+_hashes = {'MD5' : hashlib.md5,
+           'SHA1' : hashlib.sha1,
+           'SHA224' : hashlib.sha224,
+           'SHA256' : hashlib.sha256,
+           'SHA384' : hashlib.sha384,
+           'SHA512' : hashlib.sha512,
+           }
+
+def get(algorithm):
+    return _hashes[algorithm.upper()]
index 54803e140c38f599f037d25a81d30f879d11e2a6..ca17e5dea66a70a7d5961b807a5ed29de2ee1d7b 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
index cda2309e012f4a0e032be0cb429b440afc3c3f35..05651984e4ef479739a0bdabbecffb3b00be2278 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 as 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
index fe4547dfe35d12f83c7f3d5a15bd1d839d0f54a5..1feec760a86db109a9a4d23ad20d4870c33cbe5e 100644 (file)
@@ -29,6 +29,7 @@ import base64
 import io
 
 import dns.exception
+import dns.name
 import dns.rdataclass
 import dns.rdatatype
 import dns.tokenizer
@@ -256,6 +257,9 @@ class Rdata(object):
             return dns.util.cmp((self.rdclass, self.rdtype), (other.rdclass, other.rdtype))
         return self._cmp(other) > 0
 
+    def __hash__(self):
+        return hash(self.to_digestable(dns.name.root))
+
     @classmethod
     def from_text(cls, rdclass, rdtype, tok, origin = None, relativize = True):
         """Build an rdata object from text format.
index 3512b4b8785cbfc109936fad630eb6b8f1c66e62..db8a1b5da04f7936d12b60480d0aa516a70e517f 100644 (file)
@@ -61,6 +61,9 @@ class NoRootSOA(dns.exception.DNSException):
     This should never happen!"""
     pass
 
+class NoMetaqueries(dns.exception.DNSException):
+    """Metaqueries are not allowed."""
+    pass
 
 class Answer(object):
     """DNS stub resolver answer
@@ -566,8 +569,12 @@ class Resolver(object):
             qname = dns.name.from_text(qname, None)
         if isinstance(rdtype, str):
             rdtype = dns.rdatatype.from_text(rdtype)
+        if dns.rdatatype.is_metatype(rdtype):
+            raise NoMetaqueries
         if isinstance(rdclass, str):
             rdclass = dns.rdataclass.from_text(rdclass)
+        if dns.rdataclass.is_metaclass(rdclass):
+            raise NoMetaqueries
         qnames_to_try = []
         if qname.is_absolute():
             qnames_to_try.append(qname)
@@ -749,9 +756,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 317119248fd350c2836151005d0acd0b78f77a12..0ba7a3e25a52018f44232de097cf34c0992f51d3 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
 
index 2479d09b9cc1bdaff0da75511f3b89d105348acd..e1f706a6b13757778c912e3383b69120cc2e73a8 100644 (file)
@@ -18,6 +18,7 @@
 import hashlib
 import hmac
 import struct
+import sys
 
 import dns.exception
 import dns.rdataclass
@@ -51,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 +177,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,19 +203,15 @@ def get_algorithm(algorithm):
     @raises NotImplementedError: I{algorithm} is not supported
     """
 
-    hashes = {}
-    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
+    global _hashes
+    if _hashes is None:
+        _setup_hashes()
 
-    if isinstance(algorithm, (str, unicode)):
+    if isinstance(algorithm, str):
         algorithm = dns.name.from_text(algorithm)
 
-    if algorithm in hashes:
-        return (algorithm.to_digestable(), hashes[algorithm])
-
-    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 4a1a373928337d368d56dc6756fb43c0e6902344..baad00e9df5450e5e003c5f528bc74c1c55ef542 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__()
index dd135a13e520b952b784b4ffb1f3e0d7b9623a01..46799a77d97ead5836f1de56b44349784b78ecfd 100644 (file)
@@ -16,8 +16,8 @@
 """dnspython release version information."""
 
 MAJOR = 1
-MINOR = 8
-MICRO = 1
+MINOR = 9
+MICRO = 3
 RELEASELEVEL = 0x0f
 SERIAL = 0
 
diff --git a/examples/zonediff.py b/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..f84711f795b50792b8b911dba20e74d5def20ee7 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@
 import sys
 from distutils.core import setup
 
-version = '1.8.1'
+version = '1.9.3'
 
 kwargs = {
     'name' : 'dnspython',
diff --git a/tests/dnssec.py b/tests/dnssec.py
new file mode 100644 (file)
index 0000000..371a30c
--- /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 f29fa28c81b0ebbebc881279073fdb7de0dbe2c2..b88be8c3345d8a4b0bbc0a74b347b3f95cd9c5a6 100644 (file)
@@ -14,6 +14,7 @@
 # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 import io
+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()