# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
|
|
|
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
|
#
|
|
# Permission to use, copy, modify, and distribute this software and its
|
|
# documentation for any purpose with or without fee is hereby granted,
|
|
# provided that the above copyright notice and this permission notice
|
|
# appear in all copies.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
|
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
"""DNS Zones."""
|
|
|
|
from __future__ import generators
|
|
|
|
import sys
|
|
import re
|
|
import os
|
|
from io import BytesIO
|
|
|
|
import dns.exception
|
|
import dns.name
|
|
import dns.node
|
|
import dns.rdataclass
|
|
import dns.rdatatype
|
|
import dns.rdata
|
|
import dns.rdtypes.ANY.SOA
|
|
import dns.rrset
|
|
import dns.tokenizer
|
|
import dns.ttl
|
|
import dns.grange
|
|
from ._compat import string_types, text_type, PY3
|
|
|
|
|
|
class BadZone(dns.exception.DNSException):
|
|
|
|
"""The DNS zone is malformed."""
|
|
|
|
|
|
class NoSOA(BadZone):
|
|
|
|
"""The DNS zone has no SOA RR at its origin."""
|
|
|
|
|
|
class NoNS(BadZone):
|
|
|
|
"""The DNS zone has no NS RRset at its origin."""
|
|
|
|
|
|
class UnknownOrigin(BadZone):
|
|
|
|
"""The DNS zone's origin is unknown."""
|
|
|
|
|
|
class Zone(object):
|
|
|
|
"""A DNS zone.
|
|
|
|
A Zone is a mapping from names to nodes. The zone object may be
|
|
treated like a Python dictionary, e.g. zone[name] will retrieve
|
|
the node associated with that name. The I{name} may be a
|
|
dns.name.Name object, or it may be a string. In the either case,
|
|
if the name is relative it is treated as relative to the origin of
|
|
the zone.
|
|
|
|
@ivar rdclass: The zone's rdata class; the default is class IN.
|
|
@type rdclass: int
|
|
@ivar origin: The origin of the zone.
|
|
@type origin: dns.name.Name object
|
|
@ivar nodes: A dictionary mapping the names of nodes in the zone to the
|
|
nodes themselves.
|
|
@type nodes: dict
|
|
@ivar relativize: should names in the zone be relativized?
|
|
@type relativize: bool
|
|
@cvar node_factory: the factory used to create a new node
|
|
@type node_factory: class or callable
|
|
"""
|
|
|
|
node_factory = dns.node.Node
|
|
|
|
__slots__ = ['rdclass', 'origin', 'nodes', 'relativize']
|
|
|
|
def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True):
|
|
"""Initialize a zone object.
|
|
|
|
@param origin: The origin of the zone.
|
|
@type origin: dns.name.Name object
|
|
@param rdclass: The zone's rdata class; the default is class IN.
|
|
@type rdclass: int"""
|
|
|
|
if origin is not None:
|
|
if isinstance(origin, string_types):
|
|
origin = dns.name.from_text(origin)
|
|
elif not isinstance(origin, dns.name.Name):
|
|
raise ValueError("origin parameter must be convertible to a "
|
|
"DNS name")
|
|
if not origin.is_absolute():
|
|
raise ValueError("origin parameter must be an absolute name")
|
|
self.origin = origin
|
|
self.rdclass = rdclass
|
|
self.nodes = {}
|
|
self.relativize = relativize
|
|
|
|
def __eq__(self, other):
|
|
"""Two zones are equal if they have the same origin, class, and
|
|
nodes.
|
|
@rtype: bool
|
|
"""
|
|
|
|
if not isinstance(other, Zone):
|
|
return False
|
|
if self.rdclass != other.rdclass or \
|
|
self.origin != other.origin or \
|
|
self.nodes != other.nodes:
|
|
return False
|
|
return True
|
|
|
|
def __ne__(self, other):
|
|
"""Are two zones not equal?
|
|
@rtype: bool
|
|
"""
|
|
|
|
return not self.__eq__(other)
|
|
|
|
def _validate_name(self, name):
|
|
if isinstance(name, string_types):
|
|
name = dns.name.from_text(name, None)
|
|
elif not isinstance(name, dns.name.Name):
|
|
raise KeyError("name parameter must be convertible to a DNS name")
|
|
if name.is_absolute():
|
|
if not name.is_subdomain(self.origin):
|
|
raise KeyError(
|
|
"name parameter must be a subdomain of the zone origin")
|
|
if self.relativize:
|
|
name = name.relativize(self.origin)
|
|
return name
|
|
|
|
def __getitem__(self, key):
|
|
key = self._validate_name(key)
|
|
return self.nodes[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
key = self._validate_name(key)
|
|
self.nodes[key] = value
|
|
|
|
def __delitem__(self, key):
|
|
key = self._validate_name(key)
|
|
del self.nodes[key]
|
|
|
|
def __iter__(self):
|
|
return self.nodes.__iter__()
|
|
|
|
def iterkeys(self):
|
|
if PY3:
|
|
return self.nodes.keys() # pylint: disable=dict-keys-not-iterating
|
|
else:
|
|
return self.nodes.iterkeys() # pylint: disable=dict-iter-method
|
|
|
|
def keys(self):
|
|
return self.nodes.keys() # pylint: disable=dict-keys-not-iterating
|
|
|
|
def itervalues(self):
|
|
if PY3:
|
|
return self.nodes.values() # pylint: disable=dict-values-not-iterating
|
|
else:
|
|
return self.nodes.itervalues() # pylint: disable=dict-iter-method
|
|
|
|
def values(self):
|
|
return self.nodes.values() # pylint: disable=dict-values-not-iterating
|
|
|
|
def items(self):
|
|
return self.nodes.items() # pylint: disable=dict-items-not-iterating
|
|
|
|
iteritems = items
|
|
|
|
def get(self, key):
|
|
key = self._validate_name(key)
|
|
return self.nodes.get(key)
|
|
|
|
def __contains__(self, other):
|
|
return other in self.nodes
|
|
|
|
def find_node(self, name, create=False):
|
|
"""Find a node in the zone, possibly creating it.
|
|
|
|
@param name: the name of the node to find
|
|
@type name: dns.name.Name object or string
|
|
@param create: should the node be created if it doesn't exist?
|
|
@type create: bool
|
|
@raises KeyError: the name is not known and create was not specified.
|
|
@rtype: dns.node.Node object
|
|
"""
|
|
|
|
name = self._validate_name(name)
|
|
node = self.nodes.get(name)
|
|
if node is None:
|
|
if not create:
|
|
raise KeyError
|
|
node = self.node_factory()
|
|
self.nodes[name] = node
|
|
return node
|
|
|
|
def get_node(self, name, create=False):
|
|
"""Get a node in the zone, possibly creating it.
|
|
|
|
This method is like L{find_node}, except it returns None instead
|
|
of raising an exception if the node does not exist and creation
|
|
has not been requested.
|
|
|
|
@param name: the name of the node to find
|
|
@type name: dns.name.Name object or string
|
|
@param create: should the node be created if it doesn't exist?
|
|
@type create: bool
|
|
@rtype: dns.node.Node object or None
|
|
"""
|
|
|
|
try:
|
|
node = self.find_node(name, create)
|
|
except KeyError:
|
|
node = None
|
|
return node
|
|
|
|
def delete_node(self, name):
|
|
"""Delete the specified node if it exists.
|
|
|
|
It is not an error if the node does not exist.
|
|
"""
|
|
|
|
name = self._validate_name(name)
|
|
if name in self.nodes:
|
|
del self.nodes[name]
|
|
|
|
def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
|
|
create=False):
|
|
"""Look for rdata with the specified name and type in the zone,
|
|
and return an rdataset encapsulating it.
|
|
|
|
The I{name}, I{rdtype}, and I{covers} parameters may be
|
|
strings, in which case they will be converted to their proper
|
|
type.
|
|
|
|
The rdataset returned is not a copy; changes to it will change
|
|
the zone.
|
|
|
|
KeyError is raised if the name or type are not found.
|
|
Use L{get_rdataset} if you want to have None returned instead.
|
|
|
|
@param name: the owner name to look for
|
|
@type name: DNS.name.Name object or string
|
|
@param rdtype: the rdata type desired
|
|
@type rdtype: int or string
|
|
@param covers: the covered type (defaults to None)
|
|
@type covers: int or string
|
|
@param create: should the node and rdataset be created if they do not
|
|
exist?
|
|
@type create: bool
|
|
@raises KeyError: the node or rdata could not be found
|
|
@rtype: dns.rdataset.Rdataset object
|
|
"""
|
|
|
|
name = self._validate_name(name)
|
|
if isinstance(rdtype, string_types):
|
|
rdtype = dns.rdatatype.from_text(rdtype)
|
|
if isinstance(covers, string_types):
|
|
covers = dns.rdatatype.from_text(covers)
|
|
node = self.find_node(name, create)
|
|
return node.find_rdataset(self.rdclass, rdtype, covers, create)
|
|
|
|
def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
|
|
create=False):
|
|
"""Look for rdata with the specified name and type in the zone,
|
|
and return an rdataset encapsulating it.
|
|
|
|
The I{name}, I{rdtype}, and I{covers} parameters may be
|
|
strings, in which case they will be converted to their proper
|
|
type.
|
|
|
|
The rdataset returned is not a copy; changes to it will change
|
|
the zone.
|
|
|
|
None is returned if the name or type are not found.
|
|
Use L{find_rdataset} if you want to have KeyError raised instead.
|
|
|
|
@param name: the owner name to look for
|
|
@type name: DNS.name.Name object or string
|
|
@param rdtype: the rdata type desired
|
|
@type rdtype: int or string
|
|
@param covers: the covered type (defaults to None)
|
|
@type covers: int or string
|
|
@param create: should the node and rdataset be created if they do not
|
|
exist?
|
|
@type create: bool
|
|
@rtype: dns.rdataset.Rdataset object or None
|
|
"""
|
|
|
|
try:
|
|
rdataset = self.find_rdataset(name, rdtype, covers, create)
|
|
except KeyError:
|
|
rdataset = None
|
|
return rdataset
|
|
|
|
def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE):
|
|
"""Delete the rdataset matching I{rdtype} and I{covers}, if it
|
|
exists at the node specified by I{name}.
|
|
|
|
The I{name}, I{rdtype}, and I{covers} parameters may be
|
|
strings, in which case they will be converted to their proper
|
|
type.
|
|
|
|
It is not an error if the node does not exist, or if there is no
|
|
matching rdataset at the node.
|
|
|
|
If the node has no rdatasets after the deletion, it will itself
|
|
be deleted.
|
|
|
|
@param name: the owner name to look for
|
|
@type name: DNS.name.Name object or string
|
|
@param rdtype: the rdata type desired
|
|
@type rdtype: int or string
|
|
@param covers: the covered type (defaults to None)
|
|
@type covers: int or string
|
|
"""
|
|
|
|
name = self._validate_name(name)
|
|
if isinstance(rdtype, string_types):
|
|
rdtype = dns.rdatatype.from_text(rdtype)
|
|
if isinstance(covers, string_types):
|
|
covers = dns.rdatatype.from_text(covers)
|
|
node = self.get_node(name)
|
|
if node is not None:
|
|
node.delete_rdataset(self.rdclass, rdtype, covers)
|
|
if len(node) == 0:
|
|
self.delete_node(name)
|
|
|
|
def replace_rdataset(self, name, replacement):
|
|
"""Replace an rdataset at name.
|
|
|
|
It is not an error if there is no rdataset matching I{replacement}.
|
|
|
|
Ownership of the I{replacement} object is transferred to the zone;
|
|
in other words, this method does not store a copy of I{replacement}
|
|
at the node, it stores I{replacement} itself.
|
|
|
|
If the I{name} node does not exist, it is created.
|
|
|
|
@param name: the owner name
|
|
@type name: DNS.name.Name object or string
|
|
@param replacement: the replacement rdataset
|
|
@type replacement: dns.rdataset.Rdataset
|
|
"""
|
|
|
|
if replacement.rdclass != self.rdclass:
|
|
raise ValueError('replacement.rdclass != zone.rdclass')
|
|
node = self.find_node(name, True)
|
|
node.replace_rdataset(replacement)
|
|
|
|
def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
|
|
"""Look for rdata with the specified name and type in the zone,
|
|
and return an RRset encapsulating it.
|
|
|
|
The I{name}, I{rdtype}, and I{covers} parameters may be
|
|
strings, in which case they will be converted to their proper
|
|
type.
|
|
|
|
This method is less efficient than the similar
|
|
L{find_rdataset} because it creates an RRset instead of
|
|
returning the matching rdataset. It may be more convenient
|
|
for some uses since it returns an object which binds the owner
|
|
name to the rdata.
|
|
|
|
This method may not be used to create new nodes or rdatasets;
|
|
use L{find_rdataset} instead.
|
|
|
|
KeyError is raised if the name or type are not found.
|
|
Use L{get_rrset} if you want to have None returned instead.
|
|
|
|
@param name: the owner name to look for
|
|
@type name: DNS.name.Name object or string
|
|
@param rdtype: the rdata type desired
|
|
@type rdtype: int or string
|
|
@param covers: the covered type (defaults to None)
|
|
@type covers: int or string
|
|
@raises KeyError: the node or rdata could not be found
|
|
@rtype: dns.rrset.RRset object
|
|
"""
|
|
|
|
name = self._validate_name(name)
|
|
if isinstance(rdtype, string_types):
|
|
rdtype = dns.rdatatype.from_text(rdtype)
|
|
if isinstance(covers, string_types):
|
|
covers = dns.rdatatype.from_text(covers)
|
|
rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
|
|
rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
|
|
rrset.update(rdataset)
|
|
return rrset
|
|
|
|
def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
|
|
"""Look for rdata with the specified name and type in the zone,
|
|
and return an RRset encapsulating it.
|
|
|
|
The I{name}, I{rdtype}, and I{covers} parameters may be
|
|
strings, in which case they will be converted to their proper
|
|
type.
|
|
|
|
This method is less efficient than the similar L{get_rdataset}
|
|
because it creates an RRset instead of returning the matching
|
|
rdataset. It may be more convenient for some uses since it
|
|
returns an object which binds the owner name to the rdata.
|
|
|
|
This method may not be used to create new nodes or rdatasets;
|
|
use L{find_rdataset} instead.
|
|
|
|
None is returned if the name or type are not found.
|
|
Use L{find_rrset} if you want to have KeyError raised instead.
|
|
|
|
@param name: the owner name to look for
|
|
@type name: DNS.name.Name object or string
|
|
@param rdtype: the rdata type desired
|
|
@type rdtype: int or string
|
|
@param covers: the covered type (defaults to None)
|
|
@type covers: int or string
|
|
@rtype: dns.rrset.RRset object
|
|
"""
|
|
|
|
try:
|
|
rrset = self.find_rrset(name, rdtype, covers)
|
|
except KeyError:
|
|
rrset = None
|
|
return rrset
|
|
|
|
def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY,
|
|
covers=dns.rdatatype.NONE):
|
|
"""Return a generator which yields (name, rdataset) tuples for
|
|
all rdatasets in the zone which have the specified I{rdtype}
|
|
and I{covers}. If I{rdtype} is dns.rdatatype.ANY, the default,
|
|
then all rdatasets will be matched.
|
|
|
|
@param rdtype: int or string
|
|
@type rdtype: int or string
|
|
@param covers: the covered type (defaults to None)
|
|
@type covers: int or string
|
|
"""
|
|
|
|
if isinstance(rdtype, string_types):
|
|
rdtype = dns.rdatatype.from_text(rdtype)
|
|
if isinstance(covers, string_types):
|
|
covers = dns.rdatatype.from_text(covers)
|
|
for (name, node) in self.iteritems(): # pylint: disable=dict-iter-method
|
|
for rds in node:
|
|
if rdtype == dns.rdatatype.ANY or \
|
|
(rds.rdtype == rdtype and rds.covers == covers):
|
|
yield (name, rds)
|
|
|
|
def iterate_rdatas(self, rdtype=dns.rdatatype.ANY,
|
|
covers=dns.rdatatype.NONE):
|
|
"""Return a generator which yields (name, ttl, rdata) tuples for
|
|
all rdatas in the zone which have the specified I{rdtype}
|
|
and I{covers}. If I{rdtype} is dns.rdatatype.ANY, the default,
|
|
then all rdatas will be matched.
|
|
|
|
@param rdtype: int or string
|
|
@type rdtype: int or string
|
|
@param covers: the covered type (defaults to None)
|
|
@type covers: int or string
|
|
"""
|
|
|
|
if isinstance(rdtype, string_types):
|
|
rdtype = dns.rdatatype.from_text(rdtype)
|
|
if isinstance(covers, string_types):
|
|
covers = dns.rdatatype.from_text(covers)
|
|
for (name, node) in self.iteritems(): # pylint: disable=dict-iter-method
|
|
for rds in node:
|
|
if rdtype == dns.rdatatype.ANY or \
|
|
(rds.rdtype == rdtype and rds.covers == covers):
|
|
for rdata in rds:
|
|
yield (name, rds.ttl, rdata)
|
|
|
|
def to_file(self, f, sorted=True, relativize=True, nl=None):
|
|
"""Write a zone to a file.
|
|
|
|
@param f: file or string. If I{f} is a string, it is treated
|
|
as the name of a file to open.
|
|
@param sorted: if True, the file will be written with the
|
|
names sorted in DNSSEC order from least to greatest. Otherwise
|
|
the names will be written in whatever order they happen to have
|
|
in the zone's dictionary.
|
|
@param relativize: if True, domain names in the output will be
|
|
relativized to the zone's origin (if possible).
|
|
@type relativize: bool
|
|
@param nl: The end of line string. If not specified, the
|
|
output will use the platform's native end-of-line marker (i.e.
|
|
LF on POSIX, CRLF on Windows, CR on Macintosh).
|
|
@type nl: string or None
|
|
"""
|
|
|
|
if isinstance(f, string_types):
|
|
f = open(f, 'wb')
|
|
want_close = True
|
|
else:
|
|
want_close = False
|
|
|
|
# must be in this way, f.encoding may contain None, or even attribute
|
|
# may not be there
|
|
file_enc = getattr(f, 'encoding', None)
|
|
if file_enc is None:
|
|
file_enc = 'utf-8'
|
|
|
|
if nl is None:
|
|
nl_b = os.linesep.encode(file_enc) # binary mode, '\n' is not enough
|
|
nl = u'\n'
|
|
elif isinstance(nl, string_types):
|
|
nl_b = nl.encode(file_enc)
|
|
else:
|
|
nl_b = nl
|
|
nl = nl.decode()
|
|
|
|
try:
|
|
if sorted:
|
|
names = list(self.keys())
|
|
names.sort()
|
|
else:
|
|
names = self.iterkeys() # pylint: disable=dict-iter-method
|
|
for n in names:
|
|
l = self[n].to_text(n, origin=self.origin,
|
|
relativize=relativize)
|
|
if isinstance(l, text_type):
|
|
l_b = l.encode(file_enc)
|
|
else:
|
|
l_b = l
|
|
l = l.decode()
|
|
|
|
try:
|
|
f.write(l_b)
|
|
f.write(nl_b)
|
|
except TypeError: # textual mode
|
|
f.write(l)
|
|
f.write(nl)
|
|
finally:
|
|
if want_close:
|
|
f.close()
|
|
|
|
def to_text(self, sorted=True, relativize=True, nl=None):
|
|
"""Return a zone's text as though it were written to a file.
|
|
|
|
@param sorted: if True, the file will be written with the
|
|
names sorted in DNSSEC order from least to greatest. Otherwise
|
|
the names will be written in whatever order they happen to have
|
|
in the zone's dictionary.
|
|
@param relativize: if True, domain names in the output will be
|
|
relativized to the zone's origin (if possible).
|
|
@type relativize: bool
|
|
@param nl: The end of line string. If not specified, the
|
|
output will use the platform's native end-of-line marker (i.e.
|
|
LF on POSIX, CRLF on Windows, CR on Macintosh).
|
|
@type nl: string or None
|
|
"""
|
|
temp_buffer = BytesIO()
|
|
self.to_file(temp_buffer, sorted, relativize, nl)
|
|
return_value = temp_buffer.getvalue()
|
|
temp_buffer.close()
|
|
return return_value
|
|
|
|
def check_origin(self):
|
|
"""Do some simple checking of the zone's origin.
|
|
|
|
@raises dns.zone.NoSOA: there is no SOA RR
|
|
@raises dns.zone.NoNS: there is no NS RRset
|
|
@raises KeyError: there is no origin node
|
|
"""
|
|
if self.relativize:
|
|
name = dns.name.empty
|
|
else:
|
|
name = self.origin
|
|
if self.get_rdataset(name, dns.rdatatype.SOA) is None:
|
|
raise NoSOA
|
|
if self.get_rdataset(name, dns.rdatatype.NS) is None:
|
|
raise NoNS
|
|
|
|
|
|
class _MasterReader(object):
|
|
|
|
"""Read a DNS master file
|
|
|
|
@ivar tok: The tokenizer
|
|
@type tok: dns.tokenizer.Tokenizer object
|
|
@ivar last_ttl: The last seen explicit TTL for an RR
|
|
@type last_ttl: int
|
|
@ivar last_ttl_known: Has last TTL been detected
|
|
@type last_ttl_known: bool
|
|
@ivar default_ttl: The default TTL from a $TTL directive or SOA RR
|
|
@type default_ttl: int
|
|
@ivar default_ttl_known: Has default TTL been detected
|
|
@type default_ttl_known: bool
|
|
@ivar last_name: The last name read
|
|
@type last_name: dns.name.Name object
|
|
@ivar current_origin: The current origin
|
|
@type current_origin: dns.name.Name object
|
|
@ivar relativize: should names in the zone be relativized?
|
|
@type relativize: bool
|
|
@ivar zone: the zone
|
|
@type zone: dns.zone.Zone object
|
|
@ivar saved_state: saved reader state (used when processing $INCLUDE)
|
|
@type saved_state: list of (tokenizer, current_origin, last_name, file,
|
|
last_ttl, last_ttl_known, default_ttl, default_ttl_known) tuples.
|
|
@ivar current_file: the file object of the $INCLUDed file being parsed
|
|
(None if no $INCLUDE is active).
|
|
@ivar allow_include: is $INCLUDE allowed?
|
|
@type allow_include: bool
|
|
@ivar check_origin: should sanity checks of the origin node be done?
|
|
The default is True.
|
|
@type check_origin: bool
|
|
"""
|
|
|
|
def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
|
|
allow_include=False, check_origin=True):
|
|
if isinstance(origin, string_types):
|
|
origin = dns.name.from_text(origin)
|
|
self.tok = tok
|
|
self.current_origin = origin
|
|
self.relativize = relativize
|
|
self.last_ttl = 0
|
|
self.last_ttl_known = False
|
|
self.default_ttl = 0
|
|
self.default_ttl_known = False
|
|
self.last_name = self.current_origin
|
|
self.zone = zone_factory(origin, rdclass, relativize=relativize)
|
|
self.saved_state = []
|
|
self.current_file = None
|
|
self.allow_include = allow_include
|
|
self.check_origin = check_origin
|
|
|
|
def _eat_line(self):
|
|
while 1:
|
|
token = self.tok.get()
|
|
if token.is_eol_or_eof():
|
|
break
|
|
|
|
def _rr_line(self):
|
|
"""Process one line from a DNS master file."""
|
|
# Name
|
|
if self.current_origin is None:
|
|
raise UnknownOrigin
|
|
token = self.tok.get(want_leading=True)
|
|
if not token.is_whitespace():
|
|
self.last_name = dns.name.from_text(
|
|
token.value, self.current_origin)
|
|
else:
|
|
token = self.tok.get()
|
|
if token.is_eol_or_eof():
|
|
# treat leading WS followed by EOL/EOF as if they were EOL/EOF.
|
|
return
|
|
self.tok.unget(token)
|
|
name = self.last_name
|
|
if not name.is_subdomain(self.zone.origin):
|
|
self._eat_line()
|
|
return
|
|
if self.relativize:
|
|
name = name.relativize(self.zone.origin)
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError
|
|
# TTL
|
|
try:
|
|
ttl = dns.ttl.from_text(token.value)
|
|
self.last_ttl = ttl
|
|
self.last_ttl_known = True
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError
|
|
except dns.ttl.BadTTL:
|
|
if not (self.last_ttl_known or self.default_ttl_known):
|
|
raise dns.exception.SyntaxError("Missing default TTL value")
|
|
if self.default_ttl_known:
|
|
ttl = self.default_ttl
|
|
else:
|
|
ttl = self.last_ttl
|
|
# Class
|
|
try:
|
|
rdclass = dns.rdataclass.from_text(token.value)
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError
|
|
except dns.exception.SyntaxError:
|
|
raise dns.exception.SyntaxError
|
|
except Exception:
|
|
rdclass = self.zone.rdclass
|
|
if rdclass != self.zone.rdclass:
|
|
raise dns.exception.SyntaxError("RR class is not zone's class")
|
|
# Type
|
|
try:
|
|
rdtype = dns.rdatatype.from_text(token.value)
|
|
except:
|
|
raise dns.exception.SyntaxError(
|
|
"unknown rdatatype '%s'" % token.value)
|
|
n = self.zone.nodes.get(name)
|
|
if n is None:
|
|
n = self.zone.node_factory()
|
|
self.zone.nodes[name] = n
|
|
try:
|
|
rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
|
|
self.current_origin, False)
|
|
except dns.exception.SyntaxError:
|
|
# Catch and reraise.
|
|
(ty, va) = sys.exc_info()[:2]
|
|
raise va
|
|
except:
|
|
# All exceptions that occur in the processing of rdata
|
|
# are treated as syntax errors. This is not strictly
|
|
# correct, but it is correct almost all of the time.
|
|
# We convert them to syntax errors so that we can emit
|
|
# helpful filename:line info.
|
|
(ty, va) = sys.exc_info()[:2]
|
|
raise dns.exception.SyntaxError(
|
|
"caught exception {}: {}".format(str(ty), str(va)))
|
|
|
|
if not self.default_ttl_known and isinstance(rd, dns.rdtypes.ANY.SOA.SOA):
|
|
# The pre-RFC2308 and pre-BIND9 behavior inherits the zone default
|
|
# TTL from the SOA minttl if no $TTL statement is present before the
|
|
# SOA is parsed.
|
|
self.default_ttl = rd.minimum
|
|
self.default_ttl_known = True
|
|
|
|
rd.choose_relativity(self.zone.origin, self.relativize)
|
|
covers = rd.covers()
|
|
rds = n.find_rdataset(rdclass, rdtype, covers, True)
|
|
rds.add(rd, ttl)
|
|
|
|
def _parse_modify(self, side):
|
|
# Here we catch everything in '{' '}' in a group so we can replace it
|
|
# with ''.
|
|
is_generate1 = re.compile("^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")
|
|
is_generate2 = re.compile("^.*\$({(\+|-?)(\d+)}).*$")
|
|
is_generate3 = re.compile("^.*\$({(\+|-?)(\d+),(\d+)}).*$")
|
|
# Sometimes there are modifiers in the hostname. These come after
|
|
# the dollar sign. They are in the form: ${offset[,width[,base]]}.
|
|
# Make names
|
|
g1 = is_generate1.match(side)
|
|
if g1:
|
|
mod, sign, offset, width, base = g1.groups()
|
|
if sign == '':
|
|
sign = '+'
|
|
g2 = is_generate2.match(side)
|
|
if g2:
|
|
mod, sign, offset = g2.groups()
|
|
if sign == '':
|
|
sign = '+'
|
|
width = 0
|
|
base = 'd'
|
|
g3 = is_generate3.match(side)
|
|
if g3:
|
|
mod, sign, offset, width = g1.groups()
|
|
if sign == '':
|
|
sign = '+'
|
|
width = g1.groups()[2]
|
|
base = 'd'
|
|
|
|
if not (g1 or g2 or g3):
|
|
mod = ''
|
|
sign = '+'
|
|
offset = 0
|
|
width = 0
|
|
base = 'd'
|
|
|
|
if base != 'd':
|
|
raise NotImplementedError()
|
|
|
|
return mod, sign, offset, width, base
|
|
|
|
def _generate_line(self):
|
|
# range lhs [ttl] [class] type rhs [ comment ]
|
|
"""Process one line containing the GENERATE statement from a DNS
|
|
master file."""
|
|
if self.current_origin is None:
|
|
raise UnknownOrigin
|
|
|
|
token = self.tok.get()
|
|
# Range (required)
|
|
try:
|
|
start, stop, step = dns.grange.from_text(token.value)
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError
|
|
except:
|
|
raise dns.exception.SyntaxError
|
|
|
|
# lhs (required)
|
|
try:
|
|
lhs = token.value
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError
|
|
except:
|
|
raise dns.exception.SyntaxError
|
|
|
|
# TTL
|
|
try:
|
|
ttl = dns.ttl.from_text(token.value)
|
|
self.last_ttl = ttl
|
|
self.last_ttl_known = True
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError
|
|
except dns.ttl.BadTTL:
|
|
if not (self.last_ttl_known or self.default_ttl_known):
|
|
raise dns.exception.SyntaxError("Missing default TTL value")
|
|
if self.default_ttl_known:
|
|
ttl = self.default_ttl
|
|
else:
|
|
ttl = self.last_ttl
|
|
# Class
|
|
try:
|
|
rdclass = dns.rdataclass.from_text(token.value)
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError
|
|
except dns.exception.SyntaxError:
|
|
raise dns.exception.SyntaxError
|
|
except Exception:
|
|
rdclass = self.zone.rdclass
|
|
if rdclass != self.zone.rdclass:
|
|
raise dns.exception.SyntaxError("RR class is not zone's class")
|
|
# Type
|
|
try:
|
|
rdtype = dns.rdatatype.from_text(token.value)
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError
|
|
except Exception:
|
|
raise dns.exception.SyntaxError("unknown rdatatype '%s'" %
|
|
token.value)
|
|
|
|
# lhs (required)
|
|
try:
|
|
rhs = token.value
|
|
except:
|
|
raise dns.exception.SyntaxError
|
|
|
|
lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs)
|
|
rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs)
|
|
for i in range(start, stop + 1, step):
|
|
# +1 because bind is inclusive and python is exclusive
|
|
|
|
if lsign == u'+':
|
|
lindex = i + int(loffset)
|
|
elif lsign == u'-':
|
|
lindex = i - int(loffset)
|
|
|
|
if rsign == u'-':
|
|
rindex = i - int(roffset)
|
|
elif rsign == u'+':
|
|
rindex = i + int(roffset)
|
|
|
|
lzfindex = str(lindex).zfill(int(lwidth))
|
|
rzfindex = str(rindex).zfill(int(rwidth))
|
|
|
|
name = lhs.replace(u'$%s' % (lmod), lzfindex)
|
|
rdata = rhs.replace(u'$%s' % (rmod), rzfindex)
|
|
|
|
self.last_name = dns.name.from_text(name, self.current_origin)
|
|
name = self.last_name
|
|
if not name.is_subdomain(self.zone.origin):
|
|
self._eat_line()
|
|
return
|
|
if self.relativize:
|
|
name = name.relativize(self.zone.origin)
|
|
|
|
n = self.zone.nodes.get(name)
|
|
if n is None:
|
|
n = self.zone.node_factory()
|
|
self.zone.nodes[name] = n
|
|
try:
|
|
rd = dns.rdata.from_text(rdclass, rdtype, rdata,
|
|
self.current_origin, False)
|
|
except dns.exception.SyntaxError:
|
|
# Catch and reraise.
|
|
(ty, va) = sys.exc_info()[:2]
|
|
raise va
|
|
except:
|
|
# All exceptions that occur in the processing of rdata
|
|
# are treated as syntax errors. This is not strictly
|
|
# correct, but it is correct almost all of the time.
|
|
# We convert them to syntax errors so that we can emit
|
|
# helpful filename:line info.
|
|
(ty, va) = sys.exc_info()[:2]
|
|
raise dns.exception.SyntaxError("caught exception %s: %s" %
|
|
(str(ty), str(va)))
|
|
|
|
rd.choose_relativity(self.zone.origin, self.relativize)
|
|
covers = rd.covers()
|
|
rds = n.find_rdataset(rdclass, rdtype, covers, True)
|
|
rds.add(rd, ttl)
|
|
|
|
def read(self):
|
|
"""Read a DNS master file and build a zone object.
|
|
|
|
@raises dns.zone.NoSOA: No SOA RR was found at the zone origin
|
|
@raises dns.zone.NoNS: No NS RRset was found at the zone origin
|
|
"""
|
|
|
|
try:
|
|
while 1:
|
|
token = self.tok.get(True, True)
|
|
if token.is_eof():
|
|
if self.current_file is not None:
|
|
self.current_file.close()
|
|
if len(self.saved_state) > 0:
|
|
(self.tok,
|
|
self.current_origin,
|
|
self.last_name,
|
|
self.current_file,
|
|
self.last_ttl,
|
|
self.last_ttl_known,
|
|
self.default_ttl,
|
|
self.default_ttl_known) = self.saved_state.pop(-1)
|
|
continue
|
|
break
|
|
elif token.is_eol():
|
|
continue
|
|
elif token.is_comment():
|
|
self.tok.get_eol()
|
|
continue
|
|
elif token.value[0] == u'$':
|
|
c = token.value.upper()
|
|
if c == u'$TTL':
|
|
token = self.tok.get()
|
|
if not token.is_identifier():
|
|
raise dns.exception.SyntaxError("bad $TTL")
|
|
self.default_ttl = dns.ttl.from_text(token.value)
|
|
self.default_ttl_known = True
|
|
self.tok.get_eol()
|
|
elif c == u'$ORIGIN':
|
|
self.current_origin = self.tok.get_name()
|
|
self.tok.get_eol()
|
|
if self.zone.origin is None:
|
|
self.zone.origin = self.current_origin
|
|
elif c == u'$INCLUDE' and self.allow_include:
|
|
token = self.tok.get()
|
|
filename = token.value
|
|
token = self.tok.get()
|
|
if token.is_identifier():
|
|
new_origin =\
|
|
dns.name.from_text(token.value,
|
|
self.current_origin)
|
|
self.tok.get_eol()
|
|
elif not token.is_eol_or_eof():
|
|
raise dns.exception.SyntaxError(
|
|
"bad origin in $INCLUDE")
|
|
else:
|
|
new_origin = self.current_origin
|
|
self.saved_state.append((self.tok,
|
|
self.current_origin,
|
|
self.last_name,
|
|
self.current_file,
|
|
self.last_ttl,
|
|
self.last_ttl_known,
|
|
self.default_ttl,
|
|
self.default_ttl_known))
|
|
self.current_file = open(filename, 'r')
|
|
self.tok = dns.tokenizer.Tokenizer(self.current_file,
|
|
filename)
|
|
self.current_origin = new_origin
|
|
elif c == u'$GENERATE':
|
|
self._generate_line()
|
|
else:
|
|
raise dns.exception.SyntaxError(
|
|
"Unknown master file directive '" + c + "'")
|
|
continue
|
|
self.tok.unget(token)
|
|
self._rr_line()
|
|
except dns.exception.SyntaxError as detail:
|
|
(filename, line_number) = self.tok.where()
|
|
if detail is None:
|
|
detail = "syntax error"
|
|
raise dns.exception.SyntaxError(
|
|
"%s:%d: %s" % (filename, line_number, detail))
|
|
|
|
# Now that we're done reading, do some basic checking of the zone.
|
|
if self.check_origin:
|
|
self.zone.check_origin()
|
|
|
|
|
|
def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
|
|
relativize=True, zone_factory=Zone, filename=None,
|
|
allow_include=False, check_origin=True):
|
|
"""Build a zone object from a master file format string.
|
|
|
|
@param text: the master file format input
|
|
@type text: string.
|
|
@param origin: The origin of the zone; if not specified, the first
|
|
$ORIGIN statement in the master file will determine the origin of the
|
|
zone.
|
|
@type origin: dns.name.Name object or string
|
|
@param rdclass: The zone's rdata class; the default is class IN.
|
|
@type rdclass: int
|
|
@param relativize: should names be relativized? The default is True
|
|
@type relativize: bool
|
|
@param zone_factory: The zone factory to use
|
|
@type zone_factory: function returning a Zone
|
|
@param filename: The filename to emit when describing where an error
|
|
occurred; the default is '<string>'.
|
|
@type filename: string
|
|
@param allow_include: is $INCLUDE allowed?
|
|
@type allow_include: bool
|
|
@param check_origin: should sanity checks of the origin node be done?
|
|
The default is True.
|
|
@type check_origin: bool
|
|
@raises dns.zone.NoSOA: No SOA RR was found at the zone origin
|
|
@raises dns.zone.NoNS: No NS RRset was found at the zone origin
|
|
@rtype: dns.zone.Zone object
|
|
"""
|
|
|
|
# 'text' can also be a file, but we don't publish that fact
|
|
# since it's an implementation detail. The official file
|
|
# interface is from_file().
|
|
|
|
if filename is None:
|
|
filename = '<string>'
|
|
tok = dns.tokenizer.Tokenizer(text, filename)
|
|
reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
|
|
allow_include=allow_include,
|
|
check_origin=check_origin)
|
|
reader.read()
|
|
return reader.zone
|
|
|
|
|
|
def from_file(f, origin=None, rdclass=dns.rdataclass.IN,
|
|
relativize=True, zone_factory=Zone, filename=None,
|
|
allow_include=True, check_origin=True):
|
|
"""Read a master file and build a zone object.
|
|
|
|
@param f: file or string. If I{f} is a string, it is treated
|
|
as the name of a file to open.
|
|
@param origin: The origin of the zone; if not specified, the first
|
|
$ORIGIN statement in the master file will determine the origin of the
|
|
zone.
|
|
@type origin: dns.name.Name object or string
|
|
@param rdclass: The zone's rdata class; the default is class IN.
|
|
@type rdclass: int
|
|
@param relativize: should names be relativized? The default is True
|
|
@type relativize: bool
|
|
@param zone_factory: The zone factory to use
|
|
@type zone_factory: function returning a Zone
|
|
@param filename: The filename to emit when describing where an error
|
|
occurred; the default is '<file>', or the value of I{f} if I{f} is a
|
|
string.
|
|
@type filename: string
|
|
@param allow_include: is $INCLUDE allowed?
|
|
@type allow_include: bool
|
|
@param check_origin: should sanity checks of the origin node be done?
|
|
The default is True.
|
|
@type check_origin: bool
|
|
@raises dns.zone.NoSOA: No SOA RR was found at the zone origin
|
|
@raises dns.zone.NoNS: No NS RRset was found at the zone origin
|
|
@rtype: dns.zone.Zone object
|
|
"""
|
|
|
|
str_type = string_types
|
|
if PY3:
|
|
opts = 'r'
|
|
else:
|
|
opts = 'rU'
|
|
|
|
if isinstance(f, str_type):
|
|
if filename is None:
|
|
filename = f
|
|
f = open(f, opts)
|
|
want_close = True
|
|
else:
|
|
if filename is None:
|
|
filename = '<file>'
|
|
want_close = False
|
|
|
|
try:
|
|
z = from_text(f, origin, rdclass, relativize, zone_factory,
|
|
filename, allow_include, check_origin)
|
|
finally:
|
|
if want_close:
|
|
f.close()
|
|
return z
|
|
|
|
|
|
def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True):
|
|
"""Convert the output of a zone transfer generator into a zone object.
|
|
|
|
@param xfr: The xfr generator
|
|
@type xfr: generator of dns.message.Message objects
|
|
@param relativize: should names be relativized? The default is True.
|
|
It is essential that the relativize setting matches the one specified
|
|
to dns.query.xfr().
|
|
@type relativize: bool
|
|
@param check_origin: should sanity checks of the origin node be done?
|
|
The default is True.
|
|
@type check_origin: bool
|
|
@raises dns.zone.NoSOA: No SOA RR was found at the zone origin
|
|
@raises dns.zone.NoNS: No NS RRset was found at the zone origin
|
|
@rtype: dns.zone.Zone object
|
|
"""
|
|
|
|
z = None
|
|
for r in xfr:
|
|
if z is None:
|
|
if relativize:
|
|
origin = r.origin
|
|
else:
|
|
origin = r.answer[0].name
|
|
rdclass = r.answer[0].rdclass
|
|
z = zone_factory(origin, rdclass, relativize=relativize)
|
|
for rrset in r.answer:
|
|
znode = z.nodes.get(rrset.name)
|
|
if not znode:
|
|
znode = z.node_factory()
|
|
z.nodes[rrset.name] = znode
|
|
zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,
|
|
rrset.covers, True)
|
|
zrds.update_ttl(rrset.ttl)
|
|
for rd in rrset:
|
|
rd.choose_relativity(z.origin, relativize)
|
|
zrds.add(rd)
|
|
if check_origin:
|
|
z.check_origin()
|
|
return z
|