1#
2# This file is a part of DNSViz, a tool suite for DNS/DNSSEC monitoring,
3# analysis, and visualization.
4# Created by Casey Deccio (casey@deccio.net)
5#
6# Copyright 2012-2014 Sandia Corporation. Under the terms of Contract
7# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
8# certain rights in this software.
9#
10# Copyright 2014-2016 VeriSign, Inc.
11#
12# Copyright 2016-2021 Casey Deccio
13#
14# DNSViz is free software; you can redistribute it and/or modify
15# it under the terms of the GNU General Public License as published by
16# the Free Software Foundation; either version 2 of the License, or
17# (at your option) any later version.
18#
19# DNSViz is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License along
25# with DNSViz.  If not, see <http://www.gnu.org/licenses/>.
26#
27
28from __future__ import unicode_literals
29
30import base64
31import binascii
32import copy
33import errno
34import codecs
35import datetime
36import hashlib
37import io
38import logging
39import socket
40import struct
41import time
42
43# minimal support for python2.6
44try:
45    from collections import OrderedDict
46except ImportError:
47    from ordereddict import OrderedDict
48
49# python3/python2 dual compatibility
50try:
51    from html import escape
52except ImportError:
53    from cgi import escape
54
55import dns.edns, dns.flags, dns.message, dns.rcode, dns.rdataclass, dns.rdatatype, dns.rrset
56
57from . import base32
58from . import crypto
59from . import format as fmt
60from .ipaddr import IPAddr
61from .util import tuple_to_dict
62lb2s = fmt.latin1_binary_to_string
63
64class DNSResponse:
65    '''A DNS response, including meta information'''
66
67    def __init__(self, message, msg_size, error, errno1, history, response_time, query, server_cookie, server_cookie_status, review_history=True):
68        self.message = message
69        self.msg_size = msg_size
70        self.error = error
71        self.errno = errno1
72        self.history = history
73        self.response_time = response_time
74
75        self.query = query
76        self.server_cookie = server_cookie
77        self.server_cookie_status = server_cookie_status
78
79        self.effective_flags = None
80        self.effective_edns = None
81        self.effective_edns_max_udp_payload = None
82        self.effective_edns_flags = None
83        self.effective_edns_options = None
84        self.effective_tcp = None
85        self.effective_server_cookie_status = None
86
87        self.udp_attempted = None
88        self.udp_responsive = None
89        self.tcp_attempted = None
90        self.tcp_responsive = None
91        self.responsive_cause_index = None
92
93        if review_history:
94            self._review_history()
95
96    def __str__(self):
97        from . import query as Q
98        if self.message is not None:
99            return repr(self.message)
100        else:
101            return Q.response_errors.get(self.error)
102
103    def __repr__(self):
104        return '<%s: "%s">' % (self.__class__.__name__, str(self))
105
106    @classmethod
107    def _query_tag_bind(cls, tcp, flags, edns, edns_flags, edns_max_udp_payload, edns_options, qname):
108        s = []
109        if flags & dns.flags.RD:
110            s.append('+')
111        else:
112            s.append('-')
113        if edns >= 0:
114            s.append('E(%d)' % (edns))
115        if tcp:
116            s.append('T')
117        if edns >= 0 and edns_flags & dns.flags.DO:
118            s.append('D')
119        if flags & dns.flags.CD:
120            s.append('C')
121        # Flags other than the ones commonly seen in queries
122        if flags & dns.flags.AD:
123            s.append('A')
124        if flags & dns.flags.AA:
125            s.append('a')
126        if flags & dns.flags.TC:
127            s.append('t')
128        if flags & dns.flags.RA:
129            s.append('r')
130        if edns >= 0:
131            # EDNS max UDP payload
132            s.append('P(%d)' % edns_max_udp_payload)
133            # EDNS flags other than DO
134            if edns_flags & ~dns.flags.DO:
135                s.append('F(0x%x)' % edns_flags)
136            # other options
137            for opt in edns_options:
138                if opt.otype == 3:
139                    # NSID
140                    s.append('N')
141                elif opt.otype == 8:
142                    # EDNS Client Subnet
143                    s.append('s')
144                elif opt.otype == 10:
145                    # DNS cookies
146                    s.append('K')
147        if qname.to_text() != qname.to_text().lower():
148            s.append('X')
149        return s
150
151    @classmethod
152    def _query_tag_human(cls, tcp, flags, edns, edns_flags, edns_max_udp_payload, edns_options, qname):
153        s = ''
154        if tcp:
155            s += 'TCP_'
156        else:
157            s += 'UDP_'
158
159        if flags & dns.flags.RD:
160            s += '+'
161        else:
162            s += '-'
163        if flags & dns.flags.CD:
164            s += 'C'
165        # Flags other than the ones commonly seen in queries
166        if flags & dns.flags.AD:
167            s += 'A'
168        if flags & dns.flags.AA:
169            s += 'a'
170        if flags & dns.flags.TC:
171            s += 't'
172        if flags & dns.flags.RA:
173            s += 'r'
174        s += '_'
175
176        if edns < 0:
177            s += 'NOEDNS_'
178        else:
179            s += 'EDNS%d_' % (edns)
180
181            # EDNS max UDP payload
182            s += '%d_' % edns_max_udp_payload
183
184            if edns_flags & dns.flags.DO:
185                s += 'D'
186            # EDNS flags other than DO
187            if edns_flags & ~dns.flags.DO:
188                s += '%d' % edns_flags
189
190            if edns_options:
191                s += '_'
192
193            # other options
194            for opt in edns_options:
195                if opt.otype == 3:
196                    # NSID
197                    s += 'N'
198                elif opt.otype == 8:
199                    # EDNS Client Subnet
200                    s += 's'
201                elif opt.otype == 10:
202                    # DNS cookies
203                    s += 'K'
204                else:
205                    # DNS cookies
206                    s += 'O(%d)' % opt.otype
207
208        if qname.to_text() != qname.to_text().lower():
209            s += '_0x20'
210        return s
211
212    def nsid_val(self):
213        if self.message is None:
214            return None
215
216        if self.message.edns < 0:
217            return None
218
219        try:
220            nsid_opt = [o for o in self.message.options if o.otype == dns.edns.NSID][0]
221        except IndexError:
222            return None
223
224        try:
225            nsid_val = nsid_opt.data.decode('ascii')
226        except UnicodeDecodeError:
227            nsid_val = '0x' + lb2s(binascii.hexlify(nsid_opt.data))
228        return nsid_val
229
230    def request_cookie_tag(self):
231        from . import query as Q
232
233        if self.effective_server_cookie_status == Q.DNS_COOKIE_NO_COOKIE:
234            return 'NO_COOKIE'
235        elif self.effective_server_cookie_status == Q.DNS_COOKIE_IMPROPER_LENGTH:
236            return 'MALFORMED_COOKIE'
237        elif self.effective_server_cookie_status == Q.DNS_COOKIE_CLIENT_COOKIE_ONLY:
238            return 'CLIENT_COOKIE_ONLY'
239        elif self.effective_server_cookie_status == Q.DNS_COOKIE_SERVER_COOKIE_FRESH:
240            return 'VALID_SERVER_COOKIE'
241        elif self.effective_server_cookie_status == Q.DNS_COOKIE_SERVER_COOKIE_BAD:
242            return 'INVALID_SERVER_COOKIE'
243        else:
244            raise Exception('Unknown cookie status!')
245
246    def response_cookie_tag(self):
247
248        if self.message is None:
249            return 'ERROR'
250
251        if self.message.edns < 0:
252            return 'NO_EDNS'
253
254        try:
255            cookie_opt = [o for o in self.message.options if o.otype == 10][0]
256        except IndexError:
257            return 'NO_COOKIE_OPT'
258
259        if len(cookie_opt.data) < 8 or len(cookie_opt.data) > 40:
260            return 'MALFORMED_COOKIE'
261
262        elif len(cookie_opt.data) == 8:
263            return 'CLIENT_COOKIE_ONLY'
264
265        else:
266            return 'CLIENT_AND_SERVER_COOKIE'
267
268    def initial_query_tag(self):
269        return ''.join(self._query_tag_human(self.query.tcp, self.query.flags, self.query.edns, self.query.edns_flags, self.query.edns_max_udp_payload, self.query.edns_options, self.query.qname))
270
271    def effective_query_tag(self):
272        return ''.join(self._query_tag_human(self.effective_tcp, self.effective_flags, self.effective_edns, self.effective_edns_flags, self.query.edns_max_udp_payload, self.effective_edns_options, self.query.qname))
273
274    def section_rr_count(self, section):
275        if self.message is None:
276            return None
277        n = 0
278        for i in section:
279            n += len(i)
280        if section is self.message.additional and self.message.edns >= 0:
281            n += 1
282        return n
283
284    def section_digest(self, section):
285        if self.message is None:
286            return None
287        d = ''
288        rrsets = section[:]
289        rrsets.sort()
290        for rrset in rrsets:
291            d += RRsetInfo.rrset_canonicalized_to_wire(rrset, rrset.name, rrset.ttl)
292        return 'md5'+hashlib.md5(d).hexdigest()
293
294    def retries(self):
295        return len(self.history)
296
297    def total_response_time(self):
298        t = self.response_time
299        for retry in self.history:
300            t += retry.response_time
301        return t
302
303    def get_cookie_opt(self):
304        if self.message is None:
305            return None
306        try:
307            return [o for o in self.message.options if o.otype == 10][0]
308        except IndexError:
309            return None
310
311    def get_server_cookie(self):
312        cookie_opt = self.get_cookie_opt()
313        if cookie_opt is not None and len(cookie_opt.data) > 8:
314            return cookie_opt.data[8:]
315        return None
316
317    def copy(self):
318        clone = DNSResponse(self.message, self.msg_size, self.error, self.errno, self.history, self.response_time, self.query, self.server_cookie, self.server_cookie_status, review_history=False)
319        clone.set_effective_request_options(self.effective_flags, self.effective_edns, self.effective_edns_max_udp_payload, self.effective_edns_flags, self.effective_edns_options, self.effective_tcp, self.effective_server_cookie_status)
320        clone.set_responsiveness(self.udp_attempted, self.udp_responsive, self.tcp_attempted, self.tcp_responsive, self.responsive_cause_index, self.responsive_cause_index_tcp)
321        return clone
322
323    def set_effective_request_options(self, flags, edns, edns_max_udp_payload, edns_flags, edns_options, tcp, server_cookie_status):
324        self.effective_flags = flags
325        self.effective_edns = edns
326        self.effective_edns_max_udp_payload = edns_max_udp_payload
327        self.effective_edns_flags = edns_flags
328        self.effective_edns_options = edns_options
329        self.effective_tcp = tcp
330        self.effective_server_cookie_status = server_cookie_status
331
332    def set_responsiveness(self, udp_attempted, udp_responsive, tcp_attempted, tcp_responsive, responsive_cause_index, responsive_cause_index_tcp):
333        self.udp_attempted = udp_attempted
334        self.udp_responsive = udp_responsive
335        self.tcp_attempted = tcp_attempted
336        self.tcp_responsive = tcp_responsive
337        self.responsive_cause_index = responsive_cause_index
338        self.responsive_cause_index_tcp = responsive_cause_index_tcp
339
340    def _review_history(self):
341        from . import query as Q
342
343        flags = self.query.flags
344        edns = self.query.edns
345        edns_max_udp_payload = self.query.edns_max_udp_payload
346        edns_flags = self.query.edns_flags
347        edns_options = copy.deepcopy(self.query.edns_options)
348        server_cookie_status = self.server_cookie_status
349
350        # mark whether TCP or UDP was attempted initially
351        tcp_attempted = tcp = self.query.tcp
352        udp_attempted = not tcp
353
354        tcp_responsive = False
355        udp_responsive = False
356        tcp_valid = False
357        udp_valid = False
358
359        #TODO - there could be room for both a responsiveness check and a valid
360        # check here, rather than just a valid check
361
362        responsive_cause_index = None
363        responsive_cause_index_tcp = tcp
364
365        prev_index = None
366        for i, retry in enumerate(self.history):
367            # mark if TCP or UDP was attempted prior to this retry
368            if tcp:
369                tcp_attempted = True
370            else:
371                udp_attempted = True
372
373            # Mark responsiveness if this retry wasn't caused by network error
374            # or timeout.
375            if retry.cause not in (Q.RETRY_CAUSE_NETWORK_ERROR, Q.RETRY_CAUSE_TIMEOUT):
376                if tcp:
377                    tcp_responsive = True
378                else:
379                    udp_responsive = True
380
381            # If the last cause/action resulted in a valid response where there
382            # wasn't previously on the same protocol, then mark the
383            # cause/action.
384            if retry.cause in (Q.RETRY_CAUSE_TC_SET, Q.RETRY_CAUSE_DIAGNOSTIC):
385                if tcp:
386                    if responsive_cause_index is None and \
387                            not tcp_valid and prev_index is not None and self.history[prev_index].action != Q.RETRY_ACTION_USE_TCP:
388                        responsive_cause_index = prev_index
389                        responsive_cause_index_tcp = tcp
390                    tcp_valid = True
391                else:
392                    if responsive_cause_index is None and \
393                            not udp_valid and prev_index is not None and self.history[prev_index].action != Q.RETRY_ACTION_USE_UDP:
394                        responsive_cause_index = prev_index
395                        responsive_cause_index_tcp = tcp
396                    udp_valid = True
397
398            if retry.action == Q.RETRY_ACTION_NO_CHANGE:
399                pass
400            elif retry.action == Q.RETRY_ACTION_USE_TCP:
401                tcp = True
402            elif retry.action == Q.RETRY_ACTION_USE_UDP:
403                tcp = False
404            elif retry.action == Q.RETRY_ACTION_SET_FLAG:
405                flags |= retry.action_arg
406            elif retry.action == Q.RETRY_ACTION_CLEAR_FLAG:
407                flags &= ~retry.action_arg
408            elif retry.action == Q.RETRY_ACTION_DISABLE_EDNS:
409                edns = -1
410            elif retry.action == Q.RETRY_ACTION_CHANGE_UDP_MAX_PAYLOAD:
411                edns_max_udp_payload = retry.action_arg
412                tcp = False
413            elif retry.action == Q.RETRY_ACTION_SET_EDNS_FLAG:
414                edns_flags |= retry.action_arg
415            elif retry.action == Q.RETRY_ACTION_CLEAR_EDNS_FLAG:
416                edns_flags &= ~retry.action_arg
417            elif retry.action == Q.RETRY_ACTION_ADD_EDNS_OPTION:
418                #TODO option data
419                edns_options.append(dns.edns.GenericOption(retry.action_arg, b''))
420            elif retry.action == Q.RETRY_ACTION_REMOVE_EDNS_OPTION:
421                filtered_options = [x for x in edns_options if retry.action_arg == x.otype]
422                if filtered_options:
423                    edns_options.remove(filtered_options[0])
424                    # If COOKIE option was removed, then reset
425                    # server_cookie_status
426                    if filtered_options[0].otype == 10:
427                        server_cookie_status = Q.DNS_COOKIE_NO_COOKIE
428            elif retry.action == Q.RETRY_ACTION_CHANGE_SPORT:
429                pass
430            elif retry.action == Q.RETRY_ACTION_CHANGE_EDNS_VERSION:
431                edns = retry.action_arg
432            elif retry.action == Q.RETRY_ACTION_UPDATE_DNS_COOKIE:
433                server_cookie_status = Q.DNS_COOKIE_SERVER_COOKIE_FRESH
434
435            prev_index = i
436
437        # Mark responsiveness if the ultimate query didn't result in network
438        # error or timeout.
439        if self.error not in (Q.RESPONSE_ERROR_NETWORK_ERROR, Q.RESPONSE_ERROR_TIMEOUT):
440            if tcp:
441                tcp_responsive = True
442            else:
443                udp_responsive = True
444
445        # If the last cause/action resulted in a valid response where there
446        # wasn't previously on the same protocol, then mark the cause/action.
447        if self.is_valid_response():
448            if tcp:
449                if responsive_cause_index is None and \
450                        not tcp_valid and prev_index is not None and self.history[prev_index].action != Q.RETRY_ACTION_USE_TCP:
451                    responsive_cause_index = prev_index
452                    responsive_cause_index_tcp = tcp
453            else:
454                if responsive_cause_index is None and \
455                        not udp_valid and prev_index is not None and self.history[prev_index].action != Q.RETRY_ACTION_USE_UDP:
456                    responsive_cause_index = prev_index
457                    responsive_cause_index_tcp = tcp
458
459        # If EDNS was effectively disabled, reset EDNS options
460        if edns < 0:
461            edns_max_udp_payload = None
462            edns_flags = 0
463            edns_options = []
464            server_cookie_status = Q.DNS_COOKIE_NO_COOKIE
465
466        self.set_effective_request_options(flags, edns, edns_max_udp_payload, edns_flags, edns_options, tcp, server_cookie_status)
467        self.set_responsiveness(udp_attempted, udp_responsive, tcp_attempted, tcp_responsive, responsive_cause_index, responsive_cause_index_tcp)
468
469    def recursion_desired(self):
470        '''Return True if the recursion desired (RD) bit was set in the request to the
471        server.'''
472
473        return self.is_valid_response() and self.is_complete_response() and \
474                bool(self.effective_flags & dns.flags.RD)
475
476    def recursion_available(self):
477        '''Return True if the server indicated that recursion was available.'''
478
479        return self.is_valid_response() and self.is_complete_response() and \
480                bool(self.message.flags & dns.flags.RA)
481
482    def recursion_desired_and_available(self):
483        '''Return True if the recursion desired (RD) bit was set in the request to the
484        server AND the server indicated that recursion was available.'''
485
486        return self.is_valid_response() and self.is_complete_response() and \
487                bool(self.effective_flags & dns.flags.RD) and \
488                bool(self.message.flags & dns.flags.RA)
489
490    def dnssec_requested(self):
491        '''Return True if the DNSSEC OK (DO) bit was set in the request to the
492        server.'''
493
494        return self.effective_edns >= 0 and self.effective_edns_flags & dns.flags.DO
495
496    def is_valid_response(self):
497        '''Return True if the message has a sane error code, namely NOERROR or
498        NXDOMAIN.'''
499
500        return self.message is not None and self.message.rcode() in (dns.rcode.NOERROR, dns.rcode.NXDOMAIN)
501
502    def is_complete_response(self):
503        '''Return True if the message does not have the truncation (TC) bit
504        set.'''
505
506        return self.message is not None and not bool(self.message.flags & dns.flags.TC)
507
508    def is_authoritative(self):
509        '''Return True if the message has the authoritative answer (AA) bit
510        set.'''
511
512        return self.message is not None and bool(self.message.flags & dns.flags.AA)
513
514    def is_referral(self, qname, rdtype, rdclass, bailiwick, proper=False):
515        '''Return True if this response yields a referral for the queried
516        name.'''
517
518        if not (self.is_valid_response() and self.is_complete_response()):
519            return False
520        # if no bailiwick is specified, then we cannot classify it as a
521        # referral
522        if bailiwick is None:
523            return False
524        # if the qname is not a proper subdomain of the bailiwick, then it
525        # is not a referral
526        if not (qname != bailiwick and qname.is_subdomain(bailiwick)):
527            return False
528        # if the name exists in the answer section with the requested rdtype or
529        # CNAME, then it can't be a referral
530        if [x for x in self.message.answer if x.name == qname and x.rdtype in (rdtype, dns.rdatatype.CNAME) and x.rdclass == rdclass]:
531            return False
532        # if an SOA record with the given qname exists, then the server
533        # is authoritative for the name, so it is a referral
534        try:
535            self.message.find_rrset(self.message.authority, qname, rdclass, dns.rdatatype.SOA)
536            return False
537        except KeyError:
538            pass
539        # if proper referral is requested and qname is equal to of an NS RRset
540        # in the authority, then it is a referral
541        if proper:
542            if [x for x in self.message.authority if qname == x.name and x.rdtype == dns.rdatatype.NS and x.rdclass == rdclass]:
543                return True
544        # if proper referral is NOT requested, qname is a subdomain of
545        # (including equal to) an NS RRset in the authority, and qname is not
546        # equal to bailiwick, then it is a referral
547        else:
548            if [x for x in self.message.authority if qname.is_subdomain(x.name) and bailiwick != x.name and x.rdtype == dns.rdatatype.NS and x.rdclass == rdclass]:
549                return True
550        return False
551
552    def is_upward_referral(self, qname):
553        '''Return True if this response yields an upward referral (i.e., a name
554        that is a superdomain of qname).'''
555
556        if not (self.is_valid_response() and self.is_complete_response()):
557            return False
558        return bool(not self.is_authoritative() and \
559                [x for x in self.message.authority if x.name != qname and qname.is_subdomain(x.name)])
560
561    def is_answer(self, qname, rdtype, include_cname=True):
562        '''Return True if this response yields an answer for the queried name
563        and type in the answer section.  If include_cname is False, then only
564        non-CNAME records count.'''
565
566        if not (self.is_valid_response() and self.is_complete_response()):
567            return False
568        if rdtype == dns.rdatatype.ANY and [x for x in self.message.answer if x.name == qname]:
569            return True
570        rdtypes = [rdtype]
571        if include_cname:
572            rdtypes.append(dns.rdatatype.CNAME)
573        if [x for x in self.message.answer if x.name == qname and x.rdtype in rdtypes]:
574            return True
575        return False
576
577    def is_nxdomain(self, qname, rdtype):
578        '''Return True if this response indicates that the queried name does
579        not exist (i.e., is NXDOMAIN).'''
580
581        if not (self.is_valid_response() and self.is_complete_response()):
582            return False
583
584        if [x for x in self.message.answer if x.name == qname and x.rdtype in (rdtype, dns.rdatatype.CNAME)]:
585            return False
586
587        if self.message.rcode() == dns.rcode.NXDOMAIN:
588            return True
589
590        return False
591
592    def is_delegation(self, qname, rdtype):
593        '''Return True if this response (from a request to a server
594        authoritative for the immediate parent) yields NS records for the name
595        or provides a referral or NXDOMAIN or no data response.'''
596
597        # if NS or SOA records were found in the answer or authority section
598        return self.message.get_rrset(self.message.answer, qname, dns.rdataclass.IN, dns.rdatatype.NS) is not None or \
599                self.message.get_rrset(self.message.authority, qname, dns.rdataclass.IN, dns.rdatatype.NS) is not None or \
600                self.message.get_rrset(self.message.authority, qname, dns.rdataclass.IN, dns.rdatatype.SOA) is not None
601
602    def not_delegation(self, qname, rdtype):
603        return not self.is_delegation(qname, rdtype)
604
605    def ns_ip_mapping_from_additional(self, qname, bailiwick=None):
606        ip_mapping = {}
607
608        if not (self.is_valid_response() and self.is_complete_response()):
609            return ip_mapping
610
611        try:
612            ns_rrset = self.message.find_rrset(self.message.answer, qname, dns.rdataclass.IN, dns.rdatatype.NS)
613        except KeyError:
614            try:
615                ns_rrset = self.message.find_rrset(self.message.authority, qname, dns.rdataclass.IN, dns.rdatatype.NS)
616            except KeyError:
617                return ip_mapping
618
619        # iterate over each RR in the RR RRset
620        for ns_rr in ns_rrset:
621            ip_mapping[ns_rr.target] = set()
622
623            if bailiwick is not None and not ns_rr.target.is_subdomain(bailiwick):
624                continue
625
626            for rdtype in (dns.rdatatype.A, dns.rdatatype.AAAA):
627                try:
628                    a_rrset = self.message.find_rrset(self.message.additional, ns_rr.target, dns.rdataclass.IN, rdtype)
629                except KeyError:
630                    continue
631
632                ip_mapping[ns_rr.target].update([IPAddr(a_rr.to_text()) for a_rr in a_rrset])
633
634        return ip_mapping
635
636    def serialize_meta(self):
637        from . import query as Q
638
639        d = OrderedDict()
640
641        # populate history, if not already populated
642        if self.effective_flags is None:
643            self._review_history()
644
645        if self.message is None:
646            d['error'] = Q.response_errors[self.error]
647            if self.errno is not None:
648                errno_name = errno.errorcode.get(self.errno, None)
649                if errno_name is not None:
650                    d['errno'] = errno_name
651        else:
652            d['rcode'] = dns.rcode.to_text(self.message.rcode())
653            if self.message.edns >= 0:
654                d['edns_version'] = self.message.edns
655            d['answer'] = OrderedDict((
656                ('count', self.section_rr_count(self.message.answer)),
657                ('digest', self.section_digest(self.message.answer)),
658            ))
659            d['authority'] = OrderedDict((
660                ('count', self.section_rr_count(self.message.authority)),
661                ('digest', self.section_digest(self.message.authority)),
662            ))
663            d['additional'] = OrderedDict((
664                ('count', self.section_rr_count(self.message.additional)),
665                ('digest', self.section_digest(self.message.additional)),
666            ))
667            if not d['answer']['count']:
668                del d['answer']['digest']
669            if not d['authority']['count']:
670                del d['authority']['digest']
671            if not d['additional']['count']:
672                del d['additional']['digest']
673
674        if self.msg_size is not None:
675            d['msg_size'] = self.msg_size
676        d['time_elapsed'] = int(self.response_time * 1000)
677        d['retries'] = self.retries()
678        if self.history:
679            d['cumulative_response_time'] = int(self.total_response_time() * 1000)
680            d['effective_query_options'] = OrderedDict((
681                ('flags', self.effective_flags),
682                ('edns_version', self.effective_edns),
683                ('edns_max_udp_payload', self.effective_edns_max_udp_payload),
684                ('edns_flags', self.effective_edns_flags),
685                ('edns_options', []),
686            ))
687            for o in self.effective_edns_options:
688                s = io.BytesIO()
689                o.to_wire(s)
690                d['effective_query_options']['edns_options'].append((o.type, binascii.hexlify(s.getvalue())))
691            d['effective_query_options']['tcp'] = self.effective_tcp
692
693            if self.responsive_cause_index is not None:
694                d['responsiveness_impediment'] = OrderedDict((
695                    ('cause', Q.retry_causes[self.history[self.responsive_cause_index].cause]),
696                    ('action', Q.retry_actions[self.history[self.responsive_cause_index].action])
697                ))
698
699        return d
700
701    def serialize(self):
702        from . import query as Q
703
704        d = OrderedDict()
705        if self.message is None:
706            d['message'] = None
707            d['error'] = Q.response_errors[self.error]
708            if self.errno is not None:
709                errno_name = errno.errorcode.get(self.errno, None)
710                if errno_name is not None:
711                    d['errno'] = errno_name
712        else:
713            d['message'] = lb2s(base64.b64encode(self.message.to_wire()))
714        if self.msg_size is not None:
715            d['msg_size'] = self.msg_size
716        d['time_elapsed'] = int(self.response_time * 1000)
717        d['history'] = []
718        for retry in self.history:
719            d['history'].append(retry.serialize())
720        return d
721
722    @classmethod
723    def deserialize(cls, d, query, server_cookie, server_cookie_status):
724        from . import query as Q
725
726        if 'msg_size' in d:
727            msg_size = int(d['msg_size'])
728        else:
729            msg_size = None
730        if 'error' in d:
731            error = Q.response_error_codes[d['error']]
732        else:
733            error = None
734        if 'errno' in d:
735            # compatibility with version 1.0
736            if isinstance(d['errno'], int):
737                errno1 = d['errno']
738            else:
739                if hasattr(errno, d['errno']):
740                    errno1 = getattr(errno, d['errno'])
741                else:
742                    errno1 = None
743        else:
744            errno1 = None
745
746        if d['message'] is None:
747            message = None
748        else:
749            wire = base64.b64decode(d['message'])
750            try:
751                message = dns.message.from_wire(wire)
752            except Exception as e:
753                message = None
754                if isinstance(e, (struct.error, dns.exception.FormError)):
755                    error = Q.RESPONSE_ERROR_FORMERR
756                #XXX need to determine how to handle non-parsing
757                # validation errors with dnspython (e.g., signature with
758                # no keyring)
759                else:
760                    error = Q.RESPONSE_ERROR_OTHER
761
762        # compatibility with version 1.0
763        if 'response_time' in d:
764            response_time = d['response_time']
765        else:
766            response_time = d['time_elapsed']/1000.0
767        history = []
768        for retry in d['history']:
769            history.append(Q.DNSQueryRetryAttempt.deserialize(retry))
770        return DNSResponse(message, msg_size, error, errno1, history, response_time, query, server_cookie, server_cookie_status)
771
772class DNSResponseComponent(object):
773    def __init__(self):
774        self.servers_clients = {}
775
776    def add_server_client(self, server, client, response):
777        if (server, client) not in self.servers_clients:
778            self.servers_clients[(server, client)] = []
779        self.servers_clients[(server, client)].append(response)
780
781    @classmethod
782    def insert_into_list(cls, component_info, component_info_list, server, client, response):
783        try:
784            index = component_info_list.index(component_info)
785            component_info = component_info_list[index]
786        except ValueError:
787            component_info_list.append(component_info)
788        component_info.add_server_client(server, client, response)
789        return component_info
790
791class RDataMeta(DNSResponseComponent):
792    def __init__(self, name, ttl, rdtype, rdata):
793        super(RDataMeta, self).__init__()
794        self.name = name
795        self.ttl = ttl
796        self.rdtype = rdtype
797        self.rdata = rdata
798        self.rrset_info = set()
799
800class DNSKEYMeta(DNSResponseComponent):
801    def __init__(self, name, rdata, ttl):
802        super(DNSKEYMeta, self).__init__()
803        self.name = name
804        self.rdata = rdata
805        self.ttl = ttl
806        self.warnings = []
807        self.errors = []
808        self.rrset_info = []
809
810        self.key_tag = self.calc_key_tag(rdata)
811        self.key_tag_no_revoke = self.calc_key_tag(rdata, True)
812        self.key_len = self.calc_key_len(rdata)
813
814    def __str__(self):
815        return 'DNSKEY for %s (algorithm %d (%s), key tag %d)' % (fmt.humanize_name(self.name), self.rdata.algorithm, fmt.DNSKEY_ALGORITHMS.get(self.rdata.algorithm, self.rdata.algorithm), self.key_tag)
816
817    @classmethod
818    def calc_key_tag(cls, rdata, clear_revoke=False):
819        '''Return the key_tag for the key, as specified in RFC 4034.  If
820        clear_revoke is True, then clear the revoke flag of the DNSKEY RR
821        first.'''
822
823        # python3/python2 dual compatibility
824        if isinstance(rdata.key, bytes):
825            if isinstance(rdata.key, str):
826                map_func = lambda x, y: ord(x[y])
827            else:
828                map_func = lambda x, y: x[y]
829        else:
830            map_func = lambda x, y: struct.unpack(b'B',x[y])[0]
831
832        # algorithm 1 is a special case
833        if rdata.algorithm == 1:
834            b1 = map_func(rdata.key, -3)
835            b2 = map_func(rdata.key, -2)
836            return (b1 << 8) | b2
837
838        if clear_revoke:
839            flags = rdata.flags & (~fmt.DNSKEY_FLAGS['revoke'])
840        else:
841            flags = rdata.flags
842
843        key_str = struct.pack(b'!HBB', flags, rdata.protocol, rdata.algorithm) + rdata.key
844
845        ac = 0
846        for i in range(len(key_str)):
847            b = map_func(key_str, i)
848            if i & 1:
849                ac += b
850            else:
851                ac += (b << 8)
852
853        ac += (ac >> 16) & 0xffff
854        return ac & 0xffff
855
856    @classmethod
857    def calc_key_len(cls, rdata):
858        '''Return the length of the key modulus, in bits.'''
859
860        key_str = rdata.key
861
862        # python3/python2 dual compatibility
863        if isinstance(rdata.key, bytes):
864            if isinstance(rdata.key, str):
865                map_func = lambda x, y: ord(x[y])
866            else:
867                map_func = lambda x, y: x[y]
868        else:
869            map_func = lambda x, y: struct.unpack(b'B',x[y])[0]
870
871        # RSA keys
872        if rdata.algorithm in (1,5,7,8,10):
873            try:
874                # get the exponent length
875                e_len = map_func(key_str, 0)
876            except IndexError:
877                return 0
878
879            offset = 1
880            if e_len == 0:
881                b1 = map_func(key_str, 1)
882                b2 = map_func(key_str, 2)
883                e_len = (b1 << 8) | b2
884                offset = 3
885
886            # get the exponent
887            offset += e_len
888
889            # get the modulus
890            key_len = len(key_str) - offset
891
892            # if something went wrong here, use key length of rdata key
893            if key_len <= 0:
894                return len(key_str)<<3
895
896            return key_len << 3
897
898        # DSA keys
899        elif rdata.algorithm in (3,6):
900            t = map_func(key_str, 0)
901            return (64 + t*8)<<3
902
903        # GOST keys
904        elif rdata.algorithm in (12,):
905            return len(key_str)<<3
906
907        # EC keys
908        elif rdata.algorithm in (13,14):
909            return len(key_str)<<3
910
911        # EDDSA keys
912        elif rdata.algorithm in (15,16):
913            return len(key_str)<<3
914
915        # other keys - just guess, based on the length of the raw key material
916        else:
917            return len(key_str)<<3
918
919    def message_for_ds(self, clear_revoke=False):
920        '''Return the string value suitable for hashing to create a DS
921        record.'''
922
923        if clear_revoke:
924            flags = self.rdata.flags & (~fmt.DNSKEY_FLAGS['revoke'])
925        else:
926            flags = self.rdata.flags
927
928        name_wire = self.name.canonicalize().to_wire()
929
930        # write DNSKEY rdata in wire format
931        rdata_wire = struct.pack(b'!HBB', flags, self.rdata.protocol, self.rdata.algorithm)
932
933        return name_wire + rdata_wire + self.rdata.key
934
935    def serialize(self, consolidate_clients=True, show_servers=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
936        from .analysis import status as Status
937
938        show_id = loglevel <= logging.INFO or \
939                (self.warnings and loglevel <= logging.WARNING) or \
940                (self.errors and loglevel <= logging.ERROR)
941
942        d = OrderedDict()
943
944        if html_format:
945            formatter = lambda x: escape(x, True)
946        else:
947            formatter = lambda x: x
948
949        if show_id:
950            d['id'] = '%d/%d' % (self.rdata.algorithm, self.key_tag)
951        if loglevel <= logging.DEBUG:
952            d['description'] = formatter(str(self))
953            d['flags'] = self.rdata.flags
954            d['protocol'] = self.rdata.protocol
955            d['algorithm'] = self.rdata.algorithm
956            d['key'] = lb2s(base64.b64encode(self.rdata.key))
957            d['ttl'] = self.ttl
958            d['key_length'] = self.key_len
959            d['key_tag'] = self.key_tag
960            if self.rdata.flags & fmt.DNSKEY_FLAGS['revoke']:
961                d['key_tag_pre_revoke'] = self.key_tag_no_revoke
962
963            if html_format:
964                flags = [t for (t,c) in fmt.DNSKEY_FLAGS.items() if c & self.rdata.flags]
965                d['flags'] = '%d (%s)' % (self.rdata.flags, ', '.join(flags))
966                d['protocol'] = '%d (%s)' % (self.rdata.protocol, fmt.DNSKEY_PROTOCOLS.get(self.rdata.protocol, self.rdata.protocol))
967                d['algorithm'] = '%d (%s)' % (self.rdata.algorithm, fmt.DNSKEY_ALGORITHMS.get(self.rdata.algorithm, self.rdata.algorithm))
968                d['ttl'] = '%d (%s)' % (self.ttl, fmt.humanize_time(self.ttl))
969                if self.key_len is None:
970                    d['key_length'] = 'unknown'
971                else:
972                    d['key_length'] = '%d bits' % (self.key_len)
973
974        #TODO: put DNSKEY roles in meta, if it makes sense
975
976        if loglevel <= logging.INFO:
977            servers = tuple_to_dict(self.servers_clients)
978            if consolidate_clients:
979                servers = list(servers)
980                servers.sort()
981            d['servers'] = servers
982
983            if map_ip_to_ns_name is not None:
984                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
985                ns_names.sort()
986                d['ns_names'] = ns_names
987
988            tags = set()
989            nsids = set()
990            for server,client in self.servers_clients:
991                for response in self.servers_clients[(server,client)]:
992                    tags.add(response.effective_query_tag())
993                    nsid = response.nsid_val()
994                    if nsid is not None:
995                        nsids.add(nsid)
996
997            if nsids:
998                d['nsid_values'] = list(nsids)
999                d['nsid_values'].sort()
1000
1001            d['query_options'] = list(tags)
1002            d['query_options'].sort()
1003
1004        if self.warnings and loglevel <= logging.WARNING:
1005            d['warnings'] = [w.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for w in self.warnings]
1006
1007        if self.errors and loglevel <= logging.ERROR:
1008            d['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for e in self.errors]
1009
1010        return d
1011
1012#XXX This class is necessary because of a bug in dnspython, in which
1013# comparisons are not properly made for the purposes of sorting rdata for RRSIG
1014# validation
1015class RdataWrapper(object):
1016    def __init__(self, rdata):
1017        self._rdata = rdata
1018
1019    def __eq__(self, other):
1020        return self._rdata.to_digestable() == other._rdata.to_digestable()
1021
1022    def __lt__(self, other):
1023        return self._rdata.to_digestable() < other._rdata.to_digestable()
1024
1025class RRsetInfo(DNSResponseComponent):
1026    def __init__(self, rrset, ttl_cmp, dname_info=None):
1027        super(RRsetInfo, self).__init__()
1028        self.rrset = rrset
1029        self.ttl_cmp = ttl_cmp
1030        self.rrsig_info = {}
1031        self.wildcard_info = {}
1032
1033        self.dname_info = dname_info
1034        if self.dname_info is not None:
1035            self.servers_clients = dname_info.servers_clients
1036
1037        self.cname_info_from_dname = []
1038
1039    def __str__(self):
1040        if self.rrset.rdtype == dns.rdatatype.NSEC3:
1041            return 'RRset for %s/%s' % (fmt.format_nsec3_name(self.rrset.name).rstrip('.'), dns.rdatatype.to_text(self.rrset.rdtype))
1042        else:
1043            return 'RRset for %s/%s' % (fmt.humanize_name(self.rrset.name), dns.rdatatype.to_text(self.rrset.rdtype))
1044
1045    def __repr__(self):
1046        return '<%s: "%s">' % (self.__class__.__name__, str(self))
1047
1048    def __eq__(self, other):
1049        if not (self.rrset == other.rrset and self.dname_info == other.dname_info):
1050            return False
1051        if self.ttl_cmp and self.rrset.ttl != other.rrset.ttl:
1052            return False
1053        return True
1054
1055    def __hash__(self):
1056        return hash(id(self))
1057
1058    @classmethod
1059    def rrset_canonicalized_to_wire(cls, rrset, name, ttl):
1060        s = b''
1061        name_wire = name.to_wire()
1062
1063        rdata_list = [RdataWrapper(x) for x in rrset]
1064        rdata_list.sort()
1065
1066        for rdataw in rdata_list:
1067            rdata = rdataw._rdata
1068            rdata_wire = rdata.to_digestable()
1069            rdata_len = len(rdata_wire)
1070
1071            stuff = struct.pack(b'!HHIH', rrset.rdtype, rrset.rdclass,
1072                                ttl, rdata_len)
1073            s += name_wire + stuff + rdata_wire
1074
1075        return s
1076
1077    def get_rrsig_info(self, rrsig):
1078        return self.rrsig_info[rrsig]
1079
1080    def update_rrsig_info(self, server, client, response, section, rdclass, is_referral):
1081        try:
1082            rrsig_rrset = response.message.find_rrset(section, self.rrset.name, rdclass, dns.rdatatype.RRSIG, self.rrset.rdtype)
1083            for rrsig in rrsig_rrset:
1084                self.create_or_update_rrsig_info(rrsig, rrsig_rrset.ttl, server, client, response, rdclass, is_referral)
1085        except KeyError:
1086            pass
1087
1088        if self.dname_info is not None:
1089            self.dname_info.update_rrsig_info(server, client, response, section, rdclass, is_referral)
1090
1091    def create_or_update_rrsig_info(self, rrsig, ttl, server, client, response, rdclass, is_referral):
1092        try:
1093            rrsig_info = self.get_rrsig_info(rrsig)
1094        except KeyError:
1095            rrsig_info = self.rrsig_info[rrsig] = RDataMeta(self.rrset.name, ttl, dns.rdatatype.RRSIG, rrsig)
1096        rrsig_info.add_server_client(server, client, response)
1097        self.set_wildcard_info(rrsig, server, client, response, rdclass, is_referral)
1098
1099    def create_or_update_cname_from_dname_info(self, synthesized_cname_info, server, client, response, rdclass):
1100        return self.insert_into_list(synthesized_cname_info, self.cname_info_from_dname, server, client, response)
1101
1102    def is_wildcard(self, rrsig):
1103        if self.rrset.name[0] == b'*':
1104            return False
1105        return len(self.rrset.name) - 1 > rrsig.labels
1106
1107    def reduce_wildcard(self, rrsig):
1108        if self.is_wildcard(rrsig):
1109            return dns.name.Name(('*',)+self.rrset.name.labels[-(rrsig.labels+1):])
1110        return self.rrset.name
1111
1112    def set_wildcard_info(self, rrsig, server, client, response, rdclass, is_referral):
1113        if self.is_wildcard(rrsig):
1114            wildcard_name = self.reduce_wildcard(rrsig)
1115            if wildcard_name not in self.wildcard_info:
1116                self.wildcard_info[wildcard_name] = NegativeResponseInfo(self.rrset.name, self.rrset.rdtype, self.ttl_cmp)
1117            self.wildcard_info[wildcard_name].add_server_client(server, client, response)
1118            self.wildcard_info[wildcard_name].create_or_update_nsec_info(server, client, response, rdclass, is_referral)
1119
1120    def message_for_rrsig(self, rrsig):
1121
1122        # write RRSIG in wire format
1123        rdata_wire = struct.pack(b'!HBBIIIH', rrsig.type_covered,
1124                             rrsig.algorithm, rrsig.labels,
1125                             rrsig.original_ttl, rrsig.expiration,
1126                             rrsig.inception, rrsig.key_tag)
1127        signer_wire = rrsig.signer.canonicalize().to_wire()
1128        rrsig_canonicalized_wire = rdata_wire + signer_wire
1129
1130        rrset_name = self.reduce_wildcard(rrsig).canonicalize()
1131        rrset_canonicalized_wire = self.rrset_canonicalized_to_wire(self.rrset, rrset_name, rrsig.original_ttl)
1132
1133        return rrsig_canonicalized_wire + rrset_canonicalized_wire
1134
1135    def serialize(self, consolidate_clients=True, show_servers=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
1136        d = OrderedDict()
1137
1138        if html_format:
1139            formatter = lambda x: escape(x, True)
1140        else:
1141            formatter = lambda x: x
1142
1143        if self.rrset.rdtype == dns.rdatatype.NSEC3:
1144            d['name'] = formatter(fmt.format_nsec3_name(self.rrset.name))
1145        else:
1146            d['name'] = formatter(lb2s(self.rrset.name.canonicalize().to_text()))
1147        d['ttl'] = self.rrset.ttl
1148        d['type'] = dns.rdatatype.to_text(self.rrset.rdtype)
1149        d['rdata'] = []
1150        rdata_list = [RdataWrapper(x) for x in self.rrset]
1151        rdata_list.sort()
1152        for rdataw in rdata_list:
1153            rdata = rdataw._rdata
1154            if self.rrset.rdtype == dns.rdatatype.NSEC3:
1155                d['rdata'].append(fmt.format_nsec3_rrset_text(self.rrset[0].to_text()))
1156            else:
1157                s = rdata.to_text()
1158                # python3/python2 dual compatibility
1159                if not isinstance(s, str):
1160                    s = lb2s(s)
1161                d['rdata'].append(formatter(s))
1162
1163        if loglevel <= logging.INFO:
1164            servers = tuple_to_dict(self.servers_clients)
1165            if consolidate_clients:
1166                servers = list(servers)
1167                servers.sort()
1168            d['servers'] = servers
1169
1170            if map_ip_to_ns_name is not None:
1171                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
1172                ns_names.sort()
1173                d['ns_names'] = ns_names
1174
1175            tags = set()
1176            nsids = set()
1177            for server,client in self.servers_clients:
1178                for response in self.servers_clients[(server,client)]:
1179                    tags.add(response.effective_query_tag())
1180                    nsid = response.nsid_val()
1181                    if nsid is not None:
1182                        nsids.add(nsid)
1183
1184            if nsids:
1185                d['nsid_values'] = list(nsids)
1186                d['nsid_values'].sort()
1187
1188            d['query_options'] = list(tags)
1189            d['query_options'].sort()
1190
1191        return d
1192
1193def cname_from_dname(name, dname_rrset):
1194    synthesized_cname = dns.name.Name(name.labels[:-len(dname_rrset.name)] + dname_rrset[0].target.labels)
1195    rrset = dns.rrset.RRset(name, dns.rdataclass.IN, dns.rdatatype.CNAME)
1196    rrset.update_ttl(dname_rrset.ttl)
1197    rrset.add(dns.rdtypes.ANY.CNAME.CNAME(dns.rdataclass.IN, dns.rdatatype.CNAME, synthesized_cname))
1198    return rrset
1199
1200class NegativeResponseInfo(DNSResponseComponent):
1201    def __init__(self, qname, rdtype, ttl_cmp):
1202        super(NegativeResponseInfo, self).__init__()
1203        self.qname = qname
1204        self.rdtype = rdtype
1205        self.ttl_cmp = ttl_cmp
1206        self.soa_rrset_info = []
1207        self.nsec_set_info = []
1208
1209    def __repr__(self):
1210        return '<%s %s/%s>' % (self.__class__.__name__, self.qname, dns.rdatatype.to_text(self.rdtype))
1211
1212    def __eq__(self, other):
1213        return self.qname == other.qname and self.rdtype == other.rdtype
1214
1215    def __hash__(self):
1216        return hash(id(self))
1217
1218    def create_or_update_soa_info(self, server, client, response, rdclass, is_referral):
1219        soa_rrsets = [x for x in response.message.authority if x.rdtype == dns.rdatatype.SOA and x.rdclass == rdclass and self.qname.is_subdomain(x.name)]
1220        if not soa_rrsets:
1221            soa_rrsets = [x for x in response.message.authority if x.rdtype == dns.rdatatype.SOA and x.rdclass == rdclass]
1222        soa_rrsets.sort(reverse=True)
1223        try:
1224            soa_rrset = soa_rrsets[0]
1225        except IndexError:
1226            soa_rrset = None
1227
1228        if soa_rrset is None:
1229            return None
1230
1231        soa_rrset_info = RRsetInfo(soa_rrset, self.ttl_cmp)
1232        soa_rrset_info = self.insert_into_list(soa_rrset_info, self.soa_rrset_info, server, client, response)
1233        soa_rrset_info.update_rrsig_info(server, client, response, response.message.authority, rdclass, is_referral)
1234
1235        return soa_rrset_info
1236
1237    def create_or_update_nsec_info(self, server, client, response, rdclass, is_referral):
1238        for rdtype in dns.rdatatype.NSEC, dns.rdatatype.NSEC3:
1239            nsec_rrsets = [x for x in response.message.authority if x.rdtype == rdtype and x.rdclass == rdclass]
1240            if not nsec_rrsets:
1241                continue
1242
1243            nsec_set_info = NSECSet(nsec_rrsets, is_referral, self.ttl_cmp)
1244            nsec_set_info = self.insert_into_list(nsec_set_info, self.nsec_set_info, server, client, response)
1245
1246            for name in nsec_set_info.rrsets:
1247                nsec_set_info.rrsets[name].update_rrsig_info(server, client, response, response.message.authority, rdclass, is_referral)
1248
1249class NSECSet(DNSResponseComponent):
1250    def __init__(self, rrsets, referral, ttl_cmp):
1251        super(NSECSet, self).__init__()
1252        self.rrsets = {}
1253        self.referral = referral
1254        self.ttl_cmp = ttl_cmp
1255        self.nsec3_params = {}
1256        self.invalid_nsec3_owner = set()
1257        self.invalid_nsec3_hash = set()
1258        self.use_nsec3 = False
1259        for rrset in rrsets:
1260            #XXX There shouldn't be multiple NSEC(3) RRsets of the same owner
1261            # name in the same response, but check for it and address it (if
1262            # necessary)
1263            assert rrset.name not in self.rrsets
1264            self.rrsets[rrset.name] = RRsetInfo(rrset, self.ttl_cmp)
1265
1266            if rrset.rdtype == dns.rdatatype.NSEC3:
1267                self.use_nsec3 = True
1268                key = (rrset[0].salt, rrset[0].algorithm, rrset[0].iterations)
1269                if key not in self.nsec3_params:
1270                    self.nsec3_params[key] = set()
1271                self.nsec3_params[key].add(rrset.name)
1272                if not self.is_valid_nsec3_name(rrset.name, rrset[0].algorithm):
1273                    self.invalid_nsec3_owner.add(rrset.name)
1274                if not self.is_valid_nsec3_hash(rrset[0].next, rrset[0].algorithm):
1275                    self.invalid_nsec3_hash.add(rrset.name)
1276
1277        self.servers_clients = {}
1278
1279    def __repr__(self):
1280        return '<%s>' % (self.__class__.__name__)
1281
1282    def __eq__(self, other):
1283        return self.rrsets == other.rrsets
1284
1285    def __hash__(self):
1286        return hash(id(self))
1287
1288    def project(self, *names):
1289        if set(names).difference(self.rrsets):
1290            raise ValueError('NSEC name(s) don\'t exist in NSECSet')
1291
1292        obj = self.__class__((), self.referral, self.ttl_cmp)
1293        for name in names:
1294            obj.rrsets[name] = self.rrsets[name]
1295            rrset = obj.rrsets[name].rrset
1296            if rrset.rdtype == dns.rdatatype.NSEC3:
1297                obj.use_nsec3 = True
1298                key = (rrset[0].salt, rrset[0].algorithm, rrset[0].iterations)
1299                if key not in obj.nsec3_params:
1300                    obj.nsec3_params[key] = set()
1301                obj.nsec3_params[key].add(rrset.name)
1302                if not obj.is_valid_nsec3_name(rrset.name, rrset[0].algorithm):
1303                    obj.invalid_nsec3_owner.add(rrset.name)
1304                if not obj.is_valid_nsec3_hash(rrset[0].next, rrset[0].algorithm):
1305                    obj.invalid_nsec3_hash.add(rrset.name)
1306
1307        obj.servers_clients = self.servers_clients.copy()
1308        return obj
1309
1310    def add_server_client(self, server, client, response):
1311        super(NSECSet, self).add_server_client(server, client, response)
1312        for name, rrset_info in self.rrsets.items():
1313            rrset_info.add_server_client(server, client, response)
1314
1315    def create_or_update_rrsig_info(self, name, rrsig, ttl, server, client, response, rdclass, is_referral):
1316        self.rrsets[name].create_or_update_rrsig_info(rrsig, ttl, server, client, response, rdclass, is_referral)
1317
1318    def is_valid_nsec3_name(self, nsec_name, algorithm):
1319        # python3/python2 dual compatibility
1320        if isinstance(nsec_name[0], str):
1321            map_func = lambda x: codecs.encode(x.upper(), 'latin1')
1322        else:
1323            map_func = lambda x: codecs.encode(chr(x).upper(), 'latin1')
1324
1325        # check that NSEC3 name is valid
1326        if algorithm == 1:
1327            # base32hex encoding of SHA1 should be 32 bytes
1328            if len(nsec_name[0]) != 32:
1329                return False
1330        if [x for x in nsec_name[0] if map_func(x) not in base32.b32alphabet]:
1331            return False
1332        return True
1333
1334    def is_valid_nsec3_hash(self, nsec3_hash, algorithm):
1335        # check that NSEC3 hash is valid
1336        if algorithm == 1:
1337            # length of SHA1 hash should be 20 bytes
1338            if len(nsec3_hash) != 20:
1339                return False
1340        return True
1341
1342    def get_algorithm_support(self):
1343        valid_algorithms = set()
1344        invalid_algorithms = set()
1345        for (salt, alg, iterations) in self.nsec3_params:
1346            if crypto.nsec3_alg_is_supported(alg):
1347                valid_algorithms.add(alg)
1348            else:
1349                invalid_algorithms.add(alg)
1350        return valid_algorithms, invalid_algorithms
1351
1352    def rdtype_exists_in_bitmap(self, nsec_name, rdtype):
1353        '''Return True if the rdtype exists in the bitmap of the NSEC(3) record
1354        corresponding to the name; False otherwise.'''
1355
1356        rdtype_window = (rdtype >> 8)
1357        rdtype_bitmap = rdtype & 0x00ff
1358        bitmap_index, bitmap_offset = divmod(rdtype_bitmap, 8)
1359        for (window, bitmap) in self.rrsets[nsec_name].rrset[0].windows:
1360            try:
1361                # dnspython <= 1.12.x uses strings, but dnspython 1.13 uses bytearray (for python3)
1362                byte = bitmap[bitmap_index]
1363                if isinstance(bitmap, str):
1364                    byte = ord(byte)
1365                if window == rdtype_window and byte & (0x80 >> bitmap_offset):
1366                    return True
1367            except IndexError:
1368                pass
1369        return False
1370
1371    def name_for_nsec3_next(self, nsec_name):
1372        '''Convert the next field of an NSEC3 RR to a DNS name.'''
1373
1374        next_name = self.rrsets[nsec_name].rrset[0].next
1375        next_name_txt = base32.b32encode(next_name)
1376        origin = dns.name.Name(nsec_name.labels[1:])
1377        return dns.name.from_text(next_name_txt, origin)
1378
1379    def _nsec_covers_name(self, name, nsec_name):
1380        '''Return True if the NSEC record corresponding to NSEC name provided
1381        covers a name (i.e., proves its non-existence); False otherwise.'''
1382
1383        prev_name = nsec_name
1384        if self.use_nsec3:
1385            next_name = self.name_for_nsec3_next(nsec_name)
1386            # test that NSEC3 names have the same parent
1387            try:
1388                if not (name.parent() == nsec_name.parent() == next_name.parent()):
1389                    return False
1390            except dns.name.NoParent:
1391                return False
1392        else:
1393            next_name = self.rrsets[nsec_name].rrset[0].next
1394
1395        if prev_name == next_name:
1396            return prev_name != name
1397        elif prev_name > next_name:
1398            return not (next_name <= name <= prev_name)
1399        else:
1400            return (prev_name < name < next_name) and not next_name.is_subdomain(name)
1401
1402    def nsec_covering_name(self, name):
1403        '''Return the set of owner names corresponding to NSEC records in the
1404        response that cover the given name.'''
1405
1406        excluding_names = set()
1407        for nsec_name in set(self.rrsets).difference(self.invalid_nsec3_owner.union(self.invalid_nsec3_hash)):
1408            if self._nsec_covers_name(name, nsec_name):
1409                excluding_names.add(nsec_name)
1410        return excluding_names
1411
1412    def get_digest_name_for_nsec3(self, name, origin, salt, alg, iterations):
1413        '''Return the DNS name corresponding to the name, origin, and NSEC3
1414        hash parameters provided.'''
1415
1416        val = name.canonicalize().to_wire()
1417        digest = crypto.get_digest_for_nsec3(val, salt, alg, iterations)
1418        if digest is None:
1419            return None
1420        else:
1421            return dns.name.from_text(base32.b32encode(digest), origin)
1422
1423    def nsec3_covering_name(self, name, salt, alg, iterations):
1424        '''Return the set of owner names corresponding to NSEC3 records in the
1425        response that cover the given (digest) name.'''
1426
1427        excluding_names = set()
1428        for nsec_name in set(self.nsec3_params[(salt, alg, iterations)]).difference(self.invalid_nsec3_owner.union(self.invalid_nsec3_hash)):
1429            if self._nsec_covers_name(name, nsec_name):
1430                excluding_names.add(nsec_name)
1431        return excluding_names
1432
1433    def _find_potential_closest_enclosers(self, qname, origin, salt, alg, iterations):
1434        '''Return a mapping of potential closest enclosers for a given name and
1435        origin, with digests computed with the given salt, algorithm, and
1436        iterations parameters.  The mapping maps a name to a set of
1437        corresponding digest names. The algorithm follows that specified in RFC
1438        5155 8.3.'''
1439
1440        closest_enclosers = {}
1441        nsec3_names = self.nsec3_params[(salt, alg, iterations)]
1442
1443        sname = qname
1444        flag = False
1445        while len(sname) >= len(origin):
1446            digest_name = self.get_digest_name_for_nsec3(sname, origin, salt, alg, iterations)
1447
1448            # unsupported algorithm
1449            if digest_name is None:
1450                return closest_enclosers
1451
1452            if digest_name not in nsec3_names:
1453                flag = False
1454
1455            if self.nsec_covering_name(digest_name):
1456                flag = True
1457
1458            if digest_name in nsec3_names:
1459                if flag:
1460                    if sname not in closest_enclosers:
1461                        closest_enclosers[sname] = set()
1462                    closest_enclosers[sname].add(digest_name)
1463                break
1464
1465            sname = dns.name.Name(sname.labels[1:])
1466        return closest_enclosers
1467
1468    def check_closest_encloser(self, name, nsec_name, origin):
1469        '''Return True if the candidate closest encloser meets the requirements
1470        for a closest encloser in RFC 5155.'''
1471
1472        if not name.is_subdomain(origin):
1473            return False
1474        if self.rdtype_exists_in_bitmap(nsec_name, dns.rdatatype.DNAME):
1475            return False
1476        if self.rdtype_exists_in_bitmap(nsec_name, dns.rdatatype.NS) and \
1477                not self.rdtype_exists_in_bitmap(nsec_name, dns.rdatatype.SOA):
1478            return False
1479        return True
1480
1481    def get_closest_encloser(self, qname, origin):
1482        '''Return a mapping of closest enclosers for a given name and
1483        origin.'''
1484
1485        potential_closest_enclosers = {}
1486        for salt, alg, iterations in self.nsec3_params:
1487            ret = self._find_potential_closest_enclosers(qname, origin, salt, alg, iterations)
1488            for name in ret:
1489                if name in potential_closest_enclosers:
1490                    potential_closest_enclosers[name].update(ret[name])
1491                else:
1492                    potential_closest_enclosers[name] = ret[name]
1493
1494        for name in list(potential_closest_enclosers):
1495            for nsec_name in list(potential_closest_enclosers[name]):
1496                if not self.check_closest_encloser(name, nsec_name, origin):
1497                    potential_closest_enclosers[name].remove(nsec_name)
1498            if not potential_closest_enclosers[name]:
1499                del potential_closest_enclosers[name]
1500
1501        return potential_closest_enclosers
1502
1503class DNSResponseError(DNSResponseComponent):
1504    def __init__(self, code, arg):
1505        super(DNSResponseError, self).__init__()
1506        self.code = code
1507        self.arg = arg
1508
1509    def __eq__(self, other):
1510        return self.code == other.code and self.arg == other.arg
1511
1512    def __hash__(self):
1513        return hash(id(self))
1514
1515class ReferralResponse(DNSResponseComponent):
1516    def __init__(self, name):
1517        super(ReferralResponse, self).__init__()
1518        self.name = name
1519
1520    def __eq__(self, other):
1521        return self.name == other.name
1522
1523    def __hash__(self):
1524        return hash(id(self))
1525
1526class TruncatedResponse(DNSResponseComponent):
1527    def __init__(self, wire):
1528        super(TruncatedResponse, self).__init__()
1529        self.wire = wire
1530
1531    def __eq__(self, other):
1532        return self.wire == other.wire
1533
1534    def __hash__(self):
1535        return hash(id(self))
1536