codekingpro/portable-devtools
114k
1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license2 3# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.4#5# Permission to use, copy, modify, and distribute this software and its6# documentation for any purpose with or without fee is hereby granted,7# provided that the above copyright notice and this permission notice8# appear in all copies.9#10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.17 18"""DNS Zones."""19 20import re21import sys22from typing import Any, Iterable, List, Optional, Set, Tuple, Union23 24import dns.exception25import dns.grange26import dns.name27import dns.node28import dns.rdata29import dns.rdataclass30import dns.rdatatype31import dns.rdtypes.ANY.SOA32import dns.rrset33import dns.tokenizer34import dns.transaction35import dns.ttl36 37 38class UnknownOrigin(dns.exception.DNSException):39 """Unknown origin"""40 41 42class CNAMEAndOtherData(dns.exception.DNSException):43 """A node has a CNAME and other data"""44 45 46def _check_cname_and_other_data(txn, name, rdataset):47 rdataset_kind = dns.node.NodeKind.classify_rdataset(rdataset)48 node = txn.get_node(name)49 if node is None:50 # empty nodes are neutral.51 return52 node_kind = node.classify()53 if (54 node_kind == dns.node.NodeKind.CNAME55 and rdataset_kind == dns.node.NodeKind.REGULAR56 ):57 raise CNAMEAndOtherData("rdataset type is not compatible with a CNAME node")58 elif (59 node_kind == dns.node.NodeKind.REGULAR60 and rdataset_kind == dns.node.NodeKind.CNAME61 ):62 raise CNAMEAndOtherData(63 "CNAME rdataset is not compatible with a regular data node"64 )65 # Otherwise at least one of the node and the rdataset is neutral, so66 # adding the rdataset is ok67 68 69SavedStateType = Tuple[70 dns.tokenizer.Tokenizer,71 Optional[dns.name.Name], # current_origin72 Optional[dns.name.Name], # last_name73 Optional[Any], # current_file74 int, # last_ttl75 bool, # last_ttl_known76 int, # default_ttl77 bool,78] # default_ttl_known79 80 81def _upper_dollarize(s):82 s = s.upper()83 if not s.startswith("$"):84 s = "$" + s85 return s86 87 88class Reader:89 """Read a DNS zone file into a transaction."""90 91 def __init__(92 self,93 tok: dns.tokenizer.Tokenizer,94 rdclass: dns.rdataclass.RdataClass,95 txn: dns.transaction.Transaction,96 allow_include: bool = False,97 allow_directives: Union[bool, Iterable[str]] = True,98 force_name: Optional[dns.name.Name] = None,99 force_ttl: Optional[int] = None,100 force_rdclass: Optional[dns.rdataclass.RdataClass] = None,101 force_rdtype: Optional[dns.rdatatype.RdataType] = None,102 default_ttl: Optional[int] = None,103 ):104 self.tok = tok105 (self.zone_origin, self.relativize, _) = txn.manager.origin_information()106 self.current_origin = self.zone_origin107 self.last_ttl = 0108 self.last_ttl_known = False109 if force_ttl is not None:110 default_ttl = force_ttl111 if default_ttl is None:112 self.default_ttl = 0113 self.default_ttl_known = False114 else:115 self.default_ttl = default_ttl116 self.default_ttl_known = True117 self.last_name = self.current_origin118 self.zone_rdclass = rdclass119 self.txn = txn120 self.saved_state: List[SavedStateType] = []121 self.current_file: Optional[Any] = None122 self.allowed_directives: Set[str]123 if allow_directives is True:124 self.allowed_directives = {"$GENERATE", "$ORIGIN", "$TTL"}125 if allow_include:126 self.allowed_directives.add("$INCLUDE")127 elif allow_directives is False:128 # allow_include was ignored in earlier releases if allow_directives was129 # False, so we continue that.130 self.allowed_directives = set()131 else:132 # Note that if directives are explicitly specified, then allow_include133 # is ignored.134 self.allowed_directives = set(_upper_dollarize(d) for d in allow_directives)135 self.force_name = force_name136 self.force_ttl = force_ttl137 self.force_rdclass = force_rdclass138 self.force_rdtype = force_rdtype139 self.txn.check_put_rdataset(_check_cname_and_other_data)140 141 def _eat_line(self):142 while 1:143 token = self.tok.get()144 if token.is_eol_or_eof():145 break146 147 def _get_identifier(self):148 token = self.tok.get()149 if not token.is_identifier():150 raise dns.exception.SyntaxError151 return token152 153 def _rr_line(self):154 """Process one line from a DNS zone file."""155 token = None156 # Name157 if self.force_name is not None:158 name = self.force_name159 else:160 if self.current_origin is None:161 raise UnknownOrigin162 token = self.tok.get(want_leading=True)163 if not token.is_whitespace():164 self.last_name = self.tok.as_name(token, self.current_origin)165 else:166 token = self.tok.get()167 if token.is_eol_or_eof():168 # treat leading WS followed by EOL/EOF as if they were EOL/EOF.169 return170 self.tok.unget(token)171 name = self.last_name172 if not name.is_subdomain(self.zone_origin):173 self._eat_line()174 return175 if self.relativize:176 name = name.relativize(self.zone_origin)177 178 # TTL179 if self.force_ttl is not None:180 ttl = self.force_ttl181 self.last_ttl = ttl182 self.last_ttl_known = True183 else:184 token = self._get_identifier()185 ttl = None186 try:187 ttl = dns.ttl.from_text(token.value)188 self.last_ttl = ttl189 self.last_ttl_known = True190 token = None191 except dns.ttl.BadTTL:192 self.tok.unget(token)193 194 # Class195 if self.force_rdclass is not None:196 rdclass = self.force_rdclass197 else:198 token = self._get_identifier()199 try:200 rdclass = dns.rdataclass.from_text(token.value)201 except dns.exception.SyntaxError:202 raise203 except Exception:204 rdclass = self.zone_rdclass205 self.tok.unget(token)206 if rdclass != self.zone_rdclass:207 raise dns.exception.SyntaxError("RR class is not zone's class")208 209 if ttl is None:210 # support for <class> <ttl> <type> syntax211 token = self._get_identifier()212 ttl = None213 try:214 ttl = dns.ttl.from_text(token.value)215 self.last_ttl = ttl216 self.last_ttl_known = True217 token = None218 except dns.ttl.BadTTL:219 if self.default_ttl_known:220 ttl = self.default_ttl221 elif self.last_ttl_known:222 ttl = self.last_ttl223 self.tok.unget(token)224 225 # Type226 if self.force_rdtype is not None:227 rdtype = self.force_rdtype228 else:229 token = self._get_identifier()230 try:231 rdtype = dns.rdatatype.from_text(token.value)232 except Exception:233 raise dns.exception.SyntaxError("unknown rdatatype '%s'" % token.value)234 235 try:236 rd = dns.rdata.from_text(237 rdclass,238 rdtype,239 self.tok,240 self.current_origin,241 self.relativize,242 self.zone_origin,243 )244 except dns.exception.SyntaxError:245 # Catch and reraise.246 raise247 except Exception:248 # All exceptions that occur in the processing of rdata249 # are treated as syntax errors. This is not strictly250 # correct, but it is correct almost all of the time.251 # We convert them to syntax errors so that we can emit252 # helpful filename:line info.253 (ty, va) = sys.exc_info()[:2]254 raise dns.exception.SyntaxError(255 "caught exception {}: {}".format(str(ty), str(va))256 )257 258 if not self.default_ttl_known and rdtype == dns.rdatatype.SOA:259 # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default260 # TTL from the SOA minttl if no $TTL statement is present before the261 # SOA is parsed.262 self.default_ttl = rd.minimum263 self.default_ttl_known = True264 if ttl is None:265 # if we didn't have a TTL on the SOA, set it!266 ttl = rd.minimum267 268 # TTL check. We had to wait until now to do this as the SOA RR's269 # own TTL can be inferred from its minimum.270 if ttl is None:271 raise dns.exception.SyntaxError("Missing default TTL value")272 273 self.txn.add(name, ttl, rd)274 275 def _parse_modify(self, side: str) -> Tuple[str, str, int, int, str]:276 # Here we catch everything in '{' '}' in a group so we can replace it277 # with ''.278 is_generate1 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")279 is_generate2 = re.compile(r"^.*\$({(\+|-?)(\d+)}).*$")280 is_generate3 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+)}).*$")281 # Sometimes there are modifiers in the hostname. These come after282 # the dollar sign. They are in the form: ${offset[,width[,base]]}.283 # Make names284 g1 = is_generate1.match(side)285 if g1:286 mod, sign, offset, width, base = g1.groups()287 if sign == "":288 sign = "+"289 g2 = is_generate2.match(side)290 if g2:291 mod, sign, offset = g2.groups()292 if sign == "":293 sign = "+"294 width = 0295 base = "d"296 g3 = is_generate3.match(side)297 if g3:298 mod, sign, offset, width = g3.groups()299 if sign == "":300 sign = "+"301 base = "d"302 303 if not (g1 or g2 or g3):304 mod = ""305 sign = "+"306 offset = 0307 width = 0308 base = "d"309 310 offset = int(offset)311 width = int(width)312 313 if sign not in ["+", "-"]:314 raise dns.exception.SyntaxError("invalid offset sign %s" % sign)315 if base not in ["d", "o", "x", "X", "n", "N"]:316 raise dns.exception.SyntaxError("invalid type %s" % base)317 318 return mod, sign, offset, width, base319 320 def _generate_line(self):321 # range lhs [ttl] [class] type rhs [ comment ]322 """Process one line containing the GENERATE statement from a DNS323 zone file."""324 if self.current_origin is None:325 raise UnknownOrigin326 327 token = self.tok.get()328 # Range (required)329 try:330 start, stop, step = dns.grange.from_text(token.value)331 token = self.tok.get()332 if not token.is_identifier():333 raise dns.exception.SyntaxError334 except Exception:335 raise dns.exception.SyntaxError336 337 # lhs (required)338 try:339 lhs = token.value340 token = self.tok.get()341 if not token.is_identifier():342 raise dns.exception.SyntaxError343 except Exception:344 raise dns.exception.SyntaxError345 346 # TTL347 try:348 ttl = dns.ttl.from_text(token.value)349 self.last_ttl = ttl350 self.last_ttl_known = True351 token = self.tok.get()352 if not token.is_identifier():353 raise dns.exception.SyntaxError354 except dns.ttl.BadTTL:355 if not (self.last_ttl_known or self.default_ttl_known):356 raise dns.exception.SyntaxError("Missing default TTL value")357 if self.default_ttl_known:358 ttl = self.default_ttl359 elif self.last_ttl_known:360 ttl = self.last_ttl361 # Class362 try:363 rdclass = dns.rdataclass.from_text(token.value)364 token = self.tok.get()365 if not token.is_identifier():366 raise dns.exception.SyntaxError367 except dns.exception.SyntaxError:368 raise dns.exception.SyntaxError369 except Exception:370 rdclass = self.zone_rdclass371 if rdclass != self.zone_rdclass:372 raise dns.exception.SyntaxError("RR class is not zone's class")373 # Type374 try:375 rdtype = dns.rdatatype.from_text(token.value)376 token = self.tok.get()377 if not token.is_identifier():378 raise dns.exception.SyntaxError379 except Exception:380 raise dns.exception.SyntaxError("unknown rdatatype '%s'" % token.value)381 382 # rhs (required)383 rhs = token.value384 385 def _calculate_index(counter: int, offset_sign: str, offset: int) -> int:386 """Calculate the index from the counter and offset."""387 if offset_sign == "-":388 offset *= -1389 return counter + offset390 391 def _format_index(index: int, base: str, width: int) -> str:392 """Format the index with the given base, and zero-fill it393 to the given width."""394 if base in ["d", "o", "x", "X"]:395 return format(index, base).zfill(width)396 397 # base can only be n or N here398 hexa = _format_index(index, "x", width)399 nibbles = ".".join(hexa[::-1])[:width]400 if base == "N":401 nibbles = nibbles.upper()402 return nibbles403 404 lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs)405 rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs)406 for i in range(start, stop + 1, step):407 # +1 because bind is inclusive and python is exclusive408 409 lindex = _calculate_index(i, lsign, loffset)410 rindex = _calculate_index(i, rsign, roffset)411 412 lzfindex = _format_index(lindex, lbase, lwidth)413 rzfindex = _format_index(rindex, rbase, rwidth)414 415 name = lhs.replace("$%s" % (lmod), lzfindex)416 rdata = rhs.replace("$%s" % (rmod), rzfindex)417 418 self.last_name = dns.name.from_text(419 name, self.current_origin, self.tok.idna_codec420 )421 name = self.last_name422 if not name.is_subdomain(self.zone_origin):423 self._eat_line()424 return425 if self.relativize:426 name = name.relativize(self.zone_origin)427 428 try:429 rd = dns.rdata.from_text(430 rdclass,431 rdtype,432 rdata,433 self.current_origin,434 self.relativize,435 self.zone_origin,436 )437 except dns.exception.SyntaxError:438 # Catch and reraise.439 raise440 except Exception:441 # All exceptions that occur in the processing of rdata442 # are treated as syntax errors. This is not strictly443 # correct, but it is correct almost all of the time.444 # We convert them to syntax errors so that we can emit445 # helpful filename:line info.446 (ty, va) = sys.exc_info()[:2]447 raise dns.exception.SyntaxError(448 "caught exception %s: %s" % (str(ty), str(va))449 )450 451 self.txn.add(name, ttl, rd)452 453 def read(self) -> None:454 """Read a DNS zone file and build a zone object.455 456 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin457 @raises dns.zone.NoNS: No NS RRset was found at the zone origin458 """459 460 try:461 while 1:462 token = self.tok.get(True, True)463 if token.is_eof():464 if self.current_file is not None:465 self.current_file.close()466 if len(self.saved_state) > 0:467 (468 self.tok,469 self.current_origin,470 self.last_name,471 self.current_file,472 self.last_ttl,473 self.last_ttl_known,474 self.default_ttl,475 self.default_ttl_known,476 ) = self.saved_state.pop(-1)477 continue478 break479 elif token.is_eol():480 continue481 elif token.is_comment():482 self.tok.get_eol()483 continue484 elif token.value[0] == "$" and len(self.allowed_directives) > 0:485 # Note that we only run directive processing code if at least486 # one directive is allowed in order to be backwards compatible487 c = token.value.upper()488 if c not in self.allowed_directives:489 raise dns.exception.SyntaxError(490 f"zone file directive '{c}' is not allowed"491 )492 if c == "$TTL":493 token = self.tok.get()494 if not token.is_identifier():495 raise dns.exception.SyntaxError("bad $TTL")496 self.default_ttl = dns.ttl.from_text(token.value)497 self.default_ttl_known = True498 self.tok.get_eol()499 elif c == "$ORIGIN":500 self.current_origin = self.tok.get_name()501 self.tok.get_eol()502 if self.zone_origin is None:503 self.zone_origin = self.current_origin504 self.txn._set_origin(self.current_origin)505 elif c == "$INCLUDE":506 token = self.tok.get()507 filename = token.value508 token = self.tok.get()509 new_origin: Optional[dns.name.Name]510 if token.is_identifier():511 new_origin = dns.name.from_text(512 token.value, self.current_origin, self.tok.idna_codec513 )514 self.tok.get_eol()515 elif not token.is_eol_or_eof():516 raise dns.exception.SyntaxError("bad origin in $INCLUDE")517 else:518 new_origin = self.current_origin519 self.saved_state.append(520 (521 self.tok,522 self.current_origin,523 self.last_name,524 self.current_file,525 self.last_ttl,526 self.last_ttl_known,527 self.default_ttl,528 self.default_ttl_known,529 )530 )531 self.current_file = open(filename, "r")532 self.tok = dns.tokenizer.Tokenizer(self.current_file, filename)533 self.current_origin = new_origin534 elif c == "$GENERATE":535 self._generate_line()536 else:537 raise dns.exception.SyntaxError(538 f"Unknown zone file directive '{c}'"539 )540 continue541 self.tok.unget(token)542 self._rr_line()543 except dns.exception.SyntaxError as detail:544 (filename, line_number) = self.tok.where()545 if detail is None:546 detail = "syntax error"547 ex = dns.exception.SyntaxError(548 "%s:%d: %s" % (filename, line_number, detail)549 )550 tb = sys.exc_info()[2]551 raise ex.with_traceback(tb) from None552 553 554class RRsetsReaderTransaction(dns.transaction.Transaction):555 def __init__(self, manager, replacement, read_only):556 assert not read_only557 super().__init__(manager, replacement, read_only)558 self.rdatasets = {}559 560 def _get_rdataset(self, name, rdtype, covers):561 return self.rdatasets.get((name, rdtype, covers))562 563 def _get_node(self, name):564 rdatasets = []565 for (rdataset_name, _, _), rdataset in self.rdatasets.items():566 if name == rdataset_name:567 rdatasets.append(rdataset)568 if len(rdatasets) == 0:569 return None570 node = dns.node.Node()571 node.rdatasets = rdatasets572 return node573 574 def _put_rdataset(self, name, rdataset):575 self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = rdataset576 577 def _delete_name(self, name):578 # First remove any changes involving the name579 remove = []580 for key in self.rdatasets:581 if key[0] == name:582 remove.append(key)583 if len(remove) > 0:584 for key in remove:585 del self.rdatasets[key]586 587 def _delete_rdataset(self, name, rdtype, covers):588 try:589 del self.rdatasets[(name, rdtype, covers)]590 except KeyError:591 pass592 593 def _name_exists(self, name):594 for n, _, _ in self.rdatasets:595 if n == name:596 return True597 return False598 599 def _changed(self):600 return len(self.rdatasets) > 0601 602 def _end_transaction(self, commit):603 if commit and self._changed():604 rrsets = []605 for (name, _, _), rdataset in self.rdatasets.items():606 rrset = dns.rrset.RRset(607 name, rdataset.rdclass, rdataset.rdtype, rdataset.covers608 )609 rrset.update(rdataset)610 rrsets.append(rrset)611 self.manager.set_rrsets(rrsets)612 613 def _set_origin(self, origin):614 pass615 616 def _iterate_rdatasets(self):617 raise NotImplementedError # pragma: no cover618 619 def _iterate_names(self):620 raise NotImplementedError # pragma: no cover621 622 623class RRSetsReaderManager(dns.transaction.TransactionManager):624 def __init__(625 self, origin=dns.name.root, relativize=False, rdclass=dns.rdataclass.IN626 ):627 self.origin = origin628 self.relativize = relativize629 self.rdclass = rdclass630 self.rrsets = []631 632 def reader(self): # pragma: no cover633 raise NotImplementedError634 635 def writer(self, replacement=False):636 assert replacement is True637 return RRsetsReaderTransaction(self, True, False)638 639 def get_class(self):640 return self.rdclass641 642 def origin_information(self):643 if self.relativize:644 effective = dns.name.empty645 else:646 effective = self.origin647 return (self.origin, self.relativize, effective)648 649 def set_rrsets(self, rrsets):650 self.rrsets = rrsets651 652 653def read_rrsets(654 text: Any,655 name: Optional[Union[dns.name.Name, str]] = None,656 ttl: Optional[int] = None,657 rdclass: Optional[Union[dns.rdataclass.RdataClass, str]] = dns.rdataclass.IN,658 default_rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,659 rdtype: Optional[Union[dns.rdatatype.RdataType, str]] = None,660 default_ttl: Optional[Union[int, str]] = None,661 idna_codec: Optional[dns.name.IDNACodec] = None,662 origin: Optional[Union[dns.name.Name, str]] = dns.name.root,663 relativize: bool = False,664) -> List[dns.rrset.RRset]:665 """Read one or more rrsets from the specified text, possibly subject666 to restrictions.667 668 *text*, a file object or a string, is the input to process.669 670 *name*, a string, ``dns.name.Name``, or ``None``, is the owner name of671 the rrset. If not ``None``, then the owner name is "forced", and the672 input must not specify an owner name. If ``None``, then any owner names673 are allowed and must be present in the input.674 675 *ttl*, an ``int``, string, or None. If not ``None``, the the TTL is676 forced to be the specified value and the input must not specify a TTL.677 If ``None``, then a TTL may be specified in the input. If it is not678 specified, then the *default_ttl* will be used.679 680 *rdclass*, a ``dns.rdataclass.RdataClass``, string, or ``None``. If681 not ``None``, then the class is forced to the specified value, and the682 input must not specify a class. If ``None``, then the input may specify683 a class that matches *default_rdclass*. Note that it is not possible to684 return rrsets with differing classes; specifying ``None`` for the class685 simply allows the user to optionally type a class as that may be convenient686 when cutting and pasting.687 688 *default_rdclass*, a ``dns.rdataclass.RdataClass`` or string. The class689 of the returned rrsets.690 691 *rdtype*, a ``dns.rdatatype.RdataType``, string, or ``None``. If not692 ``None``, then the type is forced to the specified value, and the693 input must not specify a type. If ``None``, then a type must be present694 for each RR.695 696 *default_ttl*, an ``int``, string, or ``None``. If not ``None``, then if697 the TTL is not forced and is not specified, then this value will be used.698 if ``None``, then if the TTL is not forced an error will occur if the TTL699 is not specified.700 701 *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA702 encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder703 is used. Note that codecs only apply to the owner name; dnspython does704 not do IDNA for names in rdata, as there is no IDNA zonefile format.705 706 *origin*, a string, ``dns.name.Name``, or ``None``, is the origin for any707 relative names in the input, and also the origin to relativize to if708 *relativize* is ``True``.709 710 *relativize*, a bool. If ``True``, names are relativized to the *origin*;711 if ``False`` then any relative names in the input are made absolute by712 appending the *origin*.713 """714 if isinstance(origin, str):715 origin = dns.name.from_text(origin, dns.name.root, idna_codec)716 if isinstance(name, str):717 name = dns.name.from_text(name, origin, idna_codec)718 if isinstance(ttl, str):719 ttl = dns.ttl.from_text(ttl)720 if isinstance(default_ttl, str):721 default_ttl = dns.ttl.from_text(default_ttl)722 if rdclass is not None:723 rdclass = dns.rdataclass.RdataClass.make(rdclass)724 else:725 rdclass = None726 default_rdclass = dns.rdataclass.RdataClass.make(default_rdclass)727 if rdtype is not None:728 rdtype = dns.rdatatype.RdataType.make(rdtype)729 else:730 rdtype = None731 manager = RRSetsReaderManager(origin, relativize, default_rdclass)732 with manager.writer(True) as txn:733 tok = dns.tokenizer.Tokenizer(text, "<input>", idna_codec=idna_codec)734 reader = Reader(735 tok,736 default_rdclass,737 txn,738 allow_directives=False,739 force_name=name,740 force_ttl=ttl,741 force_rdclass=rdclass,742 force_rdtype=rdtype,743 default_ttl=default_ttl,744 )745 reader.read()746 return manager.rrsets747 