From d4a7bd69f2d678f04134556e1587633d39c74a00 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Mon, 21 Dec 2020 07:39:08 -0800 Subject: [PATCH] resolve_chaining() now returns a ChainingResult object. --- dns/message.py | 37 ++++++++++++++++++++++++++++++------- dns/resolver.py | 8 ++++++-- doc/message-query.rst | 11 ++++++++++- tests/test_message.py | 9 +++++---- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/dns/message.py b/dns/message.py index 5c35dbc7..0293e3a2 100644 --- a/dns/message.py +++ b/dns/message.py @@ -725,6 +725,31 @@ class Message: return (rdclass, rdtype, None, False) +class ChainingResult: + """The result of a call to dns.message.QueryMessage.resolve_chaining(). + + The ``rrset`` attribute is the answer RRSet, or ``None`` if it doesn't + exist. + + The ``canonical_name`` attribute is the canonical name after all + chaining has been applied (this is the name as ``rrset.name`` in cases + where rrset is not ``None``). + + The ``minimum_ttl`` attribute is the minimum TTL, i.e. the TTL to + use if caching the data. It is the smallest of all the CNAME TTLs + and either the answer TTL if it exists or the SOA TTL and SOA + minimum values for negative answers. + + The ``cnames`` attribute is a list of all the CNAME RRSets followed to + get to the canonical name. + """ + def __init__(self, canonical_name, rrset, minimum_ttl, cnames): + self.canonical_name = canonical_name + self.rrset = rrset + self.minimum_ttl = minimum_ttl + self.cnames = cnames + + class QueryMessage(Message): def resolve_chaining(self): """Follow the CNAME chain in the response to determine the answer @@ -740,11 +765,7 @@ class QueryMessage(Message): Raises ``dns.exception.FormError`` if the question count is not 1. - Returns a tuple (dns.name.Name, int, rrset) where the name is the - canonical name, the int is the minimized TTL, and rrset is their - answer RRset, which may be ``None`` if the chain was dangling or - the response is an NXDOMAIN. - + Returns a ChainingResult object. """ if self.flags & dns.flags.QR == 0: raise NotQueryResponse @@ -755,6 +776,7 @@ class QueryMessage(Message): min_ttl = dns.ttl.MAX_TTL rrset = None count = 0 + cnames = [] while count < MAX_CHAIN: try: rrset = self.find_rrset(self.answer, qname, question.rdclass, @@ -767,6 +789,7 @@ class QueryMessage(Message): crrset = self.find_rrset(self.answer, qname, question.rdclass, dns.rdatatype.CNAME) + cnames.append(crrset) min_ttl = min(min_ttl, crrset.ttl) for rd in crrset: qname = rd.target @@ -800,7 +823,7 @@ class QueryMessage(Message): auname = auname.parent() except dns.name.NoParent: break - return (qname, min_ttl, rrset) + return ChainingResult(qname, rrset, min_ttl, cnames) def canonical_name(self): """Return the canonical name of the first name in the question @@ -816,7 +839,7 @@ class QueryMessage(Message): Raises ``dns.exception.FormError`` if the question count is not 1. """ - return self.resolve_chaining()[0] + return self.resolve_chaining().canonical_name def _maybe_import_update(): diff --git a/dns/resolver.py b/dns/resolver.py index b24f78d5..d04386fc 100644 --- a/dns/resolver.py +++ b/dns/resolver.py @@ -210,8 +210,12 @@ class Answer: self.response = response self.nameserver = nameserver self.port = port - (self.canonical_name, min_ttl, self.rrset) = response.resolve_chaining() - self.expiration = time.time() + min_ttl + self.chaining_result = response.resolve_chaining() + # Copy some attributes out of chaining_result for backwards + # compatibilty and convenience. + self.canonical_name = self.chaining_result.canonical_name + self.rrset = self.chaining_result.rrset + self.expiration = time.time() + self.chaining_result.minimum_ttl def __getattr__(self, attr): # pragma: no cover if attr == 'name': diff --git a/doc/message-query.rst b/doc/message-query.rst index 462f7b43..03c50318 100644 --- a/doc/message-query.rst +++ b/doc/message-query.rst @@ -3,7 +3,16 @@ The dns.message.QueryMessage Class ---------------------------------- -The ``dns.update.QueryMessage`` class is used for ordinary DNS query messages. +The ``dns.message.QueryMessage`` class is used for ordinary DNS query messages. .. autoclass:: dns.message.QueryMessage :members: + +The dns.message.ChainingResult Class +------------------------------------ + +Objects of the ``dns.message.ChainingResult`` class are returned by the +``dns.message.QueryMessage.resolve_chaining()`` method. + +.. autoclass:: dns.message.ChainingResult + :members: diff --git a/tests/test_message.py b/tests/test_message.py index ec964584..6f62339d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -559,10 +559,11 @@ www.example. IN CNAME example. 300 IN SOA . . 1 2 3 4 5 ''') # passing is actuall not going into an infinite loop in this call - (qname, min_ttl, rrset) = r.resolve_chaining() - self.assertEqual(qname, dns.name.from_text('www.example.')) - self.assertEqual(min_ttl, 5) - self.assertIsNone(rrset) + result = r.resolve_chaining() + self.assertEqual(result.canonical_name, + dns.name.from_text('www.example.')) + self.assertEqual(result.minimum_ttl, 5) + self.assertIsNone(result.rrset) def test_bad_text_questions(self): with self.assertRaises(dns.exception.SyntaxError): -- 2.47.3