mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 05:01:14 -07:00
Bump dnspython from 2.0.0 to 2.2.0 (#1618)
* Bump dnspython from 2.0.0 to 2.2.0 Bumps [dnspython]() from 2.0.0 to 2.2.0. --- updated-dependencies: - dependency-name: dnspython dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Update dnspython==2.2.0 Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> [skip ci]
This commit is contained in:
parent
515a5d42d3
commit
3c93b5600f
143 changed files with 7498 additions and 2054 deletions
845
lib/dns/zone.py
845
lib/dns/zone.py
|
@ -18,22 +18,26 @@
|
|||
"""DNS Zones."""
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.immutable
|
||||
import dns.name
|
||||
import dns.node
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
import dns.rdata
|
||||
import dns.rdtypes.ANY.SOA
|
||||
import dns.rdtypes.ANY.ZONEMD
|
||||
import dns.rrset
|
||||
import dns.tokenizer
|
||||
import dns.transaction
|
||||
import dns.ttl
|
||||
import dns.grange
|
||||
import dns.zonefile
|
||||
|
||||
|
||||
class BadZone(dns.exception.DNSException):
|
||||
|
@ -56,7 +60,54 @@ class UnknownOrigin(BadZone):
|
|||
"""The DNS zone's origin is unknown."""
|
||||
|
||||
|
||||
class Zone:
|
||||
class UnsupportedDigestScheme(dns.exception.DNSException):
|
||||
|
||||
"""The zone digest's scheme is unsupported."""
|
||||
|
||||
|
||||
class UnsupportedDigestHashAlgorithm(dns.exception.DNSException):
|
||||
|
||||
"""The zone digest's origin is unsupported."""
|
||||
|
||||
|
||||
class NoDigest(dns.exception.DNSException):
|
||||
|
||||
"""The DNS zone has no ZONEMD RRset at its origin."""
|
||||
|
||||
|
||||
class DigestVerificationFailure(dns.exception.DNSException):
|
||||
|
||||
"""The ZONEMD digest failed to verify."""
|
||||
|
||||
|
||||
class DigestScheme(dns.enum.IntEnum):
|
||||
"""ZONEMD Scheme"""
|
||||
|
||||
SIMPLE = 1
|
||||
|
||||
@classmethod
|
||||
def _maximum(cls):
|
||||
return 255
|
||||
|
||||
|
||||
class DigestHashAlgorithm(dns.enum.IntEnum):
|
||||
"""ZONEMD Hash Algorithm"""
|
||||
|
||||
SHA384 = 1
|
||||
SHA512 = 2
|
||||
|
||||
@classmethod
|
||||
def _maximum(cls):
|
||||
return 255
|
||||
|
||||
|
||||
_digest_hashers = {
|
||||
DigestHashAlgorithm.SHA384: hashlib.sha384,
|
||||
DigestHashAlgorithm.SHA512: hashlib.sha512,
|
||||
}
|
||||
|
||||
|
||||
class Zone(dns.transaction.TransactionManager):
|
||||
|
||||
"""A DNS zone.
|
||||
|
||||
|
@ -77,7 +128,7 @@ class Zone:
|
|||
|
||||
*origin* is the origin of the zone. It may be a ``dns.name.Name``,
|
||||
a ``str``, or ``None``. If ``None``, then the zone's origin will
|
||||
be set by the first ``$ORIGIN`` line in a masterfile.
|
||||
be set by the first ``$ORIGIN`` line in a zone file.
|
||||
|
||||
*rdclass*, an ``int``, the zone's rdata class; the default is class IN.
|
||||
|
||||
|
@ -150,20 +201,21 @@ class Zone:
|
|||
return self.nodes.__iter__()
|
||||
|
||||
def keys(self):
|
||||
return self.nodes.keys() # pylint: disable=dict-keys-not-iterating
|
||||
return self.nodes.keys()
|
||||
|
||||
def values(self):
|
||||
return self.nodes.values() # pylint: disable=dict-values-not-iterating
|
||||
return self.nodes.values()
|
||||
|
||||
def items(self):
|
||||
return self.nodes.items() # pylint: disable=dict-items-not-iterating
|
||||
return self.nodes.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 __contains__(self, key):
|
||||
key = self._validate_name(key)
|
||||
return key in self.nodes
|
||||
|
||||
def find_node(self, name, create=False):
|
||||
"""Find a node in the zone, possibly creating it.
|
||||
|
@ -532,7 +584,8 @@ class Zone:
|
|||
for rdata in rds:
|
||||
yield (name, rds.ttl, rdata)
|
||||
|
||||
def to_file(self, f, sorted=True, relativize=True, nl=None):
|
||||
def to_file(self, f, sorted=True, relativize=True, nl=None,
|
||||
want_comments=False, want_origin=False):
|
||||
"""Write a zone to a file.
|
||||
|
||||
*f*, a file or `str`. If *f* is a string, it is treated
|
||||
|
@ -550,6 +603,14 @@ class Zone:
|
|||
*nl*, a ``str`` or None. The end of line string. If not
|
||||
``None``, the output will use the platform's native
|
||||
end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
|
||||
|
||||
*want_comments*, a ``bool``. If ``True``, emit end-of-line comments
|
||||
as part of writing the file. If ``False``, the default, do not
|
||||
emit them.
|
||||
|
||||
*want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at
|
||||
the start of the file. If ``False``, the default, do not emit
|
||||
one.
|
||||
"""
|
||||
|
||||
with contextlib.ExitStack() as stack:
|
||||
|
@ -572,6 +633,16 @@ class Zone:
|
|||
nl_b = nl
|
||||
nl = nl.decode()
|
||||
|
||||
if want_origin:
|
||||
l = '$ORIGIN ' + self.origin.to_text()
|
||||
l_b = l.encode(file_enc)
|
||||
try:
|
||||
f.write(l_b)
|
||||
f.write(nl_b)
|
||||
except TypeError: # textual mode
|
||||
f.write(l)
|
||||
f.write(nl)
|
||||
|
||||
if sorted:
|
||||
names = list(self.keys())
|
||||
names.sort()
|
||||
|
@ -579,12 +650,9 @@ class Zone:
|
|||
names = self.keys()
|
||||
for n in names:
|
||||
l = self[n].to_text(n, origin=self.origin,
|
||||
relativize=relativize)
|
||||
if isinstance(l, str):
|
||||
l_b = l.encode(file_enc)
|
||||
else:
|
||||
l_b = l
|
||||
l = l.decode()
|
||||
relativize=relativize,
|
||||
want_comments=want_comments)
|
||||
l_b = l.encode(file_enc)
|
||||
|
||||
try:
|
||||
f.write(l_b)
|
||||
|
@ -593,7 +661,8 @@ class Zone:
|
|||
f.write(l)
|
||||
f.write(nl)
|
||||
|
||||
def to_text(self, sorted=True, relativize=True, nl=None):
|
||||
def to_text(self, sorted=True, relativize=True, nl=None,
|
||||
want_comments=False, want_origin=False):
|
||||
"""Return a zone's text as though it were written to a file.
|
||||
|
||||
*sorted*, a ``bool``. If True, the default, then the file
|
||||
|
@ -609,10 +678,19 @@ class Zone:
|
|||
``None``, the output will use the platform's native
|
||||
end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
|
||||
|
||||
*want_comments*, a ``bool``. If ``True``, emit end-of-line comments
|
||||
as part of writing the file. If ``False``, the default, do not
|
||||
emit them.
|
||||
|
||||
*want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at
|
||||
the start of the output. If ``False``, the default, do not emit
|
||||
one.
|
||||
|
||||
Returns a ``str``.
|
||||
"""
|
||||
temp_buffer = io.StringIO()
|
||||
self.to_file(temp_buffer, sorted, relativize, nl)
|
||||
self.to_file(temp_buffer, sorted, relativize, nl, want_comments,
|
||||
want_origin)
|
||||
return_value = temp_buffer.getvalue()
|
||||
temp_buffer.close()
|
||||
return return_value
|
||||
|
@ -635,425 +713,334 @@ class Zone:
|
|||
if self.get_rdataset(name, dns.rdatatype.NS) is None:
|
||||
raise NoNS
|
||||
|
||||
def _compute_digest(self, hash_algorithm, scheme=DigestScheme.SIMPLE):
|
||||
hashinfo = _digest_hashers.get(hash_algorithm)
|
||||
if not hashinfo:
|
||||
raise UnsupportedDigestHashAlgorithm
|
||||
if scheme != DigestScheme.SIMPLE:
|
||||
raise UnsupportedDigestScheme
|
||||
|
||||
class _MasterReader:
|
||||
|
||||
"""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, str):
|
||||
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 = self.tok.as_name(token, 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
|
||||
origin_name = dns.name.empty
|
||||
else:
|
||||
origin_name = self.origin
|
||||
hasher = hashinfo()
|
||||
for (name, node) in sorted(self.items()):
|
||||
rrnamebuf = name.to_digestable(self.origin)
|
||||
for rdataset in sorted(node,
|
||||
key=lambda rds: (rds.rdtype, rds.covers)):
|
||||
if name == origin_name and \
|
||||
dns.rdatatype.ZONEMD in (rdataset.rdtype, rdataset.covers):
|
||||
continue
|
||||
rrfixed = struct.pack('!HHI', rdataset.rdtype,
|
||||
rdataset.rdclass, rdataset.ttl)
|
||||
rdatas = [rdata.to_digestable(self.origin)
|
||||
for rdata in rdataset]
|
||||
for rdata in sorted(rdatas):
|
||||
rrlen = struct.pack('!H', len(rdata))
|
||||
hasher.update(rrnamebuf + rrfixed + rrlen + rdata)
|
||||
return hasher.digest()
|
||||
|
||||
# TTL
|
||||
ttl = None
|
||||
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 self.default_ttl_known:
|
||||
ttl = self.default_ttl
|
||||
elif self.last_ttl_known:
|
||||
ttl = self.last_ttl
|
||||
def compute_digest(self, hash_algorithm, scheme=DigestScheme.SIMPLE):
|
||||
if self.relativize:
|
||||
origin_name = dns.name.empty
|
||||
else:
|
||||
origin_name = self.origin
|
||||
serial = self.get_rdataset(origin_name, dns.rdatatype.SOA)[0].serial
|
||||
digest = self._compute_digest(hash_algorithm, scheme)
|
||||
return dns.rdtypes.ANY.ZONEMD.ZONEMD(self.rdclass,
|
||||
dns.rdatatype.ZONEMD,
|
||||
serial, scheme, hash_algorithm,
|
||||
digest)
|
||||
|
||||
# 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
|
||||
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 Exception:
|
||||
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, self.relativize,
|
||||
self.zone.origin)
|
||||
except dns.exception.SyntaxError:
|
||||
# Catch and reraise.
|
||||
raise
|
||||
except Exception:
|
||||
# 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 rdtype == dns.rdatatype.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
|
||||
if ttl is None:
|
||||
# if we didn't have a TTL on the SOA, set it!
|
||||
ttl = rd.minimum
|
||||
|
||||
# TTL check. We had to wait until now to do this as the SOA RR's
|
||||
# own TTL can be inferred from its minimum.
|
||||
if ttl is None:
|
||||
raise dns.exception.SyntaxError("Missing default TTL value")
|
||||
|
||||
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(r"^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")
|
||||
is_generate2 = re.compile(r"^.*\$({(\+|-?)(\d+)}).*$")
|
||||
is_generate3 = re.compile(r"^.*\$({(\+|-?)(\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 = g3.groups()
|
||||
if sign == '':
|
||||
sign = '+'
|
||||
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 Exception:
|
||||
raise dns.exception.SyntaxError
|
||||
|
||||
# lhs (required)
|
||||
try:
|
||||
lhs = token.value
|
||||
token = self.tok.get()
|
||||
if not token.is_identifier():
|
||||
raise dns.exception.SyntaxError
|
||||
except Exception:
|
||||
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
|
||||
elif self.last_ttl_known:
|
||||
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)
|
||||
|
||||
# rhs (required)
|
||||
rhs = token.value
|
||||
|
||||
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 == '+':
|
||||
lindex = i + int(loffset)
|
||||
elif lsign == '-':
|
||||
lindex = i - int(loffset)
|
||||
|
||||
if rsign == '-':
|
||||
rindex = i - int(roffset)
|
||||
elif rsign == '+':
|
||||
rindex = i + int(roffset)
|
||||
|
||||
lzfindex = str(lindex).zfill(int(lwidth))
|
||||
rzfindex = str(rindex).zfill(int(rwidth))
|
||||
|
||||
name = lhs.replace('$%s' % (lmod), lzfindex)
|
||||
rdata = rhs.replace('$%s' % (rmod), rzfindex)
|
||||
|
||||
self.last_name = dns.name.from_text(name, self.current_origin,
|
||||
self.tok.idna_codec)
|
||||
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
|
||||
def verify_digest(self, zonemd=None):
|
||||
if zonemd:
|
||||
digests = [zonemd]
|
||||
else:
|
||||
digests = self.get_rdataset(self.origin, dns.rdatatype.ZONEMD)
|
||||
if digests is None:
|
||||
raise NoDigest
|
||||
for digest in digests:
|
||||
try:
|
||||
rd = dns.rdata.from_text(rdclass, rdtype, rdata,
|
||||
self.current_origin, self.relativize,
|
||||
self.zone.origin)
|
||||
except dns.exception.SyntaxError:
|
||||
# Catch and reraise.
|
||||
raise
|
||||
computed = self._compute_digest(digest.hash_algorithm,
|
||||
digest.scheme)
|
||||
if computed == digest.digest:
|
||||
return
|
||||
except Exception:
|
||||
# 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)))
|
||||
pass
|
||||
raise DigestVerificationFailure
|
||||
|
||||
covers = rd.covers()
|
||||
rds = n.find_rdataset(rdclass, rdtype, covers, True)
|
||||
rds.add(rd, ttl)
|
||||
# TransactionManager methods
|
||||
|
||||
def read(self):
|
||||
"""Read a DNS master file and build a zone object.
|
||||
def reader(self):
|
||||
return Transaction(self, False,
|
||||
Version(self, 1, self.nodes, self.origin))
|
||||
|
||||
@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
|
||||
"""
|
||||
def writer(self, replacement=False):
|
||||
txn = Transaction(self, replacement)
|
||||
txn._setup_version()
|
||||
return txn
|
||||
|
||||
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] == '$':
|
||||
c = token.value.upper()
|
||||
if c == '$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 == '$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 == '$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.idna_codec)
|
||||
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 == '$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"
|
||||
ex = dns.exception.SyntaxError(
|
||||
"%s:%d: %s" % (filename, line_number, detail))
|
||||
tb = sys.exc_info()[2]
|
||||
raise ex.with_traceback(tb) from None
|
||||
def origin_information(self):
|
||||
if self.relativize:
|
||||
effective = dns.name.empty
|
||||
else:
|
||||
effective = self.origin
|
||||
return (self.origin, self.relativize, effective)
|
||||
|
||||
# Now that we're done reading, do some basic checking of the zone.
|
||||
if self.check_origin:
|
||||
self.zone.check_origin()
|
||||
def get_class(self):
|
||||
return self.rdclass
|
||||
|
||||
# Transaction methods
|
||||
|
||||
def _end_read(self, txn):
|
||||
pass
|
||||
|
||||
def _end_write(self, txn):
|
||||
pass
|
||||
|
||||
def _commit_version(self, _, version, origin):
|
||||
self.nodes = version.nodes
|
||||
if self.origin is None:
|
||||
self.origin = origin
|
||||
|
||||
def _get_next_version_id(self):
|
||||
# Versions are ephemeral and all have id 1
|
||||
return 1
|
||||
|
||||
|
||||
# These classes used to be in dns.versioned, but have moved here so we can use
|
||||
# the copy-on-write transaction mechanism for both kinds of zones. In a
|
||||
# regular zone, the version only exists during the transaction, and the nodes
|
||||
# are regular dns.node.Nodes.
|
||||
|
||||
# A node with a version id.
|
||||
|
||||
class VersionedNode(dns.node.Node):
|
||||
__slots__ = ['id']
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# A proper id will get set by the Version
|
||||
self.id = 0
|
||||
|
||||
|
||||
@dns.immutable.immutable
|
||||
class ImmutableVersionedNode(VersionedNode):
|
||||
__slots__ = ['id']
|
||||
|
||||
def __init__(self, node):
|
||||
super().__init__()
|
||||
self.id = node.id
|
||||
self.rdatasets = tuple(
|
||||
[dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets]
|
||||
)
|
||||
|
||||
def find_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,
|
||||
create=False):
|
||||
if create:
|
||||
raise TypeError("immutable")
|
||||
return super().find_rdataset(rdclass, rdtype, covers, False)
|
||||
|
||||
def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,
|
||||
create=False):
|
||||
if create:
|
||||
raise TypeError("immutable")
|
||||
return super().get_rdataset(rdclass, rdtype, covers, False)
|
||||
|
||||
def delete_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE):
|
||||
raise TypeError("immutable")
|
||||
|
||||
def replace_rdataset(self, replacement):
|
||||
raise TypeError("immutable")
|
||||
|
||||
def is_immutable(self):
|
||||
return True
|
||||
|
||||
|
||||
class Version:
|
||||
def __init__(self, zone, id, nodes=None, origin=None):
|
||||
self.zone = zone
|
||||
self.id = id
|
||||
if nodes is not None:
|
||||
self.nodes = nodes
|
||||
else:
|
||||
self.nodes = {}
|
||||
self.origin = origin
|
||||
|
||||
def _validate_name(self, name):
|
||||
if name.is_absolute():
|
||||
if not name.is_subdomain(self.zone.origin):
|
||||
raise KeyError("name is not a subdomain of the zone origin")
|
||||
if self.zone.relativize:
|
||||
# XXXRTH should it be an error if self.origin is still None?
|
||||
name = name.relativize(self.origin)
|
||||
return name
|
||||
|
||||
def get_node(self, name):
|
||||
name = self._validate_name(name)
|
||||
return self.nodes.get(name)
|
||||
|
||||
def get_rdataset(self, name, rdtype, covers):
|
||||
node = self.get_node(name)
|
||||
if node is None:
|
||||
return None
|
||||
return node.get_rdataset(self.zone.rdclass, rdtype, covers)
|
||||
|
||||
def items(self):
|
||||
return self.nodes.items()
|
||||
|
||||
|
||||
class WritableVersion(Version):
|
||||
def __init__(self, zone, replacement=False):
|
||||
# The zone._versions_lock must be held by our caller in a versioned
|
||||
# zone.
|
||||
id = zone._get_next_version_id()
|
||||
super().__init__(zone, id)
|
||||
if not replacement:
|
||||
# We copy the map, because that gives us a simple and thread-safe
|
||||
# way of doing versions, and we have a garbage collector to help
|
||||
# us. We only make new node objects if we actually change the
|
||||
# node.
|
||||
self.nodes.update(zone.nodes)
|
||||
# We have to copy the zone origin as it may be None in the first
|
||||
# version, and we don't want to mutate the zone until we commit.
|
||||
self.origin = zone.origin
|
||||
self.changed = set()
|
||||
|
||||
def _maybe_cow(self, name):
|
||||
name = self._validate_name(name)
|
||||
node = self.nodes.get(name)
|
||||
if node is None or name not in self.changed:
|
||||
new_node = self.zone.node_factory()
|
||||
if hasattr(new_node, 'id'):
|
||||
# We keep doing this for backwards compatibility, as earlier
|
||||
# code used new_node.id != self.id for the "do we need to CoW?"
|
||||
# test. Now we use the changed set as this works with both
|
||||
# regular zones and versioned zones.
|
||||
new_node.id = self.id
|
||||
if node is not None:
|
||||
# moo! copy on write!
|
||||
new_node.rdatasets.extend(node.rdatasets)
|
||||
self.nodes[name] = new_node
|
||||
self.changed.add(name)
|
||||
return new_node
|
||||
else:
|
||||
return node
|
||||
|
||||
def delete_node(self, name):
|
||||
name = self._validate_name(name)
|
||||
if name in self.nodes:
|
||||
del self.nodes[name]
|
||||
self.changed.add(name)
|
||||
|
||||
def put_rdataset(self, name, rdataset):
|
||||
node = self._maybe_cow(name)
|
||||
node.replace_rdataset(rdataset)
|
||||
|
||||
def delete_rdataset(self, name, rdtype, covers):
|
||||
node = self._maybe_cow(name)
|
||||
node.delete_rdataset(self.zone.rdclass, rdtype, covers)
|
||||
if len(node) == 0:
|
||||
del self.nodes[name]
|
||||
|
||||
|
||||
@dns.immutable.immutable
|
||||
class ImmutableVersion(Version):
|
||||
def __init__(self, version):
|
||||
# We tell super() that it's a replacement as we don't want it
|
||||
# to copy the nodes, as we're about to do that with an
|
||||
# immutable Dict.
|
||||
super().__init__(version.zone, True)
|
||||
# set the right id!
|
||||
self.id = version.id
|
||||
# keep the origin
|
||||
self.origin = version.origin
|
||||
# Make changed nodes immutable
|
||||
for name in version.changed:
|
||||
node = version.nodes.get(name)
|
||||
# it might not exist if we deleted it in the version
|
||||
if node:
|
||||
version.nodes[name] = ImmutableVersionedNode(node)
|
||||
self.nodes = dns.immutable.Dict(version.nodes, True)
|
||||
|
||||
|
||||
class Transaction(dns.transaction.Transaction):
|
||||
|
||||
def __init__(self, zone, replacement, version=None, make_immutable=False):
|
||||
read_only = version is not None
|
||||
super().__init__(zone, replacement, read_only)
|
||||
self.version = version
|
||||
self.make_immutable = make_immutable
|
||||
|
||||
@property
|
||||
def zone(self):
|
||||
return self.manager
|
||||
|
||||
def _setup_version(self):
|
||||
assert self.version is None
|
||||
self.version = WritableVersion(self.zone, self.replacement)
|
||||
|
||||
def _get_rdataset(self, name, rdtype, covers):
|
||||
return self.version.get_rdataset(name, rdtype, covers)
|
||||
|
||||
def _put_rdataset(self, name, rdataset):
|
||||
assert not self.read_only
|
||||
self.version.put_rdataset(name, rdataset)
|
||||
|
||||
def _delete_name(self, name):
|
||||
assert not self.read_only
|
||||
self.version.delete_node(name)
|
||||
|
||||
def _delete_rdataset(self, name, rdtype, covers):
|
||||
assert not self.read_only
|
||||
self.version.delete_rdataset(name, rdtype, covers)
|
||||
|
||||
def _name_exists(self, name):
|
||||
return self.version.get_node(name) is not None
|
||||
|
||||
def _changed(self):
|
||||
if self.read_only:
|
||||
return False
|
||||
else:
|
||||
return len(self.version.changed) > 0
|
||||
|
||||
def _end_transaction(self, commit):
|
||||
if self.read_only:
|
||||
self.zone._end_read(self)
|
||||
elif commit and len(self.version.changed) > 0:
|
||||
if self.make_immutable:
|
||||
version = ImmutableVersion(self.version)
|
||||
else:
|
||||
version = self.version
|
||||
self.zone._commit_version(self, version, self.version.origin)
|
||||
else:
|
||||
# rollback
|
||||
self.zone._end_write(self)
|
||||
|
||||
def _set_origin(self, origin):
|
||||
if self.version.origin is None:
|
||||
self.version.origin = origin
|
||||
|
||||
def _iterate_rdatasets(self):
|
||||
for (name, node) in self.version.items():
|
||||
for rdataset in node:
|
||||
yield (name, rdataset)
|
||||
|
||||
def _get_node(self, name):
|
||||
return self.version.get_node(name)
|
||||
|
||||
|
||||
def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
|
||||
relativize=True, zone_factory=Zone, filename=None,
|
||||
allow_include=False, check_origin=True, idna_codec=None):
|
||||
"""Build a zone object from a master file format string.
|
||||
"""Build a zone object from a zone file format string.
|
||||
|
||||
*text*, a ``str``, the master file format input.
|
||||
*text*, a ``str``, the zone file format input.
|
||||
|
||||
*origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin
|
||||
of the zone; if not specified, the first ``$ORIGIN`` statement in the
|
||||
masterfile will determine the origin of the zone.
|
||||
zone file will determine the origin of the zone.
|
||||
|
||||
*rdclass*, an ``int``, the zone's rdata class; the default is class IN.
|
||||
|
||||
|
@ -1094,25 +1081,33 @@ def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
|
|||
|
||||
if filename is None:
|
||||
filename = '<string>'
|
||||
tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec)
|
||||
reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
|
||||
allow_include=allow_include,
|
||||
check_origin=check_origin)
|
||||
reader.read()
|
||||
return reader.zone
|
||||
zone = zone_factory(origin, rdclass, relativize=relativize)
|
||||
with zone.writer(True) as txn:
|
||||
tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec)
|
||||
reader = dns.zonefile.Reader(tok, rdclass, txn,
|
||||
allow_include=allow_include)
|
||||
try:
|
||||
reader.read()
|
||||
except dns.zonefile.UnknownOrigin:
|
||||
# for backwards compatibility
|
||||
raise dns.zone.UnknownOrigin
|
||||
# Now that we're done reading, do some basic checking of the zone.
|
||||
if check_origin:
|
||||
zone.check_origin()
|
||||
return 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.
|
||||
"""Read a zone file and build a zone object.
|
||||
|
||||
*f*, a file or ``str``. If *f* is a string, it is treated
|
||||
as the name of a file to open.
|
||||
|
||||
*origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin
|
||||
of the zone; if not specified, the first ``$ORIGIN`` statement in the
|
||||
masterfile will determine the origin of the zone.
|
||||
zone file will determine the origin of the zone.
|
||||
|
||||
*rdclass*, an ``int``, the zone's rdata class; the default is class IN.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue