"""DNS Zones."""
-from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
+from typing import Any, Dict, Iterator, Iterable, List, Optional, Set, Tuple, Union
import contextlib
import io
allow_include: bool = False,
check_origin: bool = True,
idna_codec: Optional[dns.name.IDNACodec] = None,
+ allow_directives: Union[bool, Iterable[str]] = True,
) -> Zone:
"""Build a zone object from a zone file format string.
encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
is used.
+ *allow_directives*, a ``bool`` or an iteratable of `str`. If ``True``, the default,
+ then directives are permitted, and the *allow_include* parameter controls whether
+ ``$INCLUDE`` is permitted. If ``False`` or an empty iterable, then no directive
+ processing is done and any directive-like text will be treated as a regular owner
+ name. If a non-empty iterable, then only the listed directives (including the
+ ``$``) are allowed.
+
Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
Raises ``dns.zone.NoNS`` if there is no NS RRset.
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)
+ reader = dns.zonefile.Reader(
+ tok,
+ rdclass,
+ txn,
+ allow_include=allow_include,
+ allow_directives=allow_directives,
+ )
try:
reader.read()
except dns.zonefile.UnknownOrigin:
allow_include: bool = True,
check_origin: bool = True,
idna_codec: Optional[dns.name.IDNACodec] = None,
+ allow_directives: Union[bool, Iterable[str]] = True,
) -> Zone:
"""Read a zone file and build a zone object.
encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
is used.
+ *allow_directives*, a ``bool`` or an iteratable of `str`. If ``True``, the default,
+ then directives are permitted, and the *allow_include* parameter controls whether
+ ``$INCLUDE`` is permitted. If ``False`` or an empty iterable, then no directive
+ processing is done and any directive-like text will be treated as a regular owner
+ name. If a non-empty iterable, then only the listed directives (including the
+ ``$``) are allowed.
+
Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
Raises ``dns.zone.NoNS`` if there is no NS RRset.
allow_include,
check_origin,
idna_codec,
+ allow_directives,
)
assert False # make mypy happy lgtm[py/unreachable-statement]
"""DNS Zones."""
-from typing import Any, List, Optional, Tuple, Union
+from typing import Any, Iterable, List, Optional, Set, Tuple, Union
import re
import sys
rdclass: dns.rdataclass.RdataClass,
txn: dns.transaction.Transaction,
allow_include: bool = False,
- allow_directives: bool = True,
+ allow_directives: Union[bool, Iterable[str]] = True,
force_name: Optional[dns.name.Name] = None,
force_ttl: Optional[int] = None,
force_rdclass: Optional[dns.rdataclass.RdataClass] = None,
self.txn = txn
self.saved_state: List[SavedStateType] = []
self.current_file: Optional[Any] = None
- self.allow_include = allow_include
- self.allow_directives = allow_directives
+ self.allowed_directives: Set[str]
+ if allow_directives is True:
+ self.allowed_directives = {"$GENERATE", "$ORIGIN", "$TTL"}
+ if allow_include:
+ self.allowed_directives.add("$INCLUDE")
+ elif allow_directives is False:
+ # allow_include was ignored in earlier releases if allow_directives was
+ # False, so we continue that.
+ self.allowed_directives = set()
+ else:
+ # Note that if directives are explicitly specified, then allow_include
+ # is ignored.
+ self.allowed_directives = set(d.upper() for d in allow_directives)
self.force_name = force_name
self.force_ttl = force_ttl
self.force_rdclass = force_rdclass
width = int(width)
if sign not in ["+", "-"]:
- raise dns.exception.SyntaxError(
- "invalid offset sign %s" % sign
- )
+ raise dns.exception.SyntaxError("invalid offset sign %s" % sign)
if base not in ["d", "o", "x", "X", "n", "N"]:
- raise dns.exception.SyntaxError(
- "invalid type %s" % base
- )
+ raise dns.exception.SyntaxError("invalid type %s" % base)
return mod, sign, offset, width, base
elif token.is_comment():
self.tok.get_eol()
continue
- elif token.value[0] == "$" and self.allow_directives:
+ elif token.value[0] == "$" and len(self.allowed_directives) > 0:
+ # Note that we only run directive processing code if at least
+ # one directive is allowed in order to be backwards compatible
c = token.value.upper()
+ if not c in self.allowed_directives:
+ raise dns.exception.SyntaxError(
+ f"zone file directive '{c}' is not allowed"
+ )
if c == "$TTL":
token = self.tok.get()
if not token.is_identifier():
if self.zone_origin is None:
self.zone_origin = self.current_origin
self.txn._set_origin(self.current_origin)
- elif c == "$INCLUDE" and self.allow_include:
+ elif c == "$INCLUDE":
token = self.tok.get()
filename = token.value
token = self.tok.get()
self._generate_line()
else:
raise dns.exception.SyntaxError(
- "Unknown zone file directive '" + c + "'"
+ f"Unknown zone file directive '{c}'"
)
continue
self.tok.unget(token)
z2 = dns.zone.from_file(here("example"), "example.", relativize=True)
self.assertEqual(z1, z2)
+ def testNoInclude(self):
+ def bad():
+ dns.zone.from_text(
+ include_text, "example.", relativize=True, allow_include=False
+ )
+
+ self.assertRaises(dns.exception.SyntaxError, bad)
+
+ def testExplicitInclude(self):
+ z1 = dns.zone.from_text(
+ include_text,
+ "example.",
+ relativize=True,
+ allow_directives={"$INCLUDE", "$ORIGIN", "$TTL"},
+ )
+ z2 = dns.zone.from_file(here("example"), "example.", relativize=True)
+ self.assertEqual(z1, z2)
+
+ def testExplicitLowerCase(self):
+ z1 = dns.zone.from_text(
+ include_text,
+ "example.",
+ relativize=True,
+ allow_directives={"$include", "$origin", "$ttl"},
+ )
+ z2 = dns.zone.from_file(here("example"), "example.", relativize=True)
+ self.assertEqual(z1, z2)
+
+ def testExplicitWithoutInclude1(self):
+ def bad():
+ dns.zone.from_text(
+ include_text,
+ "example.",
+ relativize=True,
+ allow_include=False,
+ allow_directives={"$ORIGIN", "$TTL"},
+ )
+
+ self.assertRaises(dns.exception.SyntaxError, bad)
+
+ def testExplicitWithoutInclude2(self):
+ def bad():
+ dns.zone.from_text(
+ include_text,
+ "example.",
+ relativize=True,
+ allow_include=True,
+ allow_directives={"$ORIGIN", "$TTL"},
+ )
+
+ self.assertRaises(dns.exception.SyntaxError, bad)
+
def testBadDirective(self):
def bad():
dns.zone.from_text(bad_directive_text, "example.", relativize=True)
self.assertRaises(dns.exception.SyntaxError, bad)
+ def testAllowedButNotImplementedDirective(self):
+ def bad():
+ dns.zone.from_text(
+ bad_directive_text,
+ "example.",
+ relativize=True,
+ allow_directives={"$FOO", "$ORIGIN"},
+ )
+
+ self.assertRaises(dns.exception.SyntaxError, bad)
+
def testFirstRRStartsWithWhitespace(self):
# no name is specified, so default to the initial origin
z = dns.zone.from_text(