Package dns :: Module zone |
[hide private]
[frames] | no frames]

# Source Code for Module dns.zone

       1  # Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
       2  #
       3  # Permission to use, copy, modify, and distribute this software and its
       4  # documentation for any purpose with or without fee is hereby granted,
       5  # provided that the above copyright notice and this permission notice
       6  # appear in all copies.
       7  #
       8  # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
       9  # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
      10  # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
      11  # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
      12  # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
      13  # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
      14  # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
      15
      16  """DNS Zones."""
      17
      18  from __future__ import generators
      19
      20  import sys
      21  import re
      22  import os
      23  from io import BytesIO
      24
      25  import dns.exception
      26  import dns.name
      27  import dns.node
      28  import dns.rdataclass
      29  import dns.rdatatype
      30  import dns.rdata
      31  import dns.rrset
      32  import dns.tokenizer
      33  import dns.ttl
      34  import dns.grange
      35  from ._compat import string_types, text_type
      36
      37
      38  _py3 = sys.version_info > (3,)
      39
      40
      41 -class BadZone(dns.exception.DNSException):
      42
      43      """The DNS zone is malformed."""
      44
      45
      46 -class NoSOA(BadZone):
      47
      48      """The DNS zone has no SOA RR at its origin."""
      49
      50
      51 -class NoNS(BadZone):
      52
      53      """The DNS zone has no NS RRset at its origin."""
      54
      55
      56 -class UnknownOrigin(BadZone):
      57
      58      """The DNS zone's origin is unknown."""
      59
      60
      61 -class 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()
     162          else:
     163              return self.nodes.iterkeys()  # pylint: disable=dict-iter-method
     164
     165 -    def keys(self):
     166          return self.nodes.keys()
     167
     168 -    def itervalues(self):
     169          if _py3:
     170              return self.nodes.values()
     171          else:
     172              return self.nodes.itervalues()  # pylint: disable=dict-iter-method
     173
     174 -    def values(self):
     175          return self.nodes.values()
     176
     177 -    def items(self):
     178          return self.nodes.items()
     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.rrset.RRset 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.rrset.RRset object
     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():
     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():
     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()
     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
     586 -class _MasterReader(object):
     587
     588      """Read a DNS master file
     589
     590      @ivar tok: The tokenizer
     591      @type tok: dns.tokenizer.Tokenizer object
     592      @ivar ttl: The default TTL
     593      @type ttl: int
     594      @ivar last_name: The last name read
     595      @type last_name: dns.name.Name object
     596      @ivar current_origin: The current origin
     597      @type current_origin: dns.name.Name object
     598      @ivar relativize: should names in the zone be relativized?
     599      @type relativize: bool
     600      @ivar zone: the zone
     601      @type zone: dns.zone.Zone object
     602      @ivar saved_state: saved reader state (used when processing $INCLUDE)
     603      @type saved_state: list of (tokenizer, current_origin, last_name, file)
     604      tuples.
     605      @ivar current_file: the file object of the $INCLUDed file being parsed
     606      (None if no $INCLUDE is active).
     607      @ivar allow_include: is $INCLUDE allowed?
     608      @type allow_include: bool
     609      @ivar check_origin: should sanity checks of the origin node be done?
     610      The default is True.
     611      @type check_origin: bool
     612      """
     613
     614 -    def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
     615                   allow_include=False, check_origin=True):
     616          if isinstance(origin, string_types):
     617              origin = dns.name.from_text(origin)
     618          self.tok = tok
     619          self.current_origin = origin
     620          self.relativize = relativize
     621          self.ttl = 0
     622          self.last_name = self.current_origin
     623          self.zone = zone_factory(origin, rdclass, relativize=relativize)
     624          self.saved_state = []
     625          self.current_file = None
     626          self.allow_include = allow_include
     627          self.check_origin = check_origin
     628
     629 -    def _eat_line(self):
     630          while 1:
     631              token = self.tok.get()
     632              if token.is_eol_or_eof():
     633                  break
     634
     635 -    def _rr_line(self):
     636          """Process one line from a DNS master file."""
     637          # Name
     638          if self.current_origin is None:
     639              raise UnknownOrigin
     640          token = self.tok.get(want_leading=True)
     641          if not token.is_whitespace():
     642              self.last_name = dns.name.from_text(
     643                  token.value, self.current_origin)
     644          else:
     645              token = self.tok.get()
     646              if token.is_eol_or_eof():
     647                  # treat leading WS followed by EOL/EOF as if they were EOL/EOF.
     648                  return
     649              self.tok.unget(token)
     650          name = self.last_name
     651          if not name.is_subdomain(self.zone.origin):
     652              self._eat_line()
     653              return
     654          if self.relativize:
     655              name = name.relativize(self.zone.origin)
     656          token = self.tok.get()
     657          if not token.is_identifier():
     658              raise dns.exception.SyntaxError
     659          # TTL
     660          try:
     661              ttl = dns.ttl.from_text(token.value)
     662              token = self.tok.get()
     663              if not token.is_identifier():
     664                  raise dns.exception.SyntaxError
     665          except dns.ttl.BadTTL:
     666              ttl = self.ttl
     667          # Class
     668          try:
     669              rdclass = dns.rdataclass.from_text(token.value)
     670              token = self.tok.get()
     671              if not token.is_identifier():
     672                  raise dns.exception.SyntaxError
     673          except dns.exception.SyntaxError:
     674              raise dns.exception.SyntaxError
     675          except Exception:
     676              rdclass = self.zone.rdclass
     677          if rdclass != self.zone.rdclass:
     678              raise dns.exception.SyntaxError("RR class is not zone's class")
     679          # Type
     680          try:
     681              rdtype = dns.rdatatype.from_text(token.value)
     682          except:
     683              raise dns.exception.SyntaxError(
     684                  "unknown rdatatype '%s'" % token.value)
     685          n = self.zone.nodes.get(name)
     686          if n is None:
     687              n = self.zone.node_factory()
     688              self.zone.nodes[name] = n
     689          try:
     690              rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
     691                                       self.current_origin, False)
     692          except dns.exception.SyntaxError:
     693              # Catch and reraise.
     694              (ty, va) = sys.exc_info()[:2]
     695              raise va
     696          except:
     697              # All exceptions that occur in the processing of rdata
     698              # are treated as syntax errors.  This is not strictly
     699              # correct, but it is correct almost all of the time.
     700              # We convert them to syntax errors so that we can emit
     701              # helpful filename:line info.
     702              (ty, va) = sys.exc_info()[:2]
     703              raise dns.exception.SyntaxError(
     704                  "caught exception %s: %s" % (str(ty), str(va)))
     705
     706          rd.choose_relativity(self.zone.origin, self.relativize)
     707          covers = rd.covers()
     708          rds = n.find_rdataset(rdclass, rdtype, covers, True)
     709          rds.add(rd, ttl)
     710
     711 -    def _parse_modify(self, side):
     712          # Here we catch everything in '{' '}' in a group so we can replace it
     713          # with ''.
     714          is_generate1 = re.compile("^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")
     715          is_generate2 = re.compile("^.*\$({(\+|-?)(\d+)}).*$")
     716          is_generate3 = re.compile("^.*\$({(\+|-?)(\d+),(\d+)}).*$")
     717          # Sometimes there are modifiers in the hostname. These come after
     718          # the dollar sign. They are in the form: ${offset[,width[,base]]}.
     719          # Make names
     720          g1 = is_generate1.match(side)
     721          if g1:
     722              mod, sign, offset, width, base = g1.groups()
     723              if sign == '':
     724                  sign = '+'
     725          g2 = is_generate2.match(side)
     726          if g2:
     727              mod, sign, offset = g2.groups()
     728              if sign == '':
     729                  sign = '+'
     730              width = 0
     731              base = 'd'
     732          g3 = is_generate3.match(side)
     733          if g3:
     734              mod, sign, offset, width = g1.groups()
     735              if sign == '':
     736                  sign = '+'
     737              width = g1.groups()[2]
     738              base = 'd'
     739
     740          if not (g1 or g2 or g3):
     741              mod = ''
     742              sign = '+'
     743              offset = 0
     744              width = 0
     745              base = 'd'
     746
     747          if base != 'd':
     748              raise NotImplementedError()
     749
     750          return mod, sign, offset, width, base
     751
     752 -    def _generate_line(self):
     753          # range lhs [ttl] [class] type rhs [ comment ]
     754          """Process one line containing the GENERATE statement from a DNS
     755          master file."""
     756          if self.current_origin is None:
     757              raise UnknownOrigin
     758
     759          token = self.tok.get()
     760          # Range (required)
     761          try:
     762              start, stop, step = dns.grange.from_text(token.value)
     763              token = self.tok.get()
     764              if not token.is_identifier():
     765                  raise dns.exception.SyntaxError
     766          except:
     767              raise dns.exception.SyntaxError
     768
     769          # lhs (required)
     770          try:
     771              lhs = token.value
     772              token = self.tok.get()
     773              if not token.is_identifier():
     774                  raise dns.exception.SyntaxError
     775          except:
     776              raise dns.exception.SyntaxError
     777
     778          # TTL
     779          try:
     780              ttl = dns.ttl.from_text(token.value)
     781              token = self.tok.get()
     782              if not token.is_identifier():
     783                  raise dns.exception.SyntaxError
     784          except dns.ttl.BadTTL:
     785              ttl = self.ttl
     786          # Class
     787          try:
     788              rdclass = dns.rdataclass.from_text(token.value)
     789              token = self.tok.get()
     790              if not token.is_identifier():
     791                  raise dns.exception.SyntaxError
     792          except dns.exception.SyntaxError:
     793              raise dns.exception.SyntaxError
     794          except Exception:
     795              rdclass = self.zone.rdclass
     796          if rdclass != self.zone.rdclass:
     797              raise dns.exception.SyntaxError("RR class is not zone's class")
     798          # Type
     799          try:
     800              rdtype = dns.rdatatype.from_text(token.value)
     801              token = self.tok.get()
     802              if not token.is_identifier():
     803                  raise dns.exception.SyntaxError
     804          except Exception:
     805              raise dns.exception.SyntaxError("unknown rdatatype '%s'" %
     806                                              token.value)
     807
     808          # lhs (required)
     809          try:
     810              rhs = token.value
     811          except:
     812              raise dns.exception.SyntaxError
     813
     814          lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs)
     815          rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs)
     816          for i in range(start, stop + 1, step):
     817              # +1 because bind is inclusive and python is exclusive
     818
     819              if lsign == u'+':
     820                  lindex = i + int(loffset)
     821              elif lsign == u'-':
     822                  lindex = i - int(loffset)
     823
     824              if rsign == u'-':
     825                  rindex = i - int(roffset)
     826              elif rsign == u'+':
     827                  rindex = i + int(roffset)
     828
     829              lzfindex = str(lindex).zfill(int(lwidth))
     830              rzfindex = str(rindex).zfill(int(rwidth))
     831
     832              name = lhs.replace(u'$%s' % (lmod), lzfindex)
     833              rdata = rhs.replace(u'$%s' % (rmod), rzfindex)
     834
     835              self.last_name = dns.name.from_text(name, self.current_origin)
     836              name = self.last_name
     837              if not name.is_subdomain(self.zone.origin):
     838                  self._eat_line()
     839                  return
     840              if self.relativize:
     841                  name = name.relativize(self.zone.origin)
     842
     843              n = self.zone.nodes.get(name)
     844              if n is None:
     845                  n = self.zone.node_factory()
     846                  self.zone.nodes[name] = n
     847              try:
     848                  rd = dns.rdata.from_text(rdclass, rdtype, rdata,
     849                                           self.current_origin, False)
     850              except dns.exception.SyntaxError:
     851                  # Catch and reraise.
     852                  (ty, va) = sys.exc_info()[:2]
     853                  raise va
     854              except:
     855                  # All exceptions that occur in the processing of rdata
     856                  # are treated as syntax errors.  This is not strictly
     857                  # correct, but it is correct almost all of the time.
     858                  # We convert them to syntax errors so that we can emit
     859                  # helpful filename:line info.
     860                  (ty, va) = sys.exc_info()[:2]
     861                  raise dns.exception.SyntaxError("caught exception %s: %s" %
     862                                                  (str(ty), str(va)))
     863
     864              rd.choose_relativity(self.zone.origin, self.relativize)
     865              covers = rd.covers()
     866              rds = n.find_rdataset(rdclass, rdtype, covers, True)
     867              rds.add(rd, ttl)
     868
     869 -    def read(self):
     870          """Read a DNS master file and build a zone object.
     871
     872          @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
     873          @raises dns.zone.NoNS: No NS RRset was found at the zone origin
     874          """
     875
     876          try:
     877              while 1:
     878                  token = self.tok.get(True, True)
     879                  if token.is_eof():
     880                      if self.current_file is not None:
     881                          self.current_file.close()
     882                      if len(self.saved_state) > 0:
     883                          (self.tok,
     884                           self.current_origin,
     885                           self.last_name,
     886                           self.current_file,
     887                           self.ttl) = self.saved_state.pop(-1)
     888                          continue
     889                      break
     890                  elif token.is_eol():
     891                      continue
     892                  elif token.is_comment():
     893                      self.tok.get_eol()
     894                      continue
     895                  elif token.value[0] == u'$':
     896                      c = token.value.upper()
     897                      if c == u'$TTL':
     898                          token = self.tok.get()
     899                          if not token.is_identifier():
     900                              raise dns.exception.SyntaxError("bad $TTL")
     901                          self.ttl = dns.ttl.from_text(token.value)
     902                          self.tok.get_eol()
     903                      elif c == u'$ORIGIN':
     904                          self.current_origin = self.tok.get_name()
     905                          self.tok.get_eol()
     906                          if self.zone.origin is None:
     907                              self.zone.origin = self.current_origin
     908                      elif c == u'$INCLUDE' and self.allow_include:
     909                          token = self.tok.get()
     910                          filename = token.value
     911                          token = self.tok.get()
     912                          if token.is_identifier():
     913                              new_origin =\
     914                                  dns.name.from_text(token.value,
     915                                                     self.current_origin)
     916                              self.tok.get_eol()
     917                          elif not token.is_eol_or_eof():
     918                              raise dns.exception.SyntaxError(
     919                                  "bad origin in $INCLUDE")
     920                          else:
     921                              new_origin = self.current_origin
     922                          self.saved_state.append((self.tok,
     923                                                   self.current_origin,
     924                                                   self.last_name,
     925                                                   self.current_file,
     926                                                   self.ttl))
     927                          self.current_file = open(filename, 'r')
     928                          self.tok = dns.tokenizer.Tokenizer(self.current_file,
     929                                                             filename)
     930                          self.current_origin = new_origin
     931                      elif c == u'$GENERATE':
     932                          self._generate_line()
     933                      else:
     934                          raise dns.exception.SyntaxError(
     935                              "Unknown master file directive '" + c + "'")
     936                      continue
     937                  self.tok.unget(token)
     938                  self._rr_line()
     939          except dns.exception.SyntaxError as detail:
     940              (filename, line_number) = self.tok.where()
     941              if detail is None:
     942                  detail = "syntax error"
     943              raise dns.exception.SyntaxError(
     944                  "%s:%d: %s" % (filename, line_number, detail))
     945
     946          # Now that we're done reading, do some basic checking of the zone.
     947          if self.check_origin:
     948              self.zone.check_origin()
     949
     950
     951 -def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
     952                relativize=True, zone_factory=Zone, filename=None,
     953                allow_include=False, check_origin=True):
     954      """Build a zone object from a master file format string.
     955
     956      @param text: the master file format input
     957      @type text: string.
     958      @param origin: The origin of the zone; if not specified, the first
     959      $ORIGIN statement in the master file will determine the origin of the
     960      zone.
     961      @type origin: dns.name.Name object or string
     962      @param rdclass: The zone's rdata class; the default is class IN.
     963      @type rdclass: int
     964      @param relativize: should names be relativized?  The default is True
     965      @type relativize: bool
     966      @param zone_factory: The zone factory to use
     967      @type zone_factory: function returning a Zone
     968      @param filename: The filename to emit when describing where an error
     969      occurred; the default is '<string>'.
     970      @type filename: string
     971      @param allow_include: is $INCLUDE allowed?
     972      @type allow_include: bool
     973      @param check_origin: should sanity checks of the origin node be done?
     974      The default is True.
     975      @type check_origin: bool
     976      @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
     977      @raises dns.zone.NoNS: No NS RRset was found at the zone origin
     978      @rtype: dns.zone.Zone object
     979      """
     980
     981      # 'text' can also be a file, but we don't publish that fact
     982      # since it's an implementation detail.  The official file
     983      # interface is from_file().
     984
     985      if filename is None:
     986          filename = '<string>'
     987      tok = dns.tokenizer.Tokenizer(text, filename)
     988      reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
     989                             allow_include=allow_include,
     990                             check_origin=check_origin)
     991      reader.read()
     992      return reader.zone
     993
     994
     995 -def from_file(f, origin=None, rdclass=dns.rdataclass.IN,
     996                relativize=True, zone_factory=Zone, filename=None,
     997                allow_include=True, check_origin=True):
     998      """Read a master file and build a zone object.
     999
    1000      @param f: file or string.  If I{f} is a string, it is treated
    1001      as the name of a file to open.
    1002      @param origin: The origin of the zone; if not specified, the first
    1003      $ORIGIN statement in the master file will determine the origin of the
    1004      zone.
    1005      @type origin: dns.name.Name object or string
    1006      @param rdclass: The zone's rdata class; the default is class IN.
    1007      @type rdclass: int
    1008      @param relativize: should names be relativized?  The default is True
    1009      @type relativize: bool
    1010      @param zone_factory: The zone factory to use
    1011      @type zone_factory: function returning a Zone
    1012      @param filename: The filename to emit when describing where an error
    1013      occurred; the default is '<file>', or the value of I{f} if I{f} is a
    1014      string.
    1015      @type filename: string
    1016      @param allow_include: is $INCLUDE allowed?
    1017      @type allow_include: bool
    1018      @param check_origin: should sanity checks of the origin node be done?
    1019      The default is True.
    1020      @type check_origin: bool
    1021      @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
    1022      @raises dns.zone.NoNS: No NS RRset was found at the zone origin
    1023      @rtype: dns.zone.Zone object
    1024      """
    1025
    1026      str_type = string_types
    1027      opts = 'rU'
    1028
    1029      if isinstance(f, str_type):
    1030          if filename is None:
    1031              filename = f
    1032          f = open(f, opts)
    1033          want_close = True
    1034      else:
    1035          if filename is None:
    1036              filename = '<file>'
    1037          want_close = False
    1038
    1039      try:
    1040          z = from_text(f, origin, rdclass, relativize, zone_factory,
    1041                        filename, allow_include, check_origin)
    1042      finally:
    1043          if want_close:
    1044              f.close()
    1045      return z
    1046
    1047
    1048 -def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True):
    1049      """Convert the output of a zone transfer generator into a zone object.
    1050
    1051      @param xfr: The xfr generator
    1052      @type xfr: generator of dns.message.Message objects
    1053      @param relativize: should names be relativized?  The default is True.
    1054      It is essential that the relativize setting matches the one specified
    1055      to dns.query.xfr().
    1056      @type relativize: bool
    1057      @param check_origin: should sanity checks of the origin node be done?
    1058      The default is True.
    1059      @type check_origin: bool
    1060      @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
    1061      @raises dns.zone.NoNS: No NS RRset was found at the zone origin
    1062      @rtype: dns.zone.Zone object
    1063      """
    1064
    1065      z = None
    1066      for r in xfr:
    1067          if z is None:
    1068              if relativize:
    1069                  origin = r.origin
    1070              else:
    1071                  origin = r.answer[0].name
    1072              rdclass = r.answer[0].rdclass
    1073              z = zone_factory(origin, rdclass, relativize=relativize)
    1074          for rrset in r.answer:
    1075              znode = z.nodes.get(rrset.name)
    1076              if not znode:
    1077                  znode = z.node_factory()
    1078                  z.nodes[rrset.name] = znode
    1079              zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,
    1080                                         rrset.covers, True)
    1081              zrds.update_ttl(rrset.ttl)
    1082              for rd in rrset:
    1083                  rd.choose_relativity(z.origin, relativize)
    1084                  zrds.add(rd)
    1085      if check_origin:
    1086          z.check_origin()
    1087      return z
    1088