1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
2
3# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
4#
5# Permission to use, copy, modify, and distribute this software and its
6# documentation for any purpose with or without fee is hereby granted,
7# provided that the above copyright notice and this permission notice
8# appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
18"""DNS Zones."""
19
20import contextlib
21import io
22import os
23import re
24import sys
25
26import dns.exception
27import dns.name
28import dns.node
29import dns.rdataclass
30import dns.rdatatype
31import dns.rdata
32import dns.rdtypes.ANY.SOA
33import dns.rrset
34import dns.tokenizer
35import dns.ttl
36import dns.grange
37
38
39class BadZone(dns.exception.DNSException):
40
41    """The DNS zone is malformed."""
42
43
44class NoSOA(BadZone):
45
46    """The DNS zone has no SOA RR at its origin."""
47
48
49class NoNS(BadZone):
50
51    """The DNS zone has no NS RRset at its origin."""
52
53
54class UnknownOrigin(BadZone):
55
56    """The DNS zone's origin is unknown."""
57
58
59class Zone:
60
61    """A DNS zone.
62
63    A ``Zone`` is a mapping from names to nodes.  The zone object may be
64    treated like a Python dictionary, e.g. ``zone[name]`` will retrieve
65    the node associated with that name.  The *name* may be a
66    ``dns.name.Name object``, or it may be a string.  In either case,
67    if the name is relative it is treated as relative to the origin of
68    the zone.
69    """
70
71    node_factory = dns.node.Node
72
73    __slots__ = ['rdclass', 'origin', 'nodes', 'relativize']
74
75    def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True):
76        """Initialize a zone object.
77
78        *origin* is the origin of the zone.  It may be a ``dns.name.Name``,
79        a ``str``, or ``None``.  If ``None``, then the zone's origin will
80        be set by the first ``$ORIGIN`` line in a masterfile.
81
82        *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
83
84        *relativize*, a ``bool``, determine's whether domain names are
85        relativized to the zone's origin.  The default is ``True``.
86        """
87
88        if origin is not None:
89            if isinstance(origin, str):
90                origin = dns.name.from_text(origin)
91            elif not isinstance(origin, dns.name.Name):
92                raise ValueError("origin parameter must be convertible to a "
93                                 "DNS name")
94            if not origin.is_absolute():
95                raise ValueError("origin parameter must be an absolute name")
96        self.origin = origin
97        self.rdclass = rdclass
98        self.nodes = {}
99        self.relativize = relativize
100
101    def __eq__(self, other):
102        """Two zones are equal if they have the same origin, class, and
103        nodes.
104
105        Returns a ``bool``.
106        """
107
108        if not isinstance(other, Zone):
109            return False
110        if self.rdclass != other.rdclass or \
111           self.origin != other.origin or \
112           self.nodes != other.nodes:
113            return False
114        return True
115
116    def __ne__(self, other):
117        """Are two zones not equal?
118
119        Returns a ``bool``.
120        """
121
122        return not self.__eq__(other)
123
124    def _validate_name(self, name):
125        if isinstance(name, str):
126            name = dns.name.from_text(name, None)
127        elif not isinstance(name, dns.name.Name):
128            raise KeyError("name parameter must be convertible to a DNS name")
129        if name.is_absolute():
130            if not name.is_subdomain(self.origin):
131                raise KeyError(
132                    "name parameter must be a subdomain of the zone origin")
133            if self.relativize:
134                name = name.relativize(self.origin)
135        return name
136
137    def __getitem__(self, key):
138        key = self._validate_name(key)
139        return self.nodes[key]
140
141    def __setitem__(self, key, value):
142        key = self._validate_name(key)
143        self.nodes[key] = value
144
145    def __delitem__(self, key):
146        key = self._validate_name(key)
147        del self.nodes[key]
148
149    def __iter__(self):
150        return self.nodes.__iter__()
151
152    def keys(self):
153        return self.nodes.keys()  # pylint: disable=dict-keys-not-iterating
154
155    def values(self):
156        return self.nodes.values()  # pylint: disable=dict-values-not-iterating
157
158    def items(self):
159        return self.nodes.items()  # pylint: disable=dict-items-not-iterating
160
161    def get(self, key):
162        key = self._validate_name(key)
163        return self.nodes.get(key)
164
165    def __contains__(self, other):
166        return other in self.nodes
167
168    def find_node(self, name, create=False):
169        """Find a node in the zone, possibly creating it.
170
171        *name*: the name of the node to find.
172        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
173        name must be a subdomain of the zone's origin.  If ``zone.relativize``
174        is ``True``, then the name will be relativized.
175
176        *create*, a ``bool``.  If true, the node will be created if it does
177        not exist.
178
179        Raises ``KeyError`` if the name is not known and create was
180        not specified, or if the name was not a subdomain of the origin.
181
182        Returns a ``dns.node.Node``.
183        """
184
185        name = self._validate_name(name)
186        node = self.nodes.get(name)
187        if node is None:
188            if not create:
189                raise KeyError
190            node = self.node_factory()
191            self.nodes[name] = node
192        return node
193
194    def get_node(self, name, create=False):
195        """Get a node in the zone, possibly creating it.
196
197        This method is like ``find_node()``, except it returns None instead
198        of raising an exception if the node does not exist and creation
199        has not been requested.
200
201        *name*: the name of the node to find.
202        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
203        name must be a subdomain of the zone's origin.  If ``zone.relativize``
204        is ``True``, then the name will be relativized.
205
206        *create*, a ``bool``.  If true, the node will be created if it does
207        not exist.
208
209        Raises ``KeyError`` if the name is not known and create was
210        not specified, or if the name was not a subdomain of the origin.
211
212        Returns a ``dns.node.Node`` or ``None``.
213        """
214
215        try:
216            node = self.find_node(name, create)
217        except KeyError:
218            node = None
219        return node
220
221    def delete_node(self, name):
222        """Delete the specified node if it exists.
223
224        *name*: the name of the node to find.
225        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
226        name must be a subdomain of the zone's origin.  If ``zone.relativize``
227        is ``True``, then the name will be relativized.
228
229        It is not an error if the node does not exist.
230        """
231
232        name = self._validate_name(name)
233        if name in self.nodes:
234            del self.nodes[name]
235
236    def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
237                      create=False):
238        """Look for an rdataset with the specified name and type in the zone,
239        and return an rdataset encapsulating it.
240
241        The rdataset returned is not a copy; changes to it will change
242        the zone.
243
244        KeyError is raised if the name or type are not found.
245
246        *name*: the name of the node to find.
247        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
248        name must be a subdomain of the zone's origin.  If ``zone.relativize``
249        is ``True``, then the name will be relativized.
250
251        *rdtype*, an ``int`` or ``str``, the rdata type desired.
252
253        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
254        Usually this value is ``dns.rdatatype.NONE``, but if the
255        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
256        then the covers value will be the rdata type the SIG/RRSIG
257        covers.  The library treats the SIG and RRSIG types as if they
258        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
259        This makes RRSIGs much easier to work with than if RRSIGs
260        covering different rdata types were aggregated into a single
261        RRSIG rdataset.
262
263        *create*, a ``bool``.  If true, the node will be created if it does
264        not exist.
265
266        Raises ``KeyError`` if the name is not known and create was
267        not specified, or if the name was not a subdomain of the origin.
268
269        Returns a ``dns.rdataset.Rdataset``.
270        """
271
272        name = self._validate_name(name)
273        rdtype = dns.rdatatype.RdataType.make(rdtype)
274        if covers is not None:
275            covers = dns.rdatatype.RdataType.make(covers)
276        node = self.find_node(name, create)
277        return node.find_rdataset(self.rdclass, rdtype, covers, create)
278
279    def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
280                     create=False):
281        """Look for an rdataset with the specified name and type in the zone.
282
283        This method is like ``find_rdataset()``, except it returns None instead
284        of raising an exception if the rdataset does not exist and creation
285        has not been requested.
286
287        The rdataset returned is not a copy; changes to it will change
288        the zone.
289
290        *name*: the name of the node to find.
291        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
292        name must be a subdomain of the zone's origin.  If ``zone.relativize``
293        is ``True``, then the name will be relativized.
294
295        *rdtype*, an ``int`` or ``str``, the rdata type desired.
296
297        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
298        Usually this value is ``dns.rdatatype.NONE``, but if the
299        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
300        then the covers value will be the rdata type the SIG/RRSIG
301        covers.  The library treats the SIG and RRSIG types as if they
302        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
303        This makes RRSIGs much easier to work with than if RRSIGs
304        covering different rdata types were aggregated into a single
305        RRSIG rdataset.
306
307        *create*, a ``bool``.  If true, the node will be created if it does
308        not exist.
309
310        Raises ``KeyError`` if the name is not known and create was
311        not specified, or if the name was not a subdomain of the origin.
312
313        Returns a ``dns.rdataset.Rdataset`` or ``None``.
314        """
315
316        try:
317            rdataset = self.find_rdataset(name, rdtype, covers, create)
318        except KeyError:
319            rdataset = None
320        return rdataset
321
322    def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE):
323        """Delete the rdataset matching *rdtype* and *covers*, if it
324        exists at the node specified by *name*.
325
326        It is not an error if the node does not exist, or if there is no
327        matching rdataset at the node.
328
329        If the node has no rdatasets after the deletion, it will itself
330        be deleted.
331
332        *name*: the name of the node to find.
333        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
334        name must be a subdomain of the zone's origin.  If ``zone.relativize``
335        is ``True``, then the name will be relativized.
336
337        *rdtype*, an ``int`` or ``str``, the rdata type desired.
338
339        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
340        Usually this value is ``dns.rdatatype.NONE``, but if the
341        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
342        then the covers value will be the rdata type the SIG/RRSIG
343        covers.  The library treats the SIG and RRSIG types as if they
344        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
345        This makes RRSIGs much easier to work with than if RRSIGs
346        covering different rdata types were aggregated into a single
347        RRSIG rdataset.
348        """
349
350        name = self._validate_name(name)
351        rdtype = dns.rdatatype.RdataType.make(rdtype)
352        if covers is not None:
353            covers = dns.rdatatype.RdataType.make(covers)
354        node = self.get_node(name)
355        if node is not None:
356            node.delete_rdataset(self.rdclass, rdtype, covers)
357            if len(node) == 0:
358                self.delete_node(name)
359
360    def replace_rdataset(self, name, replacement):
361        """Replace an rdataset at name.
362
363        It is not an error if there is no rdataset matching I{replacement}.
364
365        Ownership of the *replacement* object is transferred to the zone;
366        in other words, this method does not store a copy of *replacement*
367        at the node, it stores *replacement* itself.
368
369        If the node does not exist, it is created.
370
371        *name*: the name of the node to find.
372        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
373        name must be a subdomain of the zone's origin.  If ``zone.relativize``
374        is ``True``, then the name will be relativized.
375
376        *replacement*, a ``dns.rdataset.Rdataset``, the replacement rdataset.
377        """
378
379        if replacement.rdclass != self.rdclass:
380            raise ValueError('replacement.rdclass != zone.rdclass')
381        node = self.find_node(name, True)
382        node.replace_rdataset(replacement)
383
384    def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
385        """Look for an rdataset with the specified name and type in the zone,
386        and return an RRset encapsulating it.
387
388        This method is less efficient than the similar
389        ``find_rdataset()`` because it creates an RRset instead of
390        returning the matching rdataset.  It may be more convenient
391        for some uses since it returns an object which binds the owner
392        name to the rdataset.
393
394        This method may not be used to create new nodes or rdatasets;
395        use ``find_rdataset`` instead.
396
397        *name*: the name of the node to find.
398        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
399        name must be a subdomain of the zone's origin.  If ``zone.relativize``
400        is ``True``, then the name will be relativized.
401
402        *rdtype*, an ``int`` or ``str``, the rdata type desired.
403
404        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
405        Usually this value is ``dns.rdatatype.NONE``, but if the
406        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
407        then the covers value will be the rdata type the SIG/RRSIG
408        covers.  The library treats the SIG and RRSIG types as if they
409        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
410        This makes RRSIGs much easier to work with than if RRSIGs
411        covering different rdata types were aggregated into a single
412        RRSIG rdataset.
413
414        *create*, a ``bool``.  If true, the node will be created if it does
415        not exist.
416
417        Raises ``KeyError`` if the name is not known and create was
418        not specified, or if the name was not a subdomain of the origin.
419
420        Returns a ``dns.rrset.RRset`` or ``None``.
421        """
422
423        name = self._validate_name(name)
424        rdtype = dns.rdatatype.RdataType.make(rdtype)
425        if covers is not None:
426            covers = dns.rdatatype.RdataType.make(covers)
427        rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
428        rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
429        rrset.update(rdataset)
430        return rrset
431
432    def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
433        """Look for an rdataset with the specified name and type in the zone,
434        and return an RRset encapsulating it.
435
436        This method is less efficient than the similar ``get_rdataset()``
437        because it creates an RRset instead of returning the matching
438        rdataset.  It may be more convenient for some uses since it
439        returns an object which binds the owner name to the rdataset.
440
441        This method may not be used to create new nodes or rdatasets;
442        use ``get_rdataset()`` instead.
443
444        *name*: the name of the node to find.
445        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
446        name must be a subdomain of the zone's origin.  If ``zone.relativize``
447        is ``True``, then the name will be relativized.
448
449        *rdtype*, an ``int`` or ``str``, the rdata type desired.
450
451        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
452        Usually this value is ``dns.rdatatype.NONE``, but if the
453        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
454        then the covers value will be the rdata type the SIG/RRSIG
455        covers.  The library treats the SIG and RRSIG types as if they
456        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
457        This makes RRSIGs much easier to work with than if RRSIGs
458        covering different rdata types were aggregated into a single
459        RRSIG rdataset.
460
461        *create*, a ``bool``.  If true, the node will be created if it does
462        not exist.
463
464        Raises ``KeyError`` if the name is not known and create was
465        not specified, or if the name was not a subdomain of the origin.
466
467        Returns a ``dns.rrset.RRset`` or ``None``.
468        """
469
470        try:
471            rrset = self.find_rrset(name, rdtype, covers)
472        except KeyError:
473            rrset = None
474        return rrset
475
476    def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY,
477                          covers=dns.rdatatype.NONE):
478        """Return a generator which yields (name, rdataset) tuples for
479        all rdatasets in the zone which have the specified *rdtype*
480        and *covers*.  If *rdtype* is ``dns.rdatatype.ANY``, the default,
481        then all rdatasets will be matched.
482
483        *rdtype*, an ``int`` or ``str``, the rdata type desired.
484
485        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
486        Usually this value is ``dns.rdatatype.NONE``, but if the
487        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
488        then the covers value will be the rdata type the SIG/RRSIG
489        covers.  The library treats the SIG and RRSIG types as if they
490        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
491        This makes RRSIGs much easier to work with than if RRSIGs
492        covering different rdata types were aggregated into a single
493        RRSIG rdataset.
494        """
495
496        rdtype = dns.rdatatype.RdataType.make(rdtype)
497        if covers is not None:
498            covers = dns.rdatatype.RdataType.make(covers)
499        for (name, node) in self.items():
500            for rds in node:
501                if rdtype == dns.rdatatype.ANY or \
502                   (rds.rdtype == rdtype and rds.covers == covers):
503                    yield (name, rds)
504
505    def iterate_rdatas(self, rdtype=dns.rdatatype.ANY,
506                       covers=dns.rdatatype.NONE):
507        """Return a generator which yields (name, ttl, rdata) tuples for
508        all rdatas in the zone which have the specified *rdtype*
509        and *covers*.  If *rdtype* is ``dns.rdatatype.ANY``, the default,
510        then all rdatas will be matched.
511
512        *rdtype*, an ``int`` or ``str``, the rdata type desired.
513
514        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
515        Usually this value is ``dns.rdatatype.NONE``, but if the
516        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
517        then the covers value will be the rdata type the SIG/RRSIG
518        covers.  The library treats the SIG and RRSIG types as if they
519        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
520        This makes RRSIGs much easier to work with than if RRSIGs
521        covering different rdata types were aggregated into a single
522        RRSIG rdataset.
523        """
524
525        rdtype = dns.rdatatype.RdataType.make(rdtype)
526        if covers is not None:
527            covers = dns.rdatatype.RdataType.make(covers)
528        for (name, node) in self.items():
529            for rds in node:
530                if rdtype == dns.rdatatype.ANY or \
531                   (rds.rdtype == rdtype and rds.covers == covers):
532                    for rdata in rds:
533                        yield (name, rds.ttl, rdata)
534
535    def to_file(self, f, sorted=True, relativize=True, nl=None):
536        """Write a zone to a file.
537
538        *f*, a file or `str`.  If *f* is a string, it is treated
539        as the name of a file to open.
540
541        *sorted*, a ``bool``.  If True, the default, then the file
542        will be written with the names sorted in DNSSEC order from
543        least to greatest.  Otherwise the names will be written in
544        whatever order they happen to have in the zone's dictionary.
545
546        *relativize*, a ``bool``.  If True, the default, then domain
547        names in the output will be relativized to the zone's origin
548        if possible.
549
550        *nl*, a ``str`` or None.  The end of line string.  If not
551        ``None``, the output will use the platform's native
552        end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
553        """
554
555        with contextlib.ExitStack() as stack:
556            if isinstance(f, str):
557                f = stack.enter_context(open(f, 'wb'))
558
559            # must be in this way, f.encoding may contain None, or even
560            # attribute may not be there
561            file_enc = getattr(f, 'encoding', None)
562            if file_enc is None:
563                file_enc = 'utf-8'
564
565            if nl is None:
566                # binary mode, '\n' is not enough
567                nl_b = os.linesep.encode(file_enc)
568                nl = '\n'
569            elif isinstance(nl, str):
570                nl_b = nl.encode(file_enc)
571            else:
572                nl_b = nl
573                nl = nl.decode()
574
575            if sorted:
576                names = list(self.keys())
577                names.sort()
578            else:
579                names = self.keys()
580            for n in names:
581                l = self[n].to_text(n, origin=self.origin,
582                                    relativize=relativize)
583                if isinstance(l, str):
584                    l_b = l.encode(file_enc)
585                else:
586                    l_b = l
587                    l = l.decode()
588
589                try:
590                    f.write(l_b)
591                    f.write(nl_b)
592                except TypeError:  # textual mode
593                    f.write(l)
594                    f.write(nl)
595
596    def to_text(self, sorted=True, relativize=True, nl=None):
597        """Return a zone's text as though it were written to a file.
598
599        *sorted*, a ``bool``.  If True, the default, then the file
600        will be written with the names sorted in DNSSEC order from
601        least to greatest.  Otherwise the names will be written in
602        whatever order they happen to have in the zone's dictionary.
603
604        *relativize*, a ``bool``.  If True, the default, then domain
605        names in the output will be relativized to the zone's origin
606        if possible.
607
608        *nl*, a ``str`` or None.  The end of line string.  If not
609        ``None``, the output will use the platform's native
610        end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
611
612        Returns a ``str``.
613        """
614        temp_buffer = io.StringIO()
615        self.to_file(temp_buffer, sorted, relativize, nl)
616        return_value = temp_buffer.getvalue()
617        temp_buffer.close()
618        return return_value
619
620    def check_origin(self):
621        """Do some simple checking of the zone's origin.
622
623        Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
624
625        Raises ``dns.zone.NoNS`` if there is no NS RRset.
626
627        Raises ``KeyError`` if there is no origin node.
628        """
629        if self.relativize:
630            name = dns.name.empty
631        else:
632            name = self.origin
633        if self.get_rdataset(name, dns.rdatatype.SOA) is None:
634            raise NoSOA
635        if self.get_rdataset(name, dns.rdatatype.NS) is None:
636            raise NoNS
637
638
639class _MasterReader:
640
641    """Read a DNS master file
642
643    @ivar tok: The tokenizer
644    @type tok: dns.tokenizer.Tokenizer object
645    @ivar last_ttl: The last seen explicit TTL for an RR
646    @type last_ttl: int
647    @ivar last_ttl_known: Has last TTL been detected
648    @type last_ttl_known: bool
649    @ivar default_ttl: The default TTL from a $TTL directive or SOA RR
650    @type default_ttl: int
651    @ivar default_ttl_known: Has default TTL been detected
652    @type default_ttl_known: bool
653    @ivar last_name: The last name read
654    @type last_name: dns.name.Name object
655    @ivar current_origin: The current origin
656    @type current_origin: dns.name.Name object
657    @ivar relativize: should names in the zone be relativized?
658    @type relativize: bool
659    @ivar zone: the zone
660    @type zone: dns.zone.Zone object
661    @ivar saved_state: saved reader state (used when processing $INCLUDE)
662    @type saved_state: list of (tokenizer, current_origin, last_name, file,
663    last_ttl, last_ttl_known, default_ttl, default_ttl_known) tuples.
664    @ivar current_file: the file object of the $INCLUDed file being parsed
665    (None if no $INCLUDE is active).
666    @ivar allow_include: is $INCLUDE allowed?
667    @type allow_include: bool
668    @ivar check_origin: should sanity checks of the origin node be done?
669    The default is True.
670    @type check_origin: bool
671    """
672
673    def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
674                 allow_include=False, check_origin=True):
675        if isinstance(origin, str):
676            origin = dns.name.from_text(origin)
677        self.tok = tok
678        self.current_origin = origin
679        self.relativize = relativize
680        self.last_ttl = 0
681        self.last_ttl_known = False
682        self.default_ttl = 0
683        self.default_ttl_known = False
684        self.last_name = self.current_origin
685        self.zone = zone_factory(origin, rdclass, relativize=relativize)
686        self.saved_state = []
687        self.current_file = None
688        self.allow_include = allow_include
689        self.check_origin = check_origin
690
691    def _eat_line(self):
692        while 1:
693            token = self.tok.get()
694            if token.is_eol_or_eof():
695                break
696
697    def _rr_line(self):
698        """Process one line from a DNS master file."""
699        # Name
700        if self.current_origin is None:
701            raise UnknownOrigin
702        token = self.tok.get(want_leading=True)
703        if not token.is_whitespace():
704            self.last_name = self.tok.as_name(token, self.current_origin)
705        else:
706            token = self.tok.get()
707            if token.is_eol_or_eof():
708                # treat leading WS followed by EOL/EOF as if they were EOL/EOF.
709                return
710            self.tok.unget(token)
711        name = self.last_name
712        if not name.is_subdomain(self.zone.origin):
713            self._eat_line()
714            return
715        if self.relativize:
716            name = name.relativize(self.zone.origin)
717        token = self.tok.get()
718        if not token.is_identifier():
719            raise dns.exception.SyntaxError
720
721        # TTL
722        ttl = None
723        try:
724            ttl = dns.ttl.from_text(token.value)
725            self.last_ttl = ttl
726            self.last_ttl_known = True
727            token = self.tok.get()
728            if not token.is_identifier():
729                raise dns.exception.SyntaxError
730        except dns.ttl.BadTTL:
731            if self.default_ttl_known:
732                ttl = self.default_ttl
733            elif self.last_ttl_known:
734                ttl = self.last_ttl
735
736        # Class
737        try:
738            rdclass = dns.rdataclass.from_text(token.value)
739            token = self.tok.get()
740            if not token.is_identifier():
741                raise dns.exception.SyntaxError
742        except dns.exception.SyntaxError:
743            raise
744        except Exception:
745            rdclass = self.zone.rdclass
746        if rdclass != self.zone.rdclass:
747            raise dns.exception.SyntaxError("RR class is not zone's class")
748        # Type
749        try:
750            rdtype = dns.rdatatype.from_text(token.value)
751        except Exception:
752            raise dns.exception.SyntaxError(
753                "unknown rdatatype '%s'" % token.value)
754        n = self.zone.nodes.get(name)
755        if n is None:
756            n = self.zone.node_factory()
757            self.zone.nodes[name] = n
758        try:
759            rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
760                                     self.current_origin, self.relativize,
761                                     self.zone.origin)
762        except dns.exception.SyntaxError:
763            # Catch and reraise.
764            raise
765        except Exception:
766            # All exceptions that occur in the processing of rdata
767            # are treated as syntax errors.  This is not strictly
768            # correct, but it is correct almost all of the time.
769            # We convert them to syntax errors so that we can emit
770            # helpful filename:line info.
771            (ty, va) = sys.exc_info()[:2]
772            raise dns.exception.SyntaxError(
773                "caught exception {}: {}".format(str(ty), str(va)))
774
775        if not self.default_ttl_known and rdtype == dns.rdatatype.SOA:
776            # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default
777            # TTL from the SOA minttl if no $TTL statement is present before the
778            # SOA is parsed.
779            self.default_ttl = rd.minimum
780            self.default_ttl_known = True
781            if ttl is None:
782                # if we didn't have a TTL on the SOA, set it!
783                ttl = rd.minimum
784
785        # TTL check.  We had to wait until now to do this as the SOA RR's
786        # own TTL can be inferred from its minimum.
787        if ttl is None:
788            raise dns.exception.SyntaxError("Missing default TTL value")
789
790        covers = rd.covers()
791        rds = n.find_rdataset(rdclass, rdtype, covers, True)
792        rds.add(rd, ttl)
793
794    def _parse_modify(self, side):
795        # Here we catch everything in '{' '}' in a group so we can replace it
796        # with ''.
797        is_generate1 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")
798        is_generate2 = re.compile(r"^.*\$({(\+|-?)(\d+)}).*$")
799        is_generate3 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+)}).*$")
800        # Sometimes there are modifiers in the hostname. These come after
801        # the dollar sign. They are in the form: ${offset[,width[,base]]}.
802        # Make names
803        g1 = is_generate1.match(side)
804        if g1:
805            mod, sign, offset, width, base = g1.groups()
806            if sign == '':
807                sign = '+'
808        g2 = is_generate2.match(side)
809        if g2:
810            mod, sign, offset = g2.groups()
811            if sign == '':
812                sign = '+'
813            width = 0
814            base = 'd'
815        g3 = is_generate3.match(side)
816        if g3:
817            mod, sign, offset, width = g3.groups()
818            if sign == '':
819                sign = '+'
820            base = 'd'
821
822        if not (g1 or g2 or g3):
823            mod = ''
824            sign = '+'
825            offset = 0
826            width = 0
827            base = 'd'
828
829        if base != 'd':
830            raise NotImplementedError()
831
832        return mod, sign, offset, width, base
833
834    def _generate_line(self):
835        # range lhs [ttl] [class] type rhs [ comment ]
836        """Process one line containing the GENERATE statement from a DNS
837        master file."""
838        if self.current_origin is None:
839            raise UnknownOrigin
840
841        token = self.tok.get()
842        # Range (required)
843        try:
844            start, stop, step = dns.grange.from_text(token.value)
845            token = self.tok.get()
846            if not token.is_identifier():
847                raise dns.exception.SyntaxError
848        except Exception:
849            raise dns.exception.SyntaxError
850
851        # lhs (required)
852        try:
853            lhs = token.value
854            token = self.tok.get()
855            if not token.is_identifier():
856                raise dns.exception.SyntaxError
857        except Exception:
858            raise dns.exception.SyntaxError
859
860        # TTL
861        try:
862            ttl = dns.ttl.from_text(token.value)
863            self.last_ttl = ttl
864            self.last_ttl_known = True
865            token = self.tok.get()
866            if not token.is_identifier():
867                raise dns.exception.SyntaxError
868        except dns.ttl.BadTTL:
869            if not (self.last_ttl_known or self.default_ttl_known):
870                raise dns.exception.SyntaxError("Missing default TTL value")
871            if self.default_ttl_known:
872                ttl = self.default_ttl
873            elif self.last_ttl_known:
874                ttl = self.last_ttl
875        # Class
876        try:
877            rdclass = dns.rdataclass.from_text(token.value)
878            token = self.tok.get()
879            if not token.is_identifier():
880                raise dns.exception.SyntaxError
881        except dns.exception.SyntaxError:
882            raise dns.exception.SyntaxError
883        except Exception:
884            rdclass = self.zone.rdclass
885        if rdclass != self.zone.rdclass:
886            raise dns.exception.SyntaxError("RR class is not zone's class")
887        # Type
888        try:
889            rdtype = dns.rdatatype.from_text(token.value)
890            token = self.tok.get()
891            if not token.is_identifier():
892                raise dns.exception.SyntaxError
893        except Exception:
894            raise dns.exception.SyntaxError("unknown rdatatype '%s'" %
895                                            token.value)
896
897        # rhs (required)
898        rhs = token.value
899
900        lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs)
901        rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs)
902        for i in range(start, stop + 1, step):
903            # +1 because bind is inclusive and python is exclusive
904
905            if lsign == '+':
906                lindex = i + int(loffset)
907            elif lsign == '-':
908                lindex = i - int(loffset)
909
910            if rsign == '-':
911                rindex = i - int(roffset)
912            elif rsign == '+':
913                rindex = i + int(roffset)
914
915            lzfindex = str(lindex).zfill(int(lwidth))
916            rzfindex = str(rindex).zfill(int(rwidth))
917
918            name = lhs.replace('$%s' % (lmod), lzfindex)
919            rdata = rhs.replace('$%s' % (rmod), rzfindex)
920
921            self.last_name = dns.name.from_text(name, self.current_origin,
922                                                self.tok.idna_codec)
923            name = self.last_name
924            if not name.is_subdomain(self.zone.origin):
925                self._eat_line()
926                return
927            if self.relativize:
928                name = name.relativize(self.zone.origin)
929
930            n = self.zone.nodes.get(name)
931            if n is None:
932                n = self.zone.node_factory()
933                self.zone.nodes[name] = n
934            try:
935                rd = dns.rdata.from_text(rdclass, rdtype, rdata,
936                                         self.current_origin, self.relativize,
937                                         self.zone.origin)
938            except dns.exception.SyntaxError:
939                # Catch and reraise.
940                raise
941            except Exception:
942                # All exceptions that occur in the processing of rdata
943                # are treated as syntax errors.  This is not strictly
944                # correct, but it is correct almost all of the time.
945                # We convert them to syntax errors so that we can emit
946                # helpful filename:line info.
947                (ty, va) = sys.exc_info()[:2]
948                raise dns.exception.SyntaxError("caught exception %s: %s" %
949                                                (str(ty), str(va)))
950
951            covers = rd.covers()
952            rds = n.find_rdataset(rdclass, rdtype, covers, True)
953            rds.add(rd, ttl)
954
955    def read(self):
956        """Read a DNS master file and build a zone object.
957
958        @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
959        @raises dns.zone.NoNS: No NS RRset was found at the zone origin
960        """
961
962        try:
963            while 1:
964                token = self.tok.get(True, True)
965                if token.is_eof():
966                    if self.current_file is not None:
967                        self.current_file.close()
968                    if len(self.saved_state) > 0:
969                        (self.tok,
970                         self.current_origin,
971                         self.last_name,
972                         self.current_file,
973                         self.last_ttl,
974                         self.last_ttl_known,
975                         self.default_ttl,
976                         self.default_ttl_known) = self.saved_state.pop(-1)
977                        continue
978                    break
979                elif token.is_eol():
980                    continue
981                elif token.is_comment():
982                    self.tok.get_eol()
983                    continue
984                elif token.value[0] == '$':
985                    c = token.value.upper()
986                    if c == '$TTL':
987                        token = self.tok.get()
988                        if not token.is_identifier():
989                            raise dns.exception.SyntaxError("bad $TTL")
990                        self.default_ttl = dns.ttl.from_text(token.value)
991                        self.default_ttl_known = True
992                        self.tok.get_eol()
993                    elif c == '$ORIGIN':
994                        self.current_origin = self.tok.get_name()
995                        self.tok.get_eol()
996                        if self.zone.origin is None:
997                            self.zone.origin = self.current_origin
998                    elif c == '$INCLUDE' and self.allow_include:
999                        token = self.tok.get()
1000                        filename = token.value
1001                        token = self.tok.get()
1002                        if token.is_identifier():
1003                            new_origin =\
1004                                dns.name.from_text(token.value,
1005                                                   self.current_origin,
1006                                                   self.tok.idna_codec)
1007                            self.tok.get_eol()
1008                        elif not token.is_eol_or_eof():
1009                            raise dns.exception.SyntaxError(
1010                                "bad origin in $INCLUDE")
1011                        else:
1012                            new_origin = self.current_origin
1013                        self.saved_state.append((self.tok,
1014                                                 self.current_origin,
1015                                                 self.last_name,
1016                                                 self.current_file,
1017                                                 self.last_ttl,
1018                                                 self.last_ttl_known,
1019                                                 self.default_ttl,
1020                                                 self.default_ttl_known))
1021                        self.current_file = open(filename, 'r')
1022                        self.tok = dns.tokenizer.Tokenizer(self.current_file,
1023                                                           filename)
1024                        self.current_origin = new_origin
1025                    elif c == '$GENERATE':
1026                        self._generate_line()
1027                    else:
1028                        raise dns.exception.SyntaxError(
1029                            "Unknown master file directive '" + c + "'")
1030                    continue
1031                self.tok.unget(token)
1032                self._rr_line()
1033        except dns.exception.SyntaxError as detail:
1034            (filename, line_number) = self.tok.where()
1035            if detail is None:
1036                detail = "syntax error"
1037            ex = dns.exception.SyntaxError(
1038                "%s:%d: %s" % (filename, line_number, detail))
1039            tb = sys.exc_info()[2]
1040            raise ex.with_traceback(tb) from None
1041
1042        # Now that we're done reading, do some basic checking of the zone.
1043        if self.check_origin:
1044            self.zone.check_origin()
1045
1046
1047def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
1048              relativize=True, zone_factory=Zone, filename=None,
1049              allow_include=False, check_origin=True, idna_codec=None):
1050    """Build a zone object from a master file format string.
1051
1052    *text*, a ``str``, the master file format input.
1053
1054    *origin*, a ``dns.name.Name``, a ``str``, or ``None``.  The origin
1055    of the zone; if not specified, the first ``$ORIGIN`` statement in the
1056    masterfile will determine the origin of the zone.
1057
1058    *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
1059
1060    *relativize*, a ``bool``, determine's whether domain names are
1061    relativized to the zone's origin.  The default is ``True``.
1062
1063    *zone_factory*, the zone factory to use or ``None``.  If ``None``, then
1064    ``dns.zone.Zone`` will be used.  The value may be any class or callable
1065    that returns a subclass of ``dns.zone.Zone``.
1066
1067    *filename*, a ``str`` or ``None``, the filename to emit when
1068    describing where an error occurred; the default is ``'<string>'``.
1069
1070    *allow_include*, a ``bool``.  If ``True``, the default, then ``$INCLUDE``
1071    directives are permitted.  If ``False``, then encoutering a ``$INCLUDE``
1072    will raise a ``SyntaxError`` exception.
1073
1074    *check_origin*, a ``bool``.  If ``True``, the default, then sanity
1075    checks of the origin node will be made by calling the zone's
1076    ``check_origin()`` method.
1077
1078    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
1079    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
1080    is used.
1081
1082    Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
1083
1084    Raises ``dns.zone.NoNS`` if there is no NS RRset.
1085
1086    Raises ``KeyError`` if there is no origin node.
1087
1088    Returns a subclass of ``dns.zone.Zone``.
1089    """
1090
1091    # 'text' can also be a file, but we don't publish that fact
1092    # since it's an implementation detail.  The official file
1093    # interface is from_file().
1094
1095    if filename is None:
1096        filename = '<string>'
1097    tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec)
1098    reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
1099                           allow_include=allow_include,
1100                           check_origin=check_origin)
1101    reader.read()
1102    return reader.zone
1103
1104
1105def from_file(f, origin=None, rdclass=dns.rdataclass.IN,
1106              relativize=True, zone_factory=Zone, filename=None,
1107              allow_include=True, check_origin=True):
1108    """Read a master file and build a zone object.
1109
1110    *f*, a file or ``str``.  If *f* is a string, it is treated
1111    as the name of a file to open.
1112
1113    *origin*, a ``dns.name.Name``, a ``str``, or ``None``.  The origin
1114    of the zone; if not specified, the first ``$ORIGIN`` statement in the
1115    masterfile will determine the origin of the zone.
1116
1117    *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
1118
1119    *relativize*, a ``bool``, determine's whether domain names are
1120    relativized to the zone's origin.  The default is ``True``.
1121
1122    *zone_factory*, the zone factory to use or ``None``.  If ``None``, then
1123    ``dns.zone.Zone`` will be used.  The value may be any class or callable
1124    that returns a subclass of ``dns.zone.Zone``.
1125
1126    *filename*, a ``str`` or ``None``, the filename to emit when
1127    describing where an error occurred; the default is ``'<string>'``.
1128
1129    *allow_include*, a ``bool``.  If ``True``, the default, then ``$INCLUDE``
1130    directives are permitted.  If ``False``, then encoutering a ``$INCLUDE``
1131    will raise a ``SyntaxError`` exception.
1132
1133    *check_origin*, a ``bool``.  If ``True``, the default, then sanity
1134    checks of the origin node will be made by calling the zone's
1135    ``check_origin()`` method.
1136
1137    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
1138    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
1139    is used.
1140
1141    Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
1142
1143    Raises ``dns.zone.NoNS`` if there is no NS RRset.
1144
1145    Raises ``KeyError`` if there is no origin node.
1146
1147    Returns a subclass of ``dns.zone.Zone``.
1148    """
1149
1150    with contextlib.ExitStack() as stack:
1151        if isinstance(f, str):
1152            if filename is None:
1153                filename = f
1154            f = stack.enter_context(open(f))
1155        return from_text(f, origin, rdclass, relativize, zone_factory,
1156                         filename, allow_include, check_origin)
1157
1158
1159def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True):
1160    """Convert the output of a zone transfer generator into a zone object.
1161
1162    *xfr*, a generator of ``dns.message.Message`` objects, typically
1163    ``dns.query.xfr()``.
1164
1165    *relativize*, a ``bool``, determine's whether domain names are
1166    relativized to the zone's origin.  The default is ``True``.
1167    It is essential that the relativize setting matches the one specified
1168    to the generator.
1169
1170    *check_origin*, a ``bool``.  If ``True``, the default, then sanity
1171    checks of the origin node will be made by calling the zone's
1172    ``check_origin()`` method.
1173
1174    Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
1175
1176    Raises ``dns.zone.NoNS`` if there is no NS RRset.
1177
1178    Raises ``KeyError`` if there is no origin node.
1179
1180    Returns a subclass of ``dns.zone.Zone``.
1181    """
1182
1183    z = None
1184    for r in xfr:
1185        if z is None:
1186            if relativize:
1187                origin = r.origin
1188            else:
1189                origin = r.answer[0].name
1190            rdclass = r.answer[0].rdclass
1191            z = zone_factory(origin, rdclass, relativize=relativize)
1192        for rrset in r.answer:
1193            znode = z.nodes.get(rrset.name)
1194            if not znode:
1195                znode = z.node_factory()
1196                z.nodes[rrset.name] = znode
1197            zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,
1198                                       rrset.covers, True)
1199            zrds.update_ttl(rrset.ttl)
1200            for rd in rrset:
1201                zrds.add(rd)
1202    if check_origin:
1203        z.check_origin()
1204    return z
1205