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