From 13147146aa8df4b63b5e0b69545f3bed7ab977a2 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Fri, 26 Nov 2021 15:12:50 -0800 Subject: First draft of CNAME and other data handling in zones. --- dns/node.py | 24 ++++++++- dns/rdataset.py | 23 +++++++++ tests/test_zone.py | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 178 insertions(+), 10 deletions(-) diff --git a/dns/node.py b/dns/node.py index 68c1526..261de37 100644 --- a/dns/node.py +++ b/dns/node.py @@ -78,6 +78,26 @@ class Node: def __iter__(self): return iter(self.rdatasets) + def _append_rdataset(self, rdataset): + """Append rdataset to the node with special handling for CNAME and + other data conditions. + + Specifically, if the rdataset being appended is a CNAME, then + all rdatasets other than NSEC, NSEC3, and their covering RRSIGs + are deleted. If the rdataset being appended is NOT a CNAME, then + CNAME and RRSIG(CNAME) are deleted. + """ + # Make having just one rdataset at the node fast. + if len(self.rdatasets) > 0: + if rdataset.rdtype == dns.rdatatype.CNAME: + self.rdatasets = [rds for rds in self.rdatasets + if rds.ok_for_cname()] + else: + self.rdatasets = [rds for rds in self.rdatasets + if rds.ok_for_other_data()] + self.rdatasets.append(rdataset) + + def find_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE, create=False): """Find an rdataset matching the specified properties in the @@ -111,7 +131,7 @@ class Node: if not create: raise KeyError rds = dns.rdataset.Rdataset(rdclass, rdtype, covers) - self.rdatasets.append(rds) + self._append_rdataset(rds) return rds def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE, @@ -186,4 +206,4 @@ class Node: replacement = replacement.to_rdataset() self.delete_rdataset(replacement.rdclass, replacement.rdtype, replacement.covers) - self.rdatasets.append(replacement) + self._append_rdataset(replacement) diff --git a/dns/rdataset.py b/dns/rdataset.py index e69ee23..e9d8fc2 100644 --- a/dns/rdataset.py +++ b/dns/rdataset.py @@ -41,6 +41,20 @@ class IncompatibleTypes(dns.exception.DNSException): """An attempt was made to add DNS RR data of an incompatible type.""" +_ok_for_cname = { + (dns.rdatatype.CNAME, dns.rdatatype.NONE), + (dns.rdatatype.RRSIG, dns.rdatatype.CNAME), + (dns.rdatatype.NSEC, dns.rdatatype.NONE), + (dns.rdatatype.RRSIG, dns.rdatatype.NSEC), + (dns.rdatatype.NSEC3, dns.rdatatype.NONE), + (dns.rdatatype.RRSIG, dns.rdatatype.NSEC3), +} + +_delete_for_other_data = { + (dns.rdatatype.CNAME, dns.rdatatype.NONE), + (dns.rdatatype.RRSIG, dns.rdatatype.CNAME), +} + class Rdataset(dns.set.Set): """A DNS rdataset.""" @@ -323,6 +337,15 @@ class Rdataset(dns.set.Set): else: return self[0]._processing_order(iter(self)) + def ok_for_cname(self): + """Is this rdataset compatible with a CNAME node?""" + return (self.rdtype, self.covers) in _ok_for_cname + + def ok_for_other_data(self): + """Is this rdataset compatible with an 'other data' (i.e. not CNAME) + node?""" + return (self.rdtype, self.covers) not in _delete_for_other_data + @dns.immutable.immutable class ImmutableRdataset(Rdataset): diff --git a/tests/test_zone.py b/tests/test_zone.py index 3e34e72..1cd58dd 100644 --- a/tests/test_zone.py +++ b/tests/test_zone.py @@ -192,6 +192,42 @@ ns1 3600 IN A 10.0.0.1 ; comment1 ns2 3600 IN A 10.0.0.2 ; comment2 """ + +example_cname = """$TTL 3600 +$ORIGIN example. +@ soa foo bar (1 2 3 4 5) +@ ns ns1 +@ ns ns2 +ns1 a 10.0.0.1 +ns2 a 10.0.0.2 +www a 10.0.0.3 +web cname www + nsec @ CNAME RRSIG + rrsig NSEC 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= + rrsig CNAME 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= +web2 cname www + nsec3 1 1 12 aabbccdd 2t7b4g4vsa5smi47k61mv5bv1a22bojr CNAME RRSIG + rrsig NSEC3 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= + rrsig CNAME 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= +""" + + +example_other_data = """$TTL 3600 +$ORIGIN example. +@ soa foo bar (1 2 3 4 5) +@ ns ns1 +@ ns ns2 +ns1 a 10.0.0.1 +ns2 a 10.0.0.2 +www a 10.0.0.3 +web a 10.0.0.4 + nsec @ A RRSIG + rrsig A 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= + rrsig NSEC 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= + rrsig CNAME 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= +""" + + _keep_output = True def _rdata_sort(a): @@ -839,6 +875,58 @@ class ZoneTestCase(unittest.TestCase): self.assertTrue(rds is not rrs) self.assertFalse(isinstance(rds, dns.rrset.RRset)) + def testCnameAndOtherDataAddOther(self): + z = dns.zone.from_text(example_cname, 'example.', relativize=True) + rds = dns.rdataset.from_text('in', 'a', 300, '10.0.0.1') + z.replace_rdataset('web', rds) + z.replace_rdataset('web2', rds.copy()) + n = z.find_node('web') + self.assertEqual(len(n.rdatasets), 3) + self.assertEqual(n.find_rdataset(dns.rdataclass.IN, dns.rdatatype.A), + rds) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.NSEC)) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.NSEC)) + n = z.find_node('web2') + self.assertEqual(len(n.rdatasets), 3) + self.assertEqual(n.find_rdataset(dns.rdataclass.IN, dns.rdatatype.A), + rds) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.NSEC3)) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.NSEC3)) + + def testCnameAndOtherDataAddCname(self): + z = dns.zone.from_text(example_other_data, 'example.', relativize=True) + rds = dns.rdataset.from_text('in', 'cname', 300, 'www') + z.replace_rdataset('web', rds) + n = z.find_node('web') + self.assertEqual(len(n.rdatasets), 4) + self.assertEqual(n.find_rdataset(dns.rdataclass.IN, + dns.rdatatype.CNAME), + rds) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.NSEC)) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.NSEC)) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.CNAME)) + + def testNameInZoneWithStr(self): + z = dns.zone.from_text(example_text, 'example.', relativize=False) + self.assertTrue('ns1.example.' in z) + self.assertTrue('bar.foo.example.' in z) + + def testNameInZoneWhereNameIsNotValid(self): + z = dns.zone.from_text(example_text, 'example.', relativize=False) + with self.assertRaises(KeyError): + self.assertTrue(1 in z) + class VersionedZoneTestCase(unittest.TestCase): def testUseTransaction(self): @@ -909,15 +997,52 @@ class VersionedZoneTestCase(unittest.TestCase): rds = txn.get('example.', 'soa') self.assertEqual(rds[0].serial, 1) - def testNameInZoneWithStr(self): - z = dns.zone.from_text(example_text, 'example.', relativize=False) - self.assertTrue('ns1.example.' in z) - self.assertTrue('bar.foo.example.' in z) + def testCnameAndOtherDataAddOther(self): + z = dns.zone.from_text(example_cname, 'example.', relativize=True, + zone_factory=dns.versioned.Zone) + rds = dns.rdataset.from_text('in', 'a', 300, '10.0.0.1') + with z.writer() as txn: + txn.replace('web', rds) + txn.replace('web2', rds.copy()) + n = z.find_node('web') + self.assertEqual(len(n.rdatasets), 3) + self.assertEqual(n.find_rdataset(dns.rdataclass.IN, dns.rdatatype.A), + rds) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.NSEC)) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.NSEC)) + n = z.find_node('web2') + self.assertEqual(len(n.rdatasets), 3) + self.assertEqual(n.find_rdataset(dns.rdataclass.IN, dns.rdatatype.A), + rds) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.NSEC3)) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.NSEC3)) + + def testCnameAndOtherDataAddCname(self): + z = dns.zone.from_text(example_other_data, 'example.', relativize=True, + zone_factory=dns.versioned.Zone) + rds = dns.rdataset.from_text('in', 'cname', 300, 'www') + with z.writer() as txn: + txn.replace('web', rds) + n = z.find_node('web') + self.assertEqual(len(n.rdatasets), 4) + self.assertEqual(n.find_rdataset(dns.rdataclass.IN, + dns.rdatatype.CNAME), + rds) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.NSEC)) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.NSEC)) + self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.CNAME)) - def testNameInZoneWhereNameIsNotValid(self): - z = dns.zone.from_text(example_text, 'example.', relativize=False) - with self.assertRaises(KeyError): - self.assertTrue(1 in z) if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From be0f6ddfe6d6f14e953f8bcd73d68f16383b38fb Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Sun, 28 Nov 2021 11:23:17 -0800 Subject: doco for CNAME-and-other-data --- doc/whatsnew.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/whatsnew.rst b/doc/whatsnew.rst index e5b1667..7f7f125 100644 --- a/doc/whatsnew.rst +++ b/doc/whatsnew.rst @@ -34,6 +34,16 @@ What's New in dnspython * The CDS rdatatype now allows digest type 0. +* Dnspython zones now enforces that a node is either a CNAME node or + an "other data" node. A CNAME node contains only CNAME, + RRSIG(CNAME), NSEC, RRSIG(NSEC), NSEC3, or RRSIG(NSEC3) rdatasets. + An "other data" node contains any rdataset other than a CNAME or + RRSIG(CNAME) rdataset. The enforcement is "last update wins". For + example, if you have a node which contains a CNAME rdataset, and + then add an MX rdataset to it, then the CNAME rdataset will be deleted. + Likewise if you have a node containing an MX rdataset and add a + CNAME rdataset, the MX rdataset will be deleted. + 2.1.0 ---------------------- -- cgit v1.2.1 From fce73919a484d3a985e1acd5a099796cd9a563f2 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Sun, 28 Nov 2021 11:29:11 -0800 Subject: more doco for CNAME-and-other-data --- dns/node.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dns/node.py b/dns/node.py index 261de37..4a750eb 100644 --- a/dns/node.py +++ b/dns/node.py @@ -26,7 +26,20 @@ import dns.renderer class Node: - """A Node is a set of rdatasets.""" + """A Node is a set of rdatasets. + + A node is either a CNAME node or an "other data" node. A CNAME + node contains only CNAME, RRSIG(CNAME), NSEC, RRSIG(NSEC), NSEC3, + or RRSIG(NSEC3) rdatasets. An "other data" node contains any + rdataset other than a CNAME or RRSIG(CNAME) rdataset. When + changes are made to a node, the CNAME or "other data" state is + always consistent with the update, i.e. the most recent change + wins. For example, if you have a node which contains a CNAME + rdataset, and then add an MX rdataset to it, then the CNAME + rdataset will be deleted. Likewise if you have a node containing + an MX rdataset and add a CNAME rdataset, the MX rdataset will be + deleted. + """ __slots__ = ['rdatasets'] -- cgit v1.2.1 From 3b6bc40b11830eb18b56472054bb0e58f9f4072a Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 2 Dec 2021 05:33:35 -0800 Subject: incorporate review feedback --- dns/node.py | 8 +++++++- dns/rdataset.py | 15 ++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/dns/node.py b/dns/node.py index 4a750eb..7f172dd 100644 --- a/dns/node.py +++ b/dns/node.py @@ -102,7 +102,13 @@ class Node: """ # Make having just one rdataset at the node fast. if len(self.rdatasets) > 0: - if rdataset.rdtype == dns.rdatatype.CNAME: + # We don't want adding RRSIG(CNAME) to delete CNAMEs, + # so we treat it as expressing "CNAME intent" for classifying + # the node as a CNAME node, even if we haven't added the CNAME + # yet. + if rdataset.rdtype == dns.rdatatype.CNAME or \ + (rdataset.rdtype == dns.rdatatype.RRSIG and + rdataset.covers == dns.rdatatype.CNAME): self.rdatasets = [rds for rds in self.rdatasets if rds.ok_for_cname()] else: diff --git a/dns/rdataset.py b/dns/rdataset.py index e9d8fc2..868c1fc 100644 --- a/dns/rdataset.py +++ b/dns/rdataset.py @@ -42,12 +42,10 @@ class IncompatibleTypes(dns.exception.DNSException): _ok_for_cname = { - (dns.rdatatype.CNAME, dns.rdatatype.NONE), - (dns.rdatatype.RRSIG, dns.rdatatype.CNAME), - (dns.rdatatype.NSEC, dns.rdatatype.NONE), - (dns.rdatatype.RRSIG, dns.rdatatype.NSEC), - (dns.rdatatype.NSEC3, dns.rdatatype.NONE), - (dns.rdatatype.RRSIG, dns.rdatatype.NSEC3), + dns.rdatatype.CNAME, + dns.rdatatype.NSEC, # RFC 4035 section 2.5 + dns.rdatatype.NSEC3, # This is not likely to happen, but not impossible! + dns.rdatatype.KEY, # RFC 4035 section 2.5, RFC 3007 } _delete_for_other_data = { @@ -55,6 +53,7 @@ _delete_for_other_data = { (dns.rdatatype.RRSIG, dns.rdatatype.CNAME), } + class Rdataset(dns.set.Set): """A DNS rdataset.""" @@ -339,7 +338,9 @@ class Rdataset(dns.set.Set): def ok_for_cname(self): """Is this rdataset compatible with a CNAME node?""" - return (self.rdtype, self.covers) in _ok_for_cname + return self.rdtype in _ok_for_cname or \ + (self.rdtype == dns.rdatatype.RRSIG and + self.covers in _ok_for_cname) def ok_for_other_data(self): """Is this rdataset compatible with an 'other data' (i.e. not CNAME) -- cgit v1.2.1 From 73e5671c02490a4e33285d034f35de018d4d7836 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 2 Dec 2021 06:43:37 -0800 Subject: infrastructure needed for CNAME-and-other-data check in txn --- dns/node.py | 12 +++++++ dns/rdataset.py | 18 +++++++---- dns/transaction.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++------- dns/zone.py | 6 ++++ 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/dns/node.py b/dns/node.py index 7f172dd..3267de7 100644 --- a/dns/node.py +++ b/dns/node.py @@ -226,3 +226,15 @@ class Node: self.delete_rdataset(replacement.rdclass, replacement.rdtype, replacement.covers) self._append_rdataset(replacement) + + def is_cname(self): + """Is this a CNAME node? + + If the node has a CNAME or an RRSIG(CNAME) it is considered a CNAME + node for CNAME-and-other-data purposes, and ``True`` is returned. + Otherwise the node is an "other data" node, and ``False`` is returned. + """ + for rdataset in self.rdatasets: + if rdataset.implies_cname(): + return True + return False diff --git a/dns/rdataset.py b/dns/rdataset.py index 868c1fc..242e30c 100644 --- a/dns/rdataset.py +++ b/dns/rdataset.py @@ -48,11 +48,6 @@ _ok_for_cname = { dns.rdatatype.KEY, # RFC 4035 section 2.5, RFC 3007 } -_delete_for_other_data = { - (dns.rdatatype.CNAME, dns.rdatatype.NONE), - (dns.rdatatype.RRSIG, dns.rdatatype.CNAME), -} - class Rdataset(dns.set.Set): @@ -342,10 +337,21 @@ class Rdataset(dns.set.Set): (self.rdtype == dns.rdatatype.RRSIG and self.covers in _ok_for_cname) + def implies_cname(self): + """Does this rdataset imply a node is a CNAME node? + + If the rdataset's type is CNAME or RRSIG(CNAME) then it implies a + node is a CNAME node, and ``True`` is returned. Otherwise it implies + the node is an an "other data" node, and ``False`` is returned. + """ + return self.rdtype == dns.rdatatype.CNAME or \ + (self.rdtype == dns.rdatatype.RRSIG and + self.covers == dns.rdatatype.CNAME) + def ok_for_other_data(self): """Is this rdataset compatible with an 'other data' (i.e. not CNAME) node?""" - return (self.rdtype, self.covers) not in _delete_for_other_data + return not self.implies_cname() @dns.immutable.immutable diff --git a/dns/transaction.py b/dns/transaction.py index 8aec2e8..22c63a7 100644 --- a/dns/transaction.py +++ b/dns/transaction.py @@ -79,6 +79,12 @@ class AlreadyEnded(dns.exception.DNSException): """Tried to use an already-ended transaction.""" +def _ensure_immutable(rdataset): + if rdataset is None or isinstance(rdataset, dns.rdataset.ImmutableRdataset): + return rdataset + return dns.rdataset.ImmutableRdataset(rdataset) + + class Transaction: def __init__(self, manager, replacement=False, read_only=False): @@ -86,6 +92,9 @@ class Transaction: self.replacement = replacement self.read_only = read_only self._ended = False + self._check_put_rdataset = [] + self._check_delete_rdataset = [] + self._check_delete_name = [] # # This is the high level API @@ -102,10 +111,15 @@ class Transaction: name = dns.name.from_text(name, None) rdtype = dns.rdatatype.RdataType.make(rdtype) rdataset = self._get_rdataset(name, rdtype, covers) - if rdataset is not None and \ - not isinstance(rdataset, dns.rdataset.ImmutableRdataset): - rdataset = dns.rdataset.ImmutableRdataset(rdataset) - return rdataset + return _ensure_immutable(rdataset) + + def get_rdatasets(self, name): + """Return the rdatasets at *name*, if any. + + The returned rdatasets are immutable. + An empty tuple is returned if the name doesn't exist. + """ + return [_ensure_immutable(rds) for rds in self._get_rdatasets(name)] def _check_read_only(self): if self.read_only: @@ -271,6 +285,43 @@ class Transaction: """ self._end(False) + def check_put_rdataset(self, check): + """Call *check* before putting (storing) an rdataset. + + The function is called with the transaction, the name, and the rdataset. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_put_rdataset.append(check) + + def check_delete_rdataset(self, check): + """Call *check* before deleting an rdataset. + + The function is called with the transaction, the name, the rdatatype, + covered rdatatype. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_delete_rdataset.append(check) + + def check_delete_name(self, check): + """Call *check* before putting (storing) an rdataset. + + The function is called with the transaction and the name. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_delete_name.append(check) + # # Helper methods # @@ -349,7 +400,7 @@ class Transaction: trds.update(existing) existing = trds rdataset = existing.union(rdataset) - self._put_rdataset(name, rdataset) + self._checked_put_rdataset(name, rdataset) except IndexError: raise TypeError(f'not enough parameters to {method}') @@ -403,16 +454,16 @@ class Transaction: raise DeleteNotExact(f'{method}: missing rdatas') rdataset = existing.difference(rdataset) if len(rdataset) == 0: - self._delete_rdataset(name, rdataset.rdtype, - rdataset.covers) + self._checked_delete_rdataset(name, rdataset.rdtype, + rdataset.covers) else: - self._put_rdataset(name, rdataset) + self._checked_put_rdataset(name, rdataset) elif exact: raise DeleteNotExact(f'{method}: missing rdataset') else: if exact and not self._name_exists(name): raise DeleteNotExact(f'{method}: name not known') - self._delete_name(name) + self._checked_delete_name(name) except IndexError: raise TypeError(f'not enough parameters to {method}') @@ -429,6 +480,21 @@ class Transaction: finally: self._ended = True + def _checked_put_rdataset(self, name, rdataset): + for check in self._check_put_rdataset: + check(self, name, rdataset) + self._put_rdataset(name, rdataset) + + def _checked_delete_rdataset(self, name, rdtype, covers): + for check in self._check_delete_rdataset: + check(self, name, rdtype, covers) + self._delete_rdataset(name, rdtype, covers) + + def _checked_delete_name(self, name): + for check in self._check_delete_name: + check(self, name) + self._delete_name(name) + # # Transactions are context managers. # @@ -462,7 +528,7 @@ class Transaction: def _delete_name(self, name): """Delete all data associated with *name*. - It is not an error if the rdataset does not exist. + It is not an error if the name does not exist. """ raise NotImplementedError # pragma: no cover @@ -506,7 +572,12 @@ class Transaction: def _iterate_rdatasets(self): """Return an iterator that yields (name, rdataset) tuples. + """ + raise NotImplementedError # pragma: no cover + + def _get_rdatasets(self, name): + """Return the rdatasets at *name*, if any. - Not all Transaction subclasses implement this. + An empty list is returned if the name doesn't exist. """ raise NotImplementedError # pragma: no cover diff --git a/dns/zone.py b/dns/zone.py index 510be2d..9d999c7 100644 --- a/dns/zone.py +++ b/dns/zone.py @@ -1024,6 +1024,12 @@ class Transaction(dns.transaction.Transaction): for rdataset in node: yield (name, rdataset) + def _get_rdatasets(self, name): + node = self.version.get_node(name) + if node is None: + return [] + return node.rdatasets + def from_text(text, origin=None, rdclass=dns.rdataclass.IN, relativize=True, zone_factory=Zone, filename=None, -- cgit v1.2.1 From bfaa95c85e258a8c1ac808c7af87a22bdf79c857 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 2 Dec 2021 07:22:46 -0800 Subject: cname and other data check in zonefile reader --- dns/node.py | 10 ++-------- dns/rdataset.py | 8 ++++++++ dns/zonefile.py | 28 ++++++++++++++++++++++++++++ tests/test_zone.py | 30 +++++++++++++++++++++--------- 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/dns/node.py b/dns/node.py index 3267de7..12e4080 100644 --- a/dns/node.py +++ b/dns/node.py @@ -102,16 +102,10 @@ class Node: """ # Make having just one rdataset at the node fast. if len(self.rdatasets) > 0: - # We don't want adding RRSIG(CNAME) to delete CNAMEs, - # so we treat it as expressing "CNAME intent" for classifying - # the node as a CNAME node, even if we haven't added the CNAME - # yet. - if rdataset.rdtype == dns.rdatatype.CNAME or \ - (rdataset.rdtype == dns.rdatatype.RRSIG and - rdataset.covers == dns.rdatatype.CNAME): + if rdataset.implies_cname(): self.rdatasets = [rds for rds in self.rdatasets if rds.ok_for_cname()] - else: + elif rdataset.implies_other_data(): self.rdatasets = [rds for rds in self.rdatasets if rds.ok_for_other_data()] self.rdatasets.append(rdataset) diff --git a/dns/rdataset.py b/dns/rdataset.py index 242e30c..f948d76 100644 --- a/dns/rdataset.py +++ b/dns/rdataset.py @@ -353,6 +353,14 @@ class Rdataset(dns.set.Set): node?""" return not self.implies_cname() + def implies_other_data(self): + """Does this rdataset imply a node is an other data node? + + Note that implies_other_data() is not simply "not implies_cname()" as + some types, e.g. NSEC and RRSIG(NSEC) are neutral. + """ + return not self.ok_for_cname() + @dns.immutable.immutable class ImmutableRdataset(Rdataset): diff --git a/dns/zonefile.py b/dns/zonefile.py index 39c7a38..4d72c71 100644 --- a/dns/zonefile.py +++ b/dns/zonefile.py @@ -38,6 +38,26 @@ class UnknownOrigin(dns.exception.DNSException): """Unknown origin""" +class CNAMEAndOtherData(dns.exception.DNSException): + """A node has a CNAME and other data""" + + +def _check_cname_and_other_data(txn, name, rdataset): + rdatasets = txn.get_rdatasets(name) + if any(rds.implies_cname() for rds in rdatasets): + # This is a CNAME node. + if not rdataset.ok_for_cname(): + raise CNAMEAndOtherData('rdataset not ok for CNAME node') + elif any(rds.implies_other_data() for rds in rdatasets): + # This is an other data node + if not rdataset.ok_for_other_data(): + raise CNAMEAndOtherData('rdataset is a CNAME but node ' + 'has other data') + # Otherwise the node consists of neutral types that can be + # present at either a CNAME or an other data node, e.g. NSEC or + # RRSIG(NSEC) + + class Reader: """Read a DNS zone file into a transaction.""" @@ -71,6 +91,7 @@ class Reader: self.force_ttl = force_ttl self.force_rdclass = force_rdclass self.force_rdtype = force_rdtype + self.txn.check_put_rdataset(_check_cname_and_other_data) def _eat_line(self): while 1: @@ -445,6 +466,13 @@ class RRsetsReaderTransaction(dns.transaction.Transaction): def _get_rdataset(self, name, rdtype, covers): return self.rdatasets.get((name, rdtype, covers)) + def _get_rdatasets(self, name): + rdatasets = [] + for (rdataset_name, _, _), rdataset in self.rdatasets.items(): + if name == rdataset_name: + rdatasets.append(rdataset) + return rdatasets + def _put_rdataset(self, name, rdataset): self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = rdataset diff --git a/tests/test_zone.py b/tests/test_zone.py index 1cd58dd..bdc99a3 100644 --- a/tests/test_zone.py +++ b/tests/test_zone.py @@ -224,9 +224,22 @@ web a 10.0.0.4 nsec @ A RRSIG rrsig A 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= rrsig NSEC 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= - rrsig CNAME 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= """ +example_cname_and_other_data = """$TTL 3600 +$ORIGIN example. +@ soa foo bar (1 2 3 4 5) +@ ns ns1 +@ ns ns2 +ns1 a 10.0.0.1 +ns2 a 10.0.0.2 +www a 10.0.0.3 +web a 10.0.0.4 + cname www + nsec @ A RRSIG + rrsig A 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= + rrsig NSEC 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= +""" _keep_output = True @@ -904,7 +917,7 @@ class ZoneTestCase(unittest.TestCase): rds = dns.rdataset.from_text('in', 'cname', 300, 'www') z.replace_rdataset('web', rds) n = z.find_node('web') - self.assertEqual(len(n.rdatasets), 4) + self.assertEqual(len(n.rdatasets), 3) self.assertEqual(n.find_rdataset(dns.rdataclass.IN, dns.rdatatype.CNAME), rds) @@ -913,9 +926,11 @@ class ZoneTestCase(unittest.TestCase): self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.RRSIG, dns.rdatatype.NSEC)) - self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, - dns.rdatatype.RRSIG, - dns.rdatatype.CNAME)) + + def testCnameAndOtherDataInZonefile(self): + with self.assertRaises(dns.zonefile.CNAMEAndOtherData): + dns.zone.from_text(example_cname_and_other_data, 'example.', + relativize=True) def testNameInZoneWithStr(self): z = dns.zone.from_text(example_text, 'example.', relativize=False) @@ -1030,7 +1045,7 @@ class VersionedZoneTestCase(unittest.TestCase): with z.writer() as txn: txn.replace('web', rds) n = z.find_node('web') - self.assertEqual(len(n.rdatasets), 4) + self.assertEqual(len(n.rdatasets), 3) self.assertEqual(n.find_rdataset(dns.rdataclass.IN, dns.rdatatype.CNAME), rds) @@ -1039,9 +1054,6 @@ class VersionedZoneTestCase(unittest.TestCase): self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.RRSIG, dns.rdatatype.NSEC)) - self.assertIsNotNone(n.get_rdataset(dns.rdataclass.IN, - dns.rdatatype.RRSIG, - dns.rdatatype.CNAME)) if __name__ == '__main__': -- cgit v1.2.1 From d723ee70d3745e348b2659dd3ac5f723417c70d4 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 2 Dec 2021 07:39:06 -0800 Subject: simpliy node.is_cname() --- dns/node.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dns/node.py b/dns/node.py index 12e4080..4871baf 100644 --- a/dns/node.py +++ b/dns/node.py @@ -228,7 +228,4 @@ class Node: node for CNAME-and-other-data purposes, and ``True`` is returned. Otherwise the node is an "other data" node, and ``False`` is returned. """ - for rdataset in self.rdatasets: - if rdataset.implies_cname(): - return True - return False + return any(rdataset.implies_cname() for rdataset in self.rdatasets) -- cgit v1.2.1 From 7ce649d1a1a4fe692c9896dc8b2287971b480314 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 2 Dec 2021 11:40:45 -0800 Subject: fix doco errors --- dns/node.py | 2 +- dns/transaction.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dns/node.py b/dns/node.py index 4871baf..1739154 100644 --- a/dns/node.py +++ b/dns/node.py @@ -29,7 +29,7 @@ class Node: """A Node is a set of rdatasets. A node is either a CNAME node or an "other data" node. A CNAME - node contains only CNAME, RRSIG(CNAME), NSEC, RRSIG(NSEC), NSEC3, + node contains only CNAME, KEY, RRSIG(CNAME), NSEC, RRSIG(NSEC), NSEC3, or RRSIG(NSEC3) rdatasets. An "other data" node contains any rdataset other than a CNAME or RRSIG(CNAME) rdataset. When changes are made to a node, the CNAME or "other data" state is diff --git a/dns/transaction.py b/dns/transaction.py index 22c63a7..0df1e56 100644 --- a/dns/transaction.py +++ b/dns/transaction.py @@ -117,7 +117,7 @@ class Transaction: """Return the rdatasets at *name*, if any. The returned rdatasets are immutable. - An empty tuple is returned if the name doesn't exist. + An empty list is returned if the name doesn't exist. """ return [_ensure_immutable(rds) for rds in self._get_rdatasets(name)] @@ -301,7 +301,7 @@ class Transaction: """Call *check* before deleting an rdataset. The function is called with the transaction, the name, the rdatatype, - covered rdatatype. + and the covered rdatatype. The check function may safely make non-mutating transaction method calls, but behavior is undefined if mutating transaction methods are -- cgit v1.2.1 From a5cf8b0817d27abb0f27bfc6b8a78a518c534a35 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 2 Dec 2021 14:44:08 -0800 Subject: refactor to have a get_node() in the txn API --- dns/node.py | 123 +++++++++++++++++++++++++++++++++++++++++++++-------- dns/rdataset.py | 38 ----------------- dns/transaction.py | 24 ++++++----- dns/zone.py | 10 ++--- dns/zonefile.py | 37 +++++++++------- 5 files changed, 147 insertions(+), 85 deletions(-) diff --git a/dns/node.py b/dns/node.py index 1739154..b8141c4 100644 --- a/dns/node.py +++ b/dns/node.py @@ -17,13 +17,53 @@ """DNS nodes. A node is a set of rdatasets.""" +import enum import io +import dns.immutable import dns.rdataset import dns.rdatatype import dns.renderer +_cname_types = { + dns.rdatatype.CNAME, +} + +# "neutral" types can coexist with a CNAME and thus are not "other data" +_neutral_types = { + dns.rdatatype.NSEC, # RFC 4035 section 2.5 + dns.rdatatype.NSEC3, # This is not likely to happen, but not impossible! + dns.rdatatype.KEY, # RFC 4035 section 2.5, RFC 3007 +} + +def _matches_type_or_its_signature(rdtypes, rdtype, covers): + return rdtype in rdtypes or \ + (rdtype == dns.rdatatype.RRSIG and covers in rdtypes) + + +@enum.unique +class NodeKind(enum.Enum): + """Rdatasets in nodes + """ + REGULAR = 0 # a.k.a "other data" + NEUTRAL = 1 + CNAME = 2 + + @classmethod + def classify(cls, rdtype, covers): + if _matches_type_or_its_signature(_cname_types, rdtype, covers): + return NodeKind.CNAME + elif _matches_type_or_its_signature(_neutral_types, rdtype, covers): + return NodeKind.NEUTRAL + else: + return NodeKind.REGULAR + + @classmethod + def classify_rdataset(cls, rdataset): + return cls.classify(rdataset.rdtype, rdataset.covers) + + class Node: """A Node is a set of rdatasets. @@ -95,22 +135,26 @@ class Node: """Append rdataset to the node with special handling for CNAME and other data conditions. - Specifically, if the rdataset being appended is a CNAME, then - all rdatasets other than NSEC, NSEC3, and their covering RRSIGs - are deleted. If the rdataset being appended is NOT a CNAME, then - CNAME and RRSIG(CNAME) are deleted. + Specifically, if the rdataset being appended has ``NodeKind.CNAME``, + then all rdatasets other than KEY, NSEC, NSEC3, and their covering + RRSIGs are deleted. If the rdataset being appended has + ``NodeKind.REGUALAR`` then CNAME and RRSIG(CNAME) are deleted. """ # Make having just one rdataset at the node fast. if len(self.rdatasets) > 0: - if rdataset.implies_cname(): - self.rdatasets = [rds for rds in self.rdatasets - if rds.ok_for_cname()] - elif rdataset.implies_other_data(): - self.rdatasets = [rds for rds in self.rdatasets - if rds.ok_for_other_data()] + kind = NodeKind.classify_rdataset(rdataset) + if kind == NodeKind.CNAME: + self.rdatasets = [rds for rds in self.rdatasets if + NodeKind.classify_rdataset(rds) != + NodeKind.REGULAR] + elif kind == NodeKind.REGULAR: + self.rdatasets = [rds for rds in self.rdatasets if + NodeKind.classify_rdataset(rds) != + NodeKind.CNAME] + # Otherwise the rdataset is NodeKind.NEUTRAL and we do not need to + # edit self.rdatasets. self.rdatasets.append(rdataset) - def find_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE, create=False): """Find an rdataset matching the specified properties in the @@ -221,11 +265,56 @@ class Node: replacement.covers) self._append_rdataset(replacement) - def is_cname(self): - """Is this a CNAME node? + def classify(self): + """Classify a node. + + A node which contains a CNAME or RRSIG(CNAME) is a + ``NodeKind.CNAME`` node. - If the node has a CNAME or an RRSIG(CNAME) it is considered a CNAME - node for CNAME-and-other-data purposes, and ``True`` is returned. - Otherwise the node is an "other data" node, and ``False`` is returned. + A node which contains only "neutral" types, i.e. types allowed to + co-exist with a CNAME, is a ``NodeKind.NEUTRAL`` node. The neutral + types are NSEC, NSEC3, KEY, and their associated RRSIGS. An empty node + is also considered neutral. + + A node which contains some rdataset which is not a CNAME, RRSIG(CNAME), + or a neutral type is a a ``NodeKind.REGULAR`` node. Regular nodes are + also commonly referred to as "other data". """ - return any(rdataset.implies_cname() for rdataset in self.rdatasets) + for rdataset in self.rdatasets: + kind = NodeKind.classify(rdataset.rdtype, rdataset.covers) + if kind != NodeKind.NEUTRAL: + return kind + return NodeKind.NEUTRAL + + def is_immutable(self): + return False + + +dns.immutable.immutable +class ImmutableNode(Node): + def __init__(self, node): + super().__init__() + self.rdatasets = tuple( + [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets] + ) + + def find_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE, + create=False): + if create: + raise TypeError("immutable") + return super().find_rdataset(rdclass, rdtype, covers, False) + + def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE, + create=False): + if create: + raise TypeError("immutable") + return super().get_rdataset(rdclass, rdtype, covers, False) + + def delete_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE): + raise TypeError("immutable") + + def replace_rdataset(self, replacement): + raise TypeError("immutable") + + def is_immutable(self): + return True diff --git a/dns/rdataset.py b/dns/rdataset.py index f948d76..e69ee23 100644 --- a/dns/rdataset.py +++ b/dns/rdataset.py @@ -41,14 +41,6 @@ class IncompatibleTypes(dns.exception.DNSException): """An attempt was made to add DNS RR data of an incompatible type.""" -_ok_for_cname = { - dns.rdatatype.CNAME, - dns.rdatatype.NSEC, # RFC 4035 section 2.5 - dns.rdatatype.NSEC3, # This is not likely to happen, but not impossible! - dns.rdatatype.KEY, # RFC 4035 section 2.5, RFC 3007 -} - - class Rdataset(dns.set.Set): """A DNS rdataset.""" @@ -331,36 +323,6 @@ class Rdataset(dns.set.Set): else: return self[0]._processing_order(iter(self)) - def ok_for_cname(self): - """Is this rdataset compatible with a CNAME node?""" - return self.rdtype in _ok_for_cname or \ - (self.rdtype == dns.rdatatype.RRSIG and - self.covers in _ok_for_cname) - - def implies_cname(self): - """Does this rdataset imply a node is a CNAME node? - - If the rdataset's type is CNAME or RRSIG(CNAME) then it implies a - node is a CNAME node, and ``True`` is returned. Otherwise it implies - the node is an an "other data" node, and ``False`` is returned. - """ - return self.rdtype == dns.rdatatype.CNAME or \ - (self.rdtype == dns.rdatatype.RRSIG and - self.covers == dns.rdatatype.CNAME) - - def ok_for_other_data(self): - """Is this rdataset compatible with an 'other data' (i.e. not CNAME) - node?""" - return not self.implies_cname() - - def implies_other_data(self): - """Does this rdataset imply a node is an other data node? - - Note that implies_other_data() is not simply "not implies_cname()" as - some types, e.g. NSEC and RRSIG(NSEC) are neutral. - """ - return not self.ok_for_cname() - @dns.immutable.immutable class ImmutableRdataset(Rdataset): diff --git a/dns/transaction.py b/dns/transaction.py index 0df1e56..ae7417e 100644 --- a/dns/transaction.py +++ b/dns/transaction.py @@ -79,11 +79,16 @@ class AlreadyEnded(dns.exception.DNSException): """Tried to use an already-ended transaction.""" -def _ensure_immutable(rdataset): +def _ensure_immutable_rdataset(rdataset): if rdataset is None or isinstance(rdataset, dns.rdataset.ImmutableRdataset): return rdataset return dns.rdataset.ImmutableRdataset(rdataset) +def _ensure_immutable_node(node): + if node is None or node.is_immutable(): + return node + return dns.node.ImmutableNode(node) + class Transaction: @@ -111,15 +116,14 @@ class Transaction: name = dns.name.from_text(name, None) rdtype = dns.rdatatype.RdataType.make(rdtype) rdataset = self._get_rdataset(name, rdtype, covers) - return _ensure_immutable(rdataset) + return _ensure_immutable_rdataset(rdataset) - def get_rdatasets(self, name): - """Return the rdatasets at *name*, if any. + def get_node(self, name): + """Return the node at *name*, if any. - The returned rdatasets are immutable. - An empty list is returned if the name doesn't exist. + Returns an immutable node or ``None``. """ - return [_ensure_immutable(rds) for rds in self._get_rdatasets(name)] + return _ensure_immutable_node(self._get_node(name)) def _check_read_only(self): if self.read_only: @@ -575,9 +579,9 @@ class Transaction: """ raise NotImplementedError # pragma: no cover - def _get_rdatasets(self, name): - """Return the rdatasets at *name*, if any. + def _get_node(self, name): + """Return the node at *name*, if any. - An empty list is returned if the name doesn't exist. + Returns a node or ``None``. """ raise NotImplementedError # pragma: no cover diff --git a/dns/zone.py b/dns/zone.py index 9d999c7..dc4274a 100644 --- a/dns/zone.py +++ b/dns/zone.py @@ -854,6 +854,9 @@ class ImmutableVersionedNode(VersionedNode): def replace_rdataset(self, replacement): raise TypeError("immutable") + def is_immutable(self): + return True + class Version: def __init__(self, zone, id, nodes=None, origin=None): @@ -1024,11 +1027,8 @@ class Transaction(dns.transaction.Transaction): for rdataset in node: yield (name, rdataset) - def _get_rdatasets(self, name): - node = self.version.get_node(name) - if node is None: - return [] - return node.rdatasets + def _get_node(self, name): + return self.version.get_node(name) def from_text(text, origin=None, rdclass=dns.rdataclass.IN, diff --git a/dns/zonefile.py b/dns/zonefile.py index 4d72c71..bcafe1d 100644 --- a/dns/zonefile.py +++ b/dns/zonefile.py @@ -43,19 +43,22 @@ class CNAMEAndOtherData(dns.exception.DNSException): def _check_cname_and_other_data(txn, name, rdataset): - rdatasets = txn.get_rdatasets(name) - if any(rds.implies_cname() for rds in rdatasets): - # This is a CNAME node. - if not rdataset.ok_for_cname(): - raise CNAMEAndOtherData('rdataset not ok for CNAME node') - elif any(rds.implies_other_data() for rds in rdatasets): - # This is an other data node - if not rdataset.ok_for_other_data(): - raise CNAMEAndOtherData('rdataset is a CNAME but node ' - 'has other data') - # Otherwise the node consists of neutral types that can be - # present at either a CNAME or an other data node, e.g. NSEC or - # RRSIG(NSEC) + rdataset_kind = dns.node.NodeKind.classify_rdataset(rdataset) + node = txn.get_node(name) + if node is not None: + node_kind = node.classify() + else: + node_kind = dns.node.NodeKind.NEUTRAL + if node_kind == dns.node.NodeKind.CNAME and \ + rdataset_kind == dns.node.NodeKind.REGULAR: + raise CNAMEAndOtherData('rdataset type is not compatible with a ' + 'CNAME node') + elif node_kind == dns.node.NodeKind.REGULAR and \ + rdataset_kind == dns.node.NodeKind.CNAME: + raise CNAMEAndOtherData('CNAME rdataset is not compatible with a ' + 'regular data node') + # Otherwise at least one of the node and the rdataset is neutral, so + # adding the rdataset is ok class Reader: @@ -466,12 +469,16 @@ class RRsetsReaderTransaction(dns.transaction.Transaction): def _get_rdataset(self, name, rdtype, covers): return self.rdatasets.get((name, rdtype, covers)) - def _get_rdatasets(self, name): + def _get_node(self, name): rdatasets = [] for (rdataset_name, _, _), rdataset in self.rdatasets.items(): if name == rdataset_name: rdatasets.append(rdataset) - return rdatasets + if len(rdatasets) == 0: + return None + node = dns.node.Node() + node.rdatasets = rdatasets + return node def _put_rdataset(self, name, rdataset): self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = rdataset -- cgit v1.2.1 From c34af7dfaa56982b8be1ea5203f39b93288ae9d6 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 2 Dec 2021 14:57:11 -0800 Subject: add missing @ to immutable invocation --- dns/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dns/node.py b/dns/node.py index b8141c4..b3f5785 100644 --- a/dns/node.py +++ b/dns/node.py @@ -290,7 +290,7 @@ class Node: return False -dns.immutable.immutable +@dns.immutable.immutable class ImmutableNode(Node): def __init__(self, node): super().__init__() -- cgit v1.2.1 From c46a91d5926e7ddaa32a466572c4b87dea46aa69 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Fri, 3 Dec 2021 11:39:50 -0800 Subject: fix typos; simplify _check_cname_and_other_data --- dns/node.py | 6 +++--- dns/zonefile.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dns/node.py b/dns/node.py index b3f5785..63ce008 100644 --- a/dns/node.py +++ b/dns/node.py @@ -69,8 +69,8 @@ class Node: """A Node is a set of rdatasets. A node is either a CNAME node or an "other data" node. A CNAME - node contains only CNAME, KEY, RRSIG(CNAME), NSEC, RRSIG(NSEC), NSEC3, - or RRSIG(NSEC3) rdatasets. An "other data" node contains any + node contains only CNAME, KEY, NSEC, and NSEC3 rdatasets along with their + covering RRSIG rdatasets. An "other data" node contains any rdataset other than a CNAME or RRSIG(CNAME) rdataset. When changes are made to a node, the CNAME or "other data" state is always consistent with the update, i.e. the most recent change @@ -138,7 +138,7 @@ class Node: Specifically, if the rdataset being appended has ``NodeKind.CNAME``, then all rdatasets other than KEY, NSEC, NSEC3, and their covering RRSIGs are deleted. If the rdataset being appended has - ``NodeKind.REGUALAR`` then CNAME and RRSIG(CNAME) are deleted. + ``NodeKind.REGULAR`` then CNAME and RRSIG(CNAME) are deleted. """ # Make having just one rdataset at the node fast. if len(self.rdatasets) > 0: diff --git a/dns/zonefile.py b/dns/zonefile.py index bcafe1d..ce16abb 100644 --- a/dns/zonefile.py +++ b/dns/zonefile.py @@ -45,10 +45,10 @@ class CNAMEAndOtherData(dns.exception.DNSException): def _check_cname_and_other_data(txn, name, rdataset): rdataset_kind = dns.node.NodeKind.classify_rdataset(rdataset) node = txn.get_node(name) - if node is not None: - node_kind = node.classify() - else: - node_kind = dns.node.NodeKind.NEUTRAL + if node is None: + # empty nodes are neutral. + return + node_kind = node.classify() if node_kind == dns.node.NodeKind.CNAME and \ rdataset_kind == dns.node.NodeKind.REGULAR: raise CNAMEAndOtherData('rdataset type is not compatible with a ' -- cgit v1.2.1